目录
- Linux—进程学习—2
- 1.通过系统调用创建进程—fork
- 1.1fork创建子进程
- 1.2fork函数的返回值
- 1.3利用fork实现多进程
- 2.有关cpu的常识了解
- 3.进程状态
- 3.1从操作系统层面了解进程状态
- 3.1.1就绪和新建状态的理解
- 3.1.2运行和阻塞状态的理解
- 3.1.3挂起状态的理解
- 挂起和阻塞的区别
- 3.1.4总结
- 3.2Linux中对进程状态的描述
- 3.3查看进程状态
- 3.3.1 R状态和S状态
- 3.3.2 T状态
- 3.3.3前台进程与后台进程
- 3.3.4 D状态
- 3.3.5 t状态
- 3.3.6 X状态
- 3.3.7 Z状态
- 3.3.7.1为什么会有Z状态?(感性认识)
- 3.3.7.2查看Z状态进程
- 僵尸进程的危害
- 3.4进程状态总结
- 3.5孤儿进程
- 关于孤儿进程需要知道的:
Linux—进程学习—2
1.通过系统调用创建进程—fork
1.1fork创建子进程
- fork是一个用c语言写的函数,代码本身这个进程对于fork创建出来的进程就是父进程,而fork创建的进程对于代码本身被加载到内存的这个父进程就是子进程。
下图是一个使用fork创建子进程的例子:
可以看到fork创建的进程pid是20077,而父进程pid是20076,而父进程20076的父进程肯定是bash了。
1.2fork函数的返回值
下图是对于fork函数返回值的官方回答。
-
如果fork创建子进程成功,那么对于父进程会返回子进程的pid,对于子进会返回0.
-
如果fork创建子进程失败,那么对于父进程返回一个-1、
下面是一个证明返回值的例子:
代码如下:
运行结果如下:
确实是和官方说的一样,对于不同的进程一个fork函数实现了不同的返回值。
可是这里有个问题,id这个变量是没有被改变过的,那是怎么实现返回不同的值的?
目前我的能力还不能解决,还等到学习进程地址空间的知识之后,才能够明白
1.3利用fork实现多进程
代码如下:
执行结果如下:
执行结果告诉我们,走进了两个分支,执行了两个死循环,这在之前打的代码中是没有出现的。这是因为fork创建了一个进程。
fork函数之后的代码是被父进程和子进程共享的,也就是一起执行的
这种编程方式也叫并发式编程
2.有关cpu的常识了解
cpu只能被动的接受别人的指令和数据来执行和计算。
这就要求cpu必须能够认识别人的指令,在编写代码的时候,如果想要执行代码,那么代码就必须要被编译成二进制可执行文件,其实就是编译成了cpu能够读懂的指令集,为什么cpu能够读懂呢?因为cpu有内置指令集。
这个编译的过程也和此时cpu是64位环境还是32位环境有关系。如果此时cpu是64位环境,那么就要安装x64的环境去编译程序。这样cpu才能够读懂编译出来的指令,能够接受并运算。
这也可以回答为什么程序要被加载到内存中,因为硬件规定了所有设备都只能和内存进行交互、如果程序不加载到内存中,cpu读不到
3.进程状态
为了更好的管理进程,操作系统(OS)会将进程分为多种状态
进程的状态有很多种:运行、就绪、新建、停止、死亡、挂起、阻塞、挂机、等待
3.1从操作系统层面了解进程状态
3.1.1就绪和新建状态的理解
关于新建和就绪状态的区别,不同的操作系统有不同的说法。
这里可以理解就绪和新建是同一种状态,指的都是可执行二进制文件被加载到内存中时,操作系统新建的进程。此时该进程的PCB内对该进程的状态的描述就是就绪和新建
3.1.2运行和阻塞状态的理解
要想理解运行和阻塞状态、需要先了解一下运行队列
在大多数的情况下,cpu只有一个,进程却有很多,为了让每个进程都会被运行,因此cpu需要一个运行队列。【就像需要人吃饭需要排队,进程要被执行也需要排队,因为cpu只有一个】
由于cpu执行进程的速度很快,因此被cpu执行中的进程和运行队列中的进程,进程的状态都是运行状态®【注意了,不是只有在cpu中被执行的进程才叫做运行状态,进程在运行队列中准备好被cpu调用也叫运行状态】
除了运行状态R,还有很多其他状态。
进程的状态是属于进程的一种属性,在PCB中可能会以整数来区分,很可能运行状态就是1,死亡状态就是2,停止状态就是3等等【可能就会以宏定义实现】
要注意:
进程或多或少都会访问硬件资源、
不要认为进程只会占用cpu资源【在运行队列等代被cpu调用】,进程很有可能也会占有其他硬件资源,因为其他硬件资源往往也只有一个【比如网卡,显示器,磁盘】
因此,不只是cpu会有运行队列的存在,其他硬件资源也存在类似运行队列的等待队列的存在。比如有多个进程都需要访问网卡,那么就按照等待队列一个一个来。
**但是这样也有个问题:**外设处理进程的速度相比cpu处理进程的速度慢很多,访问外设的进程,往往是一个程序中有访问外设的需求,当cpu执行该进程到需要访问外设的时候,难道cpu要等待该进程访问外设完毕吗?那样的效率会很低。
因此操作系统会将需要访问外设的进程给链接到对应外设的等待队列去【此时进程的状态会从运行状态®,转变为阻塞状态】,等正在占用硬件资源的进程结束了,也就是硬件可以接受下一个进程的访问了,才会回到cpu的运行队列继续执行【此时进程状态从阻塞状态转变为运行状态】
这样cpu无需等待进程在对应硬件的等待队列的等待时间,可以一直的执行处于运行状态的进程。
总结:
- 一个cpu会有一个运行队列、
- 进程入队列的本质:将该进程的pcb对象放到运行队列里面
- pcb在运行队列中以及被运行时的状态都是运行状态®
- 进程状态是进程的一种属性,在PCB中很可能以整数(int)区分
- 进程不止会占用cpu资源,也会很可能占用外设资源
- 进程的不同状态,本质是进程在不同的队列中,等待某种资源。
3.1.3挂起状态的理解
挂起状态和阻塞状态是两种不同的状态,要区分清楚。
阻塞状态是cpu执行的进程需要访问某个硬件资源,但是这个硬件资源此时正在被别的进程占用,因此操作系统就会将该进程的pcb添加到对应硬件资源的等待队列中,此时该进程的状态就有运行状态变成阻塞状态
此时就会有一种情况:如果在内存中存在着非常多的进程,这些进程全部都要访问某个硬件资源、因此这些进程都被放进了对应硬件的等待队列,都是阻塞状态。但是一旦某个进程变成了阻塞状态,就意味着该进程短时间内无法被cpu调度。当阻塞状态的进程非常多时,内存的空间会不足!【因为有很多阻塞状态的进程,短时间内不会被调度,但是又在内存中占据着空间】
因此,如果内存空间不足了,操作系统会将部分阻塞状态的进程的代码和数据移动到磁盘中【此时的状态由阻塞状态变成了挂起状态】,这样就可以将内存的空间腾出,继续执行其他进程了。
要注意:是将阻塞状态的进程的代码和数据移动到磁盘挂起,该进程的pcb仍然存在于内存中【因为pcb不大,不占多少内存空间】
当该进程所等待的硬件资源此时准备好了之后,该进程的代码和数据就会被重新转移到内存当中去【不用担心此时内存没有空间,因为操作系统既然会将该进程挂起,自然也会为了该进程,将其他进程挂起】、并且将该进程的状态由挂起状态转变为运行状态。
而这个进程数据在内存和磁盘之间的转移,被叫做——内存数据的换入和换出
挂起和阻塞的区别
- 挂起一定阻塞、阻塞不一定挂起
如果内存空间足够大,可以阻塞不需要挂起,但是如果内存空间不够了,部分阻塞状态的进程就需要转变为挂起状态,来为内存腾出更多空间
有一些操作系统会将挂起和其他状态组成一些新状态,比如阻塞挂起状态、比如新建挂起状态、意思就是正在阻塞的进程被挂起,以及刚新建的进程就被挂起了。
3.1.4总结
上面主要讲述了运行、阻塞、挂起状态的理解
其实不同的操作系统对于不同的状态是有不同的说法的,并且在实现上也有区别。
挂起状态,在讲解操作系统的书里会叫做挂起,但是具体到操作系统中,可能不会将挂起这个状态暴露给我们知道。比如Linux
因此只要在操作系统的层面上大致理解,运行、阻塞、挂起状态的过程即可
3.2Linux中对进程状态的描述
上面所说的情况状态是一个大致的概括和总结,如果想要了解进程的具体状态,就必须挑选一个具体的操作系统来讲。
上面对进程的状态有了初步的理解,那么在Linux操作系统中,所谓各种状态是如何描述的?怎么实现的?
一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)。
下面的状态在kernel源代码里定义:
/*
* 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 */
};
R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠
D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态
这么多状态的存在,都是为了更好的应对不同的场景。
3.3查看进程状态
3.3.1 R状态和S状态
R(运行)、S(睡眠)
写一个死循环代码先,里面一直在执行重复的计算
此时如果抓取进程信息,会发现进程状态是R+,也就是运行状态。至于R+的+后面会学习什么情况
但是如果我们每次都将a打印出来,那么此时进程状态是什么呢?
此时就会变成S+,睡眠状态
这是为什么?明明一直在执行死循环。
其实是因为这里多了一个向显示器输出的代码,这意味着该进程需要访问显示屏这个硬件。但是cpu的计算是非常快的,而与IO的过程相比与cpu的计算是非常慢的,这就导致了这个进程的大部分时间都在等待显示器的就绪,显示器就绪后再输出。因此这个进程有99%的时间都在等待显示器,因此该进程的绝大多数时间的状态是S状态。【但是查到R状态的概率不为0】
要注意:
在Linux当中阻塞状态的一种就是上面所说的S状态,阻塞状态不是只有S,还有其他状态
3.3.2 T状态
其实在教材中是没有暂停状态(T)的,一般都归类到挂起或者阻塞里去。但是在Linux中,具体一点就有暂停状态
先看看暂停状态的情况:
下面是代码:
该代码执行的结果就是进程一直处于R状态,此时只需要将其19号信号kill掉即可处于暂停状态。如下图所示
这其实也可以理解成一种阻塞状态,既然该进程处于阻塞状态,代表它也有可能是挂起的状态,但是不知道到底是不是挂起的状态,这完全取决于操作系统如何处理的。Linux系统认为是我们并不需要知道该进程到底是挂起还是阻塞
既然可以通过kill来让其暂停,自然也可以通过kill让其继续执行,如下图所示:
但是此时的R少了个+,这代表其变成了后台进程
3.3.3前台进程与后台进程
其实就是R+和R的区别
-
R+代表当前进程处于运行状态并且是前台进程
-
R代表当前进程处于运行状态并且是后台进程
前台进程——当运行进程之后,shell命令行无法在获取命令行来解析了,但是可以使用ctrl + c
终止
后台进程——当运行进程后,shell命令行仍然可以获取命令行并解析,并且无法使用ctrl + c
终止。
这有什么区别呢,其实一个可以被ctrl + c
终止,一个不可以
下面是实验:
3.3.4 D状态
深度睡眠状态(D),这个状态也是比较少见的状态。
深度睡眠状态(D)和睡眠状态(S)状态的区别:
- D状态是无法被
ctrl + c
终止的,能被ctrl + c
终止的状态是S状态 - D状态无法被OS杀死,只能通过断电,或者进程自己醒来,来解决
但是D状态的实验不好做,搞不好Linux系统就死机了。
这里可以讲述一个场景,也就是会出现D状态的高IO场景:
内存当中的进程数量太多,导致操作系统的内存实在是没有空间来放下更多的进程了,但是IO的速度又特别慢,有一个进程A此时正在占用磁盘,但是后面还有几千个进程在等待磁盘,这几千个进程部分处于等待队列,部分被操作系统挂起到磁盘中来腾出更多的内存空间,但是进程A给磁盘的任务是保存100000个转账记录,需要一定的时间,但是此时内存空间要爆了,能挂起的都挂起了,但是内存还是要爆了,此时操作系统就会说你这个进程A,占用时间这么久,直接把你杀了,来腾出空间给其他进程使用。那么进程A就被杀死了,此时磁盘不幸的存储数据失败,正想和进程A汇报,存储失败的时候,发现进程A找不到了,并且来了下一个进程B带着任务来了,磁盘此时又要上班了,那么之前这个存储失败的数据怎么办呢?磁盘说不管了,丢掉了,继续上班。这就意味着数据丢失了
这个时候数据的丢失对于用户来说就很懵逼啊,不是哥们,我让你帮我存个数据,你告诉我数据没了,找不到了。
但是这个时候能怪谁呢?你怪操作系统,操作系统也很懵逼啊,你给我的任务就是管理好软硬件资源,我没死机不错了。你怪磁盘,磁盘也很懵逼啊,我虽然写入数据的成功率很高,但是我也有失败的时候啊,我反馈的时候,还反馈不了呢?你怪进程A吗,进程A也很无语啊,我就是老老实实干着自己的活,让磁盘去干活,磁盘干的慢我等,我直接被杀死了,这也能怪我吗?
这个时候用户就给操作系统制定了一个新规则:就是当遇到这个情况的时候,将进程A标记为深度睡眠状态,不要杀死进程A了,这样就不会丢失数据了、并且也能腾出内存空间给其他进程使用
因此D状态无法被OS杀死,只能通过断电,或者进程自己醒来,来解决
3.3.5 t状态
小t状态在Linux内核中其实也是T状态,即暂停状态。**t状态也是一种暂停状态,表示被追踪,**这里我们通过实验来查看t状态
先写一个程序和Makefile,方便实验。
- Makefile
这里面的**$@代表目标文件,%^代表依赖路径文件,-g是希望编译出来的可执行文件是debug模式**,可以使用gdb调试器调试
- 程序
然后make一下,然后去调试一下。
此时抓取进程状态,会发现变成了t状态**,t状态也是一种暂停状态,表示该进程正在被追踪!**
此时进程在等待gdb调试器的下一步,然后才能继续运行。也就是在等待硬件—键盘的输入
3.3.6 X状态
X(dead)是死亡状态
X状态是不好做实验的,因为一旦进程变成了X(死亡)状态,那么操作系统就会将该进程回收、这个回收过程对我们来说是很快的,操作系统一下就回收完了,因此我们很难通过查看进程状态来直接查看一个进程是(X)死亡的状态
不过X状态也不难理解,就是一个进程死亡了,然后会被操作系统回收。
3.3.7 Z状态
Z(zombie)状态可以叫做僵死状态,也可以称作僵尸状态
僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
这里所提到的读取进程中的读取过程还没有学习,等到后面学习进程控制会知道。今天只是见到为什么会有Z状态的进程,以及Z状态进程什么情况会出现即可。
僵尸进程是一个问题——进程退出了,但是没有被回收
这个问题后面会解决,今天只是知道有这个东西即可
3.3.7.1为什么会有Z状态?(感性认识)
一个进程,往往是带着任务去运行的,因此进程也被称作任务。而既然进程需要执行任务,那么作为操作系统肯定得知道当前进程是否完成任务,完成的怎么样了。
因此当一个进程退出的时候,不能直接将其标记为(X)死亡状态,然后释放资源并回收该进程。而是要让父进程或者OS去读取该进程的执行结果,读取该进程是什么原因退出的【是成功完成之后退出的,还是遇到了什么错误退出的】。
而该进程退出时,被保留资源并被父进程或者OS读取的这个状态就是僵死状态。
当父进程或者OS读取完之后,该进程就由Z状态变成X状态,然后被OS释放资源并回收该进程
结论:因为OS需要得知一个进程退出时是因为什么原因退出的【是成功完成之后退出的,还是遇到了什么错误退出的】,所以会需要僵尸状态,僵尸状态的进程就会被父进程或者OS读取。
3.3.7.2查看Z状态进程
**做个可以查看到Z状态进程的实验:**让父进程去创建一个子进程,然后让子进程正常退出,但是父进程不允许去做相关回收子进程的动作,此时子进程就是Z状态。因为父进程没有来读取子进程。
写一个实验的程序:
然后为了方便查看到子进程转变到Z状态,这里写一个简单的监控脚本
while :; do ps axj | head -1 && ps axj | grep "process" | grep -v grep; sleep 1; done
这个句指令的执行效果就是,每隔一秒不断循环抓取process进程的状态信息。
- 实验结果:
代码执行结果:
抓取进程的信息结果:
根据我们的代码,我们知道,子进程在被创建出来5秒后就退出了。而抓取进程的结果也是如此,从第5秒开始,子进程的状态就变成了Z+。并且这里出现了一个单词<defunct>
,这个单词的意思就是失效的。也说明了这个子进程其实已经死掉了,但是还没有被回收。
解释:为什么子进程在第5秒的时候变成了Z+,并且后面一直都是Z+呢?
一开始两个进程的状态都是S+,这个前面也有说,这里不详细讲,就是父进程和子进程都在等待硬件资源,并且都是前台程序。
为什么子进程在第5秒的时候变成了Z+,并且后面一直都是Z+呢?
- 这是因为代码中,子进程在第五秒的时候执行了exit(1),这个代码,导致子进程退出了,返回的是1。此时子进程等待被读取,自然变成了Z+状态【+是因为仍然是前台进程】
- 正常来说父进程这个时候要去读取子进程,知道子进程为什么退出,然后子进程才能变成X状态,从而被OS回收。
- 但是代码中,此时父进程陷入了一个死循环,一直在执行和读取子进程无关的操作,这就导致了子进程一直没有被读取,既然没有被读取,自然就一直在等待被读取
- 因此子进程在第五秒之后,一直处于Z+状态。
因此这个子进程也被叫做僵尸进程【一直处于僵尸状态】,这是有问题的!如何解决呢?需要学习进程控制的知识才能解决。
这里先了解一下僵尸进程的危害
僵尸进程的危害
- 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。
可父进程如果一直不读取,那子进程就一直处于Z状态!
- 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中。
换句话说,Z状态一直不退出,PCB一直都要维护!PCB一直都需要存在!
- **那一个父进程创建了很多子进程,就是不回收,就会造成内存资源的浪费!**因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间
也就是说,僵尸进程会导致内存泄漏!
3.4进程状态总结
- 经过上述学习,已经初步理解了全体操作系统层面下进程状态中的运行,阻塞和挂起。
- 往往理论和实践是不太一样的,每个操作系统对于进程状态的理解和实现都是不太一样的、
- 学习了Linux操作系统中,对于进程状态的理解。并实验了大部分能查看的进程状态。【R S T D t X Z】
- 在Linux中前台进程和后台进程的区别
3.5孤儿进程
前面有讲到僵尸进程——即子进程退出时没有被父进程回收,子进程一直处于僵尸状态
现在再来看看孤儿进程
-
父进程如果提前退出,那么子进程后退出,进入Z之后,是如何处理的呢?
-
父进程先退出,子进程就称之为“孤儿进程”
-
孤儿进程被1号init进程领养,当然要由init进程回收。
**下面是实验:**让将一个进程创建一个子进程,并将该进程杀掉,此时子进程就变成了孤儿进程。【父进程被操作系统回收了】
此时子进程就会被进程1接管,1号进程成为了子进程的新的父进程。【1号进程就是操作系统】,这个过程可以说是1号进程领养了这个孤儿进程
并且子进程的状态变成了S(睡眠),从前台进程变成了后台进程。
关于孤儿进程需要知道的:
孤儿进程这个情况是一定会存在的。【谁也不能保证父进程一定会比子进程晚被杀死】
为什么孤儿进程要被操作系统领养?
因为孤儿进程如果不被操作系统领养,那么等到其正常执行或者遇到某种错误,进程退出处于僵尸状态的时候,就没有父进程来读取孤儿进程,那么就会一直处于僵尸状态。变成一个僵尸进程,从而造成内存泄漏。
- 孤儿进程被领养之后会从前台进程变成后台进程