• Linux--进程控制(1)


    文章衔接:

    Linux--环境变量-CSDN博客

    Linux--地址空间-CSDN博客

    目录

    1.进程创建

     2.进程的终止

    2.1想明白:终止是在做什么? 

    2.2进程终止的三种情况 

     2.3 进程如何终止

    3.进程等待 (wait/waitpid)


    1.进程创建

            在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。

    1. #include
    2. pid_t fork(void);

    返回值:子进程中返回0,父进程返回子进程id,出错返回-1。

    进程调用fork,当控制转移到内核中的fork代码后,内核做:

    • 分配新的内存块和内核数据结构给子进程
    • 将父进程部分数据结构内容拷贝至子进程
    • 添加子进程到系统进程列表当中
    • fork返回,开始调度器调度

            当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程,看如下程序。

    1. int main(void)
    2. {
    3. pid_t pid;
    4. printf("Before: pid is %d\n", getpid());
    5. if ((pid = fork()) == -1)perror("fork()"), exit(1);
    6. printf("After:pid is %d, fork return %d\n", getpid(), pid);
    7. sleep(1);
    8. return 0;
    9. }

    运行结果:

            这里看到了三行输出,一行before,两行after。进程43676先打印before消息,然后它有打印after。另一个after消息有43677打印的。注意到进程43677没有打印before,为什么呢?如下图所示

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

    关于写时拷贝
            通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:

    fork常规用法

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

    fork也可能创建失败:1.系统中有太多的进程  2.实际用户的进程数超过了限制


     2.进程的终止


    2.1想明白:终止是在做什么? 

    创建进程=创建内核相关管理的数据结构(task_struct,mm_struct,页表)+代码和数据。

    创建进程首先就是要创建相关管理的数据结构,然后将代码和数据加载进来。

    终止的本质:释放曾经的代码和数据所占的空间,释放内核数据结构。task_struct是要被延期处理的,因为进程有一个z(僵尸)状态需要被维护。


    2.2进程终止的三种情况 

    关于退出码:

    先来看一段代码:

    运行结果:

    解释:我们在写C语言程序的时候, 我们习惯的在结束位置写上:return 0,这个操作实际上是将退出码返回给父进程,这个程序的父进程就是bash。$?是bash中储存错误码的变量,他会接收最近的一个子进程退出的退出码(echo是内建命令,可以直接打印bash内部的变量数据),退出码0:表示运行成功,!0:表示失败,1,2,3,4,5,6......不同非0 的值,一方面表示失败,一方面表示失败的原因(都有对应的错位描述)。

    下面这个函数就是将错误码转化为错误信息的

    再看一段带代码:

    运行结果:我们可以看到一共有133条错误信息。父进程bash得到子进程的退出码,就可以知道子进程退出的情况,是成功还是失败,失败的原因是什么。

    当然我们也是可以自定义退出码的,请看下面的一段程序:

    我们使用自定义的方式,来控制我们的返回信息(0:成功  1:除到0了  2 mod 0 了)

    1. // 自定义枚举常量
    2. enum
    3. {
    4. Success = 0,
    5. Div_Zero,
    6. Mod_Zero,
    7. };
    8. int exit_code = Success;
    9. const char *CodeToErrString(int code)
    10. {
    11. switch(code)
    12. {
    13. case Success:
    14. return "Success";
    15. case Div_Zero:
    16. return "div zero!";
    17. case Mod_Zero:
    18. return "mod zero!";
    19. default:
    20. return "unknow error!";
    21. }
    22. }
    23. int Div(int x, int y)
    24. {
    25. if( 0 == y )
    26. {
    27. exit_code = Div_Zero;
    28. return -1;
    29. }
    30. else
    31. {
    32. return x / y;
    33. }
    34. }
    35. int main()
    36. {
    37. int result = Div(10, 100);
    38. printf("result: %d [%s]\n", result, CodeToErrString(exit_code));
    39. result = Div(10, 0);
    40. printf("result: %d [%s]\n", result, CodeToErrString(exit_code));
    41. return 0;
    42. }

    运行结果:第一个用例符合规范测试成功,第二个用除到0了不符合规范,报错。


    我们通过上面的例子知道了进程终止的2种情况:

    a.代码跑完,结果正确 。

    b.代码跑完结果不正确 上面的两种情况,我们都可以通过进程的退出码决定!

    接下来我们来看第三种情况:

    c.代码执行时出现异常,提前退出了  ----操作系统发现你的进程做了不该做的事情,OS杀了进程(一旦出现异常,退出码就没有意义了)

    那如果出现异常了,原因是什么?进程出现异常,本质因为进程收到了OS发给进程的信号,我们通过信号是多少,就可以判断我的进程为什么异常了!!!

    例如野指针问题:

    报错:段错误,OS提前终止进程。

    段错误实际上就是OS给进程发送了kill的11号信号

    演示:让程序持续的打印自己pid

    运行程序几秒后,给16928 执行kill -11号指令,程序报段错误了。

    结论:衡量一个进程退出,我们只需要两个数字:退出码和退出信号!

    PCD(task_struct)是要被延期处理的,因为进程有一个z(僵尸)状态需要被维护,当进程退出时,进程会把退出码和退出信号(exit_code&&exit_signal)写入到PCD(task_struct)去的,让父进程读取。


     2.3 进程如何终止

    a. main函数return,表示进程终止(非main函数,函数结束,但不是进程结束)

    b. 代码调用exit函数--引起一个正常的进程终止exit内部的参数就等于main函数中的退出码

    在任意位置调用exit,都表示进程直接终止

    运行结果:

    c. _exit函数:终止一个调用进程,但这个函数是与

    运行结果:在目前来看,作用似乎和exit相同

    接下来就看一看和exit不一样的地方:

    在睡眠2 的时候printf早就执行完了,将打印的信息存在了缓存区中

    通过下面的对比我们发现:exit会帮我们我们冲刷缓冲区,但_exit不会

    在这里我们重新说明一下:exit是c语言的库函数,_exit是系统调用。所谓的缓冲区不在OS/_exit中,exit实际上调用的就是_exit,所以缓冲区只能在_exit之上。(结论:目前我们所说的缓冲区,不是内核缓冲区!)


    3.进程等待 (wait/waitpid)

    什么是等待?

    任何进程,在退出情况下,一般必须要被父进程等待!

    进程在退出的时候,如果父进程不管不顾,退出的进程就会变为,状态Z(僵尸状态),最终会导致内存泄漏

    为什么要等待?

    1.父进程通过等待,解决进程退出的僵尸问题,回收系统资源(一定要考虑的)

    2.获取子进程的退出信息,知道子进程的退出原因(可选的)

    如何等待?

    这里就要引入两个函数:wait/waitpid(系统调用)

    wait:等待一个子进程状态发生变化,等待成功时,会返回子进程的pid(等待父进程中,任意一个子进程退出)

    接下来看一段代码:

    1. #include
    2. #include
    3. void ChildRun()
    4. {
    5. int cnt = 5;
    6. while (cnt)
    7. {
    8. printf("I am child process, pid: %d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
    9. sleep(1);
    10. cnt--;
    11. }
    12. }
    13. int main()
    14. {
    15. printf("I am father, pid: %d, ppid:%d\n", getpid(), getppid());
    16. pid_t id = fork();
    17. if (id == 0)
    18. {
    19. ChildRun();
    20. printf("child quit ...\n");
    21. exit(0);
    22. }
    23. sleep(10);
    24. // fahter
    25. pid_t rid = wait(NULL);
    26. if (rid > 0)
    27. {
    28. printf("wait success, rid: %d\n", rid);
    29. }
    30. else
    31. {
    32. printf("wait failed !\n");
    33. }
    34. sleep(3);
    35. printf("father quit...\n");
    36. }

    窗口一:while :; do ps ajx | head -1 && ps ajx | grep mytest  | grep -v grep; sleep 1; done

    窗口二:执行该程序

    经过测试我们发现:开始5s子进程运行,退出后处于僵尸状态,父进程等待成功后僵尸不在,结束后父进程退出。(等待成功后,解决子进程僵尸问题)
            如果子进程没有退出,父进程就会进行阻塞等待(等待子进程的变化,实际上就是进程阻塞了),在这个案例中,子进程本质就是软件,父进程本质就是在等待某种软件条件就绪。

    waitpid方法

    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,不予以等待。若正常结束,则返回该子进程的ID。

    获取子进程status

    • wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
    • 如果传递NULL,表示不关心子进程的退出状态信息。
    • 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
    • status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):

    下图是关于status在32位机器下的使用情况,退出码占最后16比特的前八位,后8为则是留给退出信息的

    eg:野指针--段错误(使用位操作来调控status的参数)

    1. void ChildRun()
    2. {
    3. int *p = NULL;
    4. int cnt = 5;
    5. while(1)
    6. {
    7. printf("I am child process, pid: %d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
    8. sleep(1);
    9. cnt--;
    10. *p = 100;
    11. }
    12. }
    13. int main()
    14. {
    15. printf("I am father, pid: %d, ppid:%d\n", getpid(), getppid());
    16. pid_t id = fork();
    17. if(id == 0)
    18. {
    19. // child
    20. ChildRun();
    21. printf("child quit ...\n");
    22. exit(123);
    23. }
    24. sleep(7);
    25. // fahter
    26. //pid_t rid = wait(NULL);
    27. int status = 0;
    28. pid_t rid = waitpid(id, &status, 0);
    29. if(rid > 0)
    30. {
    31. printf("wait success, rid: %d\n", rid);
    32. }
    33. else
    34. {
    35. printf("wait failed !\n");
    36. }
    37. sleep(3);
    38. printf("father quit, status: %d, child quit code : %d, child quit signal: %d\n", status, (status>>8)&0xFF, status & 0x7F);
    39. }

    运行结果:kill的11信号,表示段错误,报错正确。

    eg:使用宏直接调控status的参数

    WIFEXITED判断进程是不是正常结束的,如果想单独知道退出信号还是需要位操作去调控; WEXITSTATUS在WIFEXITED为真的情况下,提取进程的退出码。

    进程正常退出,获取退出码

    在上面我们介绍了阻塞等待( 如果子进程没有退出,父进程就会进行阻塞等待(等待子进程的变化,实际上就是进程阻塞了),在这个案例中,子进程本质就是软件,父进程本质就是在等待某种软件条件就绪。

    接下来我们来了解一下非阻塞等待

            非阻塞等待是一种在编程中使用的机制,允许一个进程或线程在等待某个事件(例如子进程的结束、数据的到达或其他类型的信号)发生时,不会被阻塞或挂起,而是可以继续执行其他任务。当然你可以在一个循环中多次的对子进程进行检测,如果还没好就执行其它的任务,直到子进程准备就绪。非阻塞等待的时候+循环 = 非阻塞轮询(在这种情况下,父进程就可以去进行其它的事情了)

             waitpid函数的第三个参数options:用于修改waitpid的行为。
    为了实现非阻塞等待,我们需要使用WNOHANG这个选项。当设置了WNOHANG时,waitpid会立即返回,无论子进程是否结束。如果子进程已经结束,waitpid会返回子进程的PID;如果子进程还没有结束,waitpid会返回0;如果出错,会返回-1。

    代码演示:

    1. void ChildRun()
    2. {
    3. int cnt = 5;
    4. while(cnt)
    5. {
    6. printf("I am child process, pid: %d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
    7. sleep(1);
    8. cnt--;
    9. }
    10. }
    11. int main()
    12. {
    13. printf("I am father, pid: %d, ppid:%d\n", getpid(), getppid());
    14. pid_t id = fork();
    15. if(id == 0)
    16. {
    17. // child
    18. ChildRun();
    19. printf("child quit ...\n");
    20. exit(123);
    21. }
    22. // father
    23. while(1)
    24. {
    25. int status = 0;
    26. pid_t rid = waitpid(id, &status, WNOHANG); // non block
    27. if(rid == 0)//如果返回子为0说明子进程未就绪,父进程可以执行自己的任务
    28. {
    29. sleep(1);//每1秒检测一次
    30. printf("child is running, father check next time!\n");
    31. //DoOtherThing();
    32. }
    33. else if(rid > 0)
    34. {
    35. if(WIFEXITED(status))
    36. {
    37. printf("child quit success, child exit code : %d\n", WEXITSTATUS(status));
    38. }
    39. else
    40. {
    41. printf("child quit unnormal!\n");
    42. }
    43. break;
    44. }
    45. else
    46. {
    47. printf("waitpid failed!\n");
    48. break;
    49. }
    50. }

    运行结果:

  • 相关阅读:
    EN 14846建筑五金件锁和闩锁—CE认证
    数据爬取京东,按时间划分的,手机价格,销量
    【设计大赛】基于RT-Thread和RA6M4实现samba服务的移动网盘
    代码随想录算法训练营第三十三天 | LeetCode 1005. K 次取反后最大化的数组和、134. 加油站、135. 分发糖果
    五万字图文和代码详解kafka的安装与开启ACL权限控制,自定义SASL、ACL存储形式,实际项目使用案例剖析,kafka常用cmd命令使用总结及示例
    LCM Sum (hard version)(树状数组,筛因子)
    推荐 系统
    vue项目配置MongoDB的增删改查操作
    AEM TESTPRO K50 ROADSHOW华南区路演
    Docker安装Mysql
  • 原文地址:https://blog.csdn.net/2301_76618602/article/details/138115203