Linux操作系统:操作系统与进程之fork、相关状态、僵尸进程
文章目录
- 操作系统
- 什么是操作系统
- 操作系统如何管理
- 系统调用和库函数概念
- 为什么需要操作系统
- 进程
- 进程的概念
- 如何管理进程
- 一、描述
- 二、PCB
- 三、task_struct
- 进程相关操作
- 一、查看进程
- 二、进程与父进程
- fork-进程创建
- fork函数的提出
- fork相关问题
- 进程状态
- 状态讲解
- 僵尸进程
- 何为僵尸进程
- 例子
- 系统建立僵尸进程的原因
- 孤儿进程
操作系统
什么是操作系统
操作系统(operation system,简称OS),简单来说就是一款纯正的“搞管理”的软件,不仅管理硬件,同时也管理软件
- 与硬件交互,管理所有的软硬件资源
- 为用户程序(应用程序)提供一个良好的执行环境
操作系统主要包括一下两个:
- 内核(进程管理,内存管理,文件管理,驱动管理)
- 其他程序(例如函数库,shell程序等等)
操作系统如何管理
我们将操作系统、驱动程序、底层硬件分别比作校长、辅导员、学生
- 一个人能否进行管理,关键在于它是否具有决策权,正如校长一样,它可以决定你的去留。校长只需通知辅导员(驱动程序),就可以让你跑路(决策),辅导员则听从命令,执行命令
- 当校长想要了解某个学生的情况时,自然可以让辅导员将学生的相关信息交予给你,这就是操作系统调用驱动程序来了解硬件的过程
- 学生(硬件)这么多,如何统一进行管理呢?答案就是,信息的导入,将学生的信息包装起来,通过这些信息来管理学生,辅导员(驱动程序)则执行校长的决定
经上述的描述操作系统是如何进行管理的呢?很简单,用“先描述,再组织”的思想,如何描述?那就是把想要管理的信息用结构体装起来,组织则是用数据结构将他们一一串起来。
系统调用和库函数概念
我们刚刚只关心了操作系统、驱动程序、底层硬件,那往上看还有system call和用户操作接口,这又是用来干什么的呢?
- 在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。
- 系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。
简单的例子就是printf
函数就是用户的二次开发,以此调用系统调用接口已完成相应的功能,为什么不直接使用系统调用接口呢?使用成本太大,使用过程需要了解更多相关知识。
为什么需要操作系统
在一套系统中,需要有管理者进行统筹。对上,给用户一个稳定高效的执行环境。对下,管理好软硬件资源,提供稳定的软硬件环境。
进程
先来看一个程序
1 #include <stdio.h>2 #include <unistd.h>3 #include <sys/types.h>4 int main()5 {6 while(1)7 {8 printf("hello world: %d %d\n", getpid(), getppid());9 sleep(1);10 11 }12 return 0;13 14 }
当使用make/makefile生成一个可执行程序,这个程序一开始是会存储在硬盘上,然后当我们使用它时./test.exe
,就会把程序加载到内存中,所以开始运行后使用相关命令查找到这个进程。
进程的概念
进程说的难懂一点就是:可执行程序与管理进程所需要的数据结构的集合
通俗一点,像上述程序的一个执行实例,或者说正在执行的程序都叫做进程
我们可以通过/proc
系统文件进行查看,或者使用ps aux | grep [目标进程]
进行查看。
如何管理进程
一、描述
前文说过,操作系统主要有四大功能:内存管理,进程管理,文件管理和驱动管理。对于操作系统,只要是管理就遵从先描述,再组织的原则
每个进程的相关信息封装在一个struct中(因为Linux是由C/C++编写的),接着把这些结构体用我们学习的数据结构织起来,进行管理时只需遍历他们,然后修改相应的信息。
二、PCB
- 进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
- 课本上称之为PCB(process control block),Linux操作系统下的PCB是: task_struct
三、task_struct
task_struct——是在Linux中描述进程的结构体,它是Linux内核的数据结构,会被装在到内存里面。
如下:
标识符 | 描述本进程的唯一标识符,类似于身份证 |
状态 | 任务状态,退出代码,退出信号 |
优先级 | 相对于其他进程的优先级 |
程序计数器 | 程序中即将被执行的下一条指令地址 |
内存指针 | 包括程序代码和进程相关数据的指针等 |
上下文数据 | 进程执行时处理器的寄存器中的数据 |
I/O状态信息 | 包括显示的输入输出请求等 |
进程相关操作
一、查看进程
/proc
,Linux系统中所有的进程会被镜像在此。ps aux | grep [进程名] && ps aux | head -1
,将所需要查看的进程内容以及相关目录调出来ps axj
查看父子进程关系
二、进程与父进程
1 #include <stdio.h>2 #include <unistd.h>3 #include <sys/types.h>4 int main()5 {6 while(1)7 {8 printf("hello world: %d %d\n", getpid(), getppid());9 sleep(1);10 11 }12 return 0;13 14 }
- 使用getpid()获取进程标识符
- 使用getppid()获取父进程标识符
将19681调出来可得到
这里的-bash
叫做命令行解释器,所有命令都是由它创建的,那么自然而然它的PID就不会变化。前面的程序的执行也是依靠它的,命令行解释器要是挂了,系统也就完了。
其实这里的命令行解释器也不是其本体,因为命令行解释器特别重要,所以命令行解释器只需创建它的子进程,让这个子进程代替自己完成任务,即便子进程挂了,也不会影响其自身。当然不是所有的操作都能有子进程完成,有些特殊操作还需要本体亲自出马
fork-进程创建
fork函数的提出
- 在fork函数执行后,如果成功创建新进程就会出现两个进程,一个是子进程,一个是父进程
- fork有两个返回值
- 父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)
事例如下:
1 #include <stdio.h>2 #include <sys/types.h>3 #include <unistd.h>4 5 int main()6 {7 printf("output once\n");8 int ret=fork();9 10 printf("output twice : pid: %d, ppid: %d, ret = %d\n", getpid(), getppid(), ret); 11 sleep(1);12 return 0;13 14 }
运行结果:
可以发现fork()函数调用之后,多了一个子进程5137,且由父进程5136所创建。
接下来我们将具体fork函数展示出来(官方手册 man fork
)
- 两个返回值
- 在父进程中,fork返回新创建子进程的ID
- 在子进程中,fork返回0
- 未能创建,fork返回负值
根据上述信息,我们将其代码编写出来,用fork函数的返回值来进行分流
1 #include <stdio.h>2 #include <sys/types.h>3 #include <unistd.h>4 5 int main()6 {7 pid_t ret = fork();//其返回值是pid类型的8 if(ret > 0)//父进程返回的是子进程ID9 {10 while(1){11 printf("I am an parent! pid : %d\n", getpid());12 sleep(1);13 }14 }15 else if(ret == 0){//子进程fork返回值是016 while(1){17 printf("I am a child! pid : %d, ppid : %d\n", getpid(), getppid());18 sleep(1); 19 }20 }21 else{22 printf("fork error\n");23 }24 25 return 0;26 }
展示效果如下:
同时再使用之前的命令查看这个进程,发现也是两个进程
我们已知c语言,if-else
执行时每次只能执行一路,怎么可能同时执行多路,同时每个if语句块内都有死循环,一个循环未结束,又怎么可能去执行其他语句呢?
解释:
在Linux中,进程创建会形成链表,父进程创建子进程,那么父进程的进程指针会指向子进程ID。
所以这两个进程是同时运行的
fork相关问题
如何理解进程创建?
前面说过,操作系统在进行管理时,必然遵循“先描述,再组织”的原则,所以在进行进程管理时。首先会创建相应的task_struct
,写入有关信息,然后和你编写好的代码共同组成进程
fork拥有两个返回值的原因
根据上面的描述,可以大致描述fork函数的执行逻辑如下
pid_t fork()
{//先描述,再组织,所以首先为子进程创建结构体struct task_struct* p=malloc(struct task_struct);//以下逻辑就是写入属性信息p->XX=father->XX;....p->status=run;p->id=xxxx;//到这里之前,子进程创建完毕return p->id;}
进程数据=代码+数据,代码是共享的,但是数据子进程会从父进程那儿拷贝一份(写时拷贝),在进行return 语句时,子进程已完成拷贝,于是两个进程共同return,但是数据不同导致返回值有所差异。
进程数据=代码+数据,代码是共享的,
**这两个进程都是独立的,存在于不同地址中,不是公用的。**也就是说明一个问题,分流的两条支路独立,互不干扰。
为什么返回值不一样
指向不一样,父进程指向子进程,所以返回的是子进程的id,但是子进程并没有它的子进程,所以返回0。
就好比,一个孩子肯定知道它只有一个爹,而一个爹可能有多个孩子,所以子进程在标识父进程时就不要做那么多的区分,但是父进程可能有多个子进程,它与它在区分不同的子进程时必须要使用PID。
为什么数据私有,代码却公有
- 代码是逻辑,一般不可修改
- 数据可读可写,方便讲两个进程独立开
- 如果数据不私有,后果就是同一份数据在父子进程之间改来改去引起混乱
进程状态
进程状态反映进程执行过程的变化。这些状态随着进程的执行和外界条件的变化而转换
Linux源代码中定义进程状态如下
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char * const task_state_array[] = {"R (running)", /* 0 */ -运行或将要运行
"S (sleeping)", /* 1 */ -进程在等待事件完成
"D (disk sleep)", /* 2 */-此状态进程通常等待IO结束
"T (stopped)", /* 4 */ -停止状态
"t (tracing stop)", /* 8 */ -追踪中
"X (dead)", /* 16 */ -死亡状态
"Z (zombie)", /* 32 */ -僵尸进程
};
状态讲解
R(running)
先看死循环程序
1 #include <stdio.h>2 3 int main()4 {5 while(1){6 printf(".");7 8 }9 return 0; 10 }
运行结果:
问题的抛出:
循环在不断打印,为什么进程状态是S呢。(即为S(sleeping)-睡眠状态)
其实刚才的这样的操作属于I/O操作,字符被不断打印在屏幕上,外设的速度是远低于CPU的,所以CPU早都处理完了,但是屏幕上的还没有打印完,所以这里显示的是S。
1 #include <stdio.h>2 3 int main()4 {5 while(1){6 //printf(".");7 8 }9 return 0; 10 }
在这里插入图片描述
当去掉打印函数后,程序运行就不需要再打印再屏幕上,处理速度大大提升,这样结果就会有所不同,即变成R+状态。
结论:运行状态并不意味着进程一定在运行当中,它表明进程要么是在运行,要么在运行队列里,等待CPU调度
- 关于
S+
,R+
,其中的‘+’
表示该进程是一个前台进程,可以使用ctrl+C
终止。在运行程序时,加上取地址符&
,比如./test &
,能使进程到后台运行,后天运行的进程无法使用ctrl+C
终止,必须使用命令kill -9 【进程pid】
来终止
S(sleeping)-睡眠状态
睡眠状态意味着进程在等待事件完成,处于等待队列或阻塞队列中。上面的死循环的例子就是典型的睡眠状态。睡眠状态可以被立即唤醒
D(Disk sleep)-磁盘休眠状态
磁盘休眠状态和睡眠状态有点像,区别就是睡眠状态可被中断,也就是立即唤醒,但磁盘休眠状态不可被中断,在这个状态的进程通常会等待I/O结束。
磁盘休眠状态又可以叫做深度睡眠状态
与S状态不同的是,D状态可以防止进程再等待磁盘数据是被杀死,为什么会被杀死,一是内存空间不足,二是因为磁盘搜寻速度太慢,当内存中的进程需要访问磁盘数据时,磁盘就立马去寻找,由于时间较慢,所以进程就会进入等待状态,如果进程是睡眠状态,而此时假如内存又不足了,CPU就需要终止某些进程以保证系统的稳定性,此时此进程就有可能会被误删,这样等磁盘拿到数据时,进程早已挂掉,因此会引发一定的问题。
T(stopped)-停止
man kill
查看相关kill命令的选项,我们使用命令kill -l
,查看信号(前31为普通信号,剩余部分为实时信号)- 我们使用18 19两个选项,分别对应R状态与T状态
进程状态路线图
僵尸进程
何为僵尸进程
简单点来说:僵尸进程就是子进程已经退出了,父进程还在运行当中,父进程没有读取到子进程的状态,子进程就会进入僵尸状态
例子
1 #include <stdio.h>2 #include <sys/types.h>3 #include <unistd.h>4 5 int main()6 {7 pid_t ret = fork();//其返回值是pid类型的8 if(ret > 0)//父进程返回的是子进程ID9 {10 while(1){11 printf("I am an parent! pid : %d\n", getpid());12 sleep(1);13 }14 }15 else if(ret == 0){//子进程fork返回值是016 int count = 0;17 18 while(count < 5){ 19 printf("I am a child! pid : %d, ppid : %d\n", getpid(), getppid());20 sleep(1); 21 count++; 22 } 23 } 24 else{ 25 printf("fork error\n"); 26 } 27 28 return 0; 29 }
可以发现,在5秒后,子进程已经退出,父进程仍在运行
当子进程先退出,父进程还在运行,由于读取不到子进程的退出状态,所以子进程会变为僵尸状态。为了方便演示,使用下面的脚本,来每1s监控进程
while :; do ps axj | head -1 && ps axj | grep test | grep -v grep;sleep 1;echo "###########";done
父进程还是在运行,此时子进程变为Z,也就是僵尸状态
系统建立僵尸进程的原因
其实道理也很简单,子进程是由父进程创建的,父进程之所以要创建子进程,其目的就是要给子进程分配任务,那么在这个过程中,子进程平白无故的没了,而父进程却不知道子进程到底把自己交给它的任务完成的怎么样,成功了还好,失败的话就能再交代一个进程去操作。
所以进程结束时一定要给父进程返回一个状态,父进程一直不读取这个状态的话,那么子进程就会一直卡在僵尸状态,其中像代码这些资源已经被释放,但是这个进程却没有真正退出,因为PCB还在维护它,直到父进程读取到它的状态,才能进入死亡状态
- 进程控制块中,一个进程退出后,还有一个退出码返回给父进程,如下是Linux内核中关于这部分的定义
在Linux中一行命令就是一个进程,那么这个命令的父进程是bash,那么命令在结束的一瞬间也会给bash返回一个状态码,bash作为父进程,就是依靠这个返回码来判断命令是否正常结束,如果状态码为某一个值即可判定为没有这样的命令。
在Linux中可以用echo $?来查看上一个输入命令的状态返回码,命令正确返回0,否则返回非0
孤儿进程
孤儿进程就是父进程没了,子进程还在。那么根据上面的僵尸进程,子进程在退出后由于没有父进程来读取它的状态,所以会一直卡在僵尸状态,那么这样就会存在一个问题,它的内存资源谁来回收,通俗点将就会造成
内存泄漏
事例:父进程先挂
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{// printf("还没执行fork函数时的本进程为:%d\n",getpid());pid_t ret=fork();//其返回值类型是pid_t型的sleep(1);if(ret>0)//父进程返回的是子进程ID{int cout=0;while(cout<10){printf("----------------------------------------------------\n");printf("父进程运行了%d秒\n",cout+=1);sleep(1);}exit(0);//让父进程挂了}else if(ret==0)//子进程fork返回是0{int count=0;while(1){printf("子进程已经运行了%d秒\n",count+=1);sleep(1);}}elseprintf("进程创建失败\n");sleep(1);return 0;
}
ctrl+C此时结束的是父进程,但是父进程早已结束,子进程像孤儿一样四处游荡
用kill命令能够清除
那么问题来了,这个进程难道一直要占用资源吗,其实操作系统在设计的时候就考虑到了这一步。所以一旦父进程先挂了,那么这个子进程就会被1号进程领养(systemd)
Linux操作系统:操作系统与进程之fork、相关状态、僵尸进程相关推荐
- 【Linux】进程状态(阻塞、挂起、僵尸进程)
文章目录 1 阻塞与挂起 1.1 阻塞 1.2 挂起 2 进程状态 前言: 当我们在Windows下双击运行一个程序,或是在Linux下通过 ./ 加载运行一个程序,是否就代表对应的进程就一直处在运行 ...
- 关于进程(PCB | 父进程 | 子进程 | fork深层探讨 |僵尸进程与孤儿进程)
文章目录 一.进程与PCB 1. 进程的概念: 2. 什么是PCB task_struct task_ struct内容分类 4. 查看进程 5. 进程概念的加深 二.父进程与子进程 1. 通过系统调 ...
- Linux操作系统(fork函数,task_struct内容,僵尸进程,孤儿进程,sysytemd与init)
Linux操作系统 1. 认识fork 1.1 fork父子执行顺序,代码,和数据复制问题 1.2 为什么fork会有两个返回值?多进程怎么运行的? 1.3 为何给父进程返回pid,给子进程返回0呢? ...
- linux网络操作系统使用教程课本答案(崔升广 赵红岩)
第1章 认识Linux网络操作系统与安装 1.选择题 (1)下列中不是Linux系统的特点(B). A.多用户 B.单任务 C.开放性 D. ...
- Linux基础学习系列:对于fork()函数的学习,及进程创建相关知识
fork()函数 :由当前进程再生成一个进程出来 #include <sys/types.h> #include <unistdh> pid_t fork(void); 返回: ...
- 红旗Linux软件开发技术,中科红旗闷声研发下一代红旗Linux 11操作系统
据接触中科红旗开发内部的人士透露,中科红旗正在闷声研发下一代红旗Linux 11操作系统,即RedFlag Desktop Linux 11,对外界来说,红旗Linux这些年来的动作相当的神秘,但是一 ...
- linux 用mutex定义一个linkedlist,【基于LINUX的操作系统实验教程最终版材料】
(基于LINUX的操作系统实验教程)(最终版) <基于LINUX的操作系统实验教程.doc>由会员分享,可免费在线阅读全文,更多与<(基于LINUX的操作系统实验教程)(最终版)&g ...
- 基于RTMP实现Linux|麒麟操作系统下屏幕|系统声音采集推送
背景 Windows操作系统自问世以来,以其简单易用的图形化界面操作受到大众追捧,为计算机的普及.科技的发展做出了不可磨灭的功绩,也慢慢的成为人们最依赖的操作系统.在中国,90%以上的办公环境都是Wi ...
- 添加简单的linux内核模块,操作系统实践 第12章-添加最简单的Linux内核模块.ppt
操作系统实践 第12章-添加最简单的Linux内核模块.ppt 文档编号:310662 文档页数:16 上传时间: 2018-07-21 文档级别: 文档类型:ppt 文档大小:2.00MB 第12章 ...
最新文章
- mysql单表多timestamp的current_timestamp设置问题
- 程序员请收好:10个非常有用的 Visual Studio Code 插件!
- 在ubuntu上搭建LNMP服务器
- Jquery全选单选功能
- pandas—总结(2) 数据读写 (更新中)
- PID算法 旋转倒立摆与平衡车的区别。此贴后边会更新。
- 海康威视球形摄像头激活,web二次开发
- linux usb重定向window,基于Linux的USB设备重定向研究.pdf
- 数学建模学习笔记(三十一)模糊评价法
- zabbix通过sendmail进行邮箱警报
- 网站制作的流程是什么?网站制作的流程包括哪些步骤?
- c#-winform自定义窗体皮肤(无边框皮肤)
- 安卓手机与苹果手机安装包的区别
- dock接口_回看手机接口发展史:TypeC将实现大一统?
- JAVA程序设计:破解保险箱(LeetCode:753)
- Mac下文件Non-ISO extended-ASCII编码问题
- 签了工作之后才发现,自己太草率了.....我看过的关于职业规划最好最全面的一篇文章...
- 如何在linux的gcc中添加c语言的外部链接库(“比如说,math.h
- python邮件管理
- 微前端在小米 CRM 系统的实践