2022年,卷的第二篇,这一篇主要是描述进程的终止,回收,替换。内容是比较多。
11.1 进程终止进程既然有创建,那肯定是有终止的,都是存在一个生命周期的。
在不考虑线程的情况下,进程的退出有以下5种方式:
- 在main函数内执行return语句。
- 调用exit函数。
- 调用_exit或_Exit函数。
异常退出条件有两种:
- 调用abort
- 当进程接收到某些信号时。
正常退出的我们来学习一下,异常退出等到信号的时候再说。
11.1.1 _exit函数还是先来看看函数的原型:
#includevoid _exit(int status);
一看这个头文件就是linux系统提供的函数了,调用这个函数直接进入内核态,进程程序的退出。
如果我们需要返回子进程的状态给父进程,就需要用到status这个变量。(父进程怎么接收,下面介绍)
虽然这个status是int类型,但是其实只有低8位有效,也就是一个无符号字符型,这个我们等下写一个例子,返回-1,来看看是个什么值就明白了。
#includeint main(int argc, char **argv) { _exit(-1); }
这次代码简单了,不骗字数了,哈哈哈。
然后我们来编译执行一下:
root@ubuntu:~/c_test/10# gcc _exit.c -o _exit root@ubuntu:~/c_test/10# ./_exit root@ubuntu:~/c_test/10# $? 255: command not found root@ubuntu:~/c_test/10#
$?就是shell中,返回上一条命令的返回值,我们明明返回的是-1,shell这边竟然是255,说明这是一个无符号的字符型。
返回255就算了,竟然还显示command not found,你说气人不?
这个就跟shell编程相关了,shell对这个0-255有如下的区别和含义:
| 值 | 含义 |
|---|---|
| 0 | 命名成功执行并退出 |
| 1~125 | 命令未成功地退出,具体含义由各自的命令来定义 |
| 126 | 命令找到了,文件无法执行 |
| 127 | 命令找不到 |
| >128 | 命令因收到信号而死忙 |
我试了几个返回值,发现都是命令找不到,真奇怪,难道这些都是需要配置的?
11.1.2 exit函数我们还是来继续看函数原型:
#includevoid exit(int status);
这个函数一看就是glic中的函数,也的确是这样,exit是对_exit函数的一个封装。封装的内核有:
- 执行用户通过调用atexit函数或on_exit定义的清理函数
- 关闭所有打开的流,所有缓冲区的数据均被写入(flush),通过tmpfile创建的临时文件都会被删除。(如果进程占用内存不释放,或者打开文件,不关闭文件,这里都是可以帮忙释放)
- 调用_exit。
看着exit函数这么好,是不是就没有缺点了呢?
其实是有的一定的局限性的:当进程正常退出时,会调用C库的exit;而当程序或被kill掉时,c库的exit则不会被调用,只会执行内核退出进程的操作。
下面我们试试冲刷的例子:
// _exit例子 #include#include int main(int argc, char **argv) { printf("hello world"); _exit(126); }
// exit例子 #include#include int main(int argc, char **argv) { printf("hello world"); exit(126); }
各自编译运行的结果:
root@ubuntu:~/c_test/10# gcc _exit.c -o _exit root@ubuntu:~/c_test/10# gcc exit.c -o exit root@ubuntu:~/c_test/10# ./_exit root@ubuntu:~/c_test/10# ./exit hello worldroot@ubuntu:~/c_test/10#11.1.3 return函数
return函数是我们经常在main函数中使用的退出进程的函数,其实执行return(n)等同于执行exit(n)。
下面是《unix环境高级编程》中的图:
这个描述的还真不错。
11.1.4 atexit函数既然上面都说了这个绑定回调的函数,我们就来试一波这个函数吧。
#includeint atexit(void (*func)(void));
写一个例子测试一波就可以了:
#include#include void exit1() { printf("exit1n"); } void exit2() { printf("exit2n"); } int main(int argc, char **argv) { atexit(exit1); atexit(exit2); printf("main returnn"); return 0; }
都是比较简单的例子,编译执行:
root@ubuntu:~/c_test/10# ./atexit main return exit2 exit1 root@ubuntu:~/c_test/10#
有点想栈,先绑定的,后执行。
11.2 等待子进程上面我们已经介绍了进程的终止,但是进程终止之后,父进程是不是需要知道子进程是怎么终止的么?
比如是正常的提出,或者是被信号终止的。所以linux系统提供了等待回收子进程的函数。
11.2.1 wait()先来看看函数原型:
#includepid_t wait(int *status);
成功时,返回已退出子进程的进程ID;失败时,返回-1,并设置errno。
| errno | 说 明 |
|---|---|
| ECHLD | 调用进程时发现并没有子进程需要等待 |
| EINTR | 函数被信号中断 |
参数:
status:进程退出时的状态信息。
| 宏 | 说 明 |
|---|---|
| WIFEXITED(status) | 正常终止子进程返回状态。 WEXITSTATUS(status)获取状态 |
| WIFSIGNALED(status) | 异常终止子进程返回状态。 WTERMSIG(status)获取使子进程终止的信号编号。 |
| WIFSTOPPED(status) | 暂停子进程的返回状态。 WSTOPSIG(status)获取使子进程暂停的信号编号 |
| WIFConTINUED(status) | 在暂停后的子进程又开始运行,返回这个状态。 (仅用于waitpid) |
来写一个例子:
#include#include #include #include void pr_exit(int status) { if(WIFEXITED(status)) printf("normal exit, exit status = %dn", WEXITSTATUS(status)); else if(WIFSIGNALED(status)) printf("abnormal exit, signal number = %dn", WTERMSIG(status)); else if(WIFSTOPPED(status)) printf("child stop, signal number = %dn", WSTOPSIG(status)); } int main(int argc, char **argv) { pid_t pid = fork(); if(pid < 0 ) { printf("pid errn"); return 0; } if(pid == 0) { exit(-1); } printf("pid = %dn", pid); int status = 0; wait(&status); // 阻塞的函数 pr_exit(status); pid = fork(); if(pid < 0 ) { printf("pid errn"); return 0; } if(pid == 0) { abort(); // 信号的函数 } printf("pid = %dn", pid); status = 0; wait(&status); // 阻塞的函数 pr_exit(status); pid = fork(); if(pid < 0 ) { printf("pid errn"); return 0; } if(pid == 0) { status /= 0; } printf("pid = %dn", pid); status = 0; wait(&status); // 阻塞的函数 pr_exit(status); return 0; }
编译运行:
root@ubuntu:~/c_test/10# ./wait pid = 1997 normal exit, exit status = 255 pid = 1998 abnormal exit, signal number = 6 pid = 1999 abnormal exit, signal number = 8
这个例子把子进程中的三种状态都做了处理了。
但是这个wait函数也是有缺点的:
- 不能等待特定的子进程
- 阻塞函数,如果不存在子进程退出,只能一直阻塞
- wait函数只能获取子进程终止的事件,不能接收子进程先暂停再回复的事件。
所以linux又引入了waitpid()函数。
11.2.2 waitpid()继续,先看看函数原型:
#includepid_t waitpid(pid_t pid, int *status, int options);
功能:等待子进程终止,如果进程终止了,此函数会回收子进程资源。
来看看pid参数的意义:
| pid的值 | 说明 |
|---|---|
| pid > 0 | 等待进程ID与pid相等的子进程 |
| pid = 0 | 等待与调用进程同一个进程组的任意子进程 |
| pid = -1 | 等待任意子进程 |
| pid < -1 | 等待进程组ID与pid绝对值相等的所有子进程 |
status的参数意义跟上一节的wait是一样的。
options的参数意义:
| 参数 | 说明 |
|---|---|
| WCONTINUED | 除了关心终止进程的信息,也关心那些因收到信号而恢复执行的子进程的状态 |
| WNOHANG | 指定子进程并未发送状态变化,立刻返回,不会阻塞。 没有这个pid返回-1,并设置errno为ECHILD。如果没有子进程等待,返回0。 |
| WUNTRACE | 除了关心终止子进程信息,也关心那些因信号而停止的子进程信息 |
如果是正在返回的话,返回值也是子进程的pid。
例子:
#include#include #include #include void pr_exit(int status) { if(WIFEXITED(status)) printf("normal exit, exit status = %dn", WEXITSTATUS(status)); else if(WIFSIGNALED(status)) printf("abnormal exit, signal number = %dn", WTERMSIG(status)); else if(WIFSTOPPED(status)) printf("child stop, signal number = %dn", WSTOPSIG(status)); } int main(int argc, char** argv) { pid_t pid = -1; pid = fork(); if(pid < 0) { printf("fork errn"); return -1; } else if(pid == 0) { sleep(1); abort(); } // 父进程 int status = 0; // 非阻塞 int ret = waitpid(pid, &status, WNOHANG); printf("ret = %dn", ret); if(ret != 0) { pr_exit(status); } // 阻塞 ret = waitpid(pid, &status, 0); printf("ret = %dn", ret); if(ret != 0) { pr_exit(status); } return 0; }
这个例子测试了一下,阻塞和非阻塞。以后有机会再用其他的。
root@ubuntu:~/c_test/11# ./waitpid ret = 0 ret = 2375 abnormal exit, signal number = 6 root@ubuntu:~/c_test/11#
运行的结果,跟我们描述的差不多。
如果我们不想关心进程的终止事件,只关心进程的停止事件,好像waitpid函数也做不到,所以又引入一个新的函数,waitid()。(真的是一个接着一个)
11.2.3 waitid()还是看函数原型:
#includeint waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
不过是最后优化的函数,参数是真的多啊。
idtype跟id两个参数决定需要等待哪个进程:
| 常量 | 说明 |
|---|---|
| idtype == P_PID | 精准打击,等待进程ID等于id的进程 |
| idtype == P_PGID | 在所有子进程中等待进程组ID等于id的进程 |
| idtype == P_ALL | 等待任务子进程,id被忽略 |
options这个选项是需要位或了,(waitpid是固定的)
| 常量 | 说明 |
|---|---|
| WEXITED | 等待子进程的终止事件 |
| WSTOPPED | 等待被信号暂停的子进程事件 |
| WCONTINUED | 等待进程也暂停,然后再恢复执行的子进程 |
| WNOHANG | 指定子进程并未发送状态变化,立刻返回,不会阻塞。 没有这个pid返回-1,并设置errno为ECHILD。如果没有子进程等待,返回0。 |
| WNOWAIT | 不破坏子进程退出状态,只负责获取信息,之后可以由wait、waitpid、waitid调用取得 |
看一下第三个参数infop,这个参数是输出参数:
typedef struct
{
int si_signo;
int si_errno;
int si_code;
__pid_t si_pid;
__uid_t si_uid;
void *si_addr;
int si_status;
long int si_band;
__sigval_t si_value;
} siginfo_t;
因为是接收子进程的信号,所以si_signo=SIGCHLD.
si_code的意义:
| 常量 | 说明 |
|---|---|
| CLD_EXIT | 子进程正常退出 |
| CLD_KILLED | 子进程被信号杀死 |
| CLD_DUMPED | 子进程被信号杀死,并产生了core dump |
| CLD_STOPPED | 子进程被信号暂停 |
| CLD_CONTINUED | 子进程被SIGCONT信号恢复 |
| CLD_TRAPPED | 子进程被跟踪 |
si_status跟之前的wait()和waitpid()意义相同。
返回值:
成功等到子进程的变化,并取回响应的信息,返回0,但是si_pid返回是子进程的pid.
设置了WNOHANG标志位,并且子进程状态无变化,也返回0,但是si_pid也是0.
搞一个例子:
#include#include #include #include #include int main(int argc, char **argv) { pid_t pid = 0; pid = fork(); if(pid < 0) { printf("fork errn"); return 0; } if(pid == 0) { // 子进程 sleep(10); exit(-1); } // siginfo_t info; memset(&info, 0, sizeof(siginfo_t)); int ret = waitid(P_ALL, pid, &info, WEXITED | WNOHANG); // WNOHANG只填这个标记好像不行 printf("ret = %dn", ret); if(ret != 0) { perror("waitpid"); return 0; } if(info.si_pid == 0) { // 子进程没有发生变化 printf("info.si_pid = %dn", info.si_pid); } else { // 子进程发现了变化 printf("info.si_pid = %dn", info.si_pid); } memset(&info, 0, sizeof(siginfo_t)); ret = waitid(P_PID, pid, &info, WEXITED); printf("ret = %dn", ret); if(ret != 0) { printf("waitid errn"); return 0; } if(info.si_pid == 0) { // 子进程没有发生变化 printf("info.si_pid = %dn", info.si_pid); } else { // 子进程发现了变化 printf("info.si_pid = %dn", info.si_pid); } return 0; }
编译运行结果:
root@ubuntu:~/c_test/11# gcc waitid.c -o waitid root@ubuntu:~/c_test/11# ./waitid ret = 0 info.si_pid = 0 ret = 0 info.si_pid = 1564
都是正常反应。
11.2.4 wait4()这个函数就不做过多介绍了,这是一个系统调用,上面的wait、waitpid、waitid都是通过调用wait4这个函数来操作的,我们现在的目标先不研究内核,内核的东西太多了。
看看函数原型的就可以了:
#include11.2.5 僵尸进程#include #include #include pid_t wait4(pid_t pid, int *status, int options, struct rusage *rusage);
我们都知道进程是会终止的,终止的时候,内核会负责回收一部分资源,仍会保留子进程的pid,子进程的退出状态,就是上面我们说了好多的wait函数,就是获取子进程退出状态,并且回收剩下的资源。
但是有时候我们编程的时候,忘记调用了wait系列函数,这时候进程的一些资源得不到释放,就造成了这时候的进程处在了一个僵尸状态。(上一节讲进程状态的时候,有讲)
产生僵尸进程也很简单:
#include#include int main(int argc, char **argv) { pid_t pid = 0; pid = fork(); if(pid < 0) { printf("fork errn"); } else if(pid == 0) { exit(0); // 退出子进程 } else { sleep(300); // 这个300秒钟,子进程就是僵尸状态 wait(NULL); } return 0; }
那怎么查看这个进程是不是僵死进程,就利用到上一节讲的查看进程的状态了。
root 1690 0.0 0.0 4220 784 pts/1 S+ 18:04 0:00 ./jiangshi root 1691 0.0 0.0 0 0 pts/1 Z+ 18:04 0:00 [jiangshi]root 1692 0.0 0.3 37364 3340 pts/2 R+ 18:04 0:00 ps aux
Z的状态就是僵尸状态。
如果真的不需要回收子进程的信号,可以通过设置信号,忽略这个子进程的信号。
11.2.6 孤儿进程竟然都有忘记回收,那也有可能父进程先结束了,子进程还在,这时候的子进程被称为孤儿进程。出现孤儿进程的时候,内核是怎么处理的呢?
内核还是很人性化的,每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为init,而init进程会循环地wait()它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了自己的时候,init进程会代表政府出面处理它的一切善后工作。
写个例子:
#include#include int main(int argc, char **argv) { pid_t pid = 0; pid = fork(); if(pid < 0) { printf("fork errn"); } else if(pid == 0) { sleep(300); // 父进程先退出 exit(0); } else { exit(0); // } return 0; }
这个也比较简单,父进程就直接退出,太不负责任了。
我们用ps -ef来查看一下,就得出ppid=1就是init进程。
root 1752 1 0 18:14 pts/1 00:00:00 ./guer11.3 进程替换
还记得这篇文章写的重学计算机(六、程序是怎么运行的)execve函数么,没错终于到了exec函数家族的出现了,在程序是怎么运行的这篇execve函数只要是把程序中各个段加载到内存中,最后执行程序。
现在我们就来好好了解一波。
11.3.1 execve函数我们还是先来看看函数原型:
#includeint execve(const char *filename, char *const argv[], char *const envp);
其实这个函数才是系统调用,exec家族中其他函数都是glibc封装的。
现在介绍一下参数,这些参数后面都会提到:
- const char *filename:程序文件名。好像是需要绝对路径和相对于当前工作目录的相对路径
- char *const argv[]:这个变量是不是很熟悉,就是我们main函数中的参数。
- char *const envp:最后一个是环境变量,我们之前写代码,是不是可以直接用环境变量,就是这里传参的,但是这个需要我们传入环境变量,感觉还是不是很方便。
复制过来的,哈哈哈。这几个参数,在我们详细讲main函数的时候,就会明白了。
这个execve函数有一个特点,就是执行成功之后,就不返回了,返回的话就一定是失败。这个也是我都把父进程的内存那些东西替换成自己的了,执行成功也就不必要返回了。
我们来看看常用的返回的错误码:
| 错误码 | 说明 |
|---|---|
| EACCESS | filename不是个普通文件,或者没有执行权限,目录不可搜索。 |
| ENOENT | 文件不存在 |
| ETXTBSY | 存在其他进程尝试修改filename所指向文件 |
| ENOEXEC | 文件存在,无法执行。比如文件格式不对 |
glibc在内核的基础上有进行了一次封装,glibc真的是贴心啊,内核提供的参数比较多,特别是环境变量,真难受。是不是这里就想到main函数不是也没有环境变量么?其实main函数还真有环境变量,只不过是不写就默认有,这个等到讲到main函数的时候就明白了。
我们来看看剩下的6个兄弟函数:
#includeextern char **environ; 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 *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[]);
总结一个这6个函数的特点:
| 函数名 | 参数格式 | 是否自动搜索path | 是否使用当前环境变量 |
|---|---|---|---|
| execl | 列表 | 否 | 是 |
| execlp | 列表 | 是 | 是 |
| execle | 列表 | 否 | 否 |
| execv | 数组 | 否 | 是 |
| execvp | 数组 | 是 | 是 |
| execve | 数组 | 不是 | 否 |
写个例子来试试吧:
#include11.3.3 exec简单实现#include char *const ps_argv[] = {"ps", "-ax", NULL}; char *const ps_envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL}; int main() { // ps execl("/bin/ps", "ps", "-ax", NULL); // 第一个argv是自己,NULL是argv的结束 execlp("ps", "ps", "-ax", NULL); // 这个自己搜索path的 execle("/bin/ps", "ps", "-ax", NULL, ps_envp); // 带e环境变量自己拼 // 参数都是数组了 execv("/bin/ps", ps_argv); execvp("ps", ps_argv); //自己搜索path execve("/bin/ps", ps_argv, ps_envp); return 0; }
这个简单实现,其实在程序是怎么运行的,也应该讲过了,基本是差不多的。
我们知道linux下可以执行很多种程序,比如elf格式的,shell,python,还有java程序,那我们的linux系统是怎么知道这些格式的?
没错,就是一个文件的头信息,我记得当初有分析过hex,bin,还有elf文件,每种格式文件都有自己的头信息,所以linux就是根据这个头信息来执行的。
比如ELF的头就是0x7f、‘e’、‘l’、‘f’,java的可执行文件格式的头4个字节为’c’、‘a’、‘f’、‘e’,解释型语言,第一行就是"#!/bin/sh"或"#!/usr/bin/prel"或"#!/usr/bin/python"。
竟然头信息都找到,那接下来就,根据文件的类型,去匹配不同的加载器。不同的加载器处理不同的可执行文件。
如果是ELF文件,就可以回到这一篇重学计算机(六、程序是怎么运行的)
11.3.4 执行exec之后属性我们之前都说执行了exec函数之后,就会抛弃原来的东西,那有没有什么是没有抛弃,继承下来的呢?
继承过来的属性:
| 属性 | 属性 |
|---|---|
| 进程ID | 根目录 |
| 父进程ID | 文件模式创建掩码 |
| 进程组ID | 文件锁和记录锁 |
| 会话ID | 进程信号屏蔽 |
| 控制终端 | 进程挂起的信号 |
| 真实用户ID | 已用的时间 |
| 真实组ID | 资源限制 |
| 附加组ID | nice值 |
| 告警剩余时间 | semadj值 |
| 当前工作目录 |
进程挂起信号:子进程会将挂起信号初始化为空。
信号量调整semadj:子进程不继承父进程的改值。
记录锁(fcntl):子进程不继承父进程的记录锁。文件锁flock子进程是继承的
已用时间times:子进程将该值初始化为0.
11.3.5 system函数system其实是fork exec waitpid三个函数的集合,glibc又给我们封装出来的一个函数,方便我们调用命令。
也正是因为方便了,导致这个system这个返回值,让我很难受,system返回值,是三个系统调用的返回值凑一起的,所以很难受。
先来看看函数原型:
#includeint system(const char* command);
我们自己实现一个system函数,(因为没讲信号,所以暂时忽略信号的处理,等到信号的部分,在加进来)
#include#include #include int system(const char *command) { pid_t pid; int status; // 先判断参数 if(command == NULL) { return 1; } pid = fork(); if(pid < 0) { status = -1; // fork失败返回-1 } else if(pid == 0) { execl("/bin/sh", "sh", "-c", command, NULL); _exit(127); // 如果execl执行的有问题,返回127 } else { // 父进程负责回收 while(waitpid(pid, &status, 0) < 0) { if(errno != EINTR) // 如果不是系统调用中断的,所有有问题 { status = -1; break; } } } return status; }
考虑了一下,发现这个system函数还是不完全的,就不做分析了,等到信号的时候,写一个全面的system函数再分析吧。
11.4 总结这一篇讲的内容很多,其实可以每一节都分出来单独做一篇,但想想后面的东西还有很多,就凑一起了,不分开了。



