文章目录

  • 操作系统
    • 什么是操作系统
    • 操作系统如何管理
    • 系统调用和库函数概念
    • 为什么需要操作系统
  • 进程
    • 进程的概念
    • 如何管理进程
      • 一、描述
      • 二、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、相关状态、僵尸进程相关推荐

  1. 【Linux】进程状态(阻塞、挂起、僵尸进程)

    文章目录 1 阻塞与挂起 1.1 阻塞 1.2 挂起 2 进程状态 前言: 当我们在Windows下双击运行一个程序,或是在Linux下通过 ./ 加载运行一个程序,是否就代表对应的进程就一直处在运行 ...

  2. 关于进程(PCB | 父进程 | 子进程 | fork深层探讨 |僵尸进程与孤儿进程)

    文章目录 一.进程与PCB 1. 进程的概念: 2. 什么是PCB task_struct task_ struct内容分类 4. 查看进程 5. 进程概念的加深 二.父进程与子进程 1. 通过系统调 ...

  3. Linux操作系统(fork函数,task_struct内容,僵尸进程,孤儿进程,sysytemd与init)

    Linux操作系统 1. 认识fork 1.1 fork父子执行顺序,代码,和数据复制问题 1.2 为什么fork会有两个返回值?多进程怎么运行的? 1.3 为何给父进程返回pid,给子进程返回0呢? ...

  4. linux网络操作系统使用教程课本答案(崔升广 赵红岩)

    第1章  认识Linux网络操作系统与安装 1.选择题 (1)下列中不是Linux系统的特点(B). A.多用户            B.单任务        C.开放性           D. ...

  5. Linux基础学习系列:对于fork()函数的学习,及进程创建相关知识

    fork()函数 :由当前进程再生成一个进程出来 #include <sys/types.h> #include <unistdh> pid_t fork(void); 返回: ...

  6. 红旗Linux软件开发技术,中科红旗闷声研发下一代红旗Linux 11操作系统

    据接触中科红旗开发内部的人士透露,中科红旗正在闷声研发下一代红旗Linux 11操作系统,即RedFlag Desktop Linux 11,对外界来说,红旗Linux这些年来的动作相当的神秘,但是一 ...

  7. linux 用mutex定义一个linkedlist,【基于LINUX的操作系统实验教程最终版材料】

    (基于LINUX的操作系统实验教程)(最终版) <基于LINUX的操作系统实验教程.doc>由会员分享,可免费在线阅读全文,更多与<(基于LINUX的操作系统实验教程)(最终版)&g ...

  8. 基于RTMP实现Linux|麒麟操作系统下屏幕|系统声音采集推送

    背景 Windows操作系统自问世以来,以其简单易用的图形化界面操作受到大众追捧,为计算机的普及.科技的发展做出了不可磨灭的功绩,也慢慢的成为人们最依赖的操作系统.在中国,90%以上的办公环境都是Wi ...

  9. 添加简单的linux内核模块,操作系统实践 第12章-添加最简单的Linux内核模块.ppt

    操作系统实践 第12章-添加最简单的Linux内核模块.ppt 文档编号:310662 文档页数:16 上传时间: 2018-07-21 文档级别: 文档类型:ppt 文档大小:2.00MB 第12章 ...

最新文章

  1. mysql单表多timestamp的current_timestamp设置问题
  2. 程序员请收好:10个非常有用的 Visual Studio Code 插件!
  3. 在ubuntu上搭建LNMP服务器
  4. Jquery全选单选功能
  5. pandas—总结(2) 数据读写 (更新中)
  6. PID算法 旋转倒立摆与平衡车的区别。此贴后边会更新。
  7. 海康威视球形摄像头激活,web二次开发
  8. linux usb重定向window,基于Linux的USB设备重定向研究.pdf
  9. 数学建模学习笔记(三十一)模糊评价法
  10. zabbix通过sendmail进行邮箱警报
  11. 网站制作的流程是什么?网站制作的流程包括哪些步骤?
  12. c#-winform自定义窗体皮肤(无边框皮肤)
  13. 安卓手机与苹果手机安装包的区别
  14. dock接口_回看手机接口发展史:TypeC将实现大一统?
  15. JAVA程序设计:破解保险箱(LeetCode:753)
  16. Mac下文件Non-ISO extended-ASCII编码问题
  17. 签了工作之后才发现,自己太草率了.....我看过的关于职业规划最好最全面的一篇文章...
  18. 如何在linux的gcc中添加c语言的外部链接库(“比如说,math.h
  19. python邮件管理
  20. 微前端在小米 CRM 系统的实践

热门文章

  1. 《网络编程综合实践》:高校爬虫(厦大,南理,华大)
  2. 宝塔安装php不显示,宝塔安装php不显示
  3. 多媒体库SDL以及实时音视频库WebRTC中的多线程问题实战详解
  4. Cadence 焊盘绘制
  5. 工厂方法(Factory Method)
  6. 如何看待中国古代自然科学
  7. gradle入门(一)从Groovy DSL转化为KTS
  8. 【读书小记】学术“咸鱼”自救指南
  9. fgetc/fputc 和 fgets/fputs 的详细用法
  10. 病态的加拿大保守党政府