主页 > 创业  > 

进程控制(创建、终止、等待、替换)

进程控制(创建、终止、等待、替换)
1. 进程创建 1.1 fork()函数

fork() 函数创建一个新进程,新进程是调用它的父进程的副本。系统在内部为子进程分配一个新的进程 ID(PID),但子进程的内存和父进程的内存空间是分开的。调用 fork() 时,父进程和子进程的代码从 fork() 函数返回的地方开始分别执行

作用: fork() 是用于创建新进程的系统调用,新进程称为子进程,复制父进程的地址空间。返回值: 父进程:返回子进程的 PID。子进程:返回 0。错误:返回 -1。

#include <stdio.h> #include <unistd.h> #include <sys/types.h> int main() { pid_t pid = fork(); if (pid == 0) { printf("Child process: pid=%d, ppid=%d\n", getpid(), getppid()); } else if (pid > 0) { printf("Parent process: pid=%d, child_pid=%d\n", getpid(), pid); } else { perror("fork failed"); } return 0; }

fork创建子进程有两种用法

(1)通过if/else父子分流,各自执行代码的一部分

(2)执行全新的程序

fork() 创建失败:

(1)内存空间不足

(2)实际用户进程数超过了限制

1.2 写时拷贝

fork() 只会在父进程和子进程修改内存时才进行内存拷贝。具体来说:

父进程和子进程在 fork() 后共享相同的内存页,直到有一个进程试图修改内存。当某一进程尝试修改内存时,操作系统会为该进程分配一个新的内存页(即拷贝),从而确保父子进程不会互相影响。

这种机制可以显著提高性能,尤其是在创建大量进程时。它避免了不必要的内存复制,只有在进程修改数据时才会进行实际的拷贝。

为什么要有写时拷贝?

 - 减少创建子进程的时间

 - 减少内存浪费

2. 进程终止

在 Linux 系统中,进程的生命周期从创建到终止是一个完整的过程。进程终止时,操作系统会进行资源回收,释放进程占用的内存、文件描述符等资源。进程可以通过多种方式终止,常见的退出方法包括 main 函数的 return、exit函数等。

进程终止的本质是释放系统资源,就是释放进程申请的相关内核数据结构和对应的数据和代码。

2.1 进程常见退出方法 2.1.1 main函数return

在 C 语言程序中,main 函数是程序的入口点。当程序执行到 main 函数的最后,main 函数的 return 语句会导致进程的终止。

main函数的返回值通常表明程序的执行情况。

在 main 函数中使用 return 时,实际上是调用了 exit(0) 或 exit 的其他变体,因此 return 和 exit 具有相同的效果

- 通常,return 0; 表示程序成功结束。

- 非零返回值通常表示程序异常终止或错误退出,具体的值由程序员定义。

2.1.2 exit

exit 是标准库函数,专门用于终止程序并返回退出状态给操作系统。无论exit()被调用的位置在哪里,都会导致程序的终止,且会清理资源(如关闭文件、释放内存等)。

函数原型:

void exit(int status); status: 退出状态码,通常用 0 表示成功,非零表示出错。在调用 exit 后,程序会立即终止,不会再执行任何后续代码。

注意事项:

exit() 不仅会终止进程,还会清理进程使用的资源,包括缓冲区数据、已打开的文件等。如果进程通过 exit() 退出,操作系统会收集子进程的退出状态信息(如果有的话)。这也与 waitpid() 函数相关,父进程可以使用它来获取子进程的退出状态。

echo $?:打印最近一个程序退出时的退出码。main函数的返回值我们称为进程退出码

2.1.3 _exit

_exit() 是一个系统调用,功能是立即终止当前进程。它会直接终止进程并清理资源,但不会执行 exit() 需要执行的标准库清理过程,如关闭流、执行注册的 atexit() 函数等。_exit() 主要用于在进程中断时需要立即退出的情况。

exit(库函数) vs _exit(系统调用)

 - exit退出进程的时候,会进行缓冲区的刷新

 - _exit退出进程的时候,不会进行缓冲区的刷新

exit() 和 _exit() 的主要区别在于资源清理的行为,_exit() 更加快速、低级,适用于子进程或需要避免缓冲区刷新的场景。

2.2 退出码与strerror

退出码是操作系统用来表示进程终止状态的一个整数值。退出码通常由进程的最后一个返回值决定,表示进程的结束结果。退出码可以由程序员自定义,用于指示程序的执行情况。

退出码值: 0:表示程序成功结束(即没有错误发生)。非零值:表示程序出现了某些错误或异常,非零的退出码通常用于区分不同的错误类型。

sterror() 一个标准库函数,用于根据错误码(errno)返回一个错误信息字符串。当程序遇到错误时,通常会设置 errno 变量,strerror()函数可以根据 errno 的值返回错误的描述信息,这对于调试和错误处理非常有帮助。 

通过下面的结果可以看到,退出码一共有134种

fopen打开失败时会返回错误码errno,表示打开失败的原因

此时失败的原因就是2:没有该文件

再来一个例子

errno 和 strerror()的关系

当一个系统调用或库函数发生错误时,它会设置 errno 变量,errno 是一个全局变量,表示最后发生的错误类型。strerror() 使用 errno 的值返回一个错误描述字符串,用于帮助程序员理解错误的具体原因。

常见的 errno 错误码:

ENOMEM:内存不足EINVAL:无效的参数EAGAIN:资源暂时不可用ENOENT:没有找到文件或目录EACCES:权限不足EBADF:坏的文件描述符

上图前两种情况退出码由return返回值决定可以自己设置

第三种情况退出码无意义

3. 进程等待 子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。 另外,进程⼀旦变成僵尸状态,那就刀枪不⼊,“杀⼈不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死⼀个已经死去的进程。 最后,关于父进程派给子进程的任务,我们需要知道子进程运行是否完成,结果对还是不对,或者是否正常退出。 父进程通过进程等待的父式,回收子进程资源,获取子进程退出信息。 3.1 wait 和 waitpid

在多进程程序中,父进程可能需要等待子进程执行完毕并回收其资源。Linux 提供了两个常用的系统调用:wait 和 waitpid,它们用于父进程等待子进程结束并获取其退出状态。通过这些调用,父进程能够管理子进程的生命周期,并处理子进程终止时的各种情况。

3.1.1 wait

函数原型: 

pid_t wait(int *status); status: 指向一个整型变量的指针,用来存储子进程的退出状态。父进程可以通过该变量检查子进程的终止状态(是否正常退出、是否由于信号退出等)。返回值: 返回结束的子进程的 pid(进程ID),如果没有子进程或遇到错误,返回 -1。

 使用方式:wait() 一般会阻塞父进程,直到一个子进程退出。如果父进程有多个子进程,它会返回第一个结束的子进程的 pid。

前五秒正常运行:

在前五秒内,父进程和子进程运行正常。子进程会输出 5 次自己的 PID 和父进程 PID,然后正常退出。父进程在这时并未调用 waitpid(),因此子进程在退出时没有立刻被父进程收养。此时,子进程还没有成为僵尸进程。

子进程成为僵尸进程: 

子进程执行 exit(0) 后会变成僵尸进程,原因是父进程没有及时调用 waitpid() 来回收它的资源。僵尸进程是已经退出但尚未被父进程回收的进程,它会保留在进程表中,直到父进程通过 waitpid() 获取它的退出状态。

为什么子进程变成僵尸进程:

在父进程调用 sleep(10) 后,它延迟了 10 秒才调用 waitpid(),这个时候子进程已经结束并变成僵尸进程。虽然子进程退出了,但父进程还未及时收尸,因此子进程保持在进程表中。

后续父进程回收子进程 

父进程在调用 waitpid() 后,成功回收了子进程的退出状态,从而清除了僵尸进程。waitpid() 返回值 rid 是子进程的 PID,表明子进程已经被父进程回收。

前五秒:父子进程正常执行,子进程打印信息,父进程保持休眠。后五秒:子进程退出后变成僵尸进程,因为父进程没有及时调用 waitpid() 来回收子进程的退出状态。父进程等待回收:父进程通过 waitpid() 回收子进程,清除了僵尸进程。 3.1.2 waitpid

waitpid() 是一个更灵活的系统调用,它允许父进程等待指定的子进程,也可以通过额外的选项控制等待行为。与 wait() 不同,waitpid() 可以等待特定的子进程,或者在不阻塞父进程的情况下进行非阻塞等待。

函数原型:

pid_t waitpid(pid_t pid, int *status, int options);

pid: 可以指定要等待的子进程的 pid:

pid > 0: 等待指定 pid 的子进程。pid == 0: 等待同组的任何子进程。pid == -1: 等待任何子进程(与 wait() 类似)。pid < -1: 等待进程组中的某个子进程。

status: 用于存储子进程的退出状态,和 wait() 相同。

options: 控制行为的选项,常用的选项有:

WNOHANG:非阻塞模式,立即返回,若没有子进程终止则返回 0。WUNTRACED:等待已停止的子进程。WCONTINUED:等待已经继续运行的子进程。

返回值

返回 pid(子进程的 pid)表示父进程已成功获得该子进程的终止信息。如果没有子进程或遇到错误,返回 -1

下面的waitpid作用与wait相同 

3.2 获取子进程status

在父进程等待子进程终止时,wait() 或 waitpid() 会返回一个表示子进程状态的整数 status。通过宏函数,我们可以从 status 中提取子进程的退出信息:

WIFEXITED(status) 判断子进程是否正常退出。如果子进程是由于调用 exit() 或正常返回而终止,WIFEXITED 返回 true,可以通过 WEXITSTATUS(status) 获取子进程的退出码。

WIFSIGNALED(status) 判断子进程是否因为信号异常终止。如果子进程由于收到信号(例如 SIGKILL、SIGSEGV)而退出,WIFSIGNALED 返回 true,可以通过 WTERMSIG(status) 获取终止信号的编号。

WIFSTOPPED(status) 判断子进程是否被信号暂停。如果子进程被信号暂停(例如通过 SIGSTOP),WIFSTOPPED 返回 true,可以通过 WSTOPSIG(status) 获取导致子进程暂停的信号编号。

WIFCONTINUED(status) 判断子进程是否继续运行。如果子进程在暂停后继续运行,WIFCONTINUED 返回 true。

WEXITSTATUS(status) 如果 WIFEXITED(status) 返回 true,则可以调用 WEXITSTATUS(status) 获取子进程的退出码。通常情况下,子进程会通过 exit() 函数传递一个退出码。

WTERMSIG(status) 如果 WIFSIGNALED(status) 返回 true,则可以调用 WTERMSIG(status) 获取终止子进程的信号编号(例如,SIGSEGV 表示段错误)。

WSTOPSIG(status) 如果 WIFSTOPPED(status) 返回 true,则可以调用 WSTOPSIG(status) 获取导致子进程暂停的信号编号。

#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> int main() { pid_t pid = fork(); // 创建子进程 if (pid == 0) { // 子进程:模拟正常退出 printf("Child process exiting normally\n"); exit(42); // 子进程正常退出并返回退出码 42 } else if (pid > 0) { // 父进程 int status; pid_t child_pid = waitpid(pid, &status, 0); // 等待子进程结束 if (child_pid > 0) { if (WIFEXITED(status)) { // 子进程正常退出 int exit_code = WEXITSTATUS(status); printf("Child process exited with code %d\n", exit_code); } else if (WIFSIGNALED(status)) { // 子进程由于信号终止 int signal_number = WTERMSIG(status); printf("Child process terminated by signal %d\n", signal_number); } else if (WIFSTOPPED(status)) { // 子进程被信号暂停 int stop_signal = WSTOPSIG(status); printf("Child process stopped by signal %d\n", stop_signal); } else if (WIFCONTINUED(status)) { // 子进程继续执行 printf("Child process continued\n"); } } } else { perror("fork failed"); exit(1); } return 0; } Child process exiting normally Child process exited with code 42 父进程调用 waitpid(pid, &status, 0) 等待特定的子进程。通过 WIFEXITED(status) 判断子进程是否正常退出,并使用 WEXITSTATUS(status) 获取退出码 42。

8-15是退出状态,即退出码

退出码为1,因此8-15之间为00000001,0-7为0,所以为0000000100000000->256

那我们怎么让status和退出码一致呢?status>>8&0xFF

拿到退出信号

 

 waitppid:WEXITSTATUS(status)

非阻塞调用

4. 进程替换

在一个进程运行过程中,有时候需要执行另一个不同的程序。进程替换就是让当前进程放弃正在执行的程序,转而执行新的程序。这种替换是直接的,不会创建新的进程,而是复用当前进程的资源。一旦替换成功就去执行新的代码了,原始代码之后的部分就不存在了。

常见实现方式:在 C 语言中使用 exec 系列函数

常见的有 execl、execv、execle、execve、execlp 和 execvp。这些函数的功能基本相同,只是参数传递方式略有不同。exec*系列的函数只有失败会有返回值,没有成功返回值。

#include <unistd.h> int execl(const char *path, const char *arg, ...); int execlp(const char *file, const char *arg, ...); int execle(const char *path, const char *arg, ..., char * const envp[]); int execv(const char *path, char *const argv[]); int execvp(const char *file, char *const argv[]); int execve(const char *path, char *const argv[], char *const envp[]);

按参数传递方式分类

execl、execlp、execle:使用可变参数列表传递命令行参数,最后一个参数必须为 NULL,用于表示参数列表的结束。execv、execvp、execve:使用字符指针数组 argv 传递命令行参数,数组的最后一个元素必须为 NULL。

按查找路径分类

execl、execv、execle、execve:需要指定要执行程序的完整路径名 path。

execlp、execvp:只需要指定程序名 file,系统会在环境变量 PATH 所指定的路径中查找该程序。

按是否自定义环境变量分类

execl、execlp、execv、execvp:使用调用进程的环境变量。execle、execve:可以通过 envp 参数自定义环境变量。 4.1 execl int execl(const char *path, const char *arg, ...);

功能:用新程序替换当前进程的映像,新程序的路径由 path 指定,命令行参数以可变参数列表的形式传递。

参数:

path:要执行程序的完整路径名。

arg:可变参数列表,第一个参数通常是程序名,最后一个参数必须为 NULL。

返回值:如果执行成功,不会返回;如果执行失败,返回 -1,并设置 errno。

程序执行ls -l 命令 

4.2 execlp int execlp(const char *file, const char *arg, ...); 与 execl 类似,但不需要指定程序的完整路径,系统会在 PATH 环境变量指定的路径中查找该程序。参数: file:要执行程序的名称。arg:可变参数列表,第一个参数通常是程序名,最后一个参数必须为 NULL。 返回值:如果执行成功,不会返回;如果执行失败,返回 -1,并设置 errno。

第一个 ls 的作用是指定要执行的程序的名称。execlp 函数会在环境变量 PATH 所指定的路径中查找这个名称对应的可执行文件。 

第二个 ls 是传递给 第一个ls 程序的第一个命令行参数。后面跟着的 -l 则是 ls 命令的一个选项

4.3 execle int execle(const char *path, const char *arg, ..., char * const envp[]); 功能:用新程序替换当前进程的映像,新程序的路径由 path 指定,命令行参数以可变参数列表的形式传递,同时可以通过 envp 参数自定义环境变量。参数: path:要执行程序的完整路径名。arg:可变参数列表,第一个参数通常是程序名,最后一个参数必须为 NULL。envp:自定义的环境变量数组,数组的最后一个元素必须为 NULL。 返回值:如果执行成功,不会返回;如果执行失败,返回 -1,并设置 errno。

4.4 execv  int execv(const char *path, char *const argv[]); 用新程序替换当前进程的映像,新程序的路径由 path 指定,命令行参数以字符指针数组 argv 的形式传递。参数: path:要执行程序的完整路径名。argv:命令行参数数组,数组的第一个元素通常是程序名,最后一个元素必须为 NULL。 返回值:如果执行成功,不会返回;如果执行失败,返回 -1,并设置 errno。

4.5 execvp int execvp(const char *file, char *const argv[]); 功能:与 execv 类似,但不需要指定程序的完整路径,系统会在 PATH 环境变量指定的路径中查找该程序。参数: file:要执行程序的名称。argv:命令行参数数组,数组的第一个元素通常是程序名,最后一个元素必须为 NULL。 返回值:如果执行成功,不会返回;如果执行失败,返回 -1,并设置 errno。

4.6 execve int execve(const char *path, char *const argv[], char *const envp[]); 功能:用新程序替换当前进程的映像,新程序的路径由 path 指定,命令行参数以字符指针数组 argv 的形式传递,同时可以通过 envp 参数自定义环境变量。参数: path:要执行程序的完整路径名。argv:命令行参数数组,数组的第一个元素通常是程序名,最后一个元素必须为 NULL。envp:自定义的环境变量数组,数组的最后一个元素必须为 NULL。 返回值:如果执行成功,不会返回;如果执行失败,返回 -1,并设置 errno。
标签:

进程控制(创建、终止、等待、替换)由讯客互联创业栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“进程控制(创建、终止、等待、替换)