Linux系统编程系列之进程控制(下)


前言

进程等待 + 进程程序替换,有了这些就可以自己去模拟一个shell了


一、进程等待

进程状态中提到,一个进程进入僵尸状态,父进程不回收资源,这个僵尸进程无法被杀死,需要父进程主动回收 ----进程等待。

是什么、为什么、怎么写

什么是进程等待?

父进程通过调用系统调用接口wait或者waitpid来对子进程进行状态检测和回收的功能!

为什么要有进程等待?

可以通过进程等待的定义来看,一方面进行状态检测、另一方面进行回收。

1.僵尸进程无法被杀死,需要通过进程等待来杀掉它,回收资源,解决内存泄露问题。

2.我们可以通过进程等待来获取子进程的退出情况,知道我布置给子进程的任务的完成情况。

怎么写进程等待的代码?

wait和waitpid中都带有int* status的参数,这是一个输出型参数,并且这个int类型是被当作几部分来使用的。

wait

先来看看wait的使用,wait等待任意一个子进程退出并且进行回收,返回值就如果大于0是回收的pid,出现错误-1被返回

cpp 复制代码
int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        exit(0);
    }
    else if(id > 0)
    {
        sleep(3);
    }

    pid_t wid = wait(nullptr);
    if(wid == id)
    {
        printf("回收成功!\n");
    }
    sleep(2);
    return 0;
}

我们先不关心status,让创建的子进程直接退出成为僵尸进程,父进程先等待三秒,可以查看僵尸,然后回收,此时只有父进程,2s后,父进程也结束。

监控脚本:while :; do ps ajx | head -1 && ps ajx | grep blog | grep -v vscode | grep -v grep; sleep 1;done

结果也如预期所示。

另外:如果子进程没有退出,那么父进程在wait的时候就不会返回,这被称之为阻塞状态。有没有非阻塞呢?当然有---waitpid中的options,后面再说

如果有多个子进程的情况,wait该回收谁呢??

wait会随机选取一个子进程回收,多个子进程回收多次即可。

status

上面说它是一个输出型参数,由OS来填充,填为nullptr表示不关心子进程的退出状态,否则会将这个值交给父进程,父进程应该关心子进程的什么退出状态呢??进程退出的三种情况,代码运行完毕,结果正确;代码运行完毕,结果不正确;代码异常终止。所以int被分成多部分中一定能看到这三种情况!

int类型4个字节32的比特位,只使用了低16位

如果代码异常终止,收到信号,不关心退出状态。

如果运行完毕,就看退出状态,表示结果是否正确。

拿到对应的信号和退出状态都是位运算的知识,拿到前七位,就是和前七位全1,其他全0取&即可,这个数是0x7F,拿到8-15位,把这个数右移8位然后 & (0xFF)即可。我们可以验证一下,比如手动调用函数使得进程收到信号,再比如不让他收到信号,让他打开一个不存在的文件,看看退出码和描述。

cpp 复制代码
int main()
{
    pid_t id = fork();
    if(id == 0)
    {
	    //kill(getpid(),9);
        int fd = open("?.txt",O_RDONLY);
        cout << errno << endl;
        if(fd < 0)
        {
            exit(errno);
        }
        exit(0);
    }
    else if(id > 0)
    {
        sleep(3);
    }

    int st;
    pid_t wid = wait(&st);
    if(wid == id)
    {
        printf("回收成功!\n");
        int sig = (st & (0x7F));
        int exitcode = (st >> 8 & (0xFF));
        if(sig == 0){
            cout << "没收到信号!退出码是" << exitcode << ",描述:"<< 
            strerror(exitcode);
        }
        else {
            cout << "代码异常终止!,信号是";
            cout << sig << endl;
        }
    }
    return 0;
}

使用kill(getpid(),9)时:

打开一个不存在的文件时:

当然这样获取信号和退出状态有点麻烦,系统提供了几个宏来进行更简单的获取:

WIFEXITED(status),返回值是bool类型,若为正常终止,为真,异常终止为假

WEXITSTATUS(status)若非0,则提取退出码。

WIFSIGNALED(status) 判断子进程是否被信号终止,返回非0表示是

WTERMSIG(status) 当WIFSIGNALED为真时,提取终止子进程的信号编号

就可以这么改了:

cpp 复制代码
pid_t wid = wait(&st);
    if(wid == id)
    {
        printf("回收成功!\n");
        int sig = WTERMSIG(st);
        int exitcode = WEXITSTATUS(st);
        if(WIFEXITED(st)){
            cout << "没收到信号!退出码是" << exitcode << ",描述:"<< 
            strerror(exitcode);
        }
        else {
            cout << "代码异常终止!,信号是";
            cout << sig << endl;
        }
    }

这里讲的status都对waitpid中的status适用

waitpid

wait是waitpid功能的一个子集。

pid_t waitpid(pid_t pid,int *status,int options)

pid : pid = -1表示等待任意一个子进程,pid > 0.等待指定子进程的pid

options :0表示阻塞等待方式,如果子进程没有退出就一直在等.

非阻塞轮询:WNOHANG这是一个宏,代表着不要阻塞!非阻塞 + 循环->非阻塞轮询 + 做自己的事情

非阻塞轮询情况下怎么判断条件就没就绪呢?根据返回值

翻译出来的意思就是:大于0表示回收子进程的pid,如果选项被设置成WHOHANG并且返回值等于0,代表条件没有就绪,小于0就是出错。

非阻塞写法:因为是非阻塞轮询,所以一定要打循环

cpp 复制代码
int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        sleep(3);
        exit(0);
    }
    int st;
    while (1)
    {
        pid_t wid = waitpid(0, &st, WNOHANG);
        if (wid == id)
        {
            printf("回收成功!\n");
            int sig = WTERMSIG(st);
            int exitcode = WEXITSTATUS(st);
            if (WIFEXITED(st))
            {
                cout << "没收到信号!退出码是" << exitcode << ",描述:" << strerror(exitcode);
            }
            else
            {
                cout << "代码异常终止!,信号是";
                cout << sig << endl;
            }
            break;
        }
        else if(wid == 0)
        {
            sleep(1);
            cout << "条件没有就绪!\n";
            cout << "可以执行自己的任务了!\n";
            //这里可以执行自己的任务了!
        }
    }
    return 0;
}

原理

父进程调用wait时,内核会做两件事:

检查父进程的子进程中是否有僵尸进程;若有,将该子进程 PCB 中的退出信息(exit_code和exit_signal) 通过位运算拷贝到父进程的status参数中;将僵尸进程的状态从Z改成X。

如果没有,则阻塞等待。

这里不能用全局变量,因为进程具有独立性。

二、进程程序替换

什么是进程程序替换

fork之后,我们可以让子进程和父进程执行不同的分支,但是我如果想让子进程执行一份新的程序呢?---进程程序替换

进程程序替换是通过exec一系列接口,让一个正在运行的进程,完全替换自身的代码段、数据段、堆、栈等资源,转而执行另一个程序。

程序替换的时候也没有创建子进程,只是进行了进程的程序代码和数据的替换工作。

程序替换中环境变量信息不会被替换,创建子进程的时候环境变量就已经继承给子进程了,extern char** environ;所以,程序替换中环境变量不会被替换。

现象:如果成功进行程序替换,exec系列后续代码不会执行,只有失败了才会执行后续代码,所以exec系列函数只有失败返回值,失败返回-1,没有成功返回值。

exec系列

虽然有一系列函数,但是每个字符都是有意义的

l(list) : 表示参数采用链表

v(vector) : 参数用数组

p(path) : 有p自动搜索环境变量PATH

e(env) : 表示自己维护环境变量

就拿最基础的execl来说,要执行一个新程序,首先一定得找到这个程序,第一个参数就是什么路径下的什么程序!找到之后决定如何执行这个程序---带不带选项,带哪些选项。

具体怎么写先看写法。

cpp 复制代码
execl("/usr/bin/ls","ls","-l","-a",nullptr);

不加p表示需要自己提供路径,我们提过,这些指令不需要路径也能运行的原因就是有环境变量中的PATH!如果函数有p,那我们就不需要提供路径了!

后面的写法和shell中的写法一样,只不过以nullptr结尾!

char* const argv[] 指的是指针本身不能修改,const在前面指的是指向的内容不能修改。

演示一下其他函数的写法:

cpp 复制代码
execl("/usr/bin/ls", "ls", "-l", "-a", nullptr);
        execlp("ls", "ls", "-l", "-a", nullptr);
        execle("/usr/bin/ls", "ls", "-a", "-l", nullptr, environ);

char *const argv[] = {
    const_cast<char *>("ls"),
    const_cast<char *>("-a"),
    const_cast<char *>("-l"),
    NULL};
execv("/usr/bin/ls", argv);
execvp("ls", argv);
execvpe("ls", argv, environ);

argv里面不加const_cast不一定可以编译通过,ls是const char*类型,赋值给非const数组。

那我们的exec能不能执行自己的命令呢?当然可以,和上面的道理是一模一样的!比如执行一个hello world的可执行

cpp 复制代码
execl("./other","other",nullptr); 

我如果想给子进程传递环境变量,该怎么传递?

1.新增环境变量

putenv添加一个环境变量,添加在调用进程的环境变量中

如:putenv("VALUE=666");

execle是自己传,可以传默认的

2.彻底替换 ----这里是彻底替换不是追加

使用自己传的myenv

char* const myenv[] = {

"VALUE=666",

...

};

execle("./main","main","-a","-b","-c",nullptr,myenv);

最后我们发现这都是三号手册的,也合理,因为之前说过这些接口一定封装了系统调用接口----execve

你上面调用的所有函数都会转化为这个函数!


相关推荐
RisunJan2 小时前
Linux命令-ifconfig命令(配置和显示网络接口的信息)
linux·运维·服务器
LaoWaiHang3 小时前
Linux基础知识04:pwd命令与cd命令
linux
lbb 小魔仙3 小时前
【Linux】100 天 Linux 入门:从命令行到 Shell 脚本,告别“光标恐惧”
linux·运维·服务器
小张成长计划..3 小时前
【Linux】1:基本指令
linux
OliverH-yishuihan3 小时前
在win10上借助WSL用VS2019开发跨平台项目实例
linux·c++·windows
早川9194 小时前
Linux系统
linux·运维·服务器
郝学胜-神的一滴4 小时前
Linux进程与线程控制原语对比:双刃出鞘,各显锋芒
linux·服务器·开发语言·数据结构·c++·程序人生
山上三树4 小时前
进程状态详解
linux·运维·服务器
oMcLin5 小时前
如何打造Linux运维监控平台:Prometheus + Grafana实战与性能优化
linux·运维·prometheus