Linux(进程控制)

进程控制

进程创建

fork函数初识

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

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

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

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

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

我们会发现,fork之前,父进程是单独执行的,fork以后父进程和子进程就分流进行执行了,但是要注意的是父子代码共享是fork以后共享,并不是程序所有的都共享,而且fork以后谁先执行是由调度器决定的。

fork函数返回值

那么为什么要给父进程返回子进程PID呢?

一个子进程只能拥有一个父进程,但是一个父进程可以拥有多个子进程,父进程创建子进程是为了给子进程指派任务,返回子进程的PID就可以很好的对诸多子进程进行管理。

为什么fork以后就会有两个返回值呢?

父进程在调用fork函数以后,fork函数就会进行一系列操作,创建子进程PCB,创建子进程虚拟地址空间,创建页表...,也就是说,在return之前,子进程就已经创建完成了,return就需要父进程子进程都执行,而return的本质就是对id的写入,父进程返回一个id,子进程返回一个id,对于父子进程返回的id程序都需要进行执行,所以此时就会有两个返回值。

写时拷贝

父进程创建子进程,并不会对所有代码和数据都进行拷贝,因为有些东西子进程只需要进行读取,并不需要修改,与父进程共享即可,我们只有在需要修改的时候,对数据进行拷贝即可,这种延时拷贝策略,极大的提升了效率。

fork常规用法

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

fork调用失败的原因

  • 系统中有太多的进程。
  • 实际用户的进程数超过了限制。

进程终止

进程退出场景

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

进程退出码

我们平时写代码过程中,一直是return 0,这是为什么呢?main函数也是个函数,系统要调用他,就需要有返回值,而return 0就表示代码执行成功,结果正确,我们一般用非0表示结果不正确,原因在于成功了就成功了,只有一种可能,但是失败确有多种原因。

我们可以使用echo $?命令查看最近一次进程退出的退出码信息:

C语言当中的strerror函数可以通过错误码,获取该错误码在C语言当中对应的错误信息:

实际上我们Linux中各种指令也是可执行程序,我们也可以看见相应的退出码:

进程常见退出方法

正常退出

  1. return退出
    在main函数中使用return终止是我们最常见的方式。
  1. 调用exit
    (1)执行用户通过 at;
    (2) 关闭所有打开的;
    (3) 调用_exit;
    我们要注意,return只能在main函数中退出,在其他位置都是返回值,而exit可以再任意位置退出,包括调用的函数内部。


  1. 调用_exit


我们会发现exit与_exit的区别就是_exit会直接终止进程,不做任何后续处理,而exit会刷新缓冲区。

我们需要知道的是,保存数据的缓冲器并不是操作系统再给我们维护,因为_exit之后,并没有刷新缓冲区,而是C标准库给我们维护的。
异常退出

ctrl + c,信号终止

在进程运行过程中向进程发生kill -9信号使得进程异常退出,或是使用Ctrl+C使得进程异常退出等。

进程等待

进程等待必要性

  • 子进程退出,父进程如果不管不顾,就可能造成'僵尸进程'的问题,进而造成内存泄漏。
  • 进程一旦变成僵尸状态,那就刀枪不入,"杀人不眨眼"的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
  • 父进程派给子进程的任务完成的如何,我们需要知道。子进程运行完成,结果对还是不对, 或者是否正常退出。
  • 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。

获取子进程status

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

在status的低16比特位当中,高8位表示进程的退出状态,即退出码。进程若是被信号所杀,则低7位表示终止信号,而第8位比特位是core dump标志。

进程退出码:(status >> 8) & 0xFF

进程退出信号:status & 0x7F

进程等待的方法

1. wait方法

c 复制代码
pid_t wait(int*status);
//返回值:成功返回被等待进程pid,失败返回-1。
//参数:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL


此时程序处于僵尸状态,父进程并没有对子进程进行回收,当我们使用wait以后:

c 复制代码
  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<stdlib.h>
  4 #include<sys/wait.h>
  5 #include<sys/types.h>
  6 
  7 int main()
  8 {
  9     pid_t id = fork();
 10     if(id == -1)
 11     {
 12         perror("fork()");
 13         return 1;
 14     }
 15     else if(id == 0)
 16     {
 17         int cnt = 5;
 18         while(cnt)
 19         {
 20             printf("I am chlid: cnt:%d, pid:%d, ppid:%d\n", cnt, getpid(), getppid());
 21             sleep(1);
 22             cnt--;
 23         }
 24         exit(1);
 25     }
 26     else
 27     {
 28         pid_t ret = wait(NULL);                                                                          
 29         if(ret > 0)
 30         {
 31             printf("wait child sucess: ret:%d\n", ret);
 32         }
 33         while(1)
 34         {
 35             printf("I am father: pid:%d, ppid:%d\n", getpid(), getppid());
 36             sleep(1);
 37         }
 38     }
 39     return 0;
 40 }


父进程一直在等待当子进程运行完成,子进程运行以后,父进程对子进程进行了回收,此时程序中就只剩下父进程,子进程已经成功被回收了。

2. waitpid方法

c 复制代码
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。

创建子进程后,父进程可使用waitpid函数一直等待子进程(此时将waitpid的第三个参数设置为0),直到子进程退出后读取子进程的退出信息。

c 复制代码
  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<sys/wait.h>
  4 #include<stdlib.h>
  5 int main()
  6 {
  7     pid_t id = fork();
  8     if(id < 0)
  9     {
 10         perror("fork()");
 11     }
 12     else if(id == 0)
 13     {
 14         int cnt = 5;
 15         while(cnt)
 16         {
 17             printf("I am child: pid:%d, ppid:%d\n", getpid(), getppid());
 18             sleep(1);
 19             cnt--;
 20         }
 21         exit(1);
 22     }
 23     else
 24     {
 25         int status = 0;
 26         pid_t result = waitpid(id, &status, 0);
 27         if(result > 0)
 28         {
 29             printf("wait child sucess: result:%d\n", result);                                        
 30             if(WIFEXITED(status))
 31             {
 32                 printf("子进程退出码:%d\n",WEXITSTATUS(status));
 33             }
 34             else
 35             {
 36                 printf("子进程收到的退出信号:%d\n", status & 0x7F);
 37             }
 38         }
 39     }
 40     return 0;
 41 }

当子进程正常退出时,父进程等待子进程成功:

我们可以尝试使用kill -9命令将子进程杀死,这时父进程也能等待子进程成功,但是子进程属于异常退出。

注意: 被信号杀死而退出的进程,其退出码将没有意义。

阻塞等待与非阻塞等待

阻塞等待

上述例子中,我们可以发现,当子进程未退出时,父进程一直就在等待子进程,其他什么事都没有做,此时进程父进程就会进入阻塞状态,当子进程运行完毕,父进程立马被唤醒,接收子进程pid,此时状态就叫做阻塞状态。

非阻塞等待

父进程调用waitpid函数来进行等待,如果子进程没有退出,waitpid这个系统调用立马返回,父进程可以干其他事情,而且父进程会不间断的调用waitpid函数来获取子进程的运行情况,一旦子进程运行结束,父进程就立马接收到,这就叫做非阻塞等待。

做法很简单,向waitpid函数的第三个参数potions传入WNOHANG,这样一来,等待的子进程若是没有结束,那么waitpid函数将直接返回0,不予以等待。而等待的子进程若是正常结束,则返回该子进程的pid。下面以一段伪代码展示一下:

cpp 复制代码
  1 #include<iostream>
  2 #include<vector>
  3 #include<stdio.h>
  4 #include<unistd.h>
  5 #include<sys/wait.h>
  6 #include<stdlib.h>
  7 
  8 typedef void (*hander_t)();
  9 std::vector<hander_t> handers;
 10 void fun_one()
 11 {
 12     printf("这是一个临时任务1\n");
 13 }
 14 void fun_two()
 15 {
 16     printf("这是一个临时任务2\n");
 17 }
 18 void Load()
 19 {
 20     handers.push_back(fun_one);
 21     handers.push_back(fun_two);
 22 }
 23 int main()
 24 {
 25     pid_t id = fork();
 26     if(id == 0)
 27     {
 28         int cnt = 5;
 29         while(cnt)
 30         {
 31             printf("I am child: %d\n", cnt);
 32             sleep(1);
 33             cnt--;
 34         }
 35         exit(1);
 36     }
 37     else
 38     {
 39         int quit = 0;
 40         while(!quit)
 41         {
 42             int status = 0;
 43             pid_t ret = waitpid(-1, &status, WNOHANG);
 44             if(ret > 0)
 45             {
 46                 printf("wait child sucess: exit code:%d\n", WIFEXITED(status));
 47                 break;
 48             }                                                                                                                                                                                                                                                                                                                                        
 49             else if(ret == 0)
 50             {
 51                 printf("The child process is still running, the parent processcan handle other things!!\n");
 52                 if(handers.empty())
 53                 {
 54                     Load();
 55                 }
 56                 for(auto e : handers)
 57                 {
 58                     e();
 59                 }
 60             }
 61             else
 62             {
 63                 printf("wait failed\n");
 64                 break;
 65             }
 66             sleep(1);
 67         }
 68     }
 69 }

此刻在运行程序我们可以发现,在等待期间,父进程也可以处理其他事情:

进程替换

替换原理

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

当程序替换之后,有没有创建新的进程?

程序替换只是将物理内存空间的代码以及数据进行了替换,程序的PCB,虚拟地址空间以及页表并没发生改变,只是改变当前页表的映射关系,所以并没有创建新进程。

子进程进行替换后,会影响父进程的代码和数据吗?

子进程刚被创建时,与父进程共享代码和数据,此时子进程需要被替换,就需要将父子进程共享的代码和数据进行写时拷贝,父子进程的代码和数据就发生了分离,所以对子进程替换是不会影响父进程的代码和数据的。

替换函数

其实有六种以exec开头的函数,统称exec函数:

cpp 复制代码
1.int execl(const char* path, const char* arg, ...);

第一个参数为需要执行程序的路径,第二个参数为可变参数列表,表示你要如何执行这个程序,并以NULL结尾。


调用execl函数以后,当前进程的所有数据和代码都会被进行替换,包括已经执行的和未执行的,上述程序中printf已经被执行完毕,打印出来了,所以会显示出来,本质上其实他已经被替换了。

cpp 复制代码
2. int execv(const char* path, char* const argv[]);

第一个参数表示可执行程序的路径,第二个参数是一个指针数组,存放的是你要如何执行这个可执行程序,数组以NULL结尾。


cpp 复制代码
3. int execlp(const char* file, const char* arg, ...);

第一个参数是要执行程序的名字,第二个参数是可变参数列表,表示你要如何执行这个程序,并以NULL结尾。

cpp 复制代码
4. int execvp(const char* file, const char* const argv[]);

第一个参数表示可执行程序的路径,第二个参数是一个指针数组,存放的是你要如何执行这个可执行程序,数组以NULL结尾。


cpp 复制代码
5. int execle(const char *path, const char *arg, ...,char *const envp[]);

第一个参数是要执行程序的路径,第二个参数是可变参数列表,表示你要如何执行这个程序,并以NULL结尾,第三个参数是你自己设置的环境变量。


cpp 复制代码
6. int execve(const char *path, char *const argv[], char *const envp[]);

第一个参数是要执行程序的路径,第二个参数是一个指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾,第三个参数是你自己设置的环境变量。


下面我们可以看见子进程进行替换是的状态:

函数解释

  • 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
  • 如果调用出错则返回-1
  • 所以exec函数只有出错的返回值而没有成功的返回值。

命名理解

  • l(list) : 表示参数采用列表 ;
  • v(vector) : 参数用数组 ;
  • p(path) : 有p自动搜索环境变量 PATH;
  • e(env) 表示自己维护环境变量;
函数名 参数格式 是否带路径 是否使用当前环境变量
execl 列表 不是
execlp 列表
execle 列表 不是 不是,必须自己组装环境变量
execv 数组 不是
execvp 数组 不是,必须自己组装环境变量
execve 数组 不是

事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册 第2节,其它函数在man手册第3节。这些函数之间的关系如下图所示:

做一个简易的shell

shell建立一个新的进程,然后在那个进程中运行ls程序并等待那个进程结

束,然后shell读取新的一行输入,建立一个新的进程,在这个进程中运行程序 并等待这个进程结束。

所以要写一个shell,需要循环以下过程:

  1. 获取命令行
  2. 解析命令行
  3. 建立一个子进程(fork)
  4. 替换子进程(execvp)
  5. 父进程等待子进程退出(wait)
cpp 复制代码
    1 #include<stdio.h>
    2 #include<stdlib.h>
    3 #include<string.h>
    4 #include<unistd.h>
    5 #include<sys/wait.h>
    6 
    7 #define NUM 1024
    8 #define SIZE 32
    9 #define SEP " "
   10 //保存完整的字符
   11 char cmd_line[NUM];
   12 //保存打散之后的字符串
   13 char* g_argv[SIZE];
   14 
   15  int main()
   16 {
   17     while(1)
   18     {
   19         //1.打印出提示信息:"[root@localhost myshell]# "
   20         printf("[root@localhost myshell]# ");
   21         fflush(stdout);
   22         memset(cmd_line, '\0', sizeof cmd_line);
   23         //2.获取用户输入的各种键盘指令:"ls -a -l -i"
   24         if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL)
   25         {
   26             continue;
   27         }
   28         cmd_line[strlen(cmd_line) - 1] = '\0';
   29         //3.命令行字符串解析:"la -a -l -i" -> "ls" "-a" "-l" "-i"
   30         g_argv[0] = strtok(cmd_line, SEP);
   31         int index = 1;
   32         if(strcmp(g_argv[0], "ls") == 0)
   33         {
W> 34             g_argv[index++] = "--color=auto";
   35         }
   36         if(strcmp(g_argv[0], "ll") == 0)
   37         {
W> 38             g_argv[0] = "ls";
W> 39             g_argv[index++] = "-l";
W> 40             g_argv[index++] = "--color=auto";
   41         }
W> 42         while(g_argv[index++] = strtok(NULL, SEP));//第二次解析原始字符串,出入NULL
   43         //4.让父进程自己执行命令
   44         if(strcmp(g_argv[0], "cd") == 0)\
   45         {
   46             if(g_argv[1] != NULL)
   47             {
   48                 chdir(g_argv[1]);
   49             }
   50             continue;
   51         }
   52         //5. fork()
   53         pid_t id = fork();
   54         if(id < 0)
   55         {
   56             perror("fork()");
   57             return 1;
   58         }
   59         else if(id == 0)
   60         {
   61             printf("下面程序是由子进程运行的\n");
   62             execvp(g_argv[0], g_argv);
   63             exit(1);
   64         }                                                                                                                                                                                                                                                                                                                                                                                                                     
   65         else
   66         {
   67             int status = 0;
   68             pid_t ret = waitpid(-1, &status, 0);
   69             if(ret > 0)
   70             {
   71                 printf("wait sucesss: exit code:%d\n", WEXITSTATUS(status));
   72             }
   73         }
   74 
   75     }
   76     return 0;
   77 }

结果演示:

相关推荐
tokepson3 小时前
Mysql下载部署方法备份(Windows/Linux)
linux·服务器·windows·mysql
zz_nj5 小时前
工作的环境
linux·运维·服务器
极客先躯5 小时前
如何自动提取Git指定时间段的修改文件?Win/Linux双平台解决方案
linux·git·elasticsearch
suijishengchengde6 小时前
****LINUX时间同步配置*****
linux·运维
qiuqyue6 小时前
基于虹软Linux Pro SDK的多路RTSP流并发接入、解码与帧级处理实践
linux·运维·网络
切糕师学AI7 小时前
Linux 操作系统简介
linux
南烟斋..7 小时前
GDB调试核心指南
linux·服务器
爱跑马的程序员7 小时前
Linux 如何查看文件夹的大小(du、df、ls、find)
linux·运维·ubuntu
oMcLin10 小时前
如何在 Ubuntu 22.04 LTS 上部署并优化 Magento 电商平台,提升高并发请求的响应速度与稳定性?
linux·运维·ubuntu
Qinti_mm10 小时前
Linux io_uring:高性能异步I/O革命
linux·i/o·io_uring