【Linux】 -- 进程控制
进程控制
- 本文介绍
- 进程创建
-
- fork函数
-
- fork函数的返回值
- fork函数的使用
- 写时拷贝
- fork用法
- fork调用失败的原因
- 进程终止
-
- 进程退出的场景
- 进程退出码
- 进程正常退出
-
- return exit _exit之间的区别和联系
- 进程异常退出
- 进程等待
-
- 进程等待的必要性
- 如何进行进程等待
-
- wait
- waitpid
-
- status
- 多进程等待模型
- 进程替换
-
- 进程替换是什么
- 为什么要进行进程替换
- 进程替换的原理
- 六大替换函数
-
- exec*函数的返回值
- execl
- execlp
- execle
- execv
- execvp
- execve
- 替换函数的命名理解
本文介绍
1.从进程的 创建 终止 等待 替换四个方面来介绍进程控制相关内容
2.了解其底层原理
3.写一个简单的shell程序
进程创建
fork函数
一共有两种比较常见的创建进程方式
1.直接输入可执行文件 比如 ls ll等 这实际上就是创建了一个进程
2.通过调用fork()函数来在已存在的进程中创建一个进程
新创建的进程叫做子进程 原来的进程叫做父进程
fork函数的返回值
它有两个返回值 一个是给父进程的返回值 一个是给子进程的返回值
在调用fork这个函数的时候 操作系统会对子进程进行创建PCB以及mm_struct和页表映射等一系列操作 当程序计数器走到return pid这一步的时候事实上的子进程已经被创建完毕了
所以说父子进程都走到了这一步 所以会有两个返回值也就不奇怪了
- 如果创建子进程失败便会对父进程返回-1
- 如果创建子进程成功便会对父进程返回子进程pid 对子进程返回0
为什么我们对于父进程就是返回pid 对于子进程就直接返回0呢?
因为父进程对于子进程是一个 一对多的关系 一个父进程能够创建很多个子进程 所以说父进程需要知道子进程的pid才能够唯一标识之
而子进程对于父进程是一个一对一的关系 一个子进程只能有一个父进程 所以说对于子进程来说并不需要特别标识什么
fork函数的使用
我们使用fork函数一般是因为要通过多个进程去处理问题
所以说一般是让父子进程去做不同的事情
我们可以通过返回值来分辨父子进程
那么一个简单的多进程C++示例程序就可以这么写
#include <unistd.h>
#include <stdio.h>
#include <iostream>
using namespace std; int main()
{ int ret = fork(); if (ret == 0) { // child printf("im child my pid is:%d my ppid is:%d\\n",getpid(),getppid()); } else { // father printf("im father my pid is:%d my ppid is:%d\\n",getpid(),getppid()); sleep(1); } return 0;
}
我们让父子进程分别打印它们的pid和它们父进程的pid 在linux下运行效果如下
我们可以发现子进程的ppid就是父进程的pid
我们在写业务的时候如果需要使用多进程 可以直接将if else里面的逻辑替换
写时拷贝
子进程的创建过程中会伴随着PCB mm_struct的创建 页表映射
但是对于子进程来说 我们有可能会修改它的各项数据 此时便会发生写时拷贝
子进程的页表会重新映射一份物理内存给子进程
但是此时子进程的mm_struct地址却没有改变
所以打印出来的地址相同 可是地址里面的数值不同
造成这个现象的原因其实就是虚拟地址没有变而物理地址变化了
为什么要有写时拷贝?
因为进程具有独立性 如果我们不进行写时拷贝的话 子进程数据的修改就会影响到父进程了
为什么不在创建子进程的时候就进行数据的拷贝?
因为子进程有可能不会修改数据 所以说我们没有必要进行数据拷贝 直接共享父进程的数据可以更高效的使用内存空间
代码会不会进行写时拷贝?
代码也可以进行写时拷贝 进程替换就用到了代码的写时拷贝
fork用法
- 一个进程希望复制自己 使子进程同时执行不同的代码段 例如父进程等待客户端请求 生成子进程来处理请求
- 一个进程要执行一个不同的程序 例如子进程从fork返回后 调用exec函数
fork调用失败的原因
- 系统中有太多的进程 内存空间不足 子进程创建失败
- 实际用户的进程数超过了限制 子进程创建失败
进程终止
进程退出的场景
进程退出一共有三种场景
- 代码运行完毕 结果正确
- 代码运行完毕 结果错误不正确
- 代码异常终止
进程退出码
我们在写c/c++语言程序的时候 一般会在最后面加上return 0
对于这个返回值 我们称它为进程退出码 对于正确的进程一般都以0作为进程退出码 而非0就作为错误的进程的退出码 因此不同的错误对应的退出码也是不同的
这个错误码可能是一个整数 这个整数代表着各种各样的错误
在c语言中 我们可以使用下面的这段代码来获取所有的错误码
#include <stdio.h>#include <string.h>int main() { for(int i = 0; i < 140; ++i) { printf("%d : %s\\n", i, strerror(i)); } return 0; }
在linux中 我们可以通过下面的命令来获取上一个进程的错误码
echo $?
当我们正常使用的时候 这个返回码就是0
如果我们使用失败的话 它也会返回我们一个错误码 来告诉我们为什么失败
进程正常退出
在linux中 我们让进程退出一般有三种方式
- return退出
- exit函数退出
- _exit函数退出
return exit _exit之间的区别和联系
区别
return只有在主函数中才有退出进程的功能 在子函数中是没有这个功能 而exit和_exit在任何地方都可以退出进程
exit和return在结束进程的时候会执行用户的清除函数 刷新缓冲区 关闭流等 而_exit则会直接退出 什么都不会做
联系
实际上我们在主函数中的return num 就相当于调用了 exit(num)
而exit(num)在执行完用户定义的清理函数 清空缓冲区 关闭流之后还是会调用_exit函数
进程异常退出
在linux中 进程异常退出一般有两种方式
- 释放kill信号给进程后退出
- 出现如 除0问题 指针越界等问题进程终止
进程等待
进程等待的必要性
- 子进程退出 父进程如果不读取子进程的退出信息 子进程就会变成僵尸进程 进而造成内存泄漏
- 进程一旦变成僵尸进程 那么就算是kill -9命令也无法将其杀死 因为谁也无法杀死一个已经死去的进程
- 父进程派给子进程的任务完成的如何 我们需要知道 子进程运行完成 结果对还是不对 是否正常退出
- 父进程通过进程等待的方式 回收子进程资源 获取子进程退出信息
为了让父进程等待子进程结束回收资源 获取信息 避免子进程变成僵尸进程
如何进行进程等待
wait
我们可以使用wait函数来让父进程等待子进程运行结束
它的函数使用方法如下
pid_t wait(int* status);
它的返回值是一个 pid_t 类型的数据 如果等待成功会返回这个进程的pid 等待失败则会返回-1
验证wait的作用
创建两个进程 子进程五秒后结束 父进程先休眠十五秒让子进程进入僵尸状态 当父进程休眠完毕之后使用wait函数回收子进程 之后打印出父进程回收的结果
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
// 演示wait函数的作用
int main()
{ int ret = fork(); if (ret == 0) { // child int count = 5; while(count--) { printf("hello world!\\n"); sleep(1); } exit(0); // 子进程退出 } // father sleep(15); pid_t id = wait(NULL); if (id > 0) { printf("wait success!\\n"); } sleep(10); return 0;
}
下面是这段代码的执行结果
waitpid
它的函数使用方法如下
pid_t waitpid(pid_t pid, int* status, int options);
如果等待成功会返回等待进程的pid 如果等待失败则返回-1
pid_t pid
填入一个子进程的pid来指定等待该子进程结束
如果我们想等待任意进程都可以 这里可以填-1
int* status
它是一个输出型参数 当我们填入一个地址后该函数会在该地址处写入子进程的退出状态
如果我们不关心这个退出状态 这里可以填写NULL
int options
此项参数需要我们填入选项
如果我们想要父进程一直等待子进程结束 则可以填入0 (阻塞等待)
如果我们只想父进程问一次子进程有没有结束 则可以填入WNOHANG (非阻塞等待)
status
注意: 我们这里只研究的status低16位!
status是一个整型变量 但是我们不能单独的把它看作一个整型
分别三个部分研究更合适
- 高八位表示的是退出状态 即我们的退出码
- 低七位表示的是终止信号 如果我们的进程被信号所杀则此处会有终止信号
- 第八位表示的是core dump表示
可以通过一系列的位操作就能够获取到位信号
exitCode = (status >> 8) & 0xFF; //退出码
exitSignal = status & 0x7F; //退出信号
linux中提供了两个宏来让我们获取退出码和退出信号
- WIFEXITED(status):用于查看进程是否是正常退出 本质是检查是否收到信号
- WEXITSTATUS(status):用于获取进程的退出码
多进程等待模型
一个父进程可以创建多个子进程并且可以等待它们退出
这个叫做多进程的创建和等待的代码模型
进程替换
进程替换是什么
我们在执行一个进程的时候 可能想要这个进程去执行其他程序的代码 我们使用进程替换函数替换当前进程的数据段和代码段的过程就叫做进程替换
为什么要进行进程替换
因为这个进程跑完一部分之后我们想要调用另外一个程序 或者说要使用多种语言执行任务的时候需要用到进程替换
进程替换的原理
用fork创建子进程后 子进程执行的是和父进程相同的程序(但有可能执行不同的代码分支) 若想让子进程执行另一个程序 往往需要调用一种exec函数
当我们调用exec函数之后该进程的数据段和代码段全部被替换
并且重载程序计数器 让它从新程序的开头开始执行
当新程序重载时 有没有创建新的进程
没有 因为新程序的重载仅仅是替换了物理内存的数据段和代码段 并没有改变PCB和mm_struct 所以说并没有创建新的内存 内存的pid还是和原来一样的
当子进程进行程序替换后 会不会影响父进程
不会 虽然说子进程和父进程的数据段和代码段大部分是共享的 但是如果我们修改了子进程的数据段和代码段此时便会发生写时拷贝 从而保证进程之间的独立性
六大替换函数
exec*函数的返回值
如果进程替换失败则返回 -1 这个时候可以让我们的被替换的进程知道替换没有成功 从而能够决定下一步怎么走
如果进程替换成功则没有返回值 因为进程替换成功之后原来的进程事实上就不存在了 返回一个值没有任何的意义
对于exec*函数来说它们的返回值都遵循我们上面的原则 所以对于下面的函数我们就只讨论它们的参数
execl
int execl(const char *path, const char *arg, ...);
l 是列表的意思 意味着它的参数要使用列表的形式传入
它的第一个参数是 const char *path 它代表着要执行程序的路径
它的第二个参数是 const char *arg, … 它代表着可变参数列表 是使用NULL结尾的
例如我们要执行ls程序的话 就可以写出下面的代码
execl("/usr/bin/ls" , "ls" , "-a" , "-i" , NULL);
execlp
int execlp(const char *file, const char *arg, ...);
p代表的是path 路径 意味着这个函数能够自动推导路径
它的第一个参数是 const char *file 它代表着要执行的程序名
它的第二个参数是 const char *arg, … 它代表着可变参数列表 是使用NULL结尾的
例如我们要执行ls程序的话 就可以写出下面的代码
execlp("ls" , "ls" , "-a" , "-i" , NULL);
execle
int execle(const char *path, const char *arg, ..., char *const envp[]);
e意味着它可以自己配置一个环境变量 我们在自己的函数内部就可以使用我们配置的这个环境变量
它的第一个参数是const char *path 它代表着要执行程序的路径
它的第二个参数是 const char *arg, … 它代表着可变参数列表 是使用NULL结尾的
它的第三个参数是 *const envp[] 它代表着一个数组 数组里面是我们自己配置的环境变量
char* envp[] = { "val", NULL };
execle("./mycmd", "mycmd", NULL, envp);
execv
int execv(const char *path, char *const argv[]);
v我们可以将它理解为vector 数组的意思 我们传递的参数要以数组的形式传递
它的第一个参数是 const char *path 它代表着要执行程序的路径
它的第二个参数是 char *const argv[] 它代表着一个数组 我们将要执行的命令放在数组中并且以null结尾
例如我们要执行ls程序的话 就可以写出下面的代码
char* myargv[] = { "ls", "-a", "-i", NULL };
execvp("/usr/bin/ls", myargv);
execvp
int execvp(const char *file, char *const argv[]);
v我们可以将它理解为vector 数组的意思 我们传递的参数要以数组的形式传递
p代表的是path 路径 意味着这个函数能够自动推导路径
它的第一个参数是 const char *file 它代表着要执行的程序名
它的第二个参数是 char *const argv[] 它代表着一个数组 我们将要执行的命令放在数组中并且以null结尾
例如我们要执行ls程序的话 就可以写出下面的代码
char* myargv[] = { "ls", "-a", "-i", "-l", NULL };
execvp("ls", myargv);
execve
int execve(const char *path, char *const argv[], char *const envp[]);
e意味着它可以自己配置一个环境变量 我们在自己的函数内部就可以使用我们配置的这个环境变量
例如 我们可以自己设置一个env环境变量 并在我们的程序中使用它
char* myargv[] = { "mycmd", NULL };
char* env[] = { "val", NULL };
execve("./mycmd", myargv, env);
替换函数的命名理解
这六个替换函数都是以 exec开头的 我们可以根据它们最后的1~2两个字符来理解它们的参数
- l (list) 意味着它的参数要使用列表的形式传入以NULL结尾
- v (vector) 我们传递的参数要以数组的形式传递
- p (path) 表示能自动搜索环境变量PATH
- e (env) 表示可以传入自己设置的环境变量
只有execve才是真正的系统调用 其他的函数都是根据execve封装而来的