Linux_进程控制

目录

1、进程创建

1.1 fork

1.2 进程执行顺序

2、进程终止

2.1 echo指令查看退出码

2.2 strerror 

2.3 error 

2.4 error的意义

2.4 异常情况 

3、进程等待 

3.1 wait 

3.2 waitpid

3.3 status位图结构 

3.4 options-非阻塞等待

3.5 小结 

4、进程替换 

4.1 execl

4.2 多进程下的进程替换

结语 


前言:

        在Linux下,进程控制是操作系统对进程进行管理的功能,具体涉及到以下几个步骤:进程创建,进程终止、进程等待、进程替换,通过这些模块就可以将进程的功能运用极致,并且能够避免因错误操作而出现的进程异常问题。

1、进程创建

        在Linux下创建一个进程必须调用的接口就是fork,他的功能是在当前进程下创建一个新的进程作为当前进程的子进程,通常把当前进程叫做父进程,fork用法如下:

#include <unistd.h>
pid_t fork(void);//pid_t是Linux自定义的类型,底层实现是int,void表示不需要传参

返回值:子进程拿到的返回值是0,父进程拿到的返回值是子进程id,出错返回-1

        当调用fork接口后,操作系统在底层会做下面四件事情完成子进程的创建和执行:

        1、给子进程分配内核数据结构和对应的内核空间。

        2、将父进程内核数据结构的部分数据拷贝至子进程的内核数据结构上,并建立虚拟、物理地址的映射关系。

        3、把子进程添加到内核的进程管理列表中。

        4、让cpu进行调度。

        使用fork创建进程的具体示意图:

        fork创建的子进程执行代码的内容和fork接口后面内容是一样的,即fork之后会形成分流,父子进程都会执行同一份代码,但是fork之前的代码只有父进程会执行。

1.1 fork

        测试fork接口代码如下:

#include <stdio.h>
#include <unistd.h>

int main()
{
    printf("当前进程的pid:%d\n", getpid());
    pid_t in = fork();
    if (in == 0)
    {
        while (1)
        {
            printf("这是一个子进程:PID: %d PPID:%d\n", getpid(), getppid());
            sleep(1);
        }
    }
    else
    {
        while (1)
        {
            printf("这是一个父进程:PID: %d PPID:%d\n", getpid(), getppid());
            sleep(1);
        }
    }
    return 0;
}

         运行结果:

1.2 进程执行顺序

        上述的测试结果中是由父进程先执行的,所以先打印父进程的代码,但是实际上fork之后并不一定是父进程先执行,执行顺序完全是由调度器决定的。比如:循环创建5个子进程,那么这5个子进程的执行顺序不一定是0 1 2 3 4,示例代码如下:

#include <stdio.h>
#include <unistd.h>

#define N 5

void runChild(int i)
{
    int cnt = 10;
    while (cnt)
    {
        printf("I am child%d: %d, ppid:%d\n", i, getpid(), getppid());
        sleep(1);
        cnt--;
    }
}

int main()
{
    int i = 0;
    for (; i < N; i++)
    {
        pid_t id = fork();
        if (id == 0)
        {
            runChild(i);
            exit(0);//退出进程
        }
    }

    sleep(100);
    return 0;
}

        运行结果:

        从结果可以发现,第二个被创建出来的子进程是这5个子进程中最后被运行的,所以进程的执行顺序完全是由调度器决定的,并不是由创建的顺序决定的。 

2、进程终止

        终止一个进程用的是exit函数,但是在某些场景下用return语句也可以起到终止进程的作用,exit的用法如下:

#include <unistd.h>

void exit(int status);//status表示退出码

        return和exit的区别在于return用于结束一个函数,而exit用于结束一个进程,若一个进程中调用了某个函数,在该函数内用return则不会结束该进程,只会结束调用的函数,该进程后续的代码会正常执行,但是在该函数中调用exit则直接结束该进程,该进程后面的代码都不会执行。

        示例代码如下:

#include <stdio.h>
#include <unistd.h>

#define N 5

void runChild(int i)
{

    printf("I am child%d: %d, ppid:%d\n", i, getpid(), getppid());
    sleep(1);
    exit(0); // 退出进程
}

int main()
{
    int i = 0;
    for (; i < N; i++)
    {
        pid_t id = fork();
        if (id == 0)
        {
            runChild(i);
            printf("此处代码不会被执行\n");
            printf("此处代码不会被执行\n");
            printf("此处代码不会被执行\n");
        }
    }

    sleep(1);
    return 0;
}

        运行结果:


        return和exit相同的点在于他们都会有一个退出码,那么退出码的意义是什么呢,进程运行结束会有三种状态:1、代码正常退出,结果也是正确的,2、代码正常退出、但是结果不正确,3、异常退出。

        这里主要谈进程的退出码,因为函数的退出码我们都知道是调用者接收并对该函数运行结果做出关心,而进程的退出码是该进程的父进程接收,因为父进程要关心子进程的结果,唯一方式就是通过子进程的退出码,只要能把退出码给到父进程,则子进程用return还是exit都可以。

2.1 echo指令查看退出码

        实际上在Linux下执行的每一个可执行程序都是系统分配给我们的子进程来完成的,我们只需要对该子进程进行发送指令即可,Linux下一切皆为文件,所以每个指令执行完毕后都会有退出码,因此我们可以用echo &?来查看上一条指令的退出码。 

        示例如下:

        0通常表示进程是正常退出,且结果正确,当指令报错时说明进程的退出码不是0,出现报错时也可以用echo &?来查看报错信息对应的退出码。


        执行一个可执行程序时,该程序的退出码依然可以用指令echo &?查到,代码如下:

#include <stdio.h>

int main()
{

  printf("模拟一个程序的退出码\n");
  return 1; // 进程的退出码,表征进程的运行结果是否正确。
}

        运行结果:

        可以看到,因为程序的退出码是1,因此echo &?打印出来的就是1,这里用exit(1)效果也是一样的,因为他们的目的都是把退出码带出来。 

2.2 strerror 

        因为退出码是给系统看的,人看退出码获取到的信息不够具体,因此在某些函数调用出错时常常将函数的退出码放入到函数strerror中,函数strerror会根据退出码以字符串形式返回错误信息,使用strerror来查看退出码对应的错误信息具体有哪些:

#include <string.h>

char *strerror(int errnum);//参数是退出码

        使用strerror的测试代码如下:

#include <stdio.h>
#include <string.h>

int main()
{
  int i = 0;
   for(; i < 200; i++)
   {
       printf("%d: %s\n", i, strerror(i));
   }
   return 0; // 进程的退出码,表征进程的运行结果是否正确. 0->success
}

        运行结果(节选一部分):

2.3 error 

        error是一个全局变量,是系统提供给用户的错误码,当调用某些系统接口或者库接口时,若出现错误则系统会自动把错误码码设置到error里,然后用户就可以拿着error传递给strerror打印出错误信息了,方便用户快速的查看错误信息。 

        测试代码如下:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

int main()
{
    int ret = 0;
    char *p = (char *)malloc(1000 * 1000 * 1000 * 4);
    if (p == NULL)
    {
        printf("malloc error, %d: %s\n", errno, strerror(errno));
        ret = errno;
    }
    else
    {
        // 使用申请的内存
        printf("malloc success\n");
    }

    return ret;
}

        运行结果:

        从结果可以看到,malloc出现错误时会把错误码写进error中,因为malloc没有退出码,他返回的只是一个指针,所以他只能用error来告诉程序员错误信息。

2.4 error的意义

1、可以帮助程序员方便、快速查看错误信息。

2、告诉父进程具体的退出码是多少,比如一个进程的内部调用函数出了问题,那么该进程为了拿到函数出现的错误码,就可以通过error拿到错误码并且返回给父进程,这样父进程就知道了该进程出错的原因了。

2.4 异常情况 

        上述谈论了进程退出三种状态的前两种,还有一种状态就是进程异常退出,异常退出表示程序运行过程中肯定出问题了,即发生异常后,退出码就没有意义了。所以一个程序运行完毕后先关心该程序是否因为异常而退出,然后才关心该程序是否正确退出,若不正确退出才关心退出码(因为正确无需关心退出码)

        而进程出现异常的本质是收到了信号,当对一个进程发送信号,则该进程就会因为信号而异常退出,比如经典了kill指令就是给进程发送信号强行终止进程,信号列表如下:

        进程因为信号退出的示例代码:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

int main()
{
  int ret = 0;
   char *p = (char*)malloc(1000*1000*1000*4);
   int a = 10;
   a/=0;//执行到此处进程就会因为异常而退出
   if(p == NULL)
   {
      printf("malloc error, %d: %s\n", errno, strerror(errno));
      ret = errno;
   }
   else{
      // 使用申请的内存
      printf("malloc success\n");
   }

   return ret;
}

        运行结果:

        并且此时子进程的退出码是无效的136,因为该进程是因为8号信号退出的,并不是正常退出。 

3、进程等待 

         进程等待指的是当子进程运行结束时子进程必须要看到父进程在“等他”,施行进程等待的两个必要原因:1、上文提到子进程结束时必须给父进程“一个交代”,即父进程要知道派发给子进程的任务最终有没有完成或者有没有出问题,所以父进程必须在子进程结束的时候做一个等待的动作。2、如果子进程退出了,但是父进程没有等待,就会导致子进程变成僵尸进程,从而浪费不必要的空间资源。

        父进程实现等待的接口:

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

 wait的返回值,如果wait等待成功则返回等待的子进程的pid。wait等待失败则返回小于0。
 status是一个输出型参数,用来记录子进程退出的状态,和waitpid中的第二个参数效果一样。
 pid_t wait(int *status);
///
 返回值:
 正常返回时,waitpid返回等待到的子进程的进程pid;
 如果设置了选项WNOHANG,而调用中waitpid发现没有可等待的子进程,则返回0;
 如果调用中出错,则返回-1,并设置错误码error;

 参数:
pid:
 pid=-1,等待任一个子进程。与wait效果一样。
 pid>0.等待其进程ID与pid相等的子进程。
status(对应的宏函数):
 WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
 WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
options:
 WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进
 程的ID。

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

3.1 wait 

        wait函数进行进程等待测试代码:

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

int main()
{
    pid_t in = fork();
    if (in == 0)
    {
        int count = 5;
        while (count--)
        {
            printf("这是一个子进程:PID: %d PPID:%d\n", getpid(), getppid());
            sleep(1);
        }
        exit(1);
    }
    else
    {
        printf("这是一个父进程:PID: %d PPID:%d\n", getpid(), getppid());
        sleep(8);
        printf("开始等待子进程\n");
        pid_t child1 = wait(NULL);
        printf("等待结束,等待的子进程pid为:%d\n",child1);
    }
    sleep(3);
    return 0;
}

        注意:若传递NULL给到wait的形参,则说明父进程不关心子进程的结束信息。 

        运行结果:

        wait的目的就让父进程阻塞在该函数处等待子进程,先把子进程的资源回收了,然后再继续执行后续代码,所以wait函数被调用的时候若子进程还未变成僵尸状态则父进程就会一直阻塞在wait函数处。

3.2 waitpid

        waitpid也是等待函数,但他较wait更加灵活也更精准,因为wait有可能会让父进程阻塞住,父进程被阻塞了也会损耗效率,而waitpid可以让父进程在等待的过程中去执行其他代码,把这种模式叫做非阻塞轮询,所以waitpid更加灵活,并且waitpid可以指定等待某个子进程,当waitpid的参数如下时,他的作用和wait一模一样。

        waitpid的第二个参数status是一个输出型参数,和wait一样,用于接收子进程退出的退出码和异常信息,只不过status存储信息的方式不是用整形的值来表示的。

3.3 status位图结构 

         status不能单纯的看作是一个整形值,实际上他是一个位图,他的二进制位被分成了三个部分:退出状态、core dump、终止信号,示意图如下(虽说有32个bit位,但是只用到了他的低16位):

         若用上述的结构来存储子进程的退出信息,那么三个部分存储两个独立信息就绰绰有余了,其中退出码的值对应9-15bit位,若收到信号则对应0-7bit位。

        至于core  dump标志位表示是否生成core dump文件,core dump文件是当前程序收到某种信号时(或者是程序直接被终止)自动生成的内存映像,作用就是根据core dump文件可以方便调试和代码分析,core dump标志位为1表示程序崩溃时会生成core dump文件。


        综上所述,因为status特殊的结构,所以拿到子进程的退出信息时不能直接打印status来获取信息,必须要获得其中的通过位运算来获取信息,但是系统提供了两个更简便的方法:宏函数。

WIFEXITED(status):若是正常退出,则该函数为真。(查看进程是否是正常退出)
WEXITSTATUS(status):若WIFEXITED为真,提取子进程退出码。(查看进程的退出码)

        用以上两个宏就可以拿到子进程的退出码了,前提是正常退出,若是收到信号异常退出,则可以用WTERMSIG(status)获取具体信号值,示例代码如下:

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

#define N 3

void RunChild()
{
    int cnt = 15;
    while (cnt)
    {
        printf("I am Child Process, pid: %d, ppid:%d\n", getpid(), getppid());
        sleep(1);
        cnt--;
    }
}

int main()
{
    for (int i = 0; i < N; i++)
    {
        pid_t id = fork();
        if (id == 0)
        {
            RunChild();
            exit(i);
        }
        printf("create child process: %d success\n", id); // 这句话只有父进程才会执行
    }

    // sleep(10);

    // 等待
    for (int i = 0; i < N; i++)
    {
        // wait当任意一个子进程退出的时候,wait回收子进程
        // 如果任意一个子进程都不退出呢?
        // pid_t id = wait(NULL);
        int status = 0;
        pid_t id = waitpid(-1, &status, 0);
        if (id > 0)
        {
            if (WIFEXITED(status))//判断代码是否正常退出
            {
                printf("进程是正常跑完的, 退出码:%d\n", WEXITSTATUS(status));
            }
            else
            {
                printf("进程出异常了,异常信号:%d\n",WTERMSIG(status));
            }
        }
    }
    return 0;

    sleep(5);
}

         运行结果:

3.4 options-非阻塞等待

        上述代码中waitpid的第三个参数options是0表示父进程会阻塞式的等待子进程结束,若options的值是WNOHANG则当子进程没有结束,waitpid()函数返回0,不会一直阻塞式的等待子进程结束。当然,如果父进程一刻也等不了子进程就放弃等待,那么就失去了等待的意义,此时调用waitpid就形同虚设了,所以当options设为WNOHANG时,父进程必须要循环调用waitpid,调用一次waitpid就等于问一次子进程是否结束了,把这种模式叫做非阻塞轮询。

        非阻塞轮询的核心点在于父进程进行轮询的过程中在做其他的事情,将时间利用率发挥到极致,如果只是一味轮询则和阻塞没什么区别,所以这个时间空窗期才是非阻塞轮询和阻塞的区别所在,非阻塞轮询示例代码如下:

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

#define TASK_NUM 10

//模拟父进程的任务
typedef void(*task_t)();
task_t tasks[TASK_NUM];

void task1()
{
    printf("非阻塞轮询期间执行任务1, pid: %d\n", getpid());
}

void task2()
{
    printf("非阻塞轮询期间执行任务2, pid: %d\n", getpid());
}

void task3()
{
    printf("非阻塞轮询期间执行任务3, pid: %d\n", getpid());
}

int AddTask(task_t t);

void InitTask()// 任务的管理代码
{
    for(int i = 0; i < TASK_NUM; i++) tasks[i] = NULL;//初始化数组都为NULL
    AddTask(task1);
    AddTask(task2);
    AddTask(task3);
}

int AddTask(task_t t)//添加任务到任务列表中
{
    int pos = 0;
    for(; pos < TASK_NUM; pos++) {//遍历到空位
        if(!tasks[pos]) break;
    }
    if(pos == TASK_NUM) return -1;//说明列表满了
    tasks[pos] = t;
    return 0;
}

void ExecuteTask()//执行列表中的任务
{
    for(int i = 0; i < TASK_NUM; i++)
    {
        if(!tasks[i]) break;
        tasks[i]();
    }
}

int main()
{
    pid_t id = fork();
    if (id < 0)
    {
        perror("fork");
        return 1;
    }
    else if (id == 0)//子进程
    {
        int cnt = 5;
        while (cnt--)
        {
            printf("I am child, pid:%d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
            sleep(1);
        }
        exit(11);
    }
    else//父进程
    {
        int status = 0;
        InitTask();

        while (1)
        {                                              // 轮询
            pid_t ret = waitpid(id, &status, WNOHANG); // 非阻塞
            if (ret > 0)
            {
                if (WIFEXITED(status))
                {
                    printf("进程是正常跑完的, 退出码:%d\n", WEXITSTATUS(status));
                }
                else
                {
                    printf("进程出异常了,异常信号:%d\n",WTERMSIG(status));
                }
                break;
            }
            else if (ret < 0)
            {
                printf("wait failed!\n");
                break;
            }
            else
            {
                ExecuteTask();
                sleep(1);
            }
        }
    }

    return 12;
}

         运行结果:

        从测试结果可以发现在轮询的期间父进程执行了三个任务,并没有阻塞在waitpid处或者单纯的轮询。 

3.5 小结 

        waitpid和wait的区别在于:waitpid可以在等待子进程结束的这个时间里做一些别的事情而wait不能,wait只能干等进程结束,造成他们之间的差异就是waitpid有第三个参数,可以让waitpid返回等于0的值。

        创建子进程时,为了防止子进程变成孤儿进程,父进程必须要最后结束,有了waitpid和wait后可以保证在多进程当中,一定是父进程先执行并且一定是父进程最后结束。

4、进程替换 

        进程替换指的是当进程从上往下执行代码时,调用了一种exec函数,使得该进程后面的代码和数据都被新的代码、数据替换了,即从exec函数处开始执行新代码,但是进程依旧是原来的进程只是后续的执行逻辑变了。

        进行替换的逻辑示意图如下: 

4.1 execl

        快速使用execl函数测试进程替换的代码:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
    printf("执行execl之前\n");
    execl("/usr/bin/ls","ls","-l",NULL);
    printf("执行execl之后\n");
    return 0;
}

        测试结果:

         从测试结果可以看到,第二句printf没有被打印出来,原因就是当执行到execl并成功发送替换时,该进程的执行流就变成了执行/usr/bin/ls路径下的指令ls -l,并且ls -l指令执行完毕后该进程就正常退出。execl函数的使用方法如下:

int execl(const char *path, const char *arg0, ... /* (char *) NULL */);
//path表示替换后的可执行文件的路径,arg0表示替换后的可执行程序的名称,即和path中的程序名一样
//第三个参数是可变参数(表示可执行程序的选项),但是最终要以NULL结尾

//exec函数成功替换没有返回值,替换失败了才有返回值

4.2 多进程下的进程替换

        在实际使用中,一般都是fork出来的子进程发生进程替换,因为实际生活中,可能有多个不同种类的任务让一个程序处理,若直接在主进程上发生替换,则只能处理一个任务,因此让主进程fork出多个子进程,让子进程去完成任务,最后主进程只需要等待子进程结束即可(因为上文提到进程替换后的进程本身是不会变的,所以子进程即使发生了替换则他还是主进程的子进程)。

        示例代码如下:

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

int main()
{
    pid_t id = fork();
    if(id == 0){ 
        printf("我是一个子进程,pid: %d, ppid:%d\n", getpid(), getppid());
        printf("子进程开始替换\n");
        sleep(5);
        execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
        printf("替换失败\n");
        exit(1);
    }
    //father
    sleep(2);
    printf("父进程开始等待子进程\n");
    pid_t ret = waitpid(id, NULL, 0);
    if(ret > 0) printf("wait success, father pid: %d, ret id: %d\n", getpid(), ret);

    sleep(5);
    return 0;
}

        运行结果:

结语 

        以上就是关于进程控制的全部讲解,通过进程创建,进程终止、进程等待、进程替换这四个步骤就能完整的实现进程控制并将进程的使用性发挥最大。

        最后希望本文可以给你带来更多的收获,如果本文对你起到了帮助,希望可以动动小指头帮忙点赞👍+关注😎+收藏👌!如果有遗漏或者有误的地方欢迎大家在评论区补充,谢谢大家!!  

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

安权_code

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值