您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Model Center   Code  
会员   
   
 
     
   
 
 订阅
Linux进程的理解 --- 进程描述符、状态、优先级、切换…
作者 :rygttm
  830  次浏览      16 次
 2023-2-7
 
编辑推荐:
本文主要介绍了Linux进程的相关知识:进程描述符、状态、优先级、切换等。希望对您的学习有所帮助。
本文来自CSDN,由火龙果软件Linda编辑、推荐。

一、冯诺依曼体系结构(硬件)

1.冯诺依曼体系结构中的存储器指的是内存,带电存储,具有掉电易失的特点。

2.CPU中含有能够解释计算机指令的指令集,指令集又可分为精简指令集和复杂指令集,这也正是为什么你的程序能够运行起来的原因,因为CPU认识并理解你的二进制程序代码,你的二进制程序会被CPU认为是一堆指令的集合,CPU直接执行这些二进制指令就OK了。

3.某些外部设备例如磁盘、网卡等,既属于输入设备又属于输出设备。

4.CPU在读取和写入的时候,在数据层面,只和内存打交道,不和外设直接沟通,这样有利于提高整个计算机的运行效率。

5.磁盘上的文件程序想要运行,必须要加载到内存里面,因为CPU只能从内存中访问你写的数据和代码,我们平常所进行的编程其实就是在为CPU准备数据和代码,等CPU过来读取这些代码并执行他,这些都是冯诺依曼体系结构所决定的

下面从软件数据流的角度来更深入的理解冯诺依曼硬件结构体系:

1.如果我向我的好友发送消息,从硬件结构来看,前提应该是我们各自都打开了QQ程序,并将QQ这个程序加载到了内存里面,CPU会执行QQ程序的代码,我就可以通过代码里面的scanf或cin等语句的执行,利用键盘发送消息“你好呀!你今天吃饭没?”等等消息,这些消息数据会由输入设备键盘加载到内存中的QQ程序里面,然后CPU对这些消息做出处理,将处理后的结果返回给内存里面,然后这些消息会从内存进一步加载到外部设备网卡和显示器等里面,我的朋友的笔记本的输入设备网卡会接收这些消息,并将这些消息加载到他的内存中的QQ程序里面,然后CPU做出信息的分析将结果返回到内存里面,最后这些处理过后的信息会进一步加载到我朋友笔记本上的显示器中,这样就完成了信息的发送和接收等。

2.如果是发送文件,其实就是从磁盘这样的外设中将信息加载到内存中的QQ程序里面,然后通过网卡将信息传输过去,我的朋友就可以用他的网卡接收到这些信息,并把我发送的信息加载到内存里面,最后在输出设备磁盘里面重新刷新,就会出现接收到的文件信息了。

二、操作系统(软件)

1.操作系统是什么?

1.操作系统是一个进行软硬件资源管理的软件

2.操作系统包括进程管理,内存管理,文件系统,驱动管理,这些都是操作系统对于软件的管理,除了管理这些,操作系统还承担管理冯诺依曼硬件体系结构。

3.为什么操作系统要进行管理呢?因为操作系统可以通过合理的对于软硬件资源管理(手段),来为用户提供良好的(稳定的、安全的、高效的)执行环境(目的)

2.如何理解管理(管理的本质)

1.管理者和被管理者之间是一个什么样的逻辑关系呢?就像我们在大学生活中的校长,我们很少直接与校长进行交互,但校长依旧管理着我们,这是怎么做到的呢?还比如公司中的员工和CEO,CEO不会和员工直接对话、嘘寒问暖等等,但公司依旧可以正常运行,正常的被管理,这又是怎么做的呢?

2.我们可以通过这样的现象得出一个既定的事实,那就是管理者和被管理者不需要直接进行交互,管理者依旧能把被管理对象管理起来。

3.首先管理者需要有重大事宜的决策能力,并且决策是要有依据的,同时管理者要拥有被管理对象的全部数据,只要拥有了被管理对象的数据,通过数据的变化和更新,管理者就可以与之对应做出管理的方案。

4.所以管理的本质就是对数据做出管理

3.操作系统如何一直拿到硬件的数据?(驱动程序的引出)

1.我们在上面谈到过,管理者和被管理者是不直接进行交互的,就比如校长是不和我们大学生直接进行面对面的谈论和交流的,那么校方是如何拿到每一个大学生的数据呢?操作系统又是如何拿到以冯诺依曼体系为基础的所有硬件结构的数据呢?

2.这都需要一个执行者,执行者负责拿到被管理者的全部信息,现实中的管理者可能就是我们的班长或辅导员等等,他可以拿到我们的全部数据,最后将这些数据统计汇报给校方,自然计算机中也需要这么一个执行者,这个执行者就是驱动程序

3.所以,驱动程序会做两件事情,第一件当然是和被管理者也就是底层硬件直接接触,拿到被管理者的所有数据,第二件事情就是执行管理者的命令也就是操作系统的命令,如何执行,执行什么等决策都是由管理者操作系统来决定的

4.OS可以根据硬件的数据变化来命令驱动程序对硬件进行相应的管理,这样就可以实现对硬件的管理,命令中枢在OS,驱动程序既要将硬件数据返回给操作系统,又要执行操作系统的命令以此来管理好硬件资源。

4.操作系统对于庞大的软硬件的数据量如何进行管理?(先描述,再组织)

1.我们知道计算机的底层硬件是非常多的,光单个硬件的数据量其实就已经非常多了,更别说所有的底层硬件的数据了,那将是非常庞大的数字,操作系统对于驱动程序返回来的如此庞大的数据量一定会非常的头疼,如果没有合理的管理,这么多的数据一定会乱起来的,计算机就无法正常运行了。

2.例如校方获得学生的庞大信息之后,他一定不会拿许许多多的表格和文件来存放这些信息,这样的效率非常的低,所以校方会有自己的一个系统,这个系统里面会存储学生的信息,以这样的方式来对学生进行管理,所以可以总结出来管理的方法本质逻辑就是先描述,再组织,校方通过一个程序,例如这个程序利用了链表将学生信息存储起来,那么每个节点就需要描述好每个学生,入学时间,高考成绩,在校表现等等,然后将节点组织成链表,通过数据结构的方式来将学生的信息组织起来,进行统一的管理。

3.所以,所有的“管理”,本质逻辑都是“先描述,再组织。”描述是编程语言的话题,组织是数据结构的话题,组织其实就是对被管理对象进行建模的过程

4.操作系统对于硬件是先描述再组织进行管理的,那对于软件的管理呢?进程、文件系统、内存、驱动、系统调用接口等软件,操作系统又是怎么管理呢?答案还是“先描述,再组织”,操作系统依旧通过获得他们的数据,通过类或结构体(因为Linux内核是用C语言写的)将这些获得的数据描述起来,然后再通过链表或者其他更高效的数据结构来将这些数据组织起来,然后进行管理。

5.人能管理事物,人也能管理人。软件能管理硬件,软件也能管理软件。

5.计算机的软硬件结构体系(计算机的层状结构)

1.首先操作系统是不相信任何的用户的,如果用户随意篡改操作系统的源代码,那计算机就无法正常的使用,并且如果这些源代码被公开,这很有可能让操作系统受到 “伤害” 所以操作系统是无法分辨一个用户会不会乱来的,那么操作系统就有保护自己的义务,但同时操作系统又需要对上服务好用户,所以可以得出来一个矛盾,操作系统不可以直接开放给用户使用,但是操作系统还需要对用户开放良好的服务。

2.这时,操作系统会提供给用户一些系统调用接口,这些接口可以被用户所调用,如果用户的操作不合法,那就不能正常调用到这些系统接口,我们的请求就不会被操作系统所接收,如此一来,这些系统调用接口就可以变相的保护操作系统,并且还可以给用户提供服务。

3.这些系统调用接口,都是由C语言写出来的,所以这些接口都是C式的接口,说白了就是操作系统通过C语言给我们提供了一些系统级别的函数调用的接口。

4.当然,这些接口普通人是不会使用的,随之在这些系统级别的接口外面一层又开放了用户操作接口,用户可以通过自身的一些操作来调用这些用户操作接口,以此使得操作系统能够更加先进的服务用户。

6.系统调用和库函数的区别(上下层关系)

1.在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用接口。

2.系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于上层用户或者开发者进行二次开发,许多的C/C++库其实就是系统调用接口封装得来的,所以两者其实是上下层的关系。

三、进程

1.OS如何管理进程?(先描述,再组织:进程控制块PCB)

1.首先程序的本质其实就是文件,该文件可以被永久性的存放在磁盘当中,一个加载到内存中的程序,我们就称之为进程,在windows下我们可以看到许多进程,包括正在运行的和后台运行的进程,对于如此多的进程,操作系统是一定要进行管理的,那该如何进行管理呢?

2.管理的本质是对数据进行管理,管理的逻辑是先描述,再组织,在Linux中,操作系统会通过task_struct结构体task_struct(转载博客园童嫣博主的文章)将每一个进程的所有属性抽象化描述起来,Linux操作系统再通过双向循环链表的数据结构将数量庞大的进程进行组织,这样的话,管理进程就变成了对进程所对应的PCB进行相关的管理。

3.进程的数据被存放在一个叫做进程控制块的数据结构当中,进程控制块又可以称之为PCB(process control block),进程控制块中包含进程标识符、上下文数据、进程调度信息、进程控制信息、IO状态信息以及进程对应的磁盘代码等,Linux操作系统中进程控制块其实就是struct task_struct结构体,windows操作系统中进程控制块其实就是执行体进程块(struct _EPROCESS )

4.进程和程序相比进程是具有动态属性的,程序仅仅只是一堆代码形成的文件而已,所以我们将进程提炼出来,进程=进程控制块(内核数据结构 struct task_struct)+ 进程对应的磁盘代码

2.查看进程的两种方式

进程在调度运行的时候,进程就具有动态属性!

一、ps指令

ppid父进程id,pid是进程id,pgid是进程组id,sid会话id,TTY终端,STAT状态,uid用户id,COMMAND代表哪个进程

[wyn@VM-8-2-centos test_dir]$ ps ajx | head -1 && ps ajx | grep "myproc"
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
12672 15583 15583 12672 pts/1 15583 S+ 1001 0:00 ./myproc
13962 16742 16741 13962 pts/0 16741 S+ 1001 0:00 grep --color=auto myproc

 

二、ls指令

我们还可以通过根目录下的proc目录来查看进程,我们的进程也可以被当作一个目录,Linux下一切皆文件

ls /proc/进程的pid

三、如果删除掉了进程对应的磁盘上的二进制可执行程序,进程还会运行吗?

答案是没有任何影响,完全可以继续运行,所以理论上来讲,一旦程序加载到内存之后,进程和程序就没关系了,但是进程中的可执行程序文件会冒红。

杀掉进程

kill -l --- 查看kill指令选项
kill -9 + 进程id --- 杀掉进程

3.与进程相关的系统调用

3.1 getpid() && getppid()(获取进程的标识符)

1 #include <stdio.h>
2 #include <unistd.h>
3 #include <sys/types.h>
4
5     int main()
6 {
7      while(1)
8 {
9      printf("我是一个进程!,我的进程id是%d,
我的父进程pid是:%d\n",getpid(),getppid());
10      sleep(1);
11 }
12      return 0;
13 }

1.我们可以看到进程的id随着我们的多次运行会不断的变化,但是父进程的id一直不变,并且我们还查看到了父进程的名字是bash,shell有多种,但bash是最常见的一种,bash就是centos系统下的shellLinux下有几种shell(转载自cunchi4221

博主的文章)所以当前进程的父进程就是命令行解释器bash,bash的进程id,系统会自动给我们分配好,如果我们杀掉了命令行解释器bash这个进程的话,我们就会自动退出xshell工具。

2.下面便可以看到父进程id始终不变,我的代码对应的进程id会因为程序的多次运行而变化,并且我的进程的父进程是bash,所以我们可以得出结论:命令行上启动的进程,一般它的父进程没有特殊情况的话,都是bash!

 

3.2 fork()(创建进程)

1 #include <stdio.h>
2 #include <unistd.h>
3 #include <sys/types.h>
4
5      int main()
6 {
7 // 创建子进程 -- fork是一个函数 -- 函数执行前:
只有一个父进程(bash派生的)-- 函数执行后:就会有父进程和父进程创建的子进程。
8      fork();
9
10 printf("我是一个进程!,我的进程id是%d,我的父进程pid是:%d\n",getpid(),getppid());
11      sleep(3);
12
13 return 0;
14 }

1.由程序运行结果可以看到,printf被执行了两次,这是怎么一回事啊?这其实是因为子进程进程做了父进程一模一样的事情,他把代码也执行了一遍。

下面的7088进程的父进程就是1492,1492实际上就是bash,7088的子进程是7089,这个进程就是fork函数创建出来的子进程,这个子进程的父进程是7088,也就是bash的子进程,所以7088即是bash的儿子,又是fork产生的进程7089的爹。

2.通过man手册在底行中输入/加查找内容就可以查看到fork函数的返回值,它的意思是创建子进程如果成功的话,就会将子进程pid返回给父进程,数字0会被返回给子进程,如果创建失败的话,返回-1给父进程,没有子进程的创建。

下面我们再来看一段fork接口的常用形式的代码和其运行结果

     
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <sys/types.h>
4
5 int main()
6 {
7 // 创建子进程 -- fork是一个函数 -- 函数执行前:
只有一个父进程(bash派生的)-- 函数执行后:
就会有父进程和父进程创建的子进程。
8       pid_t ret=fork();
9 //fork使用后一般要用if进行分流
10       if(ret==0)
11 {
12 // 子进程
13       while(1)
14 {
15       printf("我是一个进程!,我的进程id是%d,
我的父进程pid是:%d,ret是:%d\n",getpid(),getppid(),ret);
16       sleep(1);
17 }
18
19 }
20       else if(ret>0)
21 {
22       // 父进程
23       while(1)
24 {
25       printf("我是一个进程!,我的进程id是%d,
我的父进程pid是:%d,ret是:%d\n",getpid(),getppid(),ret);
26      sleep(2);
27 }
28 }
29      else
30 {
31
32 }
33       return 0;
34 }

3.fork之后,会有父进程和子进程两个进程在执行后续的代码,并且后续的代码被父子进程共享,我们可以通过返回值的不同,让两个进程执行后续共享代码的不同部分。

我们也可以通过这样的手段来让两个进程执行不同的任务,这就是所谓的并发式的编程。

四、进程状态

1.普遍的操作系统层面:理解总结进程状态(运行、阻塞、挂起状态)

1.计算机在开机的时候,操作系统就会被加载到内存里面,磁盘中的程序在运行的时候也会被加载到内存里面,实际上是加载到操作系统内部,受操作系统的管理,我们知道程序运行的时候,是需要CPU进行读取进程的代码并计算的,但进程的数量一定会比CPU多,那CPU该怎么一个个的读取进程代码并计算呢?答案是通过运行队列(数据结构)来对进程的运行进行管理。

2.一个CPU匹配一个运行队列

3.让进程入队列,等待CPU资源。本质:将该进程的task_struct结构体对象放入CPU的运行队列struct runqueue中。操作系统操作的不是加载到内存中的程序,操作的是进程对应的PCB(进程控制块,内核数据结构)。

4.CPU进行进程的调度,其实就是从自己的运行队列里面,找到进程对应的PCB,然后执行进程对应的代码和数据

5.进程的运行状态指的并不是这个进程正在被运行,因为这个数据是没有意义的,CPU太快了,几微秒就可以运行完一个进程,而是指的是这个进程的PCB在CPU的运行队列runqueue当中,只要这个进程在运行队列里,那么这个进程的状态就是运行状态

6.进程的状态其实就是进程内部的属性,那么这个状态其实就是存在于进程对应的PCB当中,状态在PCB里面其实就是一些整数,每个整数对应不同的状态,可以用#define R 1类似这样的表示来区分不同的进程状态,如下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 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};

 

7.不要只以为,进程只会等待(占用)CPU资源,进程也可能随时随地,占用外设资源!

8.进程或多或少都要访问硬件,例如我们平常代码所写的printf、cout要访问显示器,对文件进行IO要访问磁盘,scanf、cin要访问键盘,但这些外设的运行速度是很慢的,另一方面,这些外设的数量也是较少的,所以很有可能出现多个进程访问一个硬件的情况,但这个硬件一次只能服务一个进程,所以其他进程就需要排队。

9.当CPU调度的某个进程需要访问外设时,那操作系统就会把这个进程放到硬件的结构体描述里面的等待队列task_struct * queue里面,直到硬件准备就绪,此时这个进程的状态就是阻塞状态,表示当前进程不能直接被CPU调度,需要等待某种硬件资源的就绪。(值得注意的是,和CPU运行队列相同,操作系统操作的依旧是进程对应的PCB(task_struct结构体对象),将PCB放到硬件的结构体内部的等待队列中)。

10.当进程访问的硬件就绪之后,表示进程又可以运行了,这个时候,操作系统就又把该进程对应的PCB中的状态改为R,然后将PCB放到CPU的运行队列里面,此时这个进程的状态就从阻塞状态改为运行状态了。

11.所谓的进程的不同的状态,本质上其实就是进程在不同的队列中,等待某种资源。

在CPU的运行队列中的进程一般称为R(运行)状态进程,在硬件所对应的内存中的结构体描述里面的阻塞队列里的进程,称之为阻塞状态进程。

12.当多个进程的状态是阻塞的时候,这些进程都无法被立即调度,也就是无法被CPU立即执行,并且排队的进程其实是要等待很长的时间的,因为外设的速度和CPU的速度相比简直是太慢了,差的不是一星半点儿,这个时候,PCB和其对应的进程的代码和数据就会占用内存,这些进程短期内不会使用,还白白的占用着内存空间,所以操作系统就把这些进程的代码和数据暂时保存到磁盘上,但进程对应的PCB还留在内存里面,操作系统这样的作法就可以节省内存空间。

13.我们将代码和数据换出到磁盘的这种进程,称之为挂起进程,该进程的内核数据结构依旧在内存,它的代码和数据被操作系统暂时换出到磁盘里面,以节省内存空间给其他需要加载到内存的程序使用。

14.等到进程对应的硬件资源就绪之后,操作系统再将进程的代码和数据换入到内存当中,当进程占用硬件资源结束后,操作系统再将PCB放入CPU的运行队列里面,使进程重新运行起来。

我们将进程的代码和数据,加载到内存和暂时保存到磁盘,称为内存数据的换入换出。

15.阻塞不一定是挂起,但挂起一定是阻塞。

如果内存空间特别够的话,操作系统没有必要将阻塞的进程挂起。

2.具体的操作系统:Linux下的进程状态

2.1 vim批量化注释

批量化注释: ctrl+v进入块选择模式,kj上下移动光标选择你要注释的行,按下大写的I并输入注释符号//,输入完毕按下esc退出即可,vim会自动在你选择的行首前面加上注释符号。

取消注释: ctrl+v进入块选择模式,利用hjkl选择两列的//注释符号,因为//是两列,最后按下d删除即可取消注释。

2.2 运行状态+休眠状态(阻塞状态的一种,是否挂起未知,这完全取决于OS,不同的OS不一定将挂起暴露出来给你)

下面的代码其实是仿造的一种计算密集型进程,演示进程的R状态。

1 #include <stdio.h>
2 #include <unistd.h>
3 #include <sys/types.h>
4
5      int main()
6 {
7
8       while(1)
9 {}
10
11 }

 

1.当程序代码仅仅只是一个死循环时,我们将程序运行起来,然后查看进程状态,可以很明显的看到状态是R,也就是运行状态。

下面的代码其实是仿造的一种IO密集型进程,演示进程的S状态

1 #include <stdio.h>
2 #include <unistd.h>
3 #include <sys/types.h>
4
5      int main()
6 {
7       int cnt=0;
8       int a=0;
9       while(1)
10 {
11       a=1+1;
12       printf("a的值是:%d,running flag=%d\n",a,cnt);
13       sleep(1);
14 }
15 }

 

2.在代码中添加printf语句之后,程序还在运行,但进程的状态是S休眠状态,这是为什么呢?

因为我们的代码中访问了显示器,显示器是外设,速度非常慢,CPU会飞速的运行完进程的所有代码,但是我们写的进程需要占用硬件资源,每一次占用硬件资源都要等显示器就绪,这会花很长的时间(和CPU相比),大概率99%的时间是进程在等显示器就绪,也就是在等IO就绪,1%的时间是CPU在运行进程的代码,所以我们在查看进程状态的时候,极大概率上查到的都是S休眠状态。更形象化的说明就是,在进程访问完毕一次显示器的时候,CPU已经将这个死循环代码执行了50、60万次,所以我们在查看进程状态的时候,进程都是在等IO就绪的,所以就会查看到进程是休眠状态,这也是阻塞状态的一种。

3.CPU计算的速度和IO的速度差别大概是几十万倍。

4.操作系统不会让你看到挂起状态,因为这对你来说毫无意义,操作系统不需要告诉你,你也不需要知道。

2.3 停止状态(stopped:阻塞状态的一种,是否挂起未知,这完全取决于OS,不同的OS不一定将挂起暴露出来给你)

1.当进程被停止的时候,其实也是阻塞状态的一种,因为当前没有代码再运行了。

这个进程当然也可以被挂起,但这一点是未知的,这完全取决于OS。

kill -19 + 进程id --- 停止运行进程
kill -18 + 进程id --- 继续运行进程

2.状态后面带+,表示前台进程,状态后面不带+,表示后台进程。

3.前台进程在运行的时候,shell命令行无法继续获取命令行解析,但是可以通过ctrl+c将进程终止掉。

后台程序在运行的时候,shell命令行可以继续获取命令行解析,但无法通过ctrl+c将进程终止掉,需要通过kill指令 + -9信号才可以将进程终止掉,也就是杀掉进程。

下面是运行状态的前台和后台进程

下面是休眠状态的前台和后台进程

2.4 磁盘休眠状态(disk sleep:阻塞状态的一种,高IO的环境可能出现这样的状态。同样挂起是未知的,这完全取决于OS,不同的OS不一定将挂起暴露出来给你)

1.S状态是浅度睡眠状态,是可以被终止的,通过ctrl+c或kill -9 pid两种方式进行分别进行前后台终止。

2.阻塞进程过多时,操作系统会将一些进程挂起,以此来解决内存空间不足的问题,如果挂起依旧无法解决内存空间不足,Linux就会将进程杀死,但是一旦杀死进程很有可能导致进程对应的IO过程失败,从而丢失大量数据,这会对用户造成巨大的损失,所以就出现了一个新的进程状态,深度睡眠状态,这样的进程无法被OS杀掉,一般情况下,只能等待IO过程结束,让进程自己醒来,重新投入CPU的运行队列,重新继续运行进程。万不得已可以通过断电的方式来杀掉深度睡眠的进程!!!

3.D状态是深度睡眠状态,在该状态的进程无法被OS杀掉!!!只能通过断电或者进程自己醒来,来解决!

4.当然深度睡眠的状态一般不会出现,只有高IO的情况下,运行某个程序时,进程才有可能出现深度睡眠的状态。

5.如果想要查看该进程状态,可以了解dd指令,它可以营造高IO的状态,这样的状态下运行的进程有可能会出现深度睡眠状态,也就是D状态。dd指令详解(转载自csdn博主远近长安博主的文章)

2.5 跟踪状态(tracing stop:阻塞状态的一种,是否挂起未知,这完全取决于OS,不同的OS不一定将挂起暴露出来给你)

Makefile文件内容:
1 myprocess:myprocess.c
2 gcc -o $@ $^ -g
3 .PHONY:clean
4 clean:
5 rm -f myprocess

$@代表冒号左侧的目标文件,
$^代表冒号右侧的依赖文件列表,这些是makefile中的特殊符号

 

1.我们在调试某个二进制程序的时候,其实就是在调试该进程,当进程中有断点的时候,gdb中按下r进行调试运行,此时就会由于断点的存在而停下来,这其实表示的就是我们当前运行的进程停下来了,等待我们查看当前进程的上下文数据,这就是tracing stop状态,跟踪状态。

2.Linux内核源代码中跟踪状态用的还是T表示,这里为了区分跟踪和停止状态,将T改为t

2.6 僵死状态(zombie:进程退出状态未被读取,PCB依旧占用内存资源,进程资源未被回收干净)

1.进程被创建的目的其实是为了完成某个任务,这个任务是OS或用户布置的,这也正是进程对应的内核数据结构叫做task_struct的原因。

2.当进程完成任务之后,父进程或者OS一定得知道这个任务完成的结果是怎么样的,所以在进程终止的时候,OS机制是不可以立即释放该进程占用的内存资源的,必须保存一段时间,让父进程或者OS来读取进程的结果。

3.进程退出的信息一般都会在其对应的PCB中保存一段时间,等待父进程或者OS读取。

4.只要某个进程退出,但是没有被父进程或者OS回收,这样的进程我们就称之为僵尸进程。

5.我们可以创建一个子进程,让父进程不要退出,而且什么都不做一直运行就好,不要回收子进程,然后再让子进程正常退出,此时这个子进程就会处于一个僵死状态,因为进程退出后没有人回收它。

下面是演示僵尸进程的代码

1 #include <stdio.h>
2 #include <unistd.h>
3 #include <stdlib.h>
4
5      int main()
6 {
7
8       pid_t id = fork();
9
10       if (id==0)
11 {
12       printf("I am a child process,pid:%d,ppid:%d\n",getpid(),getppid());
13       sleep(5);
14       exit(1);
15 }
16       else
17 {
18       while(1)
19 {
20       printf("I am a parent process,pid:%d,ppid:%d\n",getpid(),getppid());
21       sleep(1);
22 }
23 }
24       return 0;
25 }

一个可以每次间隔一秒查看进程状态的脚本

while :; do ps axj | head -1 && ps axj 
|
grep myprocess | grep -v grep ; sleep 1 ; done

6.父进程处于浅度睡眠状态,因为要等显示器就绪,这个我们前面说过,但子进程处于Z状态,就是僵死状态,表示这个进程已经退出但是没有人回收它,并且它的COMMAND后面加了defunct,这个单词的意思是失效的,不起作用的。

7.在结束进程之后,操作系统会帮我们将父进程和子进程一起回收掉。以免内存泄露的发生。

8.进程的退出状态也属于进程的基本信息,也是需要数据进行维护的,所以这种信息会被保存在进程对应的PCB里面,如果进程的状态一直是Z状态的话(父进程一直不读取子进程的退出状态),那么PCB就需要一直维护这种状态信息,虽然子进程对应的代码和数据会被释放,但是PCB是不会被释放的,因为他需要维护进程的Z状态,所以这个时候就会产生内存泄露的问题。

9.僵尸进程无法被杀掉,即使通过kill和-9信号也无法杀掉,因为它已经死亡了,所以无法被杀掉的进程有三个,深度睡眠进程,僵尸进程,死亡进程,D状态是不能杀,Z和X是无法杀,因为已经死了!!!

下面是另一种演示僵尸进程的gif动图,手动杀掉子进程,但父进程依旧运行,不回收

2.7 死亡状态(dead:Z状态之后就是X状态,PCB也已被释放)

1.当进程死亡后,操作系统会立即回收进程的所有资源和数据,因为这个过程是非常快的,所以我们无法观察到进程的X状态,但这个概念比较好理解,简单来说其实就是进程被终止掉了,它的PCB和对应代码和数据都退出内存,不再占用内存资源,此时这个进程就是实实在在的死亡状态,它的所有资源都被操作系统释放掉了。

2.8 孤儿进程(没爹的孩子,OS来当这个爹!!!)

1.子进程先退出,父进程不退出继续运行且不回收子进程,那么这个子进程就是僵尸进程,但如果父进程先退出,那么子进程就变成孤儿进程,它会被1号进程回收,1号进程另一个叫法是init进程,init进程会回收孤儿进程剩余资源。

2.在僵尸进程的最后部分,我们手动杀掉了子进程,子进程就进入了僵尸状态,这里我们手动杀掉了父进程,但父进程却没有进入僵尸状态,而是直接进入死亡状态,只不过我们看不到死亡状态,这是为什么呢?其实是因为bash回收了父进程的所有资源(PCB+代码和数据),所以父进程没有变为僵尸进程

3.通过进程状态的查看,发现子进程的父进程变为1号进程,我们称被1号进程领养的子进程为孤儿进程,1号进程其实就是操作系统,所以实际上子进程是被操作系统领养。

4.如果不领养,那在子进程退出的时候,子进程就会变为僵尸进程,此时就没有人可以回收子进程了,就会造成内存泄露,所以操作系统为了管理好这些软件资源,必须领养这个没爹的孩子!!!

5.前台进程创建的子进程如果变为孤儿进程,那么这个进程会自动被切换为后台进程

五、进程优先级(受nice值调控的priority值 )

1.什么是优先级?

1.先还是后获得某种资源的能力,被称之为优先级。

2.先获得就是优先级高,后获得就是优先级低

2.为什么会存在优先级?

1.因为资源是有限的。

2.比如现在有20多个进程要访问CPU,但CPU只有一个,另外20个进程要访问网卡,另外40个进程要访问磁盘,但是资源太少了,但访问的又太多了,此时就会出现优先级的情况。想要访问,可以!但需要排队,这就出现了优先级。

3.Linux优先级特点

1.进程的优先级和状态一样,本质都是PCB里面的一个数字(也可能是多个数字),操作系统通过这些数字来辨别进程的状态和优先级。

2.通过ps -al指令可以查看到进程的详细信息,PRI和NI的值合并在一起代表Linux进程的优先级。PRI其实就是最终优先级,只不过它受NI值的调控。

4.修改Linux下的优先级(top指令 + root身份)

1.最终优先级=老的优先级(固定为80)+nice值,Linux支持进程在运行中,通过修改nice值的改变来进行进程优先级的调整。

2.PRI的值越小代表优先级越高,输入sudo top指令(用管理员的身份去做),然后按下r,输入对应的进程pid(表示你要修改哪个进程的优先级),然后输入整数值,这个值就是你要修改的nice值。

3.优先级过度设置,会导致CPU调度失衡,所以nice值是有范围的,不能让你随意过度的调整,nice值的范围是-20到19,一共40个优先级级别,所以最终优先级的范围就是60到99.

4.下面是nice值分别调整为-100和100后,进程的优先级分别为60和99的结果,这也就说明,OS不会让我们过度随意调整nice值,nice值的调整是有一个限度范围的。

六、四个补充的进程概念(注意区分并行和并发)

竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级,值得注意的是进程排队的时候,OS操作的是进程对应的PCB而不是进程对应的代码和数据。

独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰!就算是父子进程在运行时也是互不干扰的。例如下面的子进程挂掉,变为僵尸进程,但父进程依旧可以运行,所以进程间运行是互不干扰的。

并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行。有几个CPU,就可以同时运行几个进程。

并发: 多个进程在一个CPU下采用进程切换的方式来实现多个进程的运行(不是同时运行),在一个时间段内,让多个进程都得以推进,称之为并发。

如果只有一个CPU,那么在任何时刻,只能有一个进程正在运行!多个进程的运行依靠的是进程切换,呈现给使用者的就是我们好像看到多个进程在同时运行,但其实不是这样的,任何时刻只能有一个进程在运行。

不是一个进程必须在CPU上跑完才能被CPU拿下来,当代计算机采用的是时间片轮转的策略,一个进程可以跑一会儿就换到下一个进程,在一个时间段内,每个进程都可以跑多次,呈现出来的现象就是多个进程同时运行,例如我的计算机现在可以打开画图工具,还可以开直播和别人聊天,还可以打开xshell进行linux的学习,但这些进程并不是同时在一个CPU下运行的,只是采用了进程切换的方式来完成的。

CPU可以在几毫秒的时间内,就将每个进程都运行好几次,CPU读取代码的速度非常快!

七、进程切换(进程的上下文保护和恢复)

1.一个CPU会有大量的寄存器,虽然寄存器很多,但是只有一套寄存器,寄存器分为用户可见的寄存器和用户不可见的寄存器(状态寄存器、权限寄存器等),对于寄存器的深入了解,可以看看寄存器详谈(转载自知乎博主轩辕之风的文章)

2.CPU永远做三件事情,取指令(代码被编译器翻译成二进制指令),分析指令,执行指令,在CPU中有一个叫做eip的寄存器,专门用来标识下一次应该从当前进程的具体的位置读取相应的代码和指令

3.当进程在运行的时候,一定会产生非常多的临时数据,这些临时数据只属于当前进程,虽然CPU内部只有一套寄存器硬件,但是寄存器里面保存的数据是属于当前进程的,寄存器硬件和寄存器内的数据是两码事。

4.进程在运行的时候,的确是占有CPU的,但进程并不是一直占有CPU直到进程运行结束,这个我们前面说过,计算机有时间片轮转的策略,每一个进程在运行的时候,都有自己的时间片,所以在进程重新被CPU调度的时候,CPU必须知道上一次这个进程运行到什么时候了,当进程被换下去的时候,进程的运行信息会被存在操作系统里面(具体什么位置这里不详谈),以便下次CPU重新调度时进程能够正常运行,这叫做进程的上下文保护。当进程被CPU重新调度上来时,首先要做的第一件事情就是读取操作系统中进程运行的相关数据,这叫做进程的上下文恢复。

5.在任何时刻,CPU中的寄存器虽然被所有进程共享,但寄存器内的数据是每个进程私有的,这些寄存器内的数据就是进程的上下文数据,这些寄存器内的数据是时刻在不断变化的,OS会实时加载当前运行进程的上下文数据到寄存器里面。

   
830 次浏览       16
相关文章

一文了解汽车嵌入式AUTOSAR架构
嵌入式Linux系统移植的四大步骤
嵌入式中设计模式的艺术
嵌入式软件架构设计 模块化 & 分层设计
相关文档

企点嵌入式PHP的探索实践
ARM与STM简介
ARM架构详解
华为鸿蒙深度研究
相关课程

嵌入式C高质量编程
嵌入式操作系统组件及BSP裁剪与测试
基于VxWorks的嵌入式开发、调试与测试
嵌入式单元测试最佳实践

最新活动计划
MBSE(基于模型的系统工程)4-18[北京]
自然语言处理(NLP) 4-25[北京]
基于 UML 和EA进行分析设计 4-29[北京]
以用户为中心的软件界面设计 5-16[北京]
DoDAF规范、模型与实例 5-23[北京]
信息架构建模(基于UML+EA)5-29[北京]
 
 
最新文章
基于FPGA的异构计算在多媒体中的应用
深入Linux内核架构——简介与概述
Linux内核系统架构介绍
浅析嵌入式C优化技巧
进程间通信(IPC)介绍
最新课程
嵌入式Linux驱动开发
代码整洁之道-态度、技艺与习惯
嵌入式软件测试
嵌入式C高质量编程
嵌入式软件可靠性设计
成功案例
某军工所 嵌入式软件架构
中航工业某研究所 嵌入式软件开发指南
某轨道交通 嵌入式软件高级设计实践
深圳 嵌入式软件架构设计—高级实践
某企业 基于IPD的嵌入式软件开发
更多...