• Linux:进程控制


    进程创建

    fork 函数

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

    #include

    pid_t fork(void);

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

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

    进程:内核的相关管理数据结构(task_struct+mm_struct+页表)+代码(共享)和数据(写时拷贝)

    fork 常规用法

    一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求

    一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数

     

    进程终止 

    终止是在做什么?

    释放曾经的代码和数据所占据的空间,释放内核数据结构 (注意:僵尸进程不会释放内核数据结构,只释放代码和数据所占据的空间)

    进程退出场景

    • 代码运行完毕,结果正确
    • 代码运行完毕,结果不正确
    • 代码异常终止

    进程常见退出方法

    正常终止

    (可以通过 echo $? 查看最近一个子进程退出码)

    1. 从main返回 (return)

    2. 调用exit

    3. _exit


    父进程bash获取到最近一个子进程退出的退出码

    告诉父进程,子进程的任务完成的怎么样,知道子进程(成功,失败:原因)

    0:代表成功

    !0:代表失败,[0-255]:不同的失败原因 

    exit 

    我们的代码任意位置调用exit,都表示进程退出

    参数:status 定义了进程的终止状态,父进程通过wait来获取该值

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

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

    _exit

    void _exit(int status);

    参数: status 定义了进程的终止状态,父进程通过 wait 来获取该值

    相比exit,_exit不会刷新缓冲区


    异常终止

    操作系统发现了你的进程做了不该做的事,OS杀了进程

    一旦出现异常,退出码就没有意义了

    进程出现异常,本质是:因为进程收到了OS发给进程的信号

    我们可以看进程退出的时候,退出信号是多少,就可以判断我的进程为什么异常了

    衡量一个进程的退出,父进程bash只需要两个数字: 退出码和退出信号 


    进程等待 

    进程等待的方法 

    wait 方法

    子进程本身就是软件,父进程本质是在等待某种软件条件就绪

    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 。、

    • 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退 出信息。
    • 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞
    • 如果不存在该子进程,则立即出错返回。

      options==0,阻塞等待:子进程没有等到时,会一直堵塞等待

      options==WNOHANG,非阻塞等待:对子进程进行检测,子进程没退出,直接返回0

      所以,options==0,返回值只有>0和<0两种情况

      optioons==WNOHANG,返回值有>0和<0和==0三种情况

    ptioons==WNOHANG,返回值有>0和<0和==0三种情况

    非阻塞等待的时候+循环=非阻塞轮询

    非阻塞等待允许父进程做一些其他事情

    获取子进程status

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

    子进程退出信息:进程退出码+退出信号

    这样单独取到 进程退出码和退出信号 利用WIFEXITED(status)和WEXITSTATUS(status)判断和提取退出码

    进程程序替换 

    替换原理

    用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec*并不创建新进程,所以调用exec前后该进程的id并未改变。

    进程=内核数据结构+代码和数据,只是进程的程序替换,没有创建新的进程,本质就是被替换进程的程序被加载到内存了

    父进程的代码和数据本来是和子进程共享的,但由于进程程序替换,子进程后面替换的程序用的是替换的代码和数据,在物理内存上重新开辟一块空间放置新的代码和数据

    替换函数

    • path:我们要执行的程序,需要带路径(怎么找到程序)
    • file:用户可以不传要执行的文件的路径(但文件名要传)
    • argu:在命令行中怎么执行你就怎么传参 最后一个要传NULL
    • argu[ ]: 用一个指针数组分别装命令字符串,最后一个空间传NULL,然后传给argu[ ]  
    • envp[ ]:整体替换所有环境变量,可以自定义环境变量传入

    l :list 列表

    v:vector 

    p:查找这个程序,系统会自动在环境变量PATH中进行查找

    e:环境变量 

     exec*系列的函数执行完毕后,后续的代码不见了,因为被替换了

    exec*函数的返回值不用关心,只要替换成功,就不会向后继续运行,只要继续运行了,一定是替换失败

    exec*类似Linux上的加载函数

    列如

    putenv

    系统接口函数

    向环境变量表中添加环境变量


    简易的shell (实践)(重要)

    1. #include <stdio.h>
    2. #include <stdlib.h>
    3. #include <string.h>
    4. #include <errno.h>
    5. #include <unistd.h>
    6. #include <sys/types.h>
    7. #include <sys/wait.h>
    8. #define SIZE 512
    9. #define ZERO '\0'
    10. #define SEP " "
    11. #define NUM 32
    12. #define SkipPath(p) do{ p += (strlen(p)-1); while(*p != '/') p--; }while(0)
    13. // 为了方便,我就直接定义了
    14. char cwd[SIZE*2];
    15. char *gArgv[NUM];
    16. int lastcode = 0;
    17. void Die()
    18. {
    19. exit(1);
    20. }
    21. const char *GetHome()
    22. {
    23. const char *home = getenv("HOME");
    24. if(home == NULL) return "/";
    25. return home;
    26. }
    27. const char *GetUserName()
    28. {
    29. const char *name = getenv("USER");
    30. if(name == NULL) return "None";
    31. return name;
    32. }
    33. const char *GetHostName()
    34. {
    35. const char *hostname = getenv("HOSTNAME");
    36. if(hostname == NULL) return "None";
    37. return hostname;
    38. }
    39. // 临时
    40. const char *GetCwd()
    41. {
    42. const char *cwd = getenv("PWD");
    43. if(cwd == NULL) return "None";
    44. return cwd;
    45. }
    46. // commandline : output
    47. void MakeCommandLineAndPrint()
    48. {
    49. char line[SIZE];
    50. const char *username = GetUserName();
    51. const char *hostname = GetHostName();
    52. const char *cwd = GetCwd();
    53. SkipPath(cwd);
    54. snprintf(line, sizeof(line), "[%s@%s %s]> ", username, hostname, strlen(cwd) == 1 ? "/" : cwd+1);
    55. printf("%s", line);
    56. fflush(stdout);
    57. }
    58. int GetUserCommand(char command[], size_t n)
    59. {
    60. char *s = fgets(command, n, stdin);
    61. if(s == NULL) return -1;
    62. command[strlen(command)-1] = ZERO;
    63. return strlen(command);
    64. }
    65. void SplitCommand(char command[], size_t n)
    66. {
    67. (void)n;
    68. // "ls -a -l -n" -> "ls" "-a" "-l" "-n"
    69. gArgv[0] = strtok(command, SEP);
    70. int index = 1;
    71. while((gArgv[index++] = strtok(NULL, SEP))); // done, 故意写成=,表示先赋值,在判断. 分割之后,strtok会返回NULL,刚好让gArgv最后一个元素是NULL, 并且while判断结束
    72. }
    73. void ExecuteCommand()
    74. {
    75. pid_t id = fork();
    76. if(id < 0) Die();
    77. else if(id == 0)
    78. {
    79. // child
    80. execvp(gArgv[0], gArgv);
    81. exit(errno);
    82. }
    83. else
    84. {
    85. // fahter
    86. int status = 0;
    87. pid_t rid = waitpid(id, &status, 0);
    88. if(rid > 0)
    89. {
    90. lastcode = WEXITSTATUS(status);
    91. if(lastcode != 0) printf("%s:%s:%d\n", gArgv[0], strerror(lastcode), lastcode);
    92. }
    93. }
    94. }
    95. void Cd()
    96. {
    97. const char *path = gArgv[1];
    98. if(path == NULL) path = GetHome();
    99. // path 一定存在
    100. chdir(path);//改变当前路径,系统接口
    101. // 刷新环境变量
    102. char temp[SIZE*2];
    103. getcwd(temp, sizeof(temp));//获取当前的绝对路径,系统接口
    104. snprintf(cwd, sizeof(cwd), "PWD=%s", temp);
    105. putenv(cwd); // OK //写入环境变量,系统接口
    106. }
    107. int CheckBuildin()
    108. {
    109. int yes = 0;
    110. const char *enter_cmd = gArgv[0];
    111. if(strcmp(enter_cmd, "cd") == 0)
    112. {
    113. yes = 1;
    114. Cd();
    115. }
    116. else if(strcmp(enter_cmd, "echo") == 0 && strcmp(gArgv[1], "$?") == 0)
    117. {
    118. yes = 1;
    119. printf("%d\n", lastcode);
    120. lastcode = 0;
    121. }
    122. return yes;
    123. }
    124. int main()
    125. {
    126. int quit = 0;
    127. while(!quit)
    128. {
    129. // 1. 我们需要自己输出一个命令行
    130. MakeCommandLineAndPrint();
    131. // 2. 获取用户命令字符串
    132. char usercommand[SIZE];
    133. int n = GetUserCommand(usercommand, sizeof(usercommand));
    134. if(n <= 0) return 1;
    135. // 3. 命令行字符串分割.
    136. SplitCommand(usercommand, sizeof(usercommand));
    137. // 4. 检测命令是否是内建命令
    138. n = CheckBuildin();
    139. if(n) continue;
    140. // 5. 执行命令
    141. ExecuteCommand();
    142. }
    143. return 0;
    144. }

    cd,echo,export等为内建命令 

    然后 shell 读取新的一行输入,建立一个新的进程,在这个进程中运行程序 并等待这个进程束。
    所以要写一个 shell ,需要循环以下过程 :
    1. 获取命令行
    2. 解析命令行
    3. 建立一个子进程( fork )
    4. 替换子进程( execvp )
    5. 父进程等待子进程退出( wait )

  • 相关阅读:
    8.1日java复盘
    sqllab第二十七A关通关笔记
    优先级总结
    单向的2.4G频段RF射频芯片-SI24R2E
    【JAVA数据结构系列】13_ArrayList
    【ARM】CCI集成指导整理
    platform总线
    VUE整合Echarts实现简单的数据可视化
    Ruby网络爬虫教程:从入门到精通下载图片
    基于SpringBoot和Vue的商品秒杀系统设计与实现
  • 原文地址:https://blog.csdn.net/m0_61088872/article/details/138008162