当前位置: 首页 > news >正文

【Linux】4.0进程控制

文章目录

    • Linux进程退出
    • Linux进程等待
      • wait( )函数
      • waitpid( )函数
    • 进程程序替换
    • exec*函数系列
    • Makefile创建多个程序
    • mini_shell
      • 内建命令和第三方命令

Linux进程退出

  1. 代码运行完毕,结果正确
  2. 代码运行完毕,结果错误
  3. 代码异常终止

进程退出码:传递进程退出时的状态信息
获取指令:echo $?

#include <stdio.h>
int main()
{
  printf("hello world\n");
  return 123;   //main函数return的值就是进程的退出码
}
[clx@VM-20-6-centos process_control]$ make
gcc -o my_process my_process.c -std=c99
[clx@VM-20-6-centos process_control]$ ./my_process
hello world
[clx@VM-20-6-centos process_control]$ echo $?
123

写一个小程序,当程序运行起来时就成为一个进程,当程序运行结束进程就会将其的退出码传递给它的父进程,我们可以通过指令 echo $? 来查看最近的一个进程结束的退出码

作用: 进程退出码一般来说0表示success ,非零表示failed。非零时有很多程序退出码,不同退出码代表不同的信息来反映进程退出时出错的一种可能性

exit()和 _exit()
exit() :在任何地方调用,都代表终止进程,参数是退出码!
_exit():强制终止进程,不进行进程的后续收尾工作(执行用户定义的清理函数,刷新用户级缓冲区)

进程退出,OS做了什么?
系统层面少了一个进程, 需要free PCB(进程控制块) , free mm_struct(进程地址空间), free 页表和各种映射关系, 还有代码和数据

Linux进程等待

在这里插入图片描述
为何需要进程等待?
1.通过获取子进程的推出信息,才能得到子进程的执行结果
2.可以保证:时序问题,子进程先退出,父进程后退出
3.进程退出时若退出信息未被接受,会进入僵尸状态,造成内存泄漏问题,需要通过父进程wait接收子进程的退出信息,然后释放该子进程占用的资

wait( )函数

pid_t wait(int *status); //返回子进程的pid
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>


int main()
{
  pid_t id = fork();             //创建一个运行五秒的子进程
  if (id == 0)
  {
    //child
    int count = 5;
    while (count){
      printf("I am  child. my pid = %d, count = %d\n", getpid(), count);
      sleep(1);
      count--;
    }
    exit (10);
  }
  //父进程先睡十秒
  sleep(10);
  pid_t ret = wait(NULL);
  printf("I'm father\n");
  if (ret){
    printf("Father wait success\n");
  }
  else{ 
    printf("wait failed\n");
  }
  sleep(5); //再睡五秒钟
  return 0;
}

以上这个二十秒的小程序有三个阶段。
第一个阶段(前五秒):父子都在运行,子进程在打印内容,父进程休眠中
第二个阶段(五到十秒):子进程已经执行完毕,成为僵尸进程,父进程还在执行sleep指令
第三个阶段(十秒到二十秒):十秒开始父进程接收子进程信息,子进程退出成功,然后父进程继续沉睡十秒,这时候只有一个进程

设计脚本调出监控窗口看一下
脚本代码

while :; do ps ajx | head -1 && ps ajx | grep clx_proc |grep -v grep; sleep 1; echo "############################################################"; done 
//前五秒
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
13685 20099 20099 13685 pts/4    20099 S+    1001   0:00 ./clx_proc
20099 20100 20099 13685 pts/4    20099 S+    1001   0:00 ./clx_proc
############################################################
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
13685 20099 20099 13685 pts/4    20099 S+    1001   0:00 ./clx_proc
20099 20100 20099 13685 pts/4    20099 S+    1001   0:00 ./clx_proc
############################################################
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
13685 20099 20099 13685 pts/4    20099 S+    1001   0:00 ./clx_proc
20099 20100 20099 13685 pts/4    20099 S+    1001   0:00 ./clx_proc
############################################################
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
13685 20099 20099 13685 pts/4    20099 S+    1001   0:00 ./clx_proc
20099 20100 20099 13685 pts/4    20099 S+    1001   0:00 ./clx_proc
############################################################
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
13685 20099 20099 13685 pts/4    20099 S+    1001   0:00 ./clx_proc
20099 20100 20099 13685 pts/4    20099 S+    1001   0:00 ./clx_proc
############################################################
//第六秒,子进程进入僵尸状态
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
13685 20099 20099 13685 pts/4    20099 S+    1001   0:00 ./clx_proc
20099 20100 20099 13685 pts/4    20099 Z+    1001   0:00 [clx_proc] <defunct>
############################################################
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
13685 20099 20099 13685 pts/4    20099 S+    1001   0:00 ./clx_proc
20099 20100 20099 13685 pts/4    20099 Z+    1001   0:00 [clx_proc] <defunct>
############################################################
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
13685 20099 20099 13685 pts/4    20099 S+    1001   0:00 ./clx_proc
20099 20100 20099 13685 pts/4    20099 Z+    1001   0:00 [clx_proc] <defunct>
############################################################
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
13685 20099 20099 13685 pts/4    20099 S+    1001   0:00 ./clx_proc
20099 20100 20099 13685 pts/4    20099 Z+    1001   0:00 [clx_proc] <defunct>
############################################################
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
13685 20099 20099 13685 pts/4    20099 S+    1001   0:00 ./clx_proc
20099 20100 20099 13685 pts/4    20099 Z+    1001   0:00 [clx_proc] <defunct>
############################################################
//第十秒开始只有一个进程正在运行
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
13685 20099 20099 13685 pts/4    20099 S+    1001   0:00 ./clx_proc
############################################################
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
13685 20099 20099 13685 pts/4    20099 S+    1001   0:00 ./clx_proc
############################################################
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
13685 20099 20099 13685 pts/4    20099 S+    1001   0:00 ./clx_proc
############################################################
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
13685 20099 20099 13685 pts/4    20099 S+    1001   0:00 ./clx_proc
############################################################
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
13685 20099 20099 13685 pts/4    20099 S+    1001   0:00 ./clx_proc
############################################################
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
############################################################
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
############################################################
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
############################################################
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
############################################################
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
^C

以上即是wait的功能介绍,wait可以使得父进程可以接收子进程退出信息,防止出现内存泄漏存

waitpid( )函数

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

参数介绍:
pid:接收子进程的pid,若输入-1,则可以wait任意一个子进程
status:状态信息
在这里插入图片描述
最后的7位代表收到的信号,倒数第八位是core dump标志,倒数第九位到第十六位则对应我们的退出码

  int status = 0;   //创建变量status
  pid_t ret = waitpid(id, &status, 0);   //用status的地址当作形参传给waitpid
  printf("I'm father\n");
  if (ret){
    printf("Father wait success\n");
    printf("child pid = %d, exit = %d signal = %d\n", ret, (status >> 8) & 0xFF, status& 0x7f);
    //(status >> 8) & 0xFF 取次八位的值  退出码
    //status & 0x7F        取最后七位的值 信号
  }

为了更为遍历,C语言还提供了一组宏来帮助我们提取status的信息
WIFEXITED(status):进程正常终止返回真,若进程异常终止返回假
WEXITSTATUS(status):若WIFEXITEND为真,则提取进程退出的退出码
以下是两个宏的用法

int main()
{
  pid_t id = fork();
  if (id == 0)
  {
    //child
    int count = 5;
    while (count){
      printf("I am  child. my pid = %d, count = %d\n", getpid(), count);
      sleep(1);
      //int a = 100/ 0 ; //设计错误,这样代码就会收到信号造成异常退出
      count--;
    }
    exit (10);
  }
  int status = 0;
  pid_t ret = waitpid(id, &status, 0);
  printf("I'm father\n");
  if (ret){
    if (WIFEXITED(status)) //若运行成功退出,则可以提取进程退出码
    {
      printf("exit code = %d\n", WEXITSTATUS(status));
    }
    //若异常退出说明进程收到信号异常终止,输出收到的信号
    else {
      printf("catch a singal = %d\n", status & 0x7f);
    }
  }
  else{ 
    printf("wait failed\n");
  }
  return 0;
}

options:默认行为
当输入0时,进行阻塞等待
当输入WNOHANG时,进行非阻塞等待,循环控制

阻塞的本质:其实是进程的PCB被放入等待队列,并将进程的状态设置成S状态
返回的本质:进程的PCB从等待队列中放出,被CPU进行调度

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

int main()
{
  pid_t id = fork();
  if (id == 0)
  {
    //child
    int count = 5;
    while (count){
      printf("I am  child. my pid = %d, count = %d\n", getpid(), count);
      sleep(1);
      count--;
    }
    exit (10);
  }
  int status = 0;
  while (1)
  {
    pid_t ret = waitpid(id, &status, WNOHANG);
    if (ret == 0)
    {
      //子进程还未退出,继续等待
      printf("father do something\n");
      sleep(2);
    }
    else if (ret > 0)
    {
      //子进程退出,获取退出信息
      printf("father wait : %d success, status exit cond = %d, status exit singal = %d\n", ret, WEXITSTATUS(status), status & 0x7f);
      break;
    }
    else 
    {
      //waitpid本身有问题,返回-1 退出
      printf("wait fail\n");
      break;
    }
  }

输出的结果

[clx@VM-20-6-centos process_control]$ make
gcc -o clx_proc clx_proc.c -std=c99
[clx@VM-20-6-centos process_control]$ ./clx_proc.c
-bash: ./clx_proc.c: Permission denied
[clx@VM-20-6-centos process_control]$ ./clx_proc
father do something
I am  child. my pid = 4874, count = 5
I am  child. my pid = 4874, count = 4
father do something
I am  child. my pid = 4874, count = 3
I am  child. my pid = 4874, count = 2
father do something
I am  child. my pid = 4874, count = 1
father wait : 4874 success, status exit cond = 10, status exit singal = 0

进程程序替换

概念:进程不变,仅仅替换当前进程的代码和数据
价值:想让子进程执行一个全新的程序
C/C++程序想要运行,则需要将代码和数据加载到内存中,即特定进程的上下文中,如何加载呢?
使用的就是加载器(exec* 程序替换函数)

#include <stdio.h>    
#include <stdlib.h>    
#include <string.h>    
#include <unistd.h>    
#include <sys/types.h>    
#include <sys/wait.h>    
    
int main()    
{    
  pid_t id = fork();    
  if (id == 0)    
  {    
    sleep(5);    
    printf("I'm a process, pid = %d\n", getpid());    
    execl("/usr/bin/ls", "ls", "-a", "-l", NULL);    
    printf("hello world\n");    
    printf("hello world\n");    
    printf("hello world\n");    
    printf("hello world\n");    
    exit(1);                                                                                                                                                                            
  }    
  while (1)    
  {    
    printf("I'm father\n");    
    sleep(1);    
  }    
  pid_t ret = waitpid(id, NULL, 0);    
  printf("wait success , child pid = %d", ret);    
  return 0;    
    
}    

运行结果

I'm father
I'm father
I'm father
I'm father
I'm father
I'm father
I'm a process, pid = 13236
total 28
drwxrwxr-x 2 clx clx 4096 Oct 17 17:30 .
drwxrwxr-x 3 clx clx 4096 Oct 17 10:02 ..
-rwxrwxr-x 1 clx clx 8664 Oct 17 17:30 clx_proc
-rw-rw-r-- 1 clx clx 2208 Oct 17 17:30 clx_proc.c
-rw-rw-r-- 1 clx clx   81 Oct 17 15:26 Makefile
I'm father
I'm father
I'm father
I'm father
I'm father

当子进程调用进程替换函数后,父进程不受影响,这是因为进程之间具有独立性。
当进程程序替换时,会修改代码区的代码,所以会进行写时拷贝

只要进程的程序替换成功,那么就不会执行后续代码,意味着exec函数,成功的时候并不需要返回值检验,因为只要exec函数返回了,那么就一定是调用失败了

int main()    
{    
  printf("I'm a process, pid = %d\n", getpid());    
  execl("/usr/bin/llllls", "ls", "-a", "-l", NULL);                                                                                                                                     
  exit(1);   //若程序走到这里一定是调用失败了,直接返回错误码表示错误
}    

exec*函数系列

先以execl举例
在这里插入图片描述

//库函数
EXEC(3)                                                                      Linux Programmer's Manual                                                                      EXEC(3)

NAME
       execl, execlp, execle, execv, execvp, execvpe - execute a file

SYNOPSIS
       #include <unistd.h>

extern char **environ;
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg,..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],char *const envp[]);
//系统调用接口
EXECVE(2)                                                                    Linux Programmer's Manual                                                                    EXECVE(2)

NAME
       execve - execute program

SYNOPSIS
       #include <unistd.h>

       int execve(const char *filename, char *const argv[],
                  char *const envp[]);

可以看到其实系统调用接口只有一个,但是C语言却提供了六个调用函数,C语言的调用接口本质上是对系统调用接口的一个封装,方便我们不同环境下使用进程程序替换

命名规律:
" l " : 可变列表参数一个个输入
" v ": 可变列表参数使用数组传入
" p ": 自动搜索环境变量PATH
" e ":表示自己维护环境变量

带 l 和带 v 的函数非常相近,只是一个直接传参,一个将参数放在数组中了

//以下是exec* 系列函数的调用
int main()
  8 {
  9   if (fork() == 0)
 10   {
 11     printf("I' m child, pid = %d\n", getpid());
 12     //char* argv[] = {"ls", "-a", "-i", "-l", NULL};  //将可变参数列表放在数组中
 13     //execvp("ls", argv);                             //带有p会直接到环境变量中查找指令
 14     //execlp("ls", "ls", "-a", "-i", "-l", NULL);
 15     //execv("/usr/bin/ls", argv);
 16     //execl("/usr/bin/ls", "ls", "-a", "-i", "-l", NULL);
 17     //execl("./my_exe", "my_exe", NULL);
 18     //char* env[] = {"hello world", NULL};
 19     //execle("./my_exe", "my_exe", NULL, env);
 20     execl("/usr/bin/python3", "python", "test.py", NULL);                                                                                                                           
 21     exit(1);
 22   }
 23   int status = 0;
 24   pid_t ret = waitpid(-1, &status, 0);
 25   if (ret > 0 && WIFEXITED(status))
 26   {
 27     printf("child exit code = %d, catch singal = %d\n", WEXITSTATUS(status), status & 0x7f);
 28     printf("waitpid success\n");
 29   }
 30   return 0;
 31 }

这里重点说一下execle函数

int main()    
  {    
    if (fork() == 0)    
    {    
      printf("I' m child, pid = %d\n", getpid());    
      char* env[] = {"env_val1 = hello world", "env_val2 = hello linux", NULL};    
      execle("./my_exe", "my_exe", NULL, env);     //文件所在路径就在./中
      exit(1);    
    }    
    int status = 0;    
    pid_t ret = waitpid(-1, &status, 0);    
    if (ret > 0 && WIFEXITED(status))    
    {    
      printf("child exit code = %d, catch singal = %d\n", WEXITSTATUS(status), status & 0x7f);    
      printf("waitpid success\n");    
    }                                                                                                                                                                                   
    return 0;    
  }    

//my_exe.c  //用于打印环境变量
#include <stdio.h>    
    
int main()    
{    
  extern char** environ;    
  for (size_t i = 0; environ[i]; i++)    
  {    
    printf("%s\n", environ[i]);                                                                                                                                                         
  }    
  return 0;    
}    

运行程序

[clx@VM-20-6-centos process_replace]$ ./my_test
I' m child, pid = 29917
env_val1 = hello world
env_val2 = hello linux
child exit code = 0, catch singal = 0
waitpid success

可以看到程序将我们自己传入的环境变量打印了出来,于带p的函数不同,其使用的是我们自己传入的环境变量

Makefile创建多个程序

在exec*系列函数的execle实例中我们调用了自己的my_exe可执行程序,那么如何使用Makefile一次性创建多个可执行程序呢?

.PHONY:all      //使用伪目标all
all:my_exe my_test 
	
my_exe:my_exe.c
		gcc -o $@ $^ -std=c99
my_test:my_test.c
		gcc -o $@ $^ -std=c99
.PHONY:clean
clean:
		rm -f my_test my_exe

我们可以使用伪目标all,生成all需要两个可执行程序,所以Makefile就会自动去生成两个可执行程序。如果想要生成更多的可执行文件,只需要将文件名写在all后面就行了

mini_shell

为了能更深刻的理解进程程序替换,我们写一个迷你版的shell脚本来加深印象

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

#define MAX_LEN   128//指令的最大长度
#define CMD_NUM   64//指令可包含最大参数个数

int main()
{
  char command[MAX_LEN] = {0};
  while (1)
  {
    char* argv[CMD_NUM] = {NULL};
    command[0] = 0;
    //1.打印提示符
    printf("[clx@VM-20-6-centos my_shell]$");
    fflush(stdout); //刷新缓冲区
    //2.接收命令行
    fgets(command, MAX_LEN, stdin);
    command[strlen(command) - 1] = 0; //"ls -a\n" \n的下标为5 字符串长度为6
    //3.解析命令行
    const char* sep = " ";
    argv[0] = strtok(command, sep);
    size_t i = 1;
    while (argv[i] = strtok(NULL, sep)) {i++;}
    //4.检测命令是否是shell本身执行的,内建命令
    if (strcmp(argv[0], "cd") == 0)
    {
      if (argv[1] != NULL)
      {
        chdir(argv[1]);
        continue;
      }
    }
    //5.创建子进程执行第三方命令
    if (fork() == 0)
    {
      //child
      execvp(argv[0], argv);
      exit(1);
    }
    //父进程进行夯等待
    waitpid(-1, NULL, 0);
  }
  return 0;
}

整个程序思路很简单,但是其中调用的几个函数需要注意
1.fflush() :刷新缓冲区不换行,若直接使用\n会导致换行输入命令,与实际的shell脚本不符合
2.fgets() :获取一行字符串,第一个参数接收字符串,第二个是字符串的最大长度,第三个表示输入流
3.strtok:这个实在C语言字符操作里有的,第一个参数是一个字符串,第二个参数是分割符。内部有个static全局变量来记录分割位置,第二次操作第一个参数只需要传NULL就可以继续向后切割了

若还不清楚可以调用man手册查阅文档

内建命令和第三方命令

内建命令就是进程改变自己的命令:如cd …代表进程需要修改自己的所在路径。若使用子进程进行调用,因为进程相互之间具有独立性,则将修改子进程的所在路径,父进程路径不变
第三方命令:与当前进程无关,并不需要修改当前进程的数据的命令,大多放置在usr/bin目录中,可以自行查看

相关文章:

  • 【SpringBoot】00 Maven配置及创建项目
  • Vue Router与Vite的无缝集成
  • chrome 查看版本安装路径、cmd命令行启动浏览器
  • 比 PSD.js 更强的下一代 PSD 解析器,支持 WebAssembly
  • java 中String、StringBuffer、StringBuilder有什么区别
  • PotatoPie 4.0 实验教程(21) —— FPGA实现摄像头图像二值化(RGB2Gray2Bin)
  • 如何在Spring Boot应用中进行文件预览?
  • WSL里的Ubuntu 登录密码忘了怎么更改
  • React最常用的几个hook
  • 【无标题】积鼎CFD VirtualFlow:航空及汽车燃油晃动流体仿真计算及试验对比
  • vue3+electron开发桌面应用,静态资源处理方式及路径问题总结
  • [rust] 10 project, crate, mod, pub, use: 项目目录层级组织, 概念和实战
  • 【Python应用】自制截图取词小工具-- 解锁文字识别新姿势
  • MYSQL学习笔记(DDL[数据定义]、DML[数据操作]、DQL[数据查询])
  • SpringCloud学习笔记
  • FFT 快速傅里叶变换 NTT 快速数论变换
  • 王传福赚嗨了!比亚迪单季利润超50亿 还大量出口到欧洲
  • 带你趣学算法
  • 设计模式(四)—— 装饰者模式
  • [架构之路-48]:目标系统 - 系统软件 - Linux下的网络通信-4-快速数据平面开发套件DPDK-工作原理
  • 创意电子学00课:需要准备的材料汇总
  • 什么是轨到轨?这种运放和普通运放比有什么特点和优点?
  • 面试官:小伙子,说说C/C++是如何进行内存管理的?我:……
  • STL容器 —— map和set的模拟实现
  • 软考知识点---06文件管理与作业管理---01文件管理
  • API网关基础认知
  • 腾讯云Ubuntu18.04配置深度学习环境
  • 反常积分敛散性的比较判别法专题(及常用反常积分)
  • pythond大屏可视化
  • 知识图谱-生物信息学-医学论文(Chip-2022)-BCKG-基于临床指南的中国乳腺癌知识图谱的构建与应用
  • 力扣 每日一题 902. 最大为 N 的数字组合【难度:困难,rating: 1989】(数学 / 数位dp)
  • 奇迹mu服务器安全和优化设置