重学计算机(十一、进程终止、回收、替换)

本文详细探讨了进程的终止方式,包括return、exit、_exit和abort,以及异常退出条件。重点介绍了_exit和exit函数的用法,以及如何通过wait系列函数回收子进程状态。此外,文章深入剖析了进程替换,讲解了execve、execl、execlp等函数及其应用,涉及系统调用和资源继承。
摘要由CSDN通过智能技术生成

2022年,卷的第二篇,这一篇主要是描述进程的终止,回收,替换。内容是比较多。

11.1 进程终止

进程既然有创建,那肯定是有终止的,都是存在一个生命周期的。

在不考虑线程的情况下,进程的退出有以下5种方式:

  1. 在main函数内执行return语句。
  2. 调用exit函数。
  3. 调用_exit或_Exit函数。

异常退出条件有两种:

  1. 调用abort
  2. 当进程接收到某些信号时。

正常退出的我们来学习一下,异常退出等到信号的时候再说。

11.1.1 _exit函数

还是先来看看函数的原型:

#include <unistd.h>
void _exit(int status);

一看这个头文件就是linux系统提供的函数了,调用这个函数直接进入内核态,进程程序的退出。

如果我们需要返回子进程的状态给父进程,就需要用到status这个变量。(父进程怎么接收,下面介绍)

虽然这个status是int类型,但是其实只有低8位有效,也就是一个无符号字符型,这个我们等下写一个例子,返回-1,来看看是个什么值就明白了。

#include <unistd.h>

int 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函数

我们还是来继续看函数原型:

#include <stdlib.h>
void exit(int status);

这个函数一看就是glic中的函数,也的确是这样,exit是对_exit函数的一个封装。封装的内核有:

  1. 执行用户通过调用atexit函数或on_exit定义的清理函数
  2. 关闭所有打开的流,所有缓冲区的数据均被写入(flush),通过tmpfile创建的临时文件都会被删除。(如果进程占用内存不释放,或者打开文件,不关闭文件,这里都是可以帮忙释放)
  3. 调用_exit。

看着exit函数这么好,是不是就没有缺点了呢?

其实是有的一定的局限性的:当进程正常退出时,会调用C库的exit;而当程序或被kill掉时,c库的exit则不会被调用,只会执行内核退出进程的操作。

下面我们试试冲刷的例子:

// _exit例子
#include <unistd.h>
#include <stdio.h>

int main(int argc, char **argv)
{

    printf("hello world");
    _exit(126);
}
// exit例子
#include <stdlib.h>
#include <stdio.h>

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函数

既然上面都说了这个绑定回调的函数,我们就来试一波这个函数吧。

#include <stdlib.h>
int atexit(void (*func)(void));

写一个例子测试一波就可以了:

#include <stdlib.h>
#include <stdio.h>

void exit1()
{
    printf("exit1\n");
}

void exit2()
{
    printf("exit2\n");
}

int main(int argc, char **argv)
{
    atexit(exit1);
    atexit(exit2);

    printf("main return\n");

    return 0;
}

都是比较简单的例子,编译执行:

root@ubuntu:~/c_test/10# ./atexit
main return
exit2
exit1
root@ubuntu:~/c_test/10#

有点想栈,先绑定的,后执行。

11.2 等待子进程

上面我们已经介绍了进程的终止,但是进程终止之后,父进程是不是需要知道子进程是怎么终止的么?

比如是正常的提出,或者是被信号终止的。所以linux系统提供了等待回收子进程的函数。

11.2.1 wait()

先来看看函数原型:

#include <sys/wait.h>
pid_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 <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void pr_exit(int status)
{
    if(WIFEXITED(status))
        printf("normal exit, exit status = %d\n", WEXITSTATUS(status));
    else if(WIFSIGNALED(status))
        printf("abnormal exit, signal number = %d\n", WTERMSIG(status));
    else if(WIFSTOPPED(status))
        printf("child stop, signal number = %d\n", WSTOPSIG(status));
}

int main(int argc, char **argv)
{
    pid_t pid = fork();
    if(pid < 0 )
    {
        printf("pid err\n");
        return 0;
    }

    if(pid == 0)
    {
        exit(-1);
    } 

    printf("pid = %d\n", pid);
    int status = 0;
    wait(&status);  // 阻塞的函数
    pr_exit(status);



    pid = fork();
    if(pid < 0 )
    {
        printf("pid err\n");
        return 0;
    }

    if(pid == 0)
    {
        abort();        // 信号的函数
    } 

    printf("pid = %d\n", pid);
    status = 0;
    wait(&status);  // 阻塞的函数
    pr_exit(status);


    pid = fork();
    if(pid < 0 )
    {
        printf("pid err\n");
        return 0;
    }

    if(pid == 0)
    {
        status /= 0;
    } 

    printf("pid = %d\n", 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函数也是有缺点的:

  1. 不能等待特定的子进程
  2. 阻塞函数,如果不存在子进程退出,只能一直阻塞
  3. wait函数只能获取子进程终止的事件,不能接收子进程先暂停再回复的事件。

所以linux又引入了waitpid()函数。

11.2.2 waitpid()

继续,先看看函数原型:

#include <sys/wait.h>
pid_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 <sys/wait.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

void pr_exit(int status)
{
    if(WIFEXITED(status))
        printf("normal exit, exit status = %d\n", WEXITSTATUS(status));
    else if(WIFSIGNALED(status))
        printf("abnormal exit, signal number = %d\n", WTERMSIG(status));
    else if(WIFSTOPPED(status))
        printf("child stop, signal number = %d\n", WSTOPSIG(status));
}

int main(int argc, char** argv)
{
    pid_t pid = -1;
    pid = fork();

    if(pid < 0)
    {
        printf("fork err\n");
        return -1;
    } else if(pid == 0)
    {
        sleep(1);
        abort();
    }

    // 父进程
    int status = 0;
    // 非阻塞
    int ret = waitpid(pid, &status, WNOHANG);
    printf("ret = %d\n", ret);
    if(ret != 0)
    {
        pr_exit(status);
    }

    // 阻塞
    ret = waitpid(pid, &status, 0);
    printf("ret = %d\n", 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()

还是看函数原型:

#include <sys/wait.h>
int 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;		/* Signal number.  */
    int si_errno;		/* If non-zero, an errno value associated with
				   this signal, as defined in <errno.h>.  */
    int si_code;		/* Signal code.  */
    __pid_t si_pid;		/* Sending process ID.  */
    __uid_t si_uid;		/* Real user ID of sending process.  */
    void *si_addr;		/* Address of faulting instruction.  */
    int si_status;		/* Exit value or signal.  */
    long int si_band;		/* Band event for SIGPOLL.  */
    __sigval_t si_value;	/* Signal 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 <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main(int argc, char **argv)
{

    pid_t pid = 0;
    pid = fork();
    if(pid < 0)
    {
        printf("fork err\n");
        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 = %d\n", ret);
    if(ret != 0)
    {
        perror("waitpid");
        return 0;
    }

    if(info.si_pid == 0)
    {
        // 子进程没有发生变化
        printf("info.si_pid = %d\n", info.si_pid);
    } else {
        // 子进程发现了变化
        printf("info.si_pid = %d\n", info.si_pid);
    }




    memset(&info, 0, sizeof(siginfo_t));
    ret = waitid(P_PID, pid, &info, WEXITED);
    printf("ret = %d\n", ret);
    if(ret != 0)
    {
        printf("waitid err\n");
        return 0;
    }

    if(info.si_pid == 0)
    {
        // 子进程没有发生变化
        printf("info.si_pid = %d\n", info.si_pid);
    } else {
        // 子进程发现了变化
        printf("info.si_pid = %d\n", 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这个函数来操作的,我们现在的目标先不研究内核,内核的东西太多了。

看看函数原型的就可以了:

#include <sys/types.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <sys/resource.h>

pid_t wait4(pid_t pid, int *status, int options, struct rusage *rusage);

11.2.5 僵尸进程

我们都知道进程是会终止的,终止的时候,内核会负责回收一部分资源,仍会保留子进程的pid,子进程的退出状态,就是上面我们说了好多的wait函数,就是获取子进程退出状态,并且回收剩下的资源。

但是有时候我们编程的时候,忘记调用了wait系列函数,这时候进程的一些资源得不到释放,就造成了这时候的进程处在了一个僵尸状态。(上一节讲进程状态的时候,有讲)

产生僵尸进程也很简单:

#include <stdio.h>
#include <unistd.h>

int main(int argc, char **argv)
{
    pid_t pid = 0;
    pid = fork();

    if(pid < 0)
    {
        printf("fork err\n");
    } 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] <defunct>
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 <stdio.h>
#include <unistd.h>

int main(int argc, char **argv)
{
    pid_t pid = 0;
    pid = fork();

    if(pid < 0)
    {
        printf("fork err\n");
    } 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 ./guer

11.3 进程替换

还记得这篇文章写的重学计算机(六、程序是怎么运行的)execve函数么,没错终于到了exec函数家族的出现了,在程序是怎么运行的这篇execve函数只要是把程序中各个段加载到内存中,最后执行程序。

现在我们就来好好了解一波。

11.3.1 execve函数

我们还是先来看看函数原型:

#include <unistd.h>
int execve(const char *filename, char *const argv[], char *const envp);

其实这个函数才是系统调用,exec家族中其他函数都是glibc封装的。

现在介绍一下参数,这些参数后面都会提到:

  • const char *filename:程序文件名。好像是需要绝对路径和相对于当前工作目录的相对路径
  • char *const argv[]:这个变量是不是很熟悉,就是我们main函数中的参数。
  • char *const envp:最后一个是环境变量,我们之前写代码,是不是可以直接用环境变量,就是这里传参的,但是这个需要我们传入环境变量,感觉还是不是很方便。

复制过来的,哈哈哈。这几个参数,在我们详细讲main函数的时候,就会明白了。

这个execve函数有一个特点,就是执行成功之后,就不返回了,返回的话就一定是失败。这个也是我都把父进程的内存那些东西替换成自己的了,执行成功也就不必要返回了。

我们来看看常用的返回的错误码:

错误码说明
EACCESSfilename不是个普通文件,或者没有执行权限,目录不可搜索。
ENOENT文件不存在
ETXTBSY存在其他进程尝试修改filename所指向文件
ENOEXEC文件存在,无法执行。比如文件格式不对

11.3.2 exec家族

glibc在内核的基础上有进行了一次封装,glibc真的是贴心啊,内核提供的参数比较多,特别是环境变量,真难受。是不是这里就想到main函数不是也没有环境变量么?其实main函数还真有环境变量,只不过是不写就默认有,这个等到讲到main函数的时候就明白了。

我们来看看剩下的6个兄弟函数:

#include <unistd.h>
extern 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数组不是

写个例子来试试吧:

#include <unistd.h>
#include <stdio.h>

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;
}

11.3.3 exec简单实现

这个简单实现,其实在程序是怎么运行的,也应该讲过了,基本是差不多的。

我们知道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资源限制
附加组IDnice值
告警剩余时间semadj值
当前工作目录

进程挂起信号:子进程会将挂起信号初始化为空。

信号量调整semadj:子进程不继承父进程的改值。

记录锁(fcntl):子进程不继承父进程的记录锁。文件锁flock子进程是继承的

已用时间times:子进程将该值初始化为0.

11.3.5 system函数

system其实是fork exec waitpid三个函数的集合,glibc又给我们封装出来的一个函数,方便我们调用命令。

也正是因为方便了,导致这个system这个返回值,让我很难受,system返回值,是三个系统调用的返回值凑一起的,所以很难受。

先来看看函数原型:

#include <stdlib.h>
int system(const char* command);

我们自己实现一个system函数,(因为没讲信号,所以暂时忽略信号的处理,等到信号的部分,在加进来)

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

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 总结

这一篇讲的内容很多,其实可以每一节都分出来单独做一篇,但想想后面的东西还有很多,就凑一起了,不分开了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值