Linux进程控制

Linux进程控制

文章目录

      • Linux进程控制
        • 一、进程创建
          • fork
          • fork的返回值问题
          • 写时拷贝
          • 失败原因
        • 二、进程终止
          • 进程退出码
          • 进程退出
            • return
            • exit
            • _exit
            • 三者的区别与联系
          • 进程异常退出
        • 三、进程等待
          • 为什么进行进程等待
          • 如何进行等待
          • 阻塞等待和非阻塞等待
          • status
          • WIFEXITED 与 WEXITSTATUS 宏
        • 四、进程程序替换
          • 什么是进程程序替换
          • 替换函数exec*

一、进程创建

fork

Linux进程控制_第1张图片

  • 所需头文件 #include
  • 返回值 创建成功,子进程返回0,父进程返回子进程pid。创建失败,给父进程返回-1。
  • pid_t fork(void)
  • 功能:创建一个子进程。
int main()
{
    cout << "process: PID->" << getpid() <<", PPID->"<<getppid() << endl;
    pid_t id = fork();
    if(id == -1)
    {
        perror("fork fail");
    }
    cout << "process: PID->" << getpid() <<", PPID->"<<getppid() << endl;
    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9XpS1Pjv-1690035968623)(https://raw.githubusercontent.com/roargod/typora_image/main/img/202307220942222.png)]

我们看到在fork之后的cout打印了两次,而且第二次的PPID是父进程的PID。这说明我们使用fork成功创建了一个子进程。

fork的返回值问题

我们刚刚还提到了fork函数会有两个返回值,一个返回0, 另一个返回子进程的PID。但是为什么呢?

我们在上一个文章中提到过,在调用fork函数时后,fork会进行创建进程PCB、mm_struct、页表等操作。在这些操作完成后进程就创建完成了,就会return进行返回。当父子进程都return后,就会有两个返回值。

一个父进程可以有多个子进程,每个子进程都只有一个父进程,给父进程返回子进程自己的PID可以方便父进程给子进程发派任务,这就是不同返回值的原因。

写时拷贝

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

我们在上一篇文章中具体谈到了,有兴趣可以去看一看。

失败原因
  • 系统中的进程过多,内存空间不足,导致创建子进程失败
  • 用户对进程数量进行特定的限制,导致子进程创建失败

二、进程终止

进程退出码

进程退出一般分为三种种:

  • 代码运行完毕且结果正确 – 此时退出码为0;
  • 代码运行完毕且结果不正确 – 此时退出码为非0;
  • 代码异常终止 – 此时退出码无意义。

我们可以通过以下命令来查看Linux程序执行的结果:

[---@VM-8-4-centos day02]$ echo $?

Linux进程控制_第2张图片

0就说明我们刚才的代码是执行成功的,正常退出的。(我们没有进行设定)

我们可以通过一段代码来查看各种返回值对应的错误原因。

#include 
#include 
#include 
#include 

using namespace std;

int main()
{
    for(int i = 0; i < 100; i++)
    {
        printf("%d : %s\n", i, strerror(i));
    }
    return 0;
}

Linux进程控制_第3张图片

进程退出
return
  • return 是我们使用最多的退出方式。

例如:在main函数最后退出。

Linux进程控制_第4张图片

exit
  • exit也是一种常用的退出方式,它可以在代码中任何我们想要退出的地方进行退出。

例如:代码执行一半进行退出。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HEohpmxg-1690035968624)(https://raw.githubusercontent.com/roargod/typora_image/main/img/202307221033083.png)]

而且我们可以定义退出时的状态,父进程还可以通过wait来获取这个值。

_exit
  • _exit是我们一种不常用的退出方式,_exit同样可以在代码的任何位置退出进程,但它只会直接终止进程,而进程中的部分代码不会被执行。

Linux进程控制_第5张图片

上述的部分代码不会被执行,例如:

void test()
{
    cout << "hello world" ;
    _exit(1);//#include
}

int main()
{
    test();
    return 0;
}

要是我们不强制刷新缓冲区,hello world就不会被打印出来。

三者的区别与联系
  • return只有在主函数中才能退出进程,exit和_exit可以在任何地方退出进程。

  • exit和return在结束进程的时候会执行用户的清除函数 刷新缓冲区 关闭流等 而_exit则会直接退出。

  • exit是对_exit的封装,_exit是系统调用。使用exit函数退出进程前,exit函数会先执行用户定义的清理函数、冲刷缓冲,关闭流等操作,然后再调用_exit函数终止进程。

  • 执行return num等于执行exit(num)。

进程异常退出
  • 向进程发kill信号等退出,导致程序异常。

  • 代码错误导致进程运行时异常退出,如除0、空指针等。

三、进程等待

为什么进行进程等待
  1. 创建进程的目的就是让它执行任务,父进程也需要了解任务完成的如何。
  2. 之前提到过的父进程通过进程等待,回收资源,获取子进程的退出信息。
  3. 解决我们之前提到过的僵尸进程问题,读取子进程的退出信息。如果不读取,进程变成一个僵尸进程,kill命令也无法杀死这个进程,毕竟没人能杀死一个死掉的进程。
如何进行等待

两种方法:

  • 方法一
pid_t wait (int* status)
作用:等待任意子进程。
status:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
返回值:成功返回对应子进程的PID,失败返回-1
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;

int main()
{
    pid_t id = fork();
    if (id > 0)
    {
        sleep(10);
        int status = 0;
        pid_t ret = wait(&status);
        if (ret == -1)
        {
            perror("wait fail");
            exit(1);
        }
        cout << "wait success" << endl;
        cout << "exit code-> " << status << endl;
    }
    else if (id == 0)
    {
        int count = 5;
        while (count--)
        {
            cout << "我是子进程, PID->" << getpid() << ", PPID->" << getppid() << endl;
            sleep(1);
        }
        cout << "等待回收" << endl;
        exit(1);
    }
    else
    {
        perror("fork fail");
    }
    return 0;
}

Linux进程控制_第6张图片

  • 方法二
pid_t waitpid(pid_t pid, int* status, int options)
作用:等待指定子进程或任意子进程
pid:指定等待子进程的PID,设置为-1即为等待任意子进程
status:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
options:设置为0时阻塞等待,设置为WNOHANG,非阻塞等待。
阻塞等待和非阻塞等待

阻塞等待:当父进程执行到 waitpid 函数时,如果子进程还没有退出,父进程就只能阻塞在 waitpid 函数,直到子进程退出,父进程通过 waitpid 读取退出信息后才能接着执行后面的语句。例如,你约你的女朋友出去吃饭,让她化好妆打电话叫你来接她,从你开始等她到她花好妆打电话给你,这段时间你什么都不做,这就是阻塞等待。

非阻塞等待:当父进程执行到 waitpid 函数时,如果子进程未退出,父进程会直接读取子进程的状态并返回,然后接着执行后面的语句,不会等待子进程退出。例如,你约你女朋友出去吃饭,让她化好妆打电话叫你来接她,从你开始等她到她给你说好了,这段时间你每2分钟打电话问她好了没有(当然不建议大家这么做,哈哈)。这就是非阻塞等待。

我们使用一下阻塞等待加深一下理解:

int main()
{
    pid_t id = fork();
    if (id > 0)
    {
        cout << "我是父进程,我已经开始等待了" << endl;
        int status = 0;
        pid_t ret = waitpid(-1, &status, 0);
        if (ret == -1)
        {
            perror("wait fail");
            exit(1);
        }
        cout << "wait success" << endl;
        cout << "exit code-> " << status << endl;
    }
    else if (id == 0)
    {
        int count = 5;
        while (count--)
        {
            cout << "我是子进程, PID->" << getpid() << ", PPID->" << getppid() << endl;
            sleep(1);
        }
        cout << "等待回收" << endl;
        exit(1);
    }
    else
    {
        perror("fork fail");
    }
    return 0;
}

Linux进程控制_第7张图片

父进程没有直接打印wait success,而是获取到子进程退出的状态值后才运行waitpid之后的代码。

status

status虽然是一个整型变量,但不能当作整型变量来看。

我们可以把它看作一个位图。(仅仅看低16位比特)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JZYESesH-1690035968625)(https://raw.githubusercontent.com/roargod/typora_image/main/img/202307221836635.png)]

  • 9-15:表示进程的退出状态
  • 8:core dump标志
  • 0-7:如果进程被信号杀死,则表示终止信号

原理图如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RxIpdqqN-1690035968626)(https://raw.githubusercontent.com/roargod/typora_image/main/img/202307221841483.png)]

我们可以通过操作符得到退出码和退出信号。

exit_code = (status >> 8) & 0xFF//退出码
exit_signal = status & 0x7F//退出信号

当然贴心的系统当中也提供了两个宏来获取退出码和退出信号。

WIFEXITED 与 WEXITSTATUS 宏
exit_code = WEXITSTATUS(status);//获取退出码
ret = WIFEXITED(status);//是否正常退出

PS:如果当一个进程非正常退出时,说明该进程是被信号所杀,那么该进程的退出码也就没有意义了。

四、进程程序替换

什么是进程程序替换

在我们使用fork创建出子进程后,有两种情况:一是子进程执行父进程的部分代码,二是子进程和父进程执行不同的代码。

我们说的第二种情况就是进程程序替换。

当我们使用fork创建子进程后,再使用exec*函数,使得该进程的用户空间代码和数据完全被新程序所替换,并执行新的程序。

我们之前说过进程创建时会同时创建PCB、mm_struct和页表等,在程序替换后这些都统统没有发生改变,只是物理内存中的数据和代码发生了改变。当子进程发生程序替换时还会发生我们之前提到的写时拷贝,使得父子进程间互不干扰。


替换函数exec*
  • 一、 int execl(const char *path, const char *arg, …);

第一个参数是我们执行的路径。

另一个参数是可变参数列表,我们需要进行的操作,最终以NULL结尾。

如:

execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
  • 二、 int execlp(const char *file, const char *arg, …);

第一个参数是我们执行的文件名。

另一个参数是可变参数列表,我们需要进行的操作,最终以NULL结尾。

execlp("ls", "ls", "-a", "-l", NULL);
  • 三、int execle(const char *path, const char *arg, …, char *const envp[]);

第一个参数是我们执行的路径。

第二个参数是可变参数列表,我们需要进行的操作,最终以NULL结尾。

第三个参数是我们设定的环境变量。

char* myenvp[] = { (char*)"val=100", NULL };
execle("./mycode", "./mycode", NULL, myenvp);
  • 四、int execv(const char *path, char *const argv[]);

第一个参数是我们执行的路径。

第二个参数是指针数组,表示我们需要进行的操作,以NULL结尾。

char* myargv[] = { "ls", "-a", "-i", "-l", NULL };
execv("/usr/bin/ls", myargv);
  • 五、int execvp(const char *file, char *const argv[]);

第一个参数是我们执行的文件名。

第二个是指针数组,表示我们需要进行的操作,以NULL结尾。

char* myargv[] = { "ls", "-a", "-i", "-l", NULL };
execvp("ls", myargv);
  • 六、int execve(const char *path, char *const argv[], char *const envp[]);

第一个参数是我们执行的路径。

第二个参数是指针数组,表示我们需要进行的操作,以NULL结尾。

第三个参数是我们设定的环境变量。

char* myargv[] = { "./mycode", NULL };
char* myenvp[] = { "val=100", NULL };
execve("./mycode", myargv, myenvp);

函数名理解

  • l (list): 表示参数采用列表。
  • v (vector): 表示参数采用数组。
  • p (path): 表示系统自动到环境变量PATH路径下搜索文件。ps: 对于替换Linux命令程序我们不用加路径
  • e (env): 表示我们设置的环境变量。

一个具体的代码理解exe*函数

myexe.cc

#include
#include 
using namespace std;
int main()
{
    printf("USER: %s\n", getenv("USER"));
    printf("PWD: %s\n", getenv("PWD"));
    printf("VAL: %s\n", getenv("val"));
    return 0;
}

mycode.cc

#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;

int main()
{
    pid_t id = fork();
    if (id > 0)
    {
        int status = 0;
        // cout << "我是父进程" << endl;
        int ret = waitpid(-1, &status, 0);
        if (ret == -1)
        {
            perror("wait fail");
        }
        cout << "wait success: "
             << "exit_code-> " << ((status >> 8) & 0xFF) << endl;
    }
    else if (id == 0)
    {
        // execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
        char *const myenvp[] = {(char*)"val=100", (char*)"PWD=./day02", (char*)"USER=wsj", NULL};
        int ret = execle("./myexe", "./myexe", NULL, myenvp);
        if(ret == -1) perror("execle fail");
        exit(0);
    }
    else
    {
        perror("fork fail");
    }
    return 0;
}

从execle的传参可以看出,我们通过mcode.cc代码来调用myexec.cc进行程序替换。

Linux进程控制_第8张图片

其他exec*函数,一通百通。

你可能感兴趣的:(Linux,linux)