
【Process】深入理解进程:从原理到实践
进程 Process
1. 什么是进程
程序原码:存储在硬盘中的代码,可视化文件
可执行程序:原码通过编译操作,得到的二进制可执行文件。
进程: 当前可执行程序在交给 CPU 执行对应的一个【执行实例】,包括当前程序执行所需的相关资源,例如 CPU 占用,计数器,寄存器,变量,可执行的函数和【文件描述符】......
程序是不可变的,但是进程是可变的
- 进程在运行过程中,会对当前程序执行所需的相关资源,相关逻辑根据执行的情况下,执行不同的模块,申请不同的执行方式。。。
- 程序是一堆指令的集合,而进程是变化的【执行实例】,进程存在多种状态,
- 进程执行所需的资源,包括但不限于,CPU,内存,硬盘,IO,网络,外设
2. 进程的状态和转换
进程状态有
- 就绪态:进程已经具备执行的一切条件,正在等待分配 CPU 的处理时间
- 执行态:当前进程正在 CPU 中执行
- 等待态:进程因为一些原因不具备执行条件,无法获取到 CPU 的处理时间,无法执行。

CPU 时间片概念
3. 进程控制块
操作系统 (OS) 根据进程控制块 (PCB) 对当前系统中,执行的进程进行管理和控制。当进程创建时,会在内存中申请必要的内存空间,用于管理当前进程相关资源
主要包括
- 调度数据
- 包括管理进程状态,标记,优先级和调度策略
- 时间数据
- 创建进程的时间,运行状态时间**【用户态执行时间,内核态执行时间】**
- 文件系统管理
- 核心【文件描述符】,掩码,内存数据,进程的上下文相关内容,进程号(PID)
4. 进程控制【重点】
4.1 进程号,父进程号和进程组号
每一个进程都有一个唯一的进程号,操作系统用于管理进程的方式。进程号的范围是 0 ~ 32767。
系统中 0 号和 1 号进程都是由系统占用,0 号进程是交换进程(Swapper), 1 号进程是系统的 init 初始化进程。Linux 中任何一个进程号都不可能是负数。
进程相关的编号有三个
- 进程号 PID Process ID
- 当前进程的非负数进程号
- 父进程号 PPID Parent Process ID
- 在 Linux 中,任何一个进程都需要其他进程进行创建/申请,认为创建/申请当前进程的 Process 是一个父进程
- 组进程号 PGID Process Group ID
- 进程组是多个进程的集合,进程组中的进程可以获取同一个终端中发送的信号,信息....
相关函数
涉及到的头文件
#include <unistd.h> #include <sys/types.h>
pid_t getpid(void);
函数功能: 得到当前进程对应的 PID 号,当前数据对于整个操作系统唯一
返回值类型是一个 pid_t ,可以认为是一个整数类型,对应的真实数据类型为 short 类型。
pid_t getppid(void);
函数功能: 获取当前进程的父进程 ID 数据
返回值类型是一个 pid_t ,可以认为是一个整数类型,对应的真实数据类型为 short 类型。
pid_t getpgid(pid_t pid);
函数功能: 获取指定进程 ID 对应的进程组 ID 数据,如果给予实际参数为 0,表示获取当前进程对应的进程组 ID
参数: 要求获取进程组 ID 的对应进程 PID
返回值: 当前进程对应的进程组 ID 号,如果获取失败,返回 -1
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main(int argc, char const *argv[])
{
pid_t pid = getpid();
printf("pid : %d\n", pid);
pid_t ppid = getppid();
printf("ppid : %d\n", ppid);
pid_t pgid = getpgid(pid);
printf("pgid : %d\n", pgid);
int num = 0;
scanf("%d", &num);
return 0;
}
4.2 进程创建函数 fork 【重点】
函数文档
#include <sys/types.h> #include <unistd.h> pid_t fork(void);
功能
- fork 函数用于从一个已经运行的进程,创建一个新的进程,新进程称之为【子进程】,已经存在运行的进程可以认为是【父进程】
返回值:
- 如果在子进程中,返回值为 0,对应当前子进程本身
- 如果在父进程中,返回值是对应当前子进程的 PID
- 如果创建失败,返回 -1,表示当前进程没有创建对应子进程
内存空间/地址空间
- fork 函数会将父进程相关资源完全拷贝一份提供给子进程,包括进程上下文,进程堆栈,【打开的文件描述符】,信号控制设定/规则,进程优先级设计,进程组号
- fork 操作仅在子进程中,确定了子进程自己【PID】和计时器【Timer】。fork 操作对于进程操作消耗较大。
读时共享,写时拷贝
准确来说,Linux 的 fork() 使用是通过写时拷贝 (copy- on-write) 实现。写时拷贝是一种可以推迟甚
至避免拷贝数据的技术。内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址
空间。只用在需要写入的时候才会复制地址空间,从而使各个进行拥有各自的地址空间。
也就是说,资源的复制是在需要写入的时候才会进行,在此之前,只有以只读方式共享。
注意:fork之后父子进程共享文件,fork产生的子进程与父进程相同的文件文件描述符指向相同的
文件表,引用计数增加,共享文件偏移指针。
读时共享,写时拷贝;可以降低内存消耗、减少复制所使用的时间!
解释:
(1)读时共享:这个容易理解,即不涉及写操作时,子进程并不会复制父进程的地址空间,而是和父进程共享一块地址空间;
(2)写时拷贝:拿上述代码案例来说,父进程执行部分和子进程执行部分都会对变量num进行操作,但是父进程和子进程对变量num的操作是独立的、互不相干的,此时便需要子进程复制父进程的地址空间,然后父、子进程在各自空间中对变量num进行操作;

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main(int argc, char const *argv[])
{
pid_t pid = 0;
// 利用 fork 申请创建子进程
pid = fork();
if (-1 == pid)
{
perror("fork failed");
}
/*
fork
1. 如果执行的是子进程,pid = 0
2. 如果执行的是父进程 pid 是当前父进程创建的子进程唯一 PID 数据
*/
if (0 == pid)
{
// 如果 pid 为 0 表示当前执行的是子进程内容
while (1)
{
printf("This is Son Process Running!, Flag : %d, PID : %d, PPID : %d, PGID : %d\n"
, pid // fork 函数返回值
, getpid() // 当前进程 PID
, getppid() // 当前进程的 PPID 父进程 ID
, getpgid(getpid())); // 当前进程的 PGID 进程组 ID
sleep(1);
}
}
else
{
// 如果不唯一,表示当前执行的是父进程内容
while (1)
{
printf("This is Parent Process Running!, Flag : %d, PID : %d, PPID : %d, PGID : %d\n"
, pid // fork 函数返回值
, getpid() // 当前进程 PID
, getppid() // 当前进程的 PPID 父进程 ID
, getpgid(getpid())); // 当前进程的 PGID 进程组 ID
sleep(1);
}
}
return 0;
}
自行测试,在 main 函数定义变量,子进程执行 -- ,父进制执行 ++ 操作,有什么情况
4.3 进程挂起函数 sleep
函数文档
- 在哪一个进程中执行,哪一个进程进入挂起状态
#include <unistd.h> unsigned int sleep(unsigned int seconds);
- 参数解释:
- 要求提供的参数是一个无符号整数,确定当前进程挂起的秒数
- 返回值:
- 如果请求的睡眠时间已到,则返回 0;如果该调用被信号处理程序中断,则返回剩余的睡眠时间(以秒为单位)。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main(int argc, char const *argv[])
{
pid_t pid = 0;
pid = fork();
if (-1 == pid)
{
perror("fork");
}
if (0 == pid)
{
int second = 5;
while (second > 0)
{
printf("Son Process running! %d second left!\n", second);
sleep(1);
second -= 1;
}
_exit(1);
}
else
{
int second = 5;
while (second > 0)
{
printf("Parent Process running! %d second left!\n", second);
sleep(1);
second -= 1;
}
_exit(1);
}
return 0;
}
4.4 进程等待
4.4.1 wait 函数
进程等待存在两个函数,分别是 wait 和 waitpid 函数
函数文档
#include <sys/types.h> #include <sys/wait.h> pid_t wait(int *wstatus);
- 功能:
- 函数会使调用进程(通常是父进程)阻塞,直到它的一个子进程终止。当子进程终止时,
wait
函数会返回该子进程的 PID,并通过wstatus
参数返回子进程的退出状态信息。同时会将子进程使用的内存空间进行释放- 参数:
- 参数是一个 int 类型变量的空间首地址,当前变量的作用是存储进程关闭的状态信息。
- 返回值
- 若成功,返回终止子进程的进程 ID(PID)。
- **若出错,返回 -1,并设置 **
errno
来指示错误类型。
ECHILD
:调用进程没有子进程。EINTR
:在等待子进程时被信号中断。- 补充
- wait 函数通常在开发中会配合两个系统提供的带参数宏,进行进程退出情况判断和退出信息获取
- WIFEXITED(status) 若子进程正常终止(通过 exit 或 return),则返回非零值。
- WEXITSTATUS(wstatus):若 WIFEXITED 为真,则返回子进程的退出状态码(即 exit 或 return 中指定的值)。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#define FORK_ERROR_FLAG -1
#define SON_PROCESS 0
int main(int argc, char const *argv[])
{
pid_t pid = 0;
pid = fork();
if (FORK_ERROR_FLAG == pid)
{
perror("Fork Failed!");
}
/*
子进程运行 5 秒
父进程执行时,直接 wait,当前子进程退出时,获取子进程退出状态信息。
利用 C 语言提供的两个带参宏得到当前的进程退出情况和退出状态
WIFEXITED(int status)
WEXITSTATUS(int status)
*/
if (SON_PROCESS == pid)
{
int second = 5;
while (second > 0)
{
printf("Son process running! %d second left!\n", second);
second -= 1;
sleep(1);
}
// 利用 System Calls 关闭当前线程,退出状态码为 1
_exit(2);
#if 0
// C 库函数退出
exit(1);
#endif
}
else
{
int second = 5;
int status = 0;
printf("Parent process waiting.......\n");
// wait 函数执行,将当前子进程退出的状态信息,存储到 status 中.
wait(&status);
/*
WIFEXITED(status) 若子进程正常终止(通过 exit 或 return),则返回非零值。
*/
if (WIFEXITED(status))
{
// 子进程正常退出
/*
WEXITSTATUS(wstatus):
若 WIFEXITED 为真,则返回子进程的退出状态码(即 exit 或 return
中指定的值)。
*/
int status_code = WEXITSTATUS(status);
printf("Son process exit successful, status : %d\n", status_code);
printf("Parent Process exit!\n");
_exit(1);
}
}
return 0;
}
4.4.2 waitpid 函数
函数文档
#include <sys/types.h> #include <sys/wait.h> pid_t waitpid(pid_t pid, int *wstatus, int options);
- 功能
- **函数是 **Unix 和 **类 Unix 系统(Linux macOS FreeBSD)**中用于进程同步的一个重要系统调用,它的主要功能是等待指定子进程的状态发生变化,比如子进程终止、停止或继续执行等。
- 可以认为是对子进程的监控,
OB 模式
- 参数解释
pid_t pid
:
pid > 0
: 指定当前函数监控的子进程 PID 号。pid == 0
: 等待与调用进程属于同一进程组的任意子进程。pid == -1
: 等待任意一个子进程,这种情况下waitpid
的作用和wait
函数类似。pid < -1
: 等待进程组 ID 等于pid
绝对值的任意子进程。例如实际参数为-5555
, 当前函数的作用是监控在进程组 5555 中的任何一个子进程操作。int *wstatus
:
- 当前进程退出状态,或者当前工作状态。
options
:
- 该参数是一个位掩码,用于指定 waitpid 函数的行为选项,常用的选项有:
WNOHANG
:如果没有子进程发生状态变化,函数不阻塞,立即返回 0。WUNTRACED
:除了返回终止子进程的状态信息外,还会返回因信号而停止的子进程的状态信息。WCONTINUED
:返回那些因收到SIGCONT
信号而恢复执行的子进程的状态信息。- 返回值解释
- 大于 0:返回状态发生变化的子进程的进程 ID。
- 等于 0:当使用
WNOHANG
选项且没有子进程状态发生变化时返回 0。- 等于 -1:
- **表示出现错误,此时会设置 **
errno
来指示具体的错误类型,常见错误有:ECHILD
:调用进程没有符合条件的子进程。EINTR
:在等待过程中被信号中断。EINVAL
:options
参数中包含了无效的标志。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#define FORK_ERROR_FLAG -1
#define SON_PROCESS 0
int main(int argc, char const *argv[])
{
pid_t pid;
pid = fork();
if (FORK_ERROR_FLAG == pid)
{
perror("Fork Failed!");
}
if (SON_PROCESS == pid)
{
int second = 5;
while (second > 0)
{
printf("Son process running! Parent process PID %d, %d second left!\n", getppid(), second);
second -= 1;
sleep(1);
}
// _exit(3);
}
else
{
sleep(10);
printf("Parent Process Running , PID : %d\n", getpid());
/*
pid_t waitpid(pid_t pid, int *wstatus, int options);
WNOHANG Don't block waiting.
WUNTRACED Report status of stopped
WCONTINUED Report continued child
*/
int status = 0;
pid_t child_pid = waitpid(-1, &status, WNOHANG);
/*
情况一:
pid_t child_pid = waitpid(-1, &status, WNOHANG);
非阻塞情况下,且当前程序中没有任何一个子进程发生状态改变,
返回结果为 0,父进程后续代码中,没有阻塞,没有其他任务,当前
父进程终止运行。
【注意】如果父进程关闭,当前父进程所有的子进程都会交给进程 PID 为 1
系统 init 初始化进程管理,最终销毁当前子进程使用的相关资源
情况二:
pid_t child_pid = waitpid(-1, &status, WUNTRACED);
阻塞状态下,必须监控到任意一个子进程出现关闭情况,得到对应
子进程相关信息.
*/
if (child_pid > -1)
{
printf("child_pid : %d\n", child_pid);
if (WIFEXITED(status))
{
// 如果当前进程关闭,WIFEXITED 返回值非 0
printf("WEXITSTATUS(status) : %d\n", WEXITSTATUS(status));
}
}
else
{
perror("waitpid!");
}
}
return 5;
}
4.5 进程退出函数
函数原型
_exit
- 头文件:
#include <unistd.h>
- 原型:
void _exit(int status);
- 系统调用函数
exit
- 头文件:
#include <stdlib.h>
- 原型:
void exit(int status);
- C 语言库函数
4.6 进程退出清理函数 atexit
函数文档
#include <stdlib.h> int atexit(void (*func)(void));
- 函数功能:
- C 语言库函数,用于在正常结束进程的情况下,执行目标已注册函数。
- **当调用
atexit
函数时,会把指定的函数func
注册到一个特殊的函数列表(栈结构)中。在程序正常终止(比如通过exit
函数、从main
函数返回)**时,系统会按照与注册顺序相反【底层栈结构 FILO】的次序来调用这些已注册的函数。- 不过要注意,若程序是因为调用
abort
函数、接收到未处理的信号等异常情况而终止,那么这些已注册的函数不会被调用。- 参数列表:
- func: 函数指针,要求函数是无参数无法返回值函数。注册顺序和执行顺序相反【FILO】
- 返回值类型
- 若函数注册成功,
atexit
会返回 0。- 若注册失败,就会返回非零值。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
void fun1(void);
void fun2(void);
void fun3(void);
// int fun4(int n);
int main(int argc, char const *argv[])
{
int ret = atexit(fun1);
printf("Function One atexit result : %d\n", ret);
ret = atexit(fun2);
printf("Function Two atexit result : %d\n", ret);
ret = atexit(fun3);
printf("Function Three atexit result : %d\n", ret);
// ret = atexit(fun4);
// printf("Function Parameter type is int, atexit result : %d\n", ret);
printf("Program exit 5 seconds later!\n");
sleep(5);
return 0;
}
void fun1(void)
{
printf("Function One Running...\n");
}
void fun2(void)
{
printf("Function Two Running...\n");
}
void fun3(void)
{
printf("Function Three Running...\n");
}
// int fun4(int n)
// {
// printf("Parameter Type : int\n");
// }
4.7 进程创建函数 vfork
函数文档
#include <unistd.h> pid_t vfork(void);
- 功能:
- **创建一个子进程。**重点是子进程对应内存空间,使用的实际上是父进程内存空间
- 返回值类型
- 在子进程中,返回值结果为 0
- 在父进程中,返回值结果是父进程创建的子进程 PID
- 如果创建失败返回 -1
- 【重点】fork 和 vfork 区别
- vfork 优先保证子进程执行,在子进程调用 【exec 族】或者【exit 函数】之后,父进程才可以正常执行。
- vfork 创建的子进程使用的内存空间来自于父进程,在使用时需要注意内存空间冲突问题。
- 【注意】有且只有子进程执行 【exec 族】相关函数,子进程才可以申请自行的进程执行内存空间,与父进程无关
vfork 创建进程 子进程优先执行
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
/*
当前程序证明,vfork 优先执行子进程
*/
int main(int argc, char const *argv[])
{
pid_t pid = 0;
pid = vfork();
if (-1 == pid)
{
perror("vfork failed!\n");
}
if (0 == pid)
{
int count = 5;
while (count > 0)
{
printf("Son process running! Exit in %d second\n", count);
sleep(1);
count -= 1;
}
/*
exit
_exit
*/
_exit(0);
}
else
{
int count = 5;
while (count > 0)
{
printf("Parent process running! Exit in %d second\n", count);
sleep(1);
count -= 1;
}
exit(0);
}
return 0;
}
vfork 子进程和父进程内存空间一致
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
/*
vfor 子进程和进程内存空间问题
*/
/*
全局变量
*/
int global_var = 10;
int main(int argc, char const *argv[])
{
pid_t pid = 0;
pid = vfork();
/*
局部变量,在当前函数执行,或代码块执行过程中有效。
*/
int num = 0;
if (-1 == pid)
{
perror("vfork failed!\n");
}
if (0 == pid)
{
int count = 5;
while (count > 0)
{
printf("Son process running! Exit in %d second\n", count);
num += 1;
global_var += 1;
sleep(1);
count -= 1;
}
printf("&num : %p\n", &num);
/*
exit
_exit
*/
_exit(0);
}
else
{
int count = 5;
while (count > 0)
{
printf("Parent process running! Exit in %d second\n", count);
sleep(1);
count -= 1;
}
/*
num 结果为 0,因为 num 是一个函数内部的局部变量,进程执行完毕之后,对应使用局部变量
被系统销毁了,在父进程中使用 num 可以认为是父进程自行使用的 num 局部变量,但是有可能
出现子进程和父进程局部变量地址一致
global_var 全局变量,可以证明,子进程和父进程使用的内存空间是一致的,因为在子进程中
对 global_var 进行了修改,在父进程中同样有效。
【全局变量】可以作为子进程和父进程数据交互的方式。
*/
printf("in parent process num = %d, global_var = %d\n", num, global_var);
printf("&num : %p\n", &num);
exit(0);
}
return 0;
}
4.8 进程交换函数 exec 函数族
进程执行 exec 函数族相关函数,可以认为当前进程已关闭,执行目标 exec 对应的进程内容,同时会申请 exec 执行目标进程所需的内存空间。
函数文档
#include <unistd.h> int execl(const char *path, const char *arg, ...);
参数说明:
path
:要执行的程序的路径名。arg
:传递给新程序的第一个参数,通常是程序名本身。后续可以跟可变数量的参数,以NULL
结尾。- 例如: execl("/bin/ls", "-al", NULL);
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
// execl("/bin/ls", "ls", "-a", "-l", "-g", NULL);
execl("./fork2.out", "./fork2.out", NULL);
/*ls
"/bin/ls" 执行程序的目标路径,可以是相对路径,也可以是绝对路径
"ls", "-a", "-l" 当前执行程序对应参数,也可以认为是命令行中的内容
*/
return 0;
}
函数文档
#include <unistd.h> int execv(const char *path, char *const argv[]);
参数说明:
path
:要执行的程序的路径名。argv
:一个以NULL
结尾的字符串数组,包含传递给新程序的参数。
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
/*
用户执行程序对应的命令为 ./ls -a -l
数组内容 argv = {"./ls", "-a", "-l"};
三选一
1. memcpy
2. strtok
3. sscanf
将 "./ls" ==> "ls"
最终效果是 当前程序编译采用的命令为 gcc 12-exev.c -o ls
执行
./ls -a -l
得到效果和 ls -al 一致
*/
char * argument_array[4] = {"ls", "-l" , "-a", NULL};
execv("/bin/ls", argument_array);
return 0;
}
- 感谢你赐予我前进的力量