【Linux】对Linux操作系统中进程的理解


目录

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

举个例子加深理解

二、操作系统(软件方面)

1、操作系统的理解

2、系统调用和库函数的概念

三、进程控制块PCB的概念

四、进程的概念

1、什么是进程?

2、进程如何管理?

3、进程相关指令

3.1查看进程

3.2杀掉进程

3.3查看/proc文件系统

4、系统调用

4.1子进程getpid()

4.2父进程getppid()

4.3创建子进程fork()

五、进程状态的理解

1、什么是运行队列

2、对于CPU和硬件的速度差异,系统如何调度?

3、对于过多的阻塞进程,内存占用如何处理?

六、Linux操作系统的进程状态

1、进程状态在kernel中的定义

2、进程的运行状态 

3、进程的浅度睡眠状态(阻塞状态的一种)

4、进程的深度睡眠状态(磁盘休眠状态)(阻塞状态的一种)

5、进程的暂停状态/追踪暂停状态(阻塞状态的一种)

6、进程的僵尸状态/死亡状态

7、孤儿进程

七、进程的优先级

1、进程优先级的概念

2、nice值的修改

八、进程切换

1、进程特性

2、进程切换(并发)


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

我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系。

1、冯·诺依曼体系结构的存储器指内存,掉电易失。

2、而磁盘是一种外存,可以永久性存储数据。磁盘也属于外设的一种。外设又分为输入设备和输出设备。磁盘和网卡既是输入设备又是输出设备。

3、对于中央处理器,它有自己的指令集,外部程序翻译为CPU的指令集,让CPU根据这些指令集去执行。

4、 为了保证读取和写入速度,CPU只和内存打交道。

举个例子加深理解

现在我用我的电脑向我朋友的电脑传输一份压缩包文件。根据冯·诺依曼体系结构,将会经过以下流程:

二、操作系统(软件方面)

操作系统是管理软硬件的软件。

1、操作系统的理解

1、操作系统通过持续获取数据,进行软硬件的管理;

2、操作系统的管理的方法是先描述,再组织。先对被管理对象进行抽象成类,再根据这个类定义一个个具体的对象,再将这些对象通过数据结构进行关联,将软硬件的管理转换成对数据结构的管理。这其实也是面向对象的思想。

3、数据的采集和操作系统命令的下达,由驱动来做。

2、系统调用和库函数的概念

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

库函数:用户直接使用系统调用很困难,所以有了对系统调用进行封装的库函数。

三、进程控制块PCB的概念

1、我们写的可执行程序并不包含任何进程的属性信息。这些进程信息被放在一个叫做进程控制块的结构体中,用数据结构形成关联,可以理解为进程属性的集合;

2、该结构体被称之为PCB(process control block),Linux操作系统下的PCB是: task_struct;

四、进程的概念

1、什么是进程?

进程=内核数据结构(task_struct)+进程对应的磁盘代码和数据。

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

2、进程如何管理?

先描述,再组织。

操作系统会给每一个进程创建一个PCB对象,这些进程控制块对象用链表形成连接,通过遍历PCB的方式来找到对应状态的进程进行执行。例如找到优先级最高的PCB所对应的进程进行执行或通过PCB找到已死亡的进程进行释放。

通过这种方式,操作系统对进程的管理就变成了对进程所对应的PCB的管理,即链表的增删查改!

3、进程相关指令

3.1查看进程

ps axj | grep 'myproc'

能够显示出所有有关myproc的进程信息。

ps axj | head -1 && ps axj | grep 'myproc'

这个指令可以带上进程的小标题。

3.2杀掉进程

kill -9 8833

这里的8833是目标进程的PID。

3.3查看/proc文件系统

ll /proc/

/proc目录存在于内存中,这些数字名字的目录就是进程对应的PID。

ll /proc/14456 -d

能够找到正在运行中的PID14456的进程。

如果进程在运行时,可执行文件被删除,进程不会停止,但是进程目录中该可执行程序的路径将会无效。

4、系统调用

4.1子进程getpid()

#include <stdio.h>    
#include <unistd.h>    
#include <sys/types.h>                              
int main()    
{    
    while(1)    
    {    
        printf("hello world,MyPID=%d\n",getpid());
        sleep(1);
    }                
    return 0;
}  

调用getpid函数后,会将进程的PID打印出来

4.2父进程getppid()

#include <stdio.h>    
#include <unistd.h>    
#include <sys/types.h>                              
int main()    
{    
    while(1)    
    {    
        printf("hello world,MyPID=%d,MyPPID=%d\n",getpid(),getppid());
        sleep(1);
    }                
    return 0;
}  

可以发现,这个进程的父进程就是bash。命令行上启动的程序,一般它 的父进程没有特殊情况的话,都是bash。bash通过派生子进程的方式执行程序,如果程序有bug退出了,那挂掉的仅仅是子进程,bash没有影响。

4.3创建子进程fork()

#include <stdio.h>      
#include <unistd.h>      
#include <sys/types.h>      
int main()      
{    
    pid_t id=fork();    
    printf("MyPID=%d,MyPPID=%d,%d\n",getppid(),id);                                                                                                               
    return 0;                                 
}  

这个代码的执行结果是两个打印。

再看:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{    
                                                                    
    pid_t id=fork();                                                
    if(id==0)                                                       
    {                                                               
        //子进程    
        while(1)    
        {                                                                                                                       
            printf("子进程,id);    
            sleep(1);
        }                                                                      
    }                                                                                                  
    else if(id>0)                                                                                      
    {                                                                                                  
        //父进程                                                                                       
        while(1)                                                                                       
        {                                                                                              
            printf("父进程,id);      
            sleep(1);
        }                                                                                              
    }                                                                                                  
    else                                                                                               
    { 
    }
    return 0;
}

父子进程不断循环。说明fork()之后会有父子两个进程,fork()之后的代码,父子进程共享。

五、进程状态的理解

进程在不同的队列中,表示不同的状态。

1、什么是运行队列

1个CPU只有一个运行队列,进程的执行需要排队。让进程入队列,本质上是将task_struct结构体对象放入运行队列中。

这个进程队列中的指针可以找到需要加载的进程,将其PCB对象的信息加载到CPU中,CPU可以根据PCB对象的信息找到进程对应的代码进行执行。

在CPU的运行队列中的进程状态就叫做运行状态

状态,是进程的内部属性,存放于task_struct中。这个状态可以理解为一个整数,例如这个整数为1代表运行;2代表停止,3代表死亡状态等······

2、对于CPU和硬件的速度差异,系统如何调度?

进程不仅仅会占用CPU资源,也会占用硬件资源。对于CPU,它可以很快的处理进程的请求;但是对于硬件,速度很慢,例如网卡,可能同时有迅雷、百度网盘、QQ等进程需要获取网卡的资源,所以每一个描述硬件的结构体中也有一个task_struct* queue运行队列指针,指向排队中的PCB对象的头结点。

那么CPU和硬件的速度差异巨大,系统该怎么平衡这种速度?当CPU发现运行状态的进程需要访问硬件资源时,会让该进程去所需访问的硬件的运行队列中排队,CPU继续执行下一个进程。

那么这个被CPU剥离至硬件运行队列中的进程状态被称为阻塞状态。当进程对硬件的访问结束后,进程的状态将会被修改为运行状态,即该进程重新回到CPU的运行队列。

3、对于过多的阻塞进程,内存占用如何处理?

硬件的速度较慢,但是大量的进程需要访问硬件,势必会产生较多的阻塞进程,这些阻塞进程的代码和数据在短期内不会被执行,如果全部存在于内存中将会导致内存占用。

对于这个问题,如果内存中有过多的阻塞状态的进程导致内存不足,操作系统会将其的代码和数据先挪动至磁盘,仅留PCB结构体,以节省内存空间,这种进程状态被称为挂起状态。将进程相关数据,加载或保存至磁盘的过程,称为内存数据的换入和换出。

进程的阻塞状态不一定是挂起状态,部分操作系统可能会存在新建状态挂起或运行状态挂起等。

六、Linux操作系统的进程状态

1、进程状态在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 */
};

2、进程的运行状态 

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

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

状态后面有+号的表示前台进程,没有+号表示后台进程。前台进程在执行时,用户无法继续输入指令除非ctrl+c终止程序;后台进程在执行的过程中,用户可以输入指令且ctrl+c无法杀掉该进程。可以使用kill -9 PID的指令杀掉该进程。

3、进程的浅度睡眠状态(阻塞状态的一种)

浅度睡眠状态是可以被终止的进程状态。

#include <stdio.h>    
int main()    
{    
    int a=0;    
    while(1)    
    {    
        printf("%d\n",a++);               
    }                                  
    return 0;                          
} 

虽然数值一直在打印,但是printf函数需要访问显示器,大部分时间在等显示器IO就绪,只有小部分时间在执行打印代码。所以该代码呈现睡眠状态。

需要访问外设的,一般属于睡眠状态。

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

那么有人会问了,为什么我的grep进程也处于S状态?博主试过多次执行该指令,grep进程有小概率会处于R状态,其实这个和他所查询的进程的状态也有关系,我在查询的进程在摸鱼,那我也去摸会鱼吧~

4、进程的深度睡眠状态(磁盘休眠状态)(阻塞状态的一种)

只有在高IO的情况下才会发生(Linux中有一个dd命令可以模拟高IO的状态,可以动手试逝)。

D磁盘休眠状态(Disk sleep):有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的

进程通常会等待IO的结束。

在该状态下的进程,无法被操作系统杀掉,只能通过断电或者进程自己醒来的方式中断深度睡眠状态。

5、进程的暂停状态/追踪暂停状态(阻塞状态的一种)

可以输入kill -19 PID来让一个进程进入暂停状态。或者将一个任务从前台切换到后台等,进程均会变为暂停状态。

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

我们在使用gdb对一个可执行文件调试的时候,程序运行到断点处,程序会进入t追踪暂停状态(tracing stop):表示该进程正在被追踪。

6、进程的僵尸状态/死亡状态

Z僵尸状态(zombie):为什么会存在僵尸状态?因为进程在退出的时候,不会立即释放该进程对应的资源,会保存一段时间,让父进程或操作系统来读取子进程的返回代码。那么进程怎样进入僵尸状态?

模拟子进程正常退出,父进程不回收子进程(不读取子进程的返回信息)的场景(也可以使用kill -9 PID杀掉子进程):

#include <stdio.h>    
#include <unistd.h>    
#include <sys/types.h>    
#include <stdlib.h>    
int main()    
{    
    
    pid_t id=fork();    
    if(id==0)    
    {    
        //子进程    
        while(1)    
        {    
            printf("子进程,id);    
            sleep(1);    
            exit(1);    
        }    
    }                                                                                                                        
    else if(id>0)      
    {                          
        //父进程    
        while(1)      
        {  
            printf("父进程,id);
            sleep(1);   
        }
    }
    else 
    {
        perror("fork");
        exit(-1);                                                                                                            
    }
    return 0;
}  

通过该命令循环打印进程信息,发现子进程已经变成了僵尸状态。旁边的<defunct>译为死者,进程已经死亡,但是还未被回收,这就是僵尸状态。

僵尸进程的退出结果会写在PCB中,一个进程退出了,它的代码和数据会被释放,但是它的PCB是不会被释放,如果父进程不回收这块资源,那么会造成系统的内存泄漏。那我能不能手动杀掉这个僵尸进程来手动释放僵尸资源?不可以,因为僵尸进程已经死亡,无法手动杀掉进程。

在Z状态的进程被回收后,进程状态变为X死亡状态(dead):父进程读取完子进程的返回信息后,收尸速度太快了,我们看不到,进程死亡状态立马被它的父进程回收。

7、孤儿进程

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
int main()
{

    pid_t id=fork();
    if(id==0)
    {
        //子进程
        while(1)
        {
            printf("子进程,id);
            sleep(1);
        }    
    }    
    else if(id>0)    
    {
        //父进程    
        while(1)                                                                         
        {    
            printf("父进程,id);    
            sleep(1);    
        }    
   }    
    else 
    {
        perror("fork");
        exit(-1);                                                                        
    }
    return 0;
}

前台创建的子进程,父进程被杀掉后,父进程立马被bash回收。子进程被1号进程(操作系统)领养,同时切换到后台运行(使用kill -9 PID杀掉后台进程)。因为不领养子进程的话,那么子进程退出时呈现的僵尸状态就没有谁能回收了(内存泄漏)。这种被领养的进程称为孤儿进程

七、进程的优先级

1、进程优先级的概念

进程的优先级本质就是PCB中的一个整数数字(不同操作系统可能由多个数字决定)。

使用ps -la命令显示出当前的进程信息。PRI(priority)代表优先级的意思(priority默认是80);NI(nice)用于调整优先级(nice默认是0)。

进程最终优先级=默认优先级(固定80)+nice值。

Linux支持进程在运行过程中调整优先级,调整的方式是修改nice值。

2、nice值的修改

注:1、需要root权限2、使用r调出修改nice值的命令栏。

nice的取值范围为【-20,19】,数字输的再小再大也没用。

所以Linux中进程的权限范围为【80-20,80+19】,数字越小,优先级越高。

进程优先级不要人为的调整,如果一个进程的优先级较高或较低,可能会造成其他进程获取操作系统资源不均,造成操作系统自身的调度失衡。

八、进程切换

1、进程特性

竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级 。

独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰 。

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

并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。

2、进程切换(并发)

cpu中有一个eip寄存器(PC指针),指向下一条指令的地址。

进程在运行的时候,占有CPU,会产生很多的临时数据,归属于当前进程。虽然CPU内部仅有一套寄存器硬件,但是寄存器中保存的数据属于当前进程。(寄存器是共享的,但是数据是各进程私有的)

进程在运行的时候都有自己的时间片,这个时间一到,即使进程还没有被执行完毕,但是会被操作系统剥离CPU,腾出CPU让下一个进程上来跑一跑。

那么这个进程下次再回到CPU继续运行时,操作系统是如何知道这个进程的代码被执行到哪里了?

首先,进程在切换的时候,需要进行上下文保护,一些临时数据被保存至PCB里(误);进程在恢复运行的时候,要进行上下文的恢复,后续该进程回到CPU运行时,将加载这些数据。通过PC指针继续运行下一行代码。

相关文章

文章浏览阅读1.8k次,点赞63次,收藏54次。Linux下的目录权限...
文章浏览阅读1.6k次,点赞44次,收藏38次。关于Qt的安装、Wi...
本文介绍了使用shell脚本编写一个 Hello
文章浏览阅读1.5k次,点赞37次,收藏43次。【Linux】初识Lin...
文章浏览阅读3k次,点赞34次,收藏156次。Linux超详细笔记,...
文章浏览阅读6.8k次,点赞109次,收藏114次。【Linux】 Open...