目录

一、进程创建

1.1 初识fork

1.2 函数返回值

1.3 写时拷贝技术

1.4 fork函数的使用场景

1.5 fork函数的失败原因

二、进程终止

2.1 进程退出场景

2.2 进程退出码

2.3 进程正常退出方法

2.3.1 exit函数

2.3.2 _exit函数

2.3.3 return方法

2.3.4 方法分析对比

2.4 进程异常退出

三、进程等待

3.1 进程等待的意义

3.2 获取子进程status

3.3 进程等待方法

3.3.1 wait()方法

3.3.2 waitpid()方法

3.4 多进程创建以及等待的代码模型

3.5 基于非阻塞接口的轮询检测方案

四、进程程序替换

4.1 替换原理

4.2 替换函数

4.3 命名理解


一、进程创建

1.1 初识fork

在Linux操作系统中存在一个fork()函数,其是系统调用接口,用于创建子进程。

进程调用fork,当控制转移到内核中的fork代码后,内核会执行以下工作:

  • 分配新的内存块和内核数据结构给子进程
  • 将父进程部分数据结构内容拷贝至子进程
  • 添加子进程到系统进程列表当中
  • fork返回,开始调度器调度
#include <stdio.h>
#include <unistd.h>
int main()
{
    pid_t id = fork();
    printf("PID:%d PPID:%d\n",getpid(),getppid());                                                                                                                             
    return 0;
}

通过上面的代码运行结果不难看出,通过fork()函数确实可以创建出子进程。在fork函数被调用之前的代码被父进程执行,而fork函数之后的代码则默认情况下父子进程都可以执行,此时父子进程代码数据共享,只有当需要修改时才会发生写时拷贝

注意: 父子进程的CPU调度顺序是不确定的,具体情况取决于操作系统调度算法的实现。

1.2 函数返回值

返回值:在子进程中返回0,父进程中返回子进程的PID,子进程创建失败返回-1

上面说到父子进程共享数据代码,但是让父子进程去做同样的事情并没有什么意义,这里可以使用if进行分流操作使得父子进程完成不同的工作。

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>                                                                                                                                                            

int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        printf("fork error!!!\n");
        return 0;
    }
    else if(id == 0)
    {
        printf("子进程 PID:%d PPID:%d\n",getpid(),getppid());
    }
    else//id > 0 
    {
        printf("父进程 PID:%d PPID:%d\n",getpid(),getppid());
        wait(NULL);
    }
    return 0;
}

fork函数为什么要给子进程返回0,给父进程返回子进程的PID?

一个父进程可以创建多个子进程,而一个子进程只能有一个父进程。因此,对于子进程来说,父进程是不需要被标识的;而对于父进程来说,子进程是需要被标识的。并且父进程创建子进程的目的是让其执行某些任务的,父进程必须有子进程的PID才方便对该子进程执行一些操作。

fork函数为什么有两个返回值?

父进程调用fork函数后,为了创建子进程fork函数内部将会进行一系列操作,包括创建子进程的进程控制块、创建子进程的进程地址空间、创建子进程对应的页表等等。子进程创建完毕后,操作系统还需要将子进程的进程控制块添加到系统进程列表当中,此时子进程便创建完毕了。

但在fork函数内部执行return语句之前,子进程就已经创建完毕了,那么之后的return语句不仅父进程需要执行,子进程也同样需要执行,这就是fork函数有两个返回值的原因。

1.3 写时拷贝技术

这里与进程地址空间有着很大的关联,可以结合博主的《Linux下进程以及相关概念理解》进行学习。

当子进程刚刚被创建时,子进程和父进程的数据和代码是共享的,即父子进程的代码和数据通过页表映射到物理内存的同一块空间。只有当父进程或子进程需要修改数据时,才将父进程的数据在内存当中拷贝一份,然后再进行修改。

1.4 fork函数的使用场景

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

1.5 fork函数的失败原因

fork函数创建子进程也可能会失败,有以下两种情况:

 1. 系统中已有太多的进程,内存空间不足,导致子进程创建失败。

2. 实际用户的进程数超过了限制,导致子进程创建失败。

二、进程终止

2.1 进程退出场景

进程退出的场景可以被大致分为三种:

  1. 代码运行完毕,结果正确
  2. 代码运行完毕,结果不正确
  3. 代码异常终止(进程崩溃)

前两种都属于正常退出(第二种是代码逻辑错误导致),第三种则是非正常退出

2.2 进程退出码

main函数是代码的入口,但main函数也只是用户级别代码的入口,main函数也是被其他函数调用的。譬如在VS2013中main函数就是被__tmainCRTStartup函数所调用,__tmainCRTStartup函数则是被mainCRTStartup函数调用,而mainCRTStartup函数又是通过加载器被操作系统所调用的,也就是说main函数是间接性被操作系统所调用的。

既然main函数是间接性被操作系统所调用的,那么当main函数调用结束后就应该给操作系统返回相应的退出信息,而这个所谓的退出信息在Linux中就是以退出码的形式作为main函数的返回值返回。一般以0表示代码成功执行完毕,以非0表示代码执行过程中出现错误,这就是为什么我们都在main函数的最后返回0的原因

当进程结束后main函数的返回值实际就是该进程的进程退出码,可以使用echo $?命令查看最后一次退出的进程的退出码。

为什么用0表示执行成功,用非0表示执行失败?

代码执行成功只有一种情况,成功了就是成功了,而代码执行错误却有多种原因,例如内存空间不足、非法访问以及栈溢出等等,我们就可以用这些非0的数字分别表示代码执行错误的原因。退出码都有对应的字符串含义,而这些退出码具体代表什么含义是人为规定的,不同环境下相同的退出码的含义可能不同。

2.3 进程正常退出方法

2.3.1 exit函数

exit函数是C语言提供的接口,可以在代码中的任何地方退出进程,并且在退出进程前会完成:

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

2.3.2 _exit函数

_exit函数也可以在代码中的任何地方退出进程,但是_exit函数会直接终止进程,并不会在退出进程前会做任何的收尾工作。

2.3.3 return方法

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

return num == exit(num)//在main函数中

2.3.4 方法分析对比

  1. 只有在main函数当中的return才能起到退出进程的作用,子函数当中return不能退出进程,而exit函数和_exit函数在代码中的任何地方使用都可以起到退出进程的作用
  2. 使用exit函数退出进程前,exit函数会执行用户定义的清理函数、冲刷缓冲,关闭流等操作,然后再终止进程,而_exit函数会直接终止进程,不会做任何收尾工作
  3. exit()是C语言提供的函数,_exit()则是系统调用,在Linux环境中exit()函数底层调用了_exit()函数

2.4 进程异常退出

1. 向进程发送信号导致进程异常退出

如,使用kill -9 PID命令向进程发送9号信号导致进程异常退出、使用Ctrl+C导致进程异常退出

2. 代码错误导致进程运行时异常退出

如,当代码中出现野指针问题或者除0错误等都会导致进程运行时异常退出

三、进程等待

3.1 进程等待的意义

  • 子进程退出,父进程若不读取子进程的退出信息,子进程就会变成僵尸进程,进而造成内存泄漏
  • 进程一旦变成僵尸进程,那么就算是kill -9命令也无法将其杀死,因为该进程已经死去。可以将其父进程杀死使其变成孤儿进程。
  • 父进程需要知道子进程退出状态等信息,以了解派发给子进程的任务是否完成
  • 父进程需要通过进程等待的方式,回收子进程资源,获取子进程的退出信息

3.2 获取子进程status

wait()和waitpid()函数都有一个status参数,该参数是一个输出型参数,传入后由操作系统进行填充。若对status参数传入NULL,表示不关心子进程的退出状态信息。否则,操作系统会通过该参数,将子进程的退出信息反馈给父进程。

status是一个整型变量,但应将status变量看作是一个存储信息的位图,status的不同bit位所代表的信息不同,具体细节如下(只讲解status低16个bit位):

在status的低16bit位中,高8位表示进程的退出状态,即退出码。进程若是被信号所杀,则低7位表示终止信号,此时其退出码就无意义了,所以高8位不使用。而第8位bit位是core dump标志。

exitCode = (status >> 8) & 0xFF; //退出码
exitSignal = status & 0x7F;      //退出信号

 下面是操作系统提供的宏:

  • WIFEXITED(status):用于查看进程是否是正常退出,本质是检查是否收到信号。
  • WEXITSTATUS(status):用于获取进程的退出码。
  • WTERMSIG(status):用于获得进程终止的信号编号
exitIsNormal = WIFEXITED(status); //是否正常退出
exitCode = WEXITSTATUS(status);  //获取退出码
exitSignal = WTERMSIG(status);  //用于获得进程终止的信号编号

3.3 进程等待方法

3.3.1 wait()方法

函数原型: pid_t wait(int* status);

作用:阻塞父进程以等待任一子进程

返回值:等待成功则返回被等待进程的PID,等待失败则返回-1。

参数:输出型参数,获取子进程的退出状态,不关心其状态可设置为NULL

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    pid_t id = fork();
    if(id == 0)//child
    {
        for(int i = 0;i < 10;++i){
            printf("PID:%d PPID:%d\n",getpid(),getppid());
            sleep(1);
        }
        exit(0);
    }
    else if(id > 0)//father 
    {
        int status = 0;
        pid_t ret = wait(&status);
        if(ret > 0)
        {
            printf("等待成功!\n");
            if(WIFEXITED(status)){
                printf("子程序正常退出,退出码:%d\n",WEXITSTATUS(status));
            }
            else{
                printf("子程序异常退出,终止信号:%d\n",WTERMSIG(status));
            }
        }
        else{
            printf("等待失败!\n");                                                   
        }
    }
    else{ // fork error
        exit(-1);
    }

    return 0;
}

3.3.2 waitpid()方法

函数原型: pid_t waitpid(pid_t pid, int* status, int options);

返回值:
1、等待成功则返回被等待进程的pid
2、若设置了选项WNOHANG,且调用中的waitpid发现没有已退出的子进程可回收,则返回0
3、若调用中出错,则返回-1,此时errno会被设置成相应的值以指示错误所在

pid参数:

< -1        等待其组ID等于pid的绝对值的任一子进程

-1        等待任一子进程

0        等待进程组ID与当前进程组ID相同的任一子进程

> 0        等待进程ID与pid相同的子进程

options参数:

当设置为WNOHANG时,若等待的子进程没有结束,则waitpid函数直接返回0,不予以等待。若正常结束则返回该子进程的pid。(即不会发生阻塞)

当设置为0时,则会wait()相同,会发生阻塞

 status参数:输出型参数,获取子进程的退出状态,不关心可设置为NULL

waitpid(-1, &status, 0) == wait(&status)

3.4 多进程创建以及等待的代码模型

同时创建多个子进程,然后让父进程依次等待子进程退出,即将子进程的pid存储到数组中。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
    pid_t ids[10] = {0};                                                                                                                                                         
    for (int i = 0; i < 10; i++)
    {
        pid_t id = fork();
        if (id == 0)//child
        {
            printf("child process created successfully...PID:%d\n", getpid());
            sleep(3);
            exit(i); //将子进程的退出码设置为该子进程PID在数组ids中的下标
        }
        else if(id > 0)//father
        {
            ids[i] = id;
        }
        else{ //fork error
            exit(-1);
        }
    }
    for (int i = 0; i < 10; i++)
    {
        int status = 0;
        pid_t ret = waitpid(ids[i], &status, 0);
        if (ret > 0){
            printf("wait child success..PID:%d\n", ids[i]);
            if (WIFEXITED(status)){
                printf("exit code:%d\n", WEXITSTATUS(status));
            }
            else{
                printf("killed by signal %d\n", WTERMSIG(status));
            }
        }
        else{
            printf("wait child error\n");
        }
    }
    return 0;
}

3.5 基于非阻塞接口的轮询检测方案

若当子进程未退出时,父进程阻塞等待子进程退出,在等待期间父进程不能做任何事情,这个等待时间是否能利用起来呢?可以,向waitpid()函数的参数potions传入WNOHANG实现非阻塞

方案思想: 父进程每隔一段时间调用一次waitpid()函数,若是等待的子进程未退出,则父进程可以先处理其他事务,过一段时间再调用waitpid函数读取子进程的退出信息。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
    pid_t id = fork();
    if (id == 0)//child
    {
        for(int i = 0;i < 3; ++i){
            printf("child do something...PID:%d, PPID:%d\n", getpid(), getppid());
            sleep(3);
        }
        exit(0);
    }
    else if(id > 0)//father
    {
        while (1)
        {
            int status = 0;
            pid_t ret = waitpid(id, &status, WNOHANG);
            if (ret > 0)
            {
                printf("wait child success...\n");
                if (WIFEXITED(status)){
                    printf("exit code:%d\n", WEXITSTATUS(status));
                }
                else{
                    printf("killed by signal %d\n", WTERMSIG(status));
                }
                break;
            }
            else if (ret == 0){
                printf("father do other things...\n");
                sleep(1);
            }
            else{
                printf("waitpid error...\n");
                break;
            }                                                                                                                                                                    
        }
    }
    else
    {
        printf("fork error\n");
    }
    return 0;
}

四、进程程序替换

4.1 替换原理

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

进行进程替换时有创建新的进程吗?

没有。进程程序替换之后,该进程对应的PCB、进程地址空间以及页表等数据结构都没有发生改变,只是进程在物理内存当中的数据和代码发生了改变,所以并没有创建新的进程,而且进程程序替换前后该进程的PID并没有改变。

子进程进行进程程序替换后,会影响父进程的代码和数据吗?

不会。子进程刚被创建时与父进程共享代码和数据,但当子进程需要进行进程程序替换时,也就意味着子进程需要对其数据和代码进行写入操作,这时会进行写时拷贝,此后父子进程的代码和数据也就分离了,因此子进程进行程序替换后不会影响父进程的代码和数据。

4.2 替换函数

int execl(const char *path, const char *arg, ...);

参数一是要执行程序的路径,参数二是可变参数列表,表示要如何执行该程序(以NULL结尾)

execl("/usr/bin/ls", "ls", "-a", "-i", "-l", NULL);

int execlp(const char *file, const char *arg, ...);

参数一是要执行程序的名字(在环境变量PATH中查找),参数二是可变参数列表,表示要如何执行该程序(以NULL结尾)

execlp("ls", "ls", "-a", "-i", "-l", NULL);

int execle(const char *path, const char *arg, ..., char *const envp[]);

参数一是要执行程序的路径,参数二是可变参数列表,表示要如何执行该程序(以NULL结尾),参数三是用户设置的环境变量。

char* const _envp[] = { (char*)"MYNAME=BJY", NULL };
execle("./test", "test", NULL, _envp);

int execv(const char *path, char *const argv[]);

参数一是要执行程序的路径,参数二是指针数组,数组当中的内容表示要如何执行这个程序,数组以NULL结尾。

char *const argvs[] = { "ls", "-a", "-i", "-l", NULL };
execv("/usr/bin/ls", argvs);

int execvp(const char *file, char *const argv[]);

参数一是要执行程序的名字,参数二是指针数组,数组当中的内容表示要如何执行这个程序,数组以NULL结尾。

char *const argvs[] = { "ls", "-a", "-i", "-l", NULL };
execvp("ls", argvs);

int execvpe(const char *file, char *const argv[], char *const envp[]);
参数一是要执行程序的名字,参数二是指针数组,数组当中的内容表示要如何执行这个程序,数组以NULL结尾。参数三是用户设置的环境变量。

char *const argvs[] = { "ls", "-a", "-i", "-l", NULL };
char* const _envp[] = { (char*)"MYNAME=BJY", NULL };
execvpe("ls", args, _envp);

int execve(const char *filename, char *const argv[], char *const envp[]);

参数一是要执行程序的路径,参数二是指针数组,数组当中的内容表示要如何执行这个程序,数组以NULL结尾。参数三是用户设置的环境变量。

该接口是系统调用,上述6个函数底层都调用了这个函数。

char *const argvs[] = { "ls", "-a", "-i", "-l", NULL };
char* const _envp[] = { (char*)"MYNAME=BJY", NULL };
execve("/usr/bin/ls", args, _envp);

注意:

上述函数若调用成功,则加载指定的程序并从启动代码处执行,不再返回值。若调用出错,则返回-1。

4.3 命名理解

这七个exec系列函数的函数名都以exec开头,其后缀可以用如下方式理解:

  • l(list):表示参数采用列表的形式,一一列出。
  • v(vector):表示参数采用数组的形式。
  • p(path):表示能自动搜索环境变量PATH,进行程序查找。
  • e(env):表示可以传入自己设置的环境变量。

GitHub 加速计划 / li / linux-dash
10.39 K
1.2 K
下载
A beautiful web dashboard for Linux
最近提交(Master分支:2 个月前 )
186a802e added ecosystem file for PM2 4 年前
5def40a3 Add host customization support for the NodeJS version 4 年前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐