Linux-进程控制
进程创建
进程的创建:
- 命令行启动命令(程序、指令……)
- 通过程序自身,fork出来的子进程
fork函数
在linux中fork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
语法:
#include <unistd.h>
pid_t fork(void);
返回值:自进程中返回0,父进程返回子进程id,出错返回-1
进程调用fork,当控制转移到内核中的fork代码后,内核做:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表当中
- fork返回,开始调度器调度
注:
- 当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方
- fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定。
- 所有fork出来的子进程,都是以父进程为模板,数据代码继承父进程
- 创建子进程的本质是系统多了一个进程,多了一套进程相关的数据结构(以父进程为模板)
注:
- 上端代码中的pid是变量,是父进程创建的,是父进程运行fork中的代码的
- pid中有两个不同的值,是因为fork() return返回的时候本质是把返回值写入变量,必定会写时拷贝(注:这里的写时拷贝并不是立刻进行写时拷贝,只是写时拷贝机制(策略),只用当父子进行数据写入或者数据修改时,才进行写时拷贝),一个变量名内容是不同的本质是父子页表映射数据到了不同的内存区域
- pid变量里有两个不同的值,可以从而让父子进入不同的业务逻辑
- fork函数只是创建子进程的过程,创建父子进程的依旧是操作系统
补: fork创建新进程成功后,系统中出现两个基本完全相同的进程,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略。
fork函数返回值
- 子进程返回0,
- 父进程返回的是子进程的pid
fork常规用法
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
- 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
fork调用失败的原因
- 系统中有太多的进程
- 实际用户的进程数超过了限制
写时拷贝
父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本
注:
- 写时拷贝的实现过程时,需要OS参与完成,OS时进程、内存的管理者
总结: - 写时拷贝可以保证父子进程的独立性
- 写时拷贝的意义:
-
- 并不是所有数据父子进程都会写入,不需要写入修改的数据(只读)拷贝是没有意义的,因此写时拷贝是可以避免内存和系统资源的浪费
-
- fork时,创建数据结构,如果还要将数据拷贝一份,fork函数的效率就会降低,因此写时拷贝可以提高fork的效率
-
- fork本身就是向系统索要更多的资源,如果再拷贝数据的话,索要的资源就会变多,创建子进程就很容易失败,因此写时拷贝可以提高fork创建子进程的成功率
补充:
- Fork后子进程保留了父进程的:
- 环境变量
- 当前工作目录
- 不会保留 父进程的文件锁,pending alarms和pending signals
- 不会保留 进程号
- fork调用通过复制父进程创建子进程,子进程与父进程运行的代码和数据完全一样
- fork创建子进程就是在内核中通过调用clone实现
进程终止
注:
- main函数中return 0 中的0代表程序的退出码,代表程序退出,结果是否运行正确
- 这个退出码默认是给系统看的确认进程执行结果是否正确(0代表运行成功,!0代表运行结果有问题)
- 用户想得知退出码,需要用到echo $? 来查看
- echo $? 是用来查看最近一次执行的程序的退出码
进程退出场景
进程退出情况:
- 代码跑完,结果正确,退出码0
- 代码跑完,结果不正确,逻辑问题,但是没有导致程序崩溃,退出码!0
- 代码没有运行完毕,程序崩溃,退出码没有意义
注:
- 退出码,可以是人为定义的,也可以是使用系统的错误码list
- 当程序运行失败的时候,往往最关系的是失败的原因(计算机擅长处理整数类型的数据,将int转化为string(错误码描述))
- 子进程的退出情况是父进程所关心的
总结:
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止
进程常见退出方法
- 正常终止(可以通过 echo $? 查看进程退出码):
-
- 从main返回
-
- 调用exit
-
- _exit
- 异常退出:
- ctrl + c,信号终止
注:
-
子进程运行,父进程可以关心,也可以不关心子进程的运行结果
-
进程非正常退出:野指针、/0、越界访问……,退出码是无意义的
_exit函数&&exit函数&&return退出
注:main函数return,非main函数的return不是终止进程,而是结束进程
注:任何函数exit都表示直接终止进程
注:
- exit:在退出的时候,会进行后续资源处理,包括刷新缓冲区
- _exit:在退出时,不会进行后续资源储量,直接终止进程
结论:
- 在调用exit之前:
-
- 执行用户通过 atexit或on_exit定义的清理函数。
-
- 关闭所有打开的流,所有的缓存数据均被写入
-
- 调用_exit
- return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做 exit的参数。
- void _exit(int status):
-
- 参数:status 定义了进程的终止状态,父进程通过wait来获取该值
-
- 虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行$?发现返回值是255。
总结:
- 站在OS角度,进程终止的核心思想是:归还资源
-
- 释放(释放不是真的把数据结构对象销毁,而是设置为不用的状态,然后保存起来,如果不用的对象多了就有一个数据结构池(这种规则在Linux中叫Slab分派器))曾经为了管理进程所维护的所有的数据结构对象
- 池可以提高申请空间的时间,提高用户效率
-
- 释放(不是代码和数据清空,而是把内存设置为无效就可以了)程序代码和数据占用的内存空间
-
- 取消曾经该进程的连接关系
进程等待
进程等待必要性:
- 回收僵尸进程,解决内存泄漏
- 需要获取子进程的运行结束状态(有些父进程不需要子进程的运行结束状态)
- 尽量父进程要晚于子进程退出,这样可以规范化进行资源回收(编码角度)
注:进程一旦变成僵尸状态用kill -9 也无能为力,因为没有办法杀死一个已经死去的进程。
进程等待的方法
wait方法
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
返回值:
成功返回被等待进程pid,失败返回-1。
参数:
输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
注:wait方法是等待任意一个子进程,当子进程退出时,wait就可以退出并返回该子进程的pid
循环创建进程以及循环等待:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
int i=0;
while(i<5)
{
pid_t id=fork();
if(id<0)