IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 系统运维 -> Linux操作系统:操作系统与进程之fork、相关状态、僵尸进程 -> 正文阅读

[系统运维]Linux操作系统:操作系统与进程之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)//父进程返回的是子进程ID
  9   {
 10     while(1){
 11       printf("I am an parent! pid : %d\n", getpid());
 12       sleep(1);
 13     }
 14   }
 15   else if(ret == 0){//子进程fork返回值是0
 16     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)//父进程返回的是子进程ID
  9   {
 10     while(1){
 11       printf("I am an parent! pid : %d\n", getpid());
 12       sleep(1);
 13     }
 14   }
 15   else if(ret == 0){//子进程fork返回值是0
 16     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);
      }
    }
    else
      printf("进程创建失败\n");

    sleep(1);
    return 0;
}



在这里插入图片描述
ctrl+C此时结束的是父进程,但是父进程早已结束,子进程像孤儿一样四处游荡

用kill命令能够清除

那么问题来了,这个进程难道一直要占用资源吗,其实操作系统在设计的时候就考虑到了这一步。所以一旦父进程先挂了,那么这个子进程就会被1号进程领养(systemd)

  系统运维 最新文章
配置小型公司网络WLAN基本业务(AC通过三层
如何在交付运维过程中建立风险底线意识,提
快速传输大文件,怎么通过网络传大文件给对
从游戏服务端角度分析移动同步(状态同步)
MySQL使用MyCat实现分库分表
如何用DWDM射频光纤技术实现200公里外的站点
国内顺畅下载k8s.gcr.io的镜像
自动化测试appium
ctfshow ssrf
Linux操作系统学习之实用指令(Centos7/8均
上一篇文章      下一篇文章      查看所有文章
加:2021-07-14 23:19:00  更:2021-07-14 23:19:42 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/5 0:50:30-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码