当前位置: 首页 > news >正文

Linux进程 --- 2

大家好啊,时隔多日,很高兴我们又再次见面了。这篇文章将接着介绍有关进程的相关内容。这篇文章将重点讲解一下有关fork讲解和进程状态的部分,相信大家会有所收获的。

let's go!


目录

​编辑

前言

一、查看进程

二、fork初识

三、进程状态

1.进程状态,运行、阻塞、挂起​编辑

1.1运行

1.2阻塞

1.3挂起

2.具体如何维护Linux状态

2.1 R和S状态

2.2 D状态 --- 深度休眠

2.3 T和t状态

2.4 X状态

2.5 Z状态 --- 僵尸状态

四、进程组织结构反思

结语


前言

我们在前面简单讲解了一下什么是进程,大家没有看前面的内容可以先去看一看,接下来的内容将接着前面开始讲解。难度稍大,大家可以多加思考!!


一、查看进程

在前文当中有讲到进程的查看,但是只是简单提了一下,现在我们来实际演示看看。

进程信息可以通过/proc系统文件夹查看

大多数进程信息也同样可以使用top和ps这些用户级工具来获取

我们通过cd /proc的指令到达了proc文件夹里面了

其中有很多数字(进程pid),它们就对应着一个个的进程,比如我们想要获取PID为1的进程信息,你需要查看 /proc/1 这个文件夹。

除了通过/proc系统文件夹来查看进程,我们也可以通过ps的指令来查看对应的进程

也可以通过查询man手册来查看:man ps

也可以通过系统调用来获取进程的标识符

进程id(PID)

父进程id(PPID)

通过getpid()和getppid()函数来获取当前进程的PID和PPID

我们自己来简单写一个无限循环的代码,然后再来查看它

我们将对应的代码通过gcc编译成为了proc这个可执行程序

开始运行一下它:

开始无限循环了,也打印出了对应的pid和ppid,我们通过ps去查看一下吧:

可以发现,对应的2928就可以找到对应的./proc


二、fork初识

通过系统调用创建进程fork

man fork,通过man手册可以查看具体的一个详细的介绍

fork这个函数可以帮助我们创建一个子进程~

它会给父进程返回子进程的pid,给子进程返回0

大家先这样简单理解一下,看以下代码:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>int main()
{printf("begin: 我是一个进程,pid: %d, ppid: %d\n", getpid(), getppid());pid_t id = fork();if (id == 0){// 子进程while(1){printf("我是子进程,pid: %d, ppid: %d\n", getpid(), getppid());sleep(1);}}else if(id > 0){// 父进程while(1){printf("我是父进程,pid: %d, ppid: %d\n", getpid(), getppid());sleep(1);}}else{// error}return 0;
}

我们将它编译出来,并通过监视窗口来查看一下

监视窗口指令:

(proc为我生成的可执行文件名)

while :; do ps ajx | head -1; ps ajx | grep proc | grep -v grep; echo "------------------------------------------------------"; sleep 1; done;

我们会发现以下这些问题:

1.为什么fork要给子进程返回0给父进程返回子进程pid呢?

2.对于fork这个函数是如何做到返回两次的呢?

3.对于一个变量而言是如何拥有不同的值呢?

4.fork函数是如何做到的呢?做了什么呢?

1. 要理解这些问题,我们首先要来理解一下fork干了什么事情??

在没有fork之前,只有一个进程。

进程 = 内核数据结构 + 代码和数据

操作系统的内核数据结构表示的就是PCB

创建一个进程就要为它创建一个对应的PCB

CPU去调度对应的进程:

创建子进程,就是系统中多了一个进程罢了

以父进程为模版创建出对应的子进程,然后将其中的如pid和ppid这样的数据进行重新的修改一下。这样我们有了子进程的一个内核数据结构了,那么对应的代码和数据呢?

子进程创建的时候是没有自己的代码和数据的,只能去访问父进程的代码和数据。

这就是为什么fork之后代码和数据是父子进程共享的了

所以CPU在调度子进程的时候的代码便是父进程的代码,代码是不可以被修改的!

为什么要创建子进程呢?那是因为我们想要让父子进程执行不同的代码块,干不同的事情。这就让fork具有了不同的返回值!

2.一个函数如何做到返回两次呢?

fork本质是一个函数,它在操作系统内肯定是有着自己独特的实现的。

通过fork的工作在返回前就已经创建好了子进程,最后返回的return语句就由父子进程共享了,那么当返回的时候就可以实现返回两次。

3.子进程和父进程的代码是同一份,但是子进程如何看待对应的数据呢?

对于id的值有不同的情况怎么分析呢?

在任何的平台下面,进程在运行的时候,是具有独立性的,一个进程不会影响到其他的进程。

父子进程那么就不应该享有一份数据!!

那么理论上子进程就要想办法将数据也给去拷贝一份,但是正常情况下,这会出现很多没有必要的拷贝。

这就有了父子进程之间数据层面写实拷贝。当需要修改数据的时候,就会去进行拷贝数据到新空间去使用就不会影响父进程的数据。

那么在fork最后return的时候是写入吗?

肯定是的,子进程写入的时候就会发生写实拷贝。这样就使得id的值出现了不同。

还有一个问题:父子进程创建出来了,谁先运行,谁后运行呢?

谁先运行由调度器决定!用户是无法决定的

在一个电脑上,大部分就一个CPU,但是具有许多的进程,每个进程都想要被优先执行,抢占这个CPU。我们的调度器就可以帮助我们正确的,合适的去调度我们的这些进程。

最后一个问题:子进程的ppid是父进程,那么父进程的ppid是谁呢?

我们通过ps查看一下:

我们也就可以想到bash肯定也是通过创建fork这样的方式创建出子进程去执行,然后bash自己去等待去执行其他的指令。


三、进程状态

R (running)        ---        运行状态:并不代表进程一定在运行,它表明进程要么在运行中要么在运行队列里。

S   (sleeping)          ---        睡眠状态:意味着进程在等待事件的完成(这里睡眠有时候也叫做可中断睡眠(interruptible sleep))。

D   (disk sleep)       ---        磁盘休眠状态:有时候也叫不可中断睡眠状态 (uninterruptible sleep),在这个状态的进程通常会等待IO的结束。

T    (stopped)          ---        停止状态:可以通过发送SIGSTOP信号给进程来停止(T)进程。这个被暂停的进程可以被通过发送SIGCONT信号让进程继续运行。

t     (tracing stop)    ---        

X    (dead)                ---        死亡状态:这个状态只是一个返回状态,不会在任务列表中看到它

Z     (zomble)           ---        僵尸状态

1.进程状态,运行、阻塞、挂起

由上图我们可以看到在很多地方都有着自己的一个说法。但是大概的逻辑都没什么太大的问题,可以看看上面的模型。

1.1运行

我们前面讲过一个个的进程有自己的一个task_struct,我们让每个进程的内核数据结构去CPU中的运行队列中排队,就代表了这个进程正在运行状态

当CPU去找自己的运行队列的时候,调度器(一种函数),将运行队列作为一种参数传入进去。调度器就能够找到所有排队的进程。所有处于运行队列的进程就处于运行状态,也就是R状态。

在这个运行队列中的进程代表着这个进程已经准备好了,随时都可以被调度。

一个进程只要把自己放到CPU上开始运行了,是不是一直要执行完毕,才把自己放下来?

并不是,如果是的话,当有一个进程进入死循环,那么其他进程就无法被调度了。必须要等到前面进程执行完毕才能执行自己的话就是非常的有问题。

为了防止这种情况,对于每一个进程都有一个叫做时间片的概念。

这个t就可以帮助我们避免这个进程被一直进行。当运行的时间超过t就会让该进程下来,重新排队。

这样,就可以在一个时间段之内,所有的进程代码都会被执行!

这个情况在计算机中就叫做并发执行

在计算机中就会充满大量的把进程从CPU上放上去,拿下来的动作!

这个就叫做进程切换

1.2阻塞

前面讲的那种通过运行队列对于进程的管理,本质上讲就是操作系统对于软件的管理。

那么对于外设这种硬件资源该如何去管理呢?

不管这些硬件是什么,我们通过一个结构来描述,然后再管理

当有一个进程要我们进行键盘输入的时候,出现了一直不输入的情况。

那么当前的这个进程就要想办法让自己去等待键盘资源。

我们此时将这个进程链入到键盘的这个队列当中即可,如果还有想要键盘资源的就都链在这个队列的后面排队去:

当然对于其他的一些硬件如果需要也会有对应的链接

对于这些排在后面的这个队列就叫做等待队列:waitqueue

我们就将这些处于在等待某种特定资源的进程,所处于的状态就叫做阻塞状态!!

当等待的资源就绪后,就直接将对应的这个进程拿走,放入运行队列之中。

1.3挂起

在前面的阻塞中,在阻塞的进程中,都会有自己的数据。这些每个进程都会消耗占有一部分资源,然而在等待的过程中这些资源也没有用到,导致操作系统内存严重不足了。为了保障操作系统能够正常使用的情况下,还在阻塞的这些进程的资源就被拿去使用了。

实在没有办法了,操作系统就将原有的数据给交给磁盘这种外设当中去存着。就只让对应的PCB在队列中排队,当下次执行到对应的进程的时候就将写入到外设的数据拿回来。

这个过程就叫做内存数据的换出换入

这个进程将自己的PCB去排队的了,就代表这个进程处于了挂起状态

2.具体如何维护Linux状态

2.1 R和S状态

看如下代码:

#include <stdio.h>
#include <unistd.h>int main()
{while(1){printf("hello everone!\n");sleep(1);}return 0;
}

我们将它运行起来,然后去查看一下对应进程的信息:

可以感受到一个死循环,对应的进程应该是正在运行的啊,但是为什么显示的却是S呢?不应该是R吗?

在将代码改一下:

#include <stdio.h>
#include <unistd.h>int main()
{while(1);return 0;
}

此时这个进程就显示的是R了。

那究竟为什么有了前面的打印代码就是S,没了就是R了呢?

主要是,有了打印,它会等待显示器资源。所以不要用自己的感觉来以为进程在运行。它有很大的概率是在阻塞等待中某种资源当中

没有IO的时候,它的进程状态就变成了R状态

所以,有时候,有些进程就需要等待一下资源,为S阻塞等待状态中。

R状态是存在的,S状态是比较常见的

因为操作系统是非常快的,运行只是一瞬间的事情,所以我们查的时候,大部分时间都只会查到正在等待的S状态,所以S状态是很常见的。

再看这个代码:

我们通过输入,将进程给阻塞起来:

#include <stdio.h>
#include <unistd.h>int main()
{int a = 0;printf("Please Enter# ");scanf("%d", &a);printf("echo : %d\n", a);return 0;
}

S状态就是我们前面讲的阻塞状态。。。

一直在等待我们的键盘输入。

显然可以想到我们的bash进程就是经常处于S状态

2.2 D状态 --- 深度休眠

S 浅度睡眠 --- 可以被唤醒,响应外部的一些变化

D 深度睡眠 --- 不响应操作系统的任何请求

可以这样理解D状态:

如果有一个进程要向磁盘去写入大量的数据,但是向磁盘写入的运行速度是很慢的,可能需要很久的时间才能够给进程答复。如果在这个时间段,操作系统出现了很严重的问题,如内存极度的不足,操作系统就要开始想方设法的杀死一些进程,腾出空间去使用。

如果此时操作系统将这个向磁盘写入数据的进程杀掉,磁盘写入的时候就会将数据丢失了。这将会导致很严重的问题。但是不管对于进程、磁盘、操作系统这几方来讲,都是没有错的,所以为了避免这样的错误发生,就需要一个深度睡眠的状态D去标识这个进程,避免被杀掉导致问题

当该进程完成了自己的任务后,就将自己的状态改回正常的状态。

对于这个D状态的来讲,它不响应操作系统的任何请求......

对于D状态是处于高度的IO时会出现的,一般还是很难见到的。

2.3 T和t状态

对于这两个状态,差别不大。这里讲解一下T状态

stopped暂停状态

我们先认识两个信号:18 19号信号

它们可以帮助我们进行将进程给继续和暂停的作用:

我们继续来运行一下这个代码看看现象:

#include <stdio.h>
#include <unistd.h>int main()
{while(1){printf("hello everone!\n");sleep(1);}return 0;
}

可以看到这个进程就一直在运行了

此时给它发送一个19号信号

可以发现该进程已经被我们暂停了,此时它的一个进程状态为:

此时它的进程状态就为T,已经被暂停了

自然通过18号信号可以将它恢复:

但是我们发现,它已经变成了一个后台的进程了,它的一个+号没有了

对于这种后台进程,使用简单的ctrl + c是杀不掉的,只能使用9号信号将它杀死:

好像感觉S和T状态没什么区别?那么S和T状态的区别是什么呢?

S和T都可以是操作系统层面上的阻塞状态,S状态肯定是在等待某种资源的就绪,但是T状态有可能是在等待某种资源的就绪,也有可能是单纯的被控制了,让这个进程之间就暂停了。

为什么要暂停一个进程了,有什么用?

我们使用gdb去调试一个代码的时候,打上断点,运行到那里的时候,就会出现t状态,表示被暂停了:

刚开始处于S状态,我们打上断点去运行一下:

t也是暂停状态

2.4 X状态

死亡状态:这个状态只是一个返回状态,不会在任务列表中看到它

2.5 Z状态 --- 僵尸状态

Z(zomble) 僵尸状态

对于一个进程它在死亡的时候不会立即进入死亡状态,它会先进入一个僵尸状态Z

Z状态是什么意思?为什么要维护这个Z状态呢?

当一个进程死亡了,先将自己的信息和死亡原因给维护一段时间,这个时候的进程状态就处于僵尸状态。将这些信息维护起来给父进程知道,方便父进程知道原因

看以下代码,我们来看看僵尸进程 :

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>int main()
{pid_t id = fork();if (id == 0){// 子进程int cnt = 5;while(cnt--){printf("I am child, pid: %d, ppid: %d\n", getpid(), getppid());sleep(1);}exit(0); // 直接退出}else{// 父进程while(1){printf("I am parent, pid: %d, ppid: %d\n", getpid(), getppid());sleep(1);}}return 0;
}

运行后,子进程循环结束按道理已经死亡了,我们去观察一下:

我们发现子进程并没有X状态,而是Z状态僵尸了

进程一般退出的时候,如果父进程没有主动回收子进程的信息,那么子进程就将保持着僵尸状态,让自己一直处于Z状态。进程的相关资源尤其是task_struct结构体不能被释放掉!

如果一直保持僵尸,此时就造成了内存泄漏的问题了!

至于怎么回收子进程,博主将在后续的文章里面揭示!

那通过上面的演示,可能就有人会想着如果父进程先退出了会怎么样呢???我们一样来写一段代码观察观察:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>int main()
{pid_t id = fork();if (id == 0){// 子进程while(1){printf("I am child, pid: %d, ppid: %d\n", getpid(), getppid());sleep(1);}}else{// 父进程int cnt = 5;while(cnt--){printf("I am parent, pid: %d, ppid: %d\n", getpid(), getppid());sleep(1);}exit(0);}return 0;
}

打开监控观察如下:

while :; do ps ajx | head -1 && ps ajx | grep proc | grep -v grep; echo "--------------------------------------------------------------------------------------------"; sleep 1; done;

我们可以看到当父进程退出了,子进程还在的情况,子进程将被1号进程给接管。

那么1号进程是谁呢?它就是当前的操作系统本身!(根据操作系统版本的不一样,有叫systemd也有叫init)

父子进程,父进程先退出,子进程的父进程会被改成1号进程(操作系统)

父进程是1号进程 ------ 孤儿进程

该进程被系统领养!

为什么要被领养呢?

因为孤儿进程未来也要退出,也要被释放!


四、进程组织结构反思

在Linux中,可以发现其实进程链入的结构有可能是双向链表中,也可能是在树当中,它不是单一的某种结果能够维护好这一大堆的进程的。那么对于进程来讲是如何做到的呢?

我们进程的task_struct中会含有一个link的对象,通过这个来连接其他的进程。

比如这个双链表的结构,我们通过一个start指针就可以去遍历其他的节点(进程)了

那么我们怎么去计算出每个进程的其他的数据的呢?其他的字段呢?

&((task_struct*)0->link)): 通过0号地址转换成对应的task_struct*类型,然后去调用link,再取地址就可以计算出link在task_struct里面的一个偏移量是多少。

我们是通过start来遍历的,所以start是在内存中link真实的一个起始位置,我们通过:

start - &((task_struct*)0->link)) 计算出真实地址处对应task_struct的起始的地址

然后将这个地址再转换成task_struct类型,就可以实现遍历其他的数据字段的功能了。

有了这个当我们的进程的类型不一样的时候,也是可以连接进入这个数据结构当中的,只要通过计算就可以去获取内容了!!

如果当结构变成了left ,right这样的树也是可以正确的使用的,也就可以让不同的进程链入不同的数据结构了!!


结语

很高兴大家能够有所收获!有所疑问可以私信博主,或者留言评论!

大家多多关注 + 收藏 + 点赞,谢谢大家!


http://www.dtcms.com/a/420872.html

相关文章:

  • 柳市网站设计推广网站怎么做微信接口
  • 注册自己的网站需要多少钱网站 备案查询
  • 网站界面设计策划书怎么做招远网站建设公司
  • 网站规划的原则北京建设教育协会
  • 班级网站建设的系统概述合肥建站
  • 软考 系统架构设计师系列知识点之杂项集萃(159)
  • 外国ps修图网站开发网站流程
  • 做最好的网站新新常州企业自助建站系统
  • 天津市住房和城乡建设局网站派代网
  • 大连营商环境建设局网站代理记账如何获取客户
  • 网站开发什么技术拼团网站建设
  • 朝阳网站建设公司电话上海知名装修公司排名榜
  • 中华住房和城乡建设局网站网站网页不对称
  • 广州网站优化公司金融投资网站源码
  • 亚马逊卖家可以做促销的网站网站开发实战作业答案
  • wordpress 导航站主题网站建设代理推广徽信xiala5效果好
  • 查排名的网站WordPress显示插件
  • 培训班该如何建站页面设计的步骤
  • 数据库事务(Transaction)的概念及其底层实现原理
  • Cadence(Allegro)的PCB文件转PADS的PCB文件
  • 网站建设合同交印花税么wordpress验证码
  • 东莞建设工程质量网站公司网站的实例
  • 【开题答辩全过程】以 spb+汽车租赁系统为例,包含答辩的问题和答案
  • 网站死循环南通网站建设哪家好
  • 淘宝客cms网站模板下载网站建设 用英文怎么说
  • wordpress多广告位深圳网站营销seo费用
  • pwn知识点——内平栈与外平栈
  • 单页面网站有哪些北京网站建设公司兴田德润电话
  • 南城网站建设公司咨询开封府景点网站建设的目的
  • php 网站后台海报模板免费网站