目录

一、进程创建

1.1 fork函数

1.2 fork函数返回值

1.3 写时拷贝

1.4 fork常规用法

1.5 fork调用失败的原因

二、进程终止

2.1 进程退出场景

2.2 进程常见退出方法

2.2.1 退出码

2.2.2 strerror函数

2.2.3 errno

2.2.4 异常信号

2.2.5 _exit函数

2.2.6 exit函数

2.2.7 return退出

三、进程等待

3.1 进程等待的必要性

3.2 进程等待的方法

3.2.1 wait方法

3.2.2 waitpid方法

3.2.3 获取子进程status

3.2.4 阻塞与非阻塞等待

四、进程替换

4.1 替换原理

4.2 替换函数

4.2.1 函数解释

4.2.2 命名理解


一、进程创建

1.1 fork函数

fork函数是非常重要的函数,它从已经存在的进程中创建一个新进程。新进程为子进程,原进程为父进程。

#include <unistd.h>

pid_t  fork(void);

返回值:自进程中返回0,父进程返回子进程id,出错返回-1

进程调用fork,当控制转移到内核中的fork代码后,内核做:

  • 分配新的内存块和内核数据结构给子进程
  • 将父进程部分数据结构内容拷贝到子进程
  • 添加子进程到系统进程列表中
  • fork返回,开始调度器调度

当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但是每个进程都将可以开始它们自己的旅程。

int main ( void )
{
        pid_t pid;
        printf ( "Before: pid is %d\n" , getpid());
        if ( (pid=fork()) == -1 )perror( "fork()" ), exit ( 1 );
        printf ( "After:pid is %d, fork return %d\n" , getpid(), pid);
                sleep( 1 );
        return 0 ;
}

运行结果

Before: pid is 43676
After:pid is 43676 , fork return 43677
After:pid is 43677 , fork return 0

这里我们看到三行输出。我们注意到进程43667没有打印before,这是为什么?如下图所示

所以fork之前父进程独立执行,fork之后父子两个执行流分别执行。注意fork之后执行的顺序完全由调度器完成

1.2 fork函数返回值

子进程返回0

父进程返回子进程的pid

1.3 写时拷贝

通常情况下,父子进程代码共享,父子在不写入时,数据也是共享的。当任意一方试图写入时,便以写时拷贝的方式各自一份副本。具体如下图

这里详细解释一下这张图片

父子进程默认对数据是不可被写入的(只读),原因是无论是父进程还是子进程的页表映射到代码区的部分,页表的权限是只读的(哪怕之前父进程页表是可写的,这里也会处理成可读)。修改内容之前,父子指向的物理内存页相同。修改内容之后(这里是子进程修改),会触发系统层面的权限问题,这种情况系统并不会进行异常处理,而是将数据拷贝一份,哪个进程写入的就会把它的页表重新映射,然后将两个进程之间可读的权限给去掉,这样就可以访问了。

因为有写时拷贝技术的存在,所以父子进程得以彻彻底底的分离,完成了进程独立性的技术保证!

写时拷贝是一种延时申请技术,可以提高整机的内存使用率。

1.4 fork常规用法

  • 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求
  • 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数

1.5 fork调用失败的原因

  • 系统中有太多的进程
  • 实际用户的进程数超过了限制

二、进程终止

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

2.1 进程退出场景

  • 代码运行完毕,结果正确
  • 代码运行完毕,结果不正确
  • 代码异常终止

2.2 进程常见退出方法

正常终止(可以通过 echo $? 查看进程退出码):

  1. 从main返回
  2. 调用exit
  3. _exit

异常退出:

  • ctrl + c ,信号终止

2.2.1 退出码

退出码(退出状态)可以告诉我们最后一次执行命令的状态。在命令结束以后,我们可以知道命令是成功完成的还是以错误结束的。其基本思想是,程序返回退出代码 0 时表示执行成功。代码 1 或 0 以外的任何代码都被视为不成功。

Linux Shell 中的主要退出码:

退出码 解释
0 命令执行成功
1 通用错误代码
2 命令(或参数)使用不当
126 权限被拒绝(或)无法执行
127 未找到命令,或 PATH 错误
128+n 命令被信号从外部终止,或遇到致命错误
130 通过 Ctrl + C 或 SIGINT 终止(终止代码2或键盘中断)
143 通过 SIGTERM 终止(默认终止)
255/* 退出码超过了 0-255 的范围,因此重新计算(LCTT译注:超过255后,用退出码取模)
  • 退出码 0 表示命令执行⽆误,这是完成命令的理想状态。
  • 退出码 1 我们也可以将其解释为 “不被允许的操作”。例如在没有 sudo 权限的情况下使用 yum;再例如除以 0 等操作也会返回错误码 1 ,对应的命令为 let a=1/0
  • 130 ( SIGINT ^C )和 143 SIGTERM )等终止信号是非常典型的,它们属于128+n 信号,其中 n 代表终止码。
  • 可以使用 strerror 函数来获取退出码对应的描述。

2.2.2 strerror函数

#include <string.h>

char * strerror ( int errnum );

参数:错误码

我们以前10条错误信息来演示

#include <stdio.h>      
#include <string.h>      
      
int main()      
{      
    for(int i = 0; i < 10; i++)      
    {      
        printf("%d:%s\n", i, strerror(i));    
    }    
    
    return 0;                                                                                               
}      

  

运行结果

0 : Success
1:Operation not permitted
2:No such file or directory
3:No such process
4:Interrupted system call
5:Input/output error
6:No such device or address
7:Argument list too long
8:Exec format error
9:Bad file descriptor

2.2.3 errno

errno是linux系统编程提供的全局整型变量,包含在头文件<errno.h>中,存储系统提供的api函数最后一次出错的错误码。

进程通过main函数的返回值来判断错误,对于一般的函数可以使用errno来查看它们的执行错误。

举一个例子

#include <stdio.h>    
#include <string.h>    
#include <errno.h>    
    
void func()    
{    
    errno = 10;    
}    
    
int main()    
{    
    func();    
    
    printf("%d:%s\n", errno, strerror(errno));    
                                                                                                            
    return 0;    
}    


运行结果
10:No child processes

2.2.4 异常信号

进程出现异常本质是进程收到了异常信号。

比如你出现了除以0的错误,CUP当中会出现溢出。再比如野指针错误,这时你去访问虚拟地,但是这个虚拟地址在页表当中并未建立映射关系或者页表中的权限为只读。这些都会转化成硬件问题,这些问题都会被操作系统识别进而操作系统向对应进程发信号。

我们可以通过 kill - l 来查看异常信号:

如果想要对一个进程发送异常信号,只需要kill -xxx ### 即可向pid为###的进程发送xxx号信号。

2.2.5 _exit函数

#include <unistd.h>

void _exit(int status);

参数:status 定义了进程的终止状态,父进程通过wait来获取该值

  • 说明:虽然stauts是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行$?发现返回值是255

2.2.6 exit函数

#include <unistd.h>

void exit(int status);

exit最后也会调用_exit,但是在调用之前,还做了其他工作:

  1. 执行用户通过 atexit 或 on_exit定义的清理函数
  2. 关闭所有打开的流,所有的缓存数据均被写入
  3. 调用_exit

_exit是系统调用接口,而exitman的第三页,是用户操作接口。exit会刷新缓冲区,而_exit不会刷新缓冲区。exit中封装了_exit+刷新缓冲区两个功能。

2.2.7 return退出

return是一种更常见的退出进程方式。执行return n等同于执行exit(n),因为调用mian的运行时函数会将main的返回值当做 exit 的参数

三、进程等待

3.1 进程等待的必要性

子进程退出,如果父进程不回收,就可能造成“僵尸进程”,进而导致内存泄漏。并且父进程派给子进程的任务完成的情况,我们也需要知道。比如子进程运行完成后结果对不对。父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。

3.2 进程等待的方法

3.2.1 wait方法

#include<sys/types.h>

#include<sys/wait.h>

pid_t wait(int* status);

返回值:

        成功返回被等待进程pid,失败返回-1

参数:

        输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

3.2.2 waitpid方法

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

返回值:

        当正常返回的时候waitpid返回收集到的⼦进程的进程ID

        如果设置了选项WNOHANG,⽽调⽤中waitpid发现没有已退出的⼦进程可收集,则返回0

        如果调⽤中出错,则返回-1,这时errno会被设置成相应的值以指⽰错误所在;

参数:

        pid:

                Pid=-1,等待任⼀个⼦进程。与wait等效。

                Pid>0.等待其进程IDpid相等的⼦进程。

        status: 输出型参数

                WIFEXITED(status): 若为正常终⽌⼦进程返回的状态,则为真。(查看进程

是否是正常退出)

                WEXITSTATUS(status): 若WIFEXITED⾮零,提取⼦进程退出码。(查看进程

的退出码)

options:默认为0,表示阻塞等待

                WNOHANG: 若pid指定的⼦进程没有结束,则waitpid()函数返回0,不予以等

待。若正常结束,则返回该⼦进程的ID

  • 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
  • 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
  • 如果不存在该子进程,则立即出错返回。

3.2.3 获取子进程status

  • wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充
  • 如果传递NULL,表示不关心子进程的退出状态信息。否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程
  • status不能简单的当作整型来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):

# include <sys/wait.h>
# include <stdio.h>
# include <stdlib.h>
# include <string.h>
# include <errno.h>
int main ( void )
{
        pid_t pid;
        if ( (pid=fork()) == -1 )
              perror( "fork" ), exit ( 1 );
        if ( pid == 0 ){
              sleep( 20 );
              exit ( 10 );
        } else {
              int st;
              int ret = wait(&st);
              if ( ret > 0 && ( st & 0X7F ) == 0 ){ // 正常退出
                   printf ( "child exit code:%d\n" , (st>> 8 )& 0XFF );
              } else if ( ret > 0 ) { // 异常退出
                   printf ( "sig code : %d\n" , st& 0X7F );
              }
        }
}

测试结果:

# ./a.out #20秒退出

child exit code:10

# ./a.out #在其他终端kill

sig code : 9

3.2.4 阻塞与非阻塞等待

  • 进程的阻塞等待方式
int main()
{
    pid_t pid;
    pid = fork();
    if(pid < 0)
    {
        printf("%s fork error\n",__FUNCTION__);
        return 1;
    }
    else if( pid == 0 )    //child
    {
        printf("child is run, pid is : %d\n",getpid());
        sleep(5);
        exit(257);
    }
    else
    {
        int status = 0;
        pid_t ret = waitpid(-1, &status, 0);//阻塞式等待,等待5S
        printf("this is test for wait\n");
        if( WIFEXITED(status) && ret == pid )
        {
            printf("wait child 5s success, child return code is
                   :%d.\n",WEXITSTATUS(status));
        }
        else
        {
            printf("wait child failed, return.\n");
            return 1;
        }
    }
    return 0;
}
运⾏结果 :
child is run, pid is : 45110
this is test for wait
wait child 5 s success, child return code is : 1.
  • 进程的非阻塞等待方式

#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
#include <vector>
typedef void (*handler_t)(); // 函数指针类型
std::vector<handler_t> handlers; // 函数指针数组
void fun_one()
{
    printf("这是⼀个临时任务1\n");
}
void fun_two()
{
    printf("这是⼀个临时任务2\n");
}
void Load()
{
    handlers.push_back(fun_one);
    handlers.push_back(fun_two);

}
void handler()
{
    if (handlers.empty())
        Load();
    for (auto iter : handlers)
        iter();
}
int main()
{
    pid_t pid;
    pid = fork();
    if (pid < 0)
    {
        printf("%s fork error\n", __FUNCTION__);
        return 1;
    }
    else if (pid == 0)     // child
    {
        printf("child is run, pid is : %d\n", getpid());
        sleep(5);
        exit(1);
    }
    else
    {
        int status = 0;
        pid_t ret = 0;
        do
        {
            ret = waitpid(-1, &status, WNOHANG); // ⾮阻塞式等待
            if (ret == 0)
            {
                printf("child is running\n");
            }
            handler();
        }
        while (ret == 0);
        if (WIFEXITED(status) && ret == pid)
        {
            printf("wait child 5s success, child return code is :%d.\n",
                   WEXITSTATUS(status));
        }
        else
        {
            printf("wait child failed, return.\n");
            return 1;
        }
    }
    return 0;
}
 

四、进程替换

fork()之后,父子各自执行父进程代码的一部分,当子进程想要执行一个全新的程序,这个时候就需要进程的程序替换来完成这个功能!

程序替换是通过特定的接口,加载磁盘上的一个全新的程序(代码和数据),加载到调用进程的地址空间中

4.1 替换原理

用fork创建子进程后执行的是和父进程相同的程序(但是可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变

4.2 替换函数

有六种以exec开头的函数,统称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[]);

4.2.1 函数解释

  • 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回
  • 如果调用出错则返回-1
  • 所以exec函数只有出错的返回值而没有成功的返回值

4.2.2 命名理解

  • l(list) : 表示参数采用列表
  • v(vector) : 参数用数组
  • p(path) : 有 p 自动搜索环境变量 PATH
  • e(env) : 表示自己维护环境变量

exec调用举例如下

#include <unistd.h>
int main()
{
    char *const argv[] = {"ps", "-ef", NULL};
    char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
    execl("/bin/ps", "ps", "-ef", NULL);
// 带p的,可以使⽤环境变量PATH,⽆需写全路径
    execlp("ps", "ps", "-ef", NULL);
// 带e的,需要⾃⼰组装环境变量
    execle("ps", "ps", "-ef", NULL, envp);
    execv("/bin/ps", argv);
// 带p的,可以使⽤环境变量PATH,⽆需写全路径
    execvp("ps", argv);
// 带e的,需要⾃⼰组装环境变量
    execve("/bin/ps", argv, envp);

    exit(0);
}

事实上,只有execve是真正的系统调用,其它五个函数最终都被调用execve,所以execve在man手册第二节,其他函数在man手册第三节。这些函数之间的关系如下图所示

下图exec函数簇一个完整的例子:

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐