【Linux】 Linux 进程控制

参考博客:https://blog.csdn.net/sjsjnsjnn/article/details/125581083

一、进程创建

1.1 fork()函数

  • 在linux中fork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
  • 进程调用fork,当控制转移到内核中的fork代码后,内核做:
    1. 分配新的内存块和内核数据结构给子进程
    2. 将父进程部分数据结构内容拷贝至子进程
    3. 添加子进程到系统进程列表当中
    4. fork返回,开始由调度器调度

1.2 fork函数的返回值

返回值:

  1. 给父进程返回子进程的PID;
  2. 给子进程返回0;
  3. 子进程创建失败会返回-1;
c 复制代码
void test1(){
    int ret = fork();
    std::cout << "pid = " << getpid() << ", fork()  = " << ret << std::endl;
}
  • 参照运行结果可以看出,给父进程返回的值为子进程的pid,而子进程返回的是0

创建子进程之后,也就是调用fork()函数之后,新创建的进程才开始执行之后的操作,如下图所示

fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定。

1.3 写时拷贝

  • 通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:
  • 在修改内容之前,父子进程的数据和代码都是共享的
  • 当任意一方试图写入时,操作系统会识别到缺页中断
  • 所谓的缺页中断:是指计算机在执行程序的过程中,当出现异常情况或特殊请求时,计算机停止现行程序的运行,转向对这些异常情况或特殊请求的处理,处理结束后再返回现行程序的间断处,继续执行原程序。
  • 那么,操作系统重新分配一块空间,将旧空间的数据拷贝下来,此时操作系统也会重新映射页表。

1.4 fork函数常规用法

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

1.5 fork函数调用失败的原因

  • 系统中有太多的进程,导致内存严重不足,无法加载数据
  • 实际用户的进程数超过了限制

二、进程终止

2.1 进程的退出场景以及退出码

进程一旦退出,就会存在以下三种情况:

  1. 代码运行完毕,结果正确
  2. 代码运行完毕,结果不正确
  3. 代码异常终止
  • 这三种情况,作为用户怎样才能知道某个进程是以什么样的形式退出的呢?那么就有了退出码的概念。
  • Linux 系统中,程序可以在执行终止后传递值给其父进程,这个值被称为退出码。
  • 用户就可以通过相应的退出码,对进程退出状态做以判断
  • 例如,我们的main函数,每次都会写上 return 0; 其实这就是进程的退出码。

我们可以通过 echo $? 来获取最近一次进程退出时的退出码。

shell 复制代码
echo $?
  • 上次进程退出是正常退出,因此结果为0
  • 对于每个指令,对应的都是一个个进程,我们输错指令也会有错误的进程返回值,比如下面输入lsss,返回值为127

2.2 查询返回值的含义

可以通过strerror()函数来查看对应返回值的含义

c 复制代码
void test2(){
    for(int i = 0;i<140;++i){
        std::cout << "error[" << i << "] = " << strerror(i) << std::endl;
    }
}

实际上只有133以内的才是有含义的,打印的结果如下:

三、进程常见的退出方法

3.1 return退出

  • 刚刚我们已经介绍过main函数是通过return退出进程,需要注意以其他函数(非main函数)return进行区分,非main函数的return是函数返回,而main函数的return是进程退出。

3.2 exit( )退出

  • exit可以在程序的任何位置退出,exit退出会刷新缓冲区,和return一样
c 复制代码
void test3(){
    std::cout << "test3 exit(100)" <<  std::endl;
    sleep(1);
    exit(100);
}
  • 查看进程的返回值,eixt(100)退出的返回值就为100

3.3 _exit退出

  • 除了上面两种方法来退出进程,我们还可以使用_exit函数来使进程退出。
  • _exit也是可以在代码中的任何位置终止进程,但是_exit函数终止进程时,是强制终止,不会进行进程的后续收尾工作,如:刷新缓冲区

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

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

3.4 return、exit 和 _exit 的区别

  1. _exit()执行后会立即返回给内核,而exit()要先执行一些清除操作,然后将控制权交给内核。
  2. 调用_exit()函数时,其会关闭进程所有的文件描述符,清理内存,以及其他一些内核清理函数,但不会刷新流(stdin 、stdout、stderr)。exit()函数是在_exit()函数上的一个封装,它会调用_exit,并在调用之前先刷新流。
  3. return是一种更常见的退出进程方法。执行return(num)等同于执行exit(num),因为调用main的运行时函数会将main的返回值当做 exit的参数。

3.5 异常退出

  • 以上是正常退出的情况,和进程的退出码有关;
  • 对于进程的异常退出,就是程序执行了一半后由于地址访问错误、主动终止进程(比如ctrl+c或者kill,也有对应的错误码,这个错误码实际上是包含了进程的终止信号,下面会讲解

四、进程等待

4.1 进程等待的必要性

  1. 子进程退出,父进程如果不获取到子进程的退出信息,就可能造成 僵尸进程 的问题,进而造成内存泄漏。
  2. 进程一旦变成僵尸状态,所谓的 kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
  3. 父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
  4. 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。

4.2 进程等待方法

4.2.1 wait方法

函数原型以及所需头文件

c 复制代码
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
  • 返回值:等待成功则返回等待进程的PID,等待失败,返回-1;

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

  • 下面的代码演示了创建一个子进程,子进程执行对应的任务,然后自动退出,父进程等待子进程结束,并且回收子进程的内存

c 复制代码
void test4(){
    pid_t id = fork();
    if(id == 0){
        for(int i =0 ;i< 5 ; ++i){
            std::cout << "child id = " << getpid() << ", i = " << i << std::endl;
            sleep(1);
        }
        exit(0);
    }
    else{
        sleep(10);
        std::cout << "father wait begin..." << std::endl;
        pid_t cur = wait(NULL);
        if(cur > 0){
            std::cout << "father wait: "<< cur << " sucess" << std::endl;
        }
        else{
            std::cout << "father wait failed!" << std::endl;
        }
        sleep(10);
    }
}
  • 使用下面的shell脚本来监听对应的进程状态
shell 复制代码
while :; do ps axj | head -1 && ps axj | grep mytest | grep -v grep; sleep 1; echo "**********************"; done
  • 可以发现,在运行结束后,子进程退出,然后被父进程回收了,最后父进程也退出了

运行结果

4.2.2 waitpid方法

函数原型以及所需头文件

c 复制代码
#include <sys/types.h>
#include <sys/wait.h>
 
pid_t waitpid(pid_t pid, int *status, int options);

返回值:

  • 当正常返回的时候waitpid返回收集到的子进程的进程ID;
  • 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
  • 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;

参数:

  • pid:

    Pid=-1,等待任一个子进程。与wait等效。

    Pid>0.等待其进程ID与pid相等的子进程。

  • status:

    WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)

    WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

  • options:

    WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的PID。

下面的代码为等待任意一个子进程后,回收子进程,这里options指定为0,将会阻塞在此

c 复制代码
void test5()
{
    pid_t id = fork();
    if (id == 0)
    {
        for (int i = 0; i < 5; ++i)
        {
            std::cout << "child id = " << getpid() << ", i = " << i << std::endl;
            sleep(1);
        }
        exit(100);
    }
    sleep(10);
    std::cout << "father wait begin..." << std::endl;
    // pid_t cur = wait(NULL);
    int status = 0;
    pid_t cur = waitpid(-1, &status, 0); // 等待任意一个子进程

    if (cur > 0)
    {
        std::cout << "father wait: " << cur << " sucess, WIFEXITED(status) = " << WIFEXITED(status)
         <<",WEXITSTATUS(status) = "<< WEXITSTATUS(status) <<  std::endl;
    }
    else
    {
        std::cout << "father wait failed!" << std::endl;
    }
    sleep(10);
}
  • 运行结果如下,通过WIFEXITED宏和WEXITSTATUS可以查看子进程是否正常退出以及退出码是多少

4.2.3 获取子进程status

4.2.3.1 什么是status

int status:它是一种输出型的参数 *
所谓获取子进程的status,就是获取子进程退出时的退出信息;
首先,在子进程中分别用exit(0)和exit(10)来中断子进程,父进程获取status值,判断进程的退出状态。

4.2.3.2 status的构成
  • status是由32个比特位构成的一个整数,目前阶段我们只使用低16个位来表示进程退出的结果
  • 如下图所示,就是status低16位的表示图;
c 复制代码
status exit_code = (status >> 8) & 0xFF; //退出码
status exit_code = status7 & 0x7F;       //退出信号
  • 进程正常退出有两种,与退出码有关,异常退出与信号有关
  • 所以这里我们就需要获取到两组信息:退出码与信号
  • 如果没有收到信号,就表明我们所执行的代码是正常跑完的,然后在判断进程的退出码,究竟是何原因使进程结束的
  • 反之则是异常退出,也就不需要关心退出码了
c 复制代码
void test6()
{
    pid_t id = fork();
    if (id == 0)
    {
        for (int i = 0; i < 5; ++i)
        {
            std::cout << "child id = " << getpid() << ", i = " << i << std::endl;
            sleep(1);
        }
        exit(100);
    }
    sleep(10);
    std::cout << "father wait begin..." << std::endl;
    // pid_t cur = wait(NULL);
    int status = 0;
    pid_t cur = waitpid(-1, &status, 0); // 等待任意一个子进程

    if (cur > 0)
    {
        std::cout << "father wait: " << cur << " sucess, status = " << status << std::endl;
        std::cout << "exit_code = " << ((status >> 8)& 0xff) << ", exit_signal = " <<(status & 0x7f) << std::endl;
    }
    else
    {
        std::cout << "father wait failed!" << std::endl;
    }
    sleep(10);
}
  • 正常退出,这里退出码为100,终止信号可以忽略
  • 在进程运行时使用kill -9命令终止进程,可以发现终止信号为9
  • 查询shell指令,发现9对应的是SIGKILL
shell 复制代码
kill -l
  • 这里可以通过WIFEXITED宏和WEXITSTATUS宏查看是否是正常退出,以及正常退出的返回值
c 复制代码
pid_t cur = waitpid(-1, &status, 0); // 等待任意一个子进程

if (cur > 0)
{
	std::cout << "father wait: " << cur << " sucess, WIFEXITED(status) = " << WIFEXITED(status)
	 <<",WEXITSTATUS(status) = "<< WEXITSTATUS(status) <<  std::endl;
}
else
{
	std::cout << "father wait failed!" << std::endl;
}
4.2.3.3 阻塞等待与非阻塞等待
  • 这里我们所讲的阻塞等待和非阻塞等待,其实就是waitpid函数的第三个参数,我们之前并未提及,直接给的是0,这种是默认行为,阻塞等待;阻塞等待:父进程一直在等待子进程,什么事都不干,直到子进程正常退出。

  • 如果设置为WNOHANG,表示的是非阻塞等待方式。非阻塞等待:父进程的PCB由运行队列转变为等待队列,直达子进程结束,操作系统获取到子进程退出的信号时,再将父进程从等待队列中调度到运行队列,由父进程去获取子进程的退出码以及退出信号。

通过判断返回值来查看是否子进程已经结束了,如果没有结束就继续干父进程自己的任务,否则就回收子进程

c 复制代码
void test7()
{
    pid_t id = fork();
    if (id == 0)
    {
        for (int i = 0; i < 10; ++i)
        {
            std::cout << "child id = " << getpid() << ", i = " << i << std::endl;
            sleep(1);
        }
        exit(100);
    }

    std::cout << "father wait begin..." << std::endl;

    while (true)
    {
        int status = 0;
        pid_t cur = waitpid(-1, &status,WNOHANG); // WNOHANG = 1,非阻塞

        if (cur > 0)
        {
            std::cout << "father wait: " << cur << " sucess, status = " << status << std::endl;
            std::cout << "exit_code = " << ((status >> 8) & 0xff) << ", exit_signal = " << (status & 0x7f) << std::endl;
            break;
        }
        else if(cur == 0)
        {
            std::cout << "do father process things" << std::endl;
            sleep(1);
        }
        else{
            std::cout << "father wait failed!" << std::endl;
            break;
        }
    }
}

运行结果如下

五、进程程序替换

5.1 替换原理

  • 用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。
  • 当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。
  • 调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
  • 从上图可以看出,进程程序替换前后,进程本身并没有发生任何变化,只是所执行的代码发什么改变。
  • 如果子进程进行程序替换,不会影响父进程的代码和数据吗?首先进程是具有独立性的,虽然子进程共享父进程的代码和数据,但是由于进行了函数替换,发生了代码和数据的修改,此时就会进行写时拷贝。所有子进程进行程序替换时,并不会影响父进程的代码和数据。

5.2 替换函数

有六种以exec开头的函数,统称exec函数: 他们所需的头文件均为 #include <unistd.h>

execl函数

c 复制代码
int execl(const char *path, const char *arg, ...);
// path --- 可执行程序的路径
// arg --- 可变参数列表,表示你要如何执行这个程序,并以NULL结尾
// 例如:
execl("/usr/bin/ls", "ls", "-a", "-l", NULL);

execlp函数

c 复制代码
int execlp(const char *file, const char *arg, ...);
// file --- 可执行程序的名字
// arg --- 可变参数列表,表示你要如何执行这个程序,并以NULL结尾
// 例如:
execlp("ls", "ls", "-a", "-l", NULL);

execle函数

c 复制代码
int execle(const char *path, const char *arg, ..., char * const envp[]);
// path --- 可执行程序的路径
// arg ---  可变参数列表,表示你要如何执行这个程序,并以NULL结尾
// envp --- 自己维护的环境变量
 
// 例如:
char* envp[] = { "Myval=12345", NULL };
execle("./myexe", "myexe", NULL, Myval);

execv函数

c 复制代码
int execv(const char *path, char *const argv[]);
// path --- 你要执行程序的路径
// argv --- 指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾
 
// 例如:
char* argv[] = { "ls", "-a", "-l", NULL };
execv("/usr/bin/ls", argv);

execvp函数

c 复制代码
int execvp(const char *file, char *const argv[]);
// file --- 你要执行程序的名字
// argv --- 指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾
 
// 例如:
char* argv[] = { "ls", "-a", "-l", NULL };
execvp("ls", argv);

execve函数

c 复制代码
int execvpe(const char *file, char *const argv[], char *const envp[]);
// file --- 你要执行程序的路径
// argv --- 指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾
// envp --- 自己维护的环境变量
 
//例如:
char* argv[] = { "mycmd", NULL };
char* envp[] = { "Myval=12345", NULL };
execve("./myexe", argv, envp);

函数解释

  • 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
  • 如果调用出错则返回-1
  • 所以exec函数只有出错的返回值而没有成功的返回值。也就是说,exec系列函数只要返回了,就意味着调用失败。
函数名 参数格式 是否带路径 是否使用当前环境变量
execl 列表 不是
execlp 列表
execle 列表 不是 不是,须自己装环境变量
execv 数组 不是
execvp 数组
execve 数组 不是 不是,须自己装环境变量
c 复制代码
void test8(){
    char *argv[] = {"ls","-a","-l",NULL}; 
    
    execl("/usr/bin/ls","ls","-a","-l",NULL); //可变参,NULL结尾
 
    execv("/usr/bin/ls",argv); //字符串数组形式

    execlp("ls","ls","-a","-l"); //文件名+可变参

    execvp("ls",argv); //字符串数组形式

    //可执行文件路径
    char* argv_[] = {"./process-test","-test2",NULL};
    char* env_[] = {NULL};
    execvpe("./process-test",argv_,env_);

}

这些函数原型看起来很容易混,但只要掌握了规律就很好记。

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

事实上,只有execve才是真正的系统调用,其它五个函数最终都是调用的execve,所以execve在man手册的第2节,而其它五个函数在man手册的第3节,也就是说其他五个函数实际上是对系统调用execve进行了封装,以满足不同用户的不同调用场景的。

下图为exec系列函数族之间的关系:

更多资料:https://github.com/0voice

相关推荐
杜子不疼.6 小时前
Linux】 性能调优实战:内核参数优化技巧
linux·运维·php
墨寒博客栈7 小时前
Linux基础常用命令
java·linux·运维·服务器·前端
重生之我在20年代敲代码7 小时前
【Linux网络编程】初识网络,理解TCP/IP五层模型
linux·运维·服务器·网络
**蓝桉**7 小时前
服务器管理
linux·笔记
疯狂的小强呀7 小时前
如何利用 Jupyter 从浏览器访问远程服务器
服务器·jupyter·浏览器访问服务器·jupyter远程访问服务器
GalaxyPokemon7 小时前
PlayerFeedback 插件开发日志
java·服务器·前端
没枕头我咋睡觉8 小时前
【运维】ubuntu修改镜像源
linux·运维·ubuntu
鲸鱼爱泡芙8 小时前
IMX6ULL无法通过Ubuntu22.04 NFS uboot挂载rootfs根目录解决
linux
努力学习的小廉8 小时前
深入了解linux网络—— 守护进程
linux·运维·网络
wheeldown8 小时前
【Linux】从内存布局到信号屏蔽:Linux 内核态与用户态交互核心知识点汇总
linux·运维·服务器