【Linux】进程概念&&进程状态&&进程优先级

进程

系统中存在大量的进程,操作系统是如何进行管理的呢? 先描述,再组织


基本概念

课本概念:程序的一个执行实例,正在执行的程序等 内核观点:担当分配系统资源(CPU时间,内存)的实体

例如:生成的可执行程序

可执行程序本质上是一个文件,是放在磁盘上的

当我们双击这个可执行程序将其运行起来时,本质上是将这个程序加载到内存当中了,因为只有加载到内存后,CPU才能对其进行逐行的语句执行,而一旦将这个程序加载到内存后,我们就不应该将这个程序再叫做程序了,严格意义上将应该将其称之为进程


描述进程-PCB

为什么要有PCB?

因为要管理进程就必须先描述再组织,要描述进程就要用结构体描述进程的所以相关属性

PCB是什么?

在OS上,PCB就是进程控制块,就是一个结构体类型在Linux下,PCB就是 struct task_struct{//进程的所有的属性}


进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合

课本上称之为PCB(process control block)进程控制块 ->实际就是描述进程的结构体

Linux操作系统下的PCB是: task_struct

struct PCB
{
    //描述进程的所有属性信息
};

task_struct-PCB的一种

在Linux中描述进程的结构体叫做task_struct task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息

(Linux操作系统是用C语言进行编写的==>Linux当中的进程控制块必定是用结构体来实现的)

struct task_struct
{
    //描述进程的各种属性信息
};

task_ struct内容分类 (属性字段)

task_struct是Linux当中的进程控制块,主要包含以下信息:

  • 标示符(PID): 描述本进程的唯一标示符,用来区别其他进程
  • 状态: 任务状态,退出代码,退出信号等
  • 优先级: 相对于其他进程的优先级
  • 程序计数器(pc指针): 程序中即将被执行的下一条指令的地址
  • 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
  • 上下文数据: 进程执行时处理器的寄存器中的数据
    • 进程的代码是不可能在很短时间运行完的,规定每个进程的时间片(单次运行的最长时间),用户感受到的多个进程同时运行,本质上是CPU的快速切换.CPU只有一套寄存器,为了保护上下文,进程的这些临时数据被写入在PCB中,再来执行时,恢复上下文
  • I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表
  • 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等
    • 进程创建出来,CPU要执行它对应的代码,然而CPU很少,进程很多
    • 因此OS内有一个调度模块,负责较为均衡的调度每一个进程,较为公平的获得CPU资源
      • 让每个进程都能获得CPU资源 -> 让每个进程都能被执行
  • 其他信息

image-20220514133759228


理解上下文:

image-20220514134653140

进程的代码可能不是很短的实际就能运行完毕的,规定每个进程单次运行时间片,如10ms

在单CPU情况下,用户感受到多个进程在同时运行,本质通过CPU的快速切换完成的!

寄存器保存当前正在运行的进程的临时数据,进程在运行的期间是有切换的,进程可能有大量的临时数据,临时数据在CPU的寄存器中保存


通过上下文,我们能够感受到进程是被切换的

例如:为了让你去做其他事情,但不耽误当前,并且,当你想回来继续学习的时候,可以接着之前你的学习内容,继续学习


组织进程

进程和程序的区别:

image-20220410203430460

结论:所有的程序启动,本质上都是在系统上面创建进程


有了进程控制块,所有的进程管理任务与进程对应的代码和数据毫无关系,与内核创建的该进程的PCB强相关,把进程控制块PCB用双向链表组织在一起,于是操作系统对进程的管理,变为对数据的管理,本质上就是对双链表的增删查改

image-20220410203844557

操作系统只要拿到这个双链表的头指针,便可以访问到所有的PCB 此后,操作系统对各个进程的管理就变成了对这条双链表的增删查改操作

例如: 创建一个进程实际上就是先将该进程的代码和数据加载到内存,紧接着操作系统对该进程进行描述形成对应的PCB,并将这个PCB插入到该双链表当中, 退出一个进程实际上就是先将该进程的PCB从该双链表当中删除,然后操作系统再将内存当中属于该进程的代码和数据进行释放或是置为无效


查看进程

指令:ps aux可以显示系统当中存在的进程 我们开机的时候:启动的第一个程序就是我们的操作系统(即操作系统是第一个加载到内存的)


进程的信息可以通过 /proc 系统文件夹查看 (以文件形式查看进程 )

image-20220410204403336

这些数字代表的是目录,其实是某些进程的PID,对应文件夹当中记录着对应进程的各种信息,进程启动之后,会在这个文件夹形成一个目录,以自身的PID编号作为目录文件名,当进程退出的时候,文件夹也会消失

如:要获取PID为1的进程信息,可以直接查看 /proc/1 这个文件夹

image-20220410204533754


通过ps命令查看

通过和grep行过滤指令搭配使用,只显示某一进程的信息 ps -axj | head -1 && ps -axj | grep “可执行程序名”


关闭程序:

myproc.c的内容:

#include<stdio.h>
#include<unistd.h>
int main()
{
    while(1)
    {
        //隔1s打印
        printf("I am running\n");
        sleep(1);
    }
    return 0;
}

可以使用ctrl+c 或者 kill -9 进程的pid (向目标进程发送9号信号,同时也证明了pid能标识系统上的唯一进程)

image-20220410210030046


通过系统调用获取进程标示符

  • 进程id(PID)
  • 父进程id(PPID)

通过系统调用函数: getpid和getppid可以分别获取进程的PID和PPID

image-20220410210736539

例子:

while(1)   
{      
    printf("proc: PDI:%d  parent: PPID: %d\n",getpid(),getppid());       
    sleep(1);                                                                             }

image-20220410211942555


fork初识 -通过系统调用创建进程

  • 运行 man fork 认识fork

image-20220410212713179


fork函数创建子进程

fork是一个系统调用级别的函数,其功能就是创建一个子进程 所有在命令行上运行的程序,都是bash创建的,bash创建子进程,子进程再创建子进程 在命令行上运行的指令,基本上父进程都是bash


一个奇怪现象:

image-20220410212636463

为什么fork之后,打印两次呢?

第一行数据是该进程的PID和PPID,第二行数据是代码中通过调用fork函数创建的子进程的PID和PPID

我们可以发现fork函数创建的进程的PPID就是myproc进程的PID,也就是说myproc进程与fork函数创建的进程之间是父子关系


如何理解fork创建子进程呢?

目前创建进程主要有两种方式,./cmd(可执行程序)run command(运行指令), fork在操作系统角度上,两种方式没有差别

fork本质是创建子进程,系统中多了一个进程,就多了一份与进程相关的内核数据结构 + 进程的代码和数据. 我们fork只是创建了子进程,但是子进程对应的代码和数据呢?

父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)

  • 默认情况下,子进程会“继承”父进程的代码和数据
    • 代码fork之后,产生的子进程和父进程代码是共享的
      • 代码是不可被修改的,这意味着父子代码只有一份完全共享
    • 数据:默认情况下,数据也是“共享的”,不过修改数据时,会发生写时拷贝来维护数据的独立性
      • 进程是具有独立性的,通过写时拷贝完成进程数据的独立性,如:qq挂了,不影响微信
  • 子进程内核的数据结构task_struct,也会以父进程的task_struct为模板初始化自身的task_struct

image-20220410215129820

使用fork函数创建子进程,在fork函数被调用之前的代码被父进程执行,而fork函数之后的代码,则默认情况下父子进程都可以执行,后面的父子进程代码虽然是共享的,但是父子进程的数据各自开辟空间(采用写时拷贝)

注意:使用fork函数创建子进程后就有了两个进程,这两个进程被操作系统调度的顺序是不确定的,这取决于操作系统调度算法的具体实现


fork函数的返回值

image-20220514140838922


例子:

image-20220410215857268

我们惊奇的发现:fork函数有两个返回值!

1)如何理解一个函数有两个返回值

首先我们要知道:如果一个函数已经开始return了,函数的核心功能执行完了吗? 答:执行完了

return时, 子进程已被创建,return也是语句,所以父子都会执行return语句

image-20220514141556122

2)fork函数创建出来的子进程与其父进程共同使用一份代码,但我们如果真的让父子进程做相同的事情,那么创建子进程就没有什么意义了 -> 通过if-else分流让子进程和父进程做不一样的事情 -> 通过fork的返回值来完成!

3)返回值也是数据,return时需要写入

谁先返回,就会发生写时拷贝,可以看到两个返回值的确不同

  • 注:fork之后,父子谁会先运行?这是不确定的,是由调度器来确定的

在fork函数体内,函数返回前,已经完成了子进程的创建,既然完成了子进程的创建,那么子进程就也会去到运行队列中,等待CPU调度,父子进程共享代码,数据各自开空间。由于返回值id是数据,所以虽然id的变量名相同,但是内存地址不同,所以返回的id是不同的


fork函数的返回值:
1、如果子进程创建成功,在父进程中返回子进程的PID,而在子进程中返回0 2、如果子进程创建失败,则在父进程中返回 -1

既然父进程和子进程获取到fork函数的返回值不同,那么我们就可以据此来让父子进程执行不同的代码,从而做不同的事. -> 通常要用 if 进行分流

image-20220514141218574

综合上面fork函数的返回值: 返回值为0的就是子进程执行的内容,返回值>0的就是父进程执行的内容,否则就是子进程创建失败的情况

#include<stdio.h> 
#include<unistd.h>      
int main()    
{    
    pid_t id = fork();    
    if(id == 0)    
    {    
        //child    
        printf("I am child PID:%d PPID:%d\n",getpid(),getppid());
        sleep(1);
    }    
    else if (id > 0)    
    {    
        //parent    
        printf("I am parent PID:%d PPID:%d\n",getpid(),getppid());
        sleep(1);                                                                                                                                             
    }    
    else    
    {     
        //fork error
    }    
    return 0;    
} 

image-20220410221826344

fork创建出子进程后,子进程会进入到 if 语句当中执行内容,而父进程会进入到 else if 语句当中执行内容


进程状态

问:进程的状态信息放在哪里?

进程状态信息也是在task_struct(PCB)中,CPU的调度就是对task_struct的增删查改

进程状态的意义在于,方便OS快速判断进程,并完成特定的功能

比如调度 ,本质上是一种分类


我们可以使用 ps aux 或者 ps axj 指令查看进程的状态

image-20220411092423247


摘自百度百科:

image-20220411092100594

看看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) : 运行状态不一定占用CPU, 并不意味着进程一定在运行中,一个进程处于R状态,它只是表明进程要么是在运行中要么在运行队列里,随时可以被CPU调度 也就是说,可以同时存在多个处于R状态的进程

当前进程只要在运行队列当中,所有的状态都叫R状态

注意: 所有处于运行状态,即可被调度的进程,都被放到运行队列当中,当操作系统需要切换进程运行时,就直接在运行队列中选取进程运行

image-20220411092913362


S 浅睡眠状态(sleeping): 当需要完成某种任务,而条件不具备时,需要进程进行某种等待(这里的睡眠有时候也叫做可中断睡眠,这种是浅睡眠状态,可以随时被唤醒,也可以被杀掉 (kill -9 PID编号,或者直接ctrl+c)

唤醒: 把从等待队列放到运行队列中,被CPU调度,叫做唤醒进程

等待(阻塞):把运行状态的task_struct从运行队列(run_queue)放到等待队列(wait_queue)中,叫做挂起等待阻塞

注意:不要认为进程只会等待CPU的资源,等CPU资源的时候,叫做运行队列,等其它资源的时候,叫等待队列!!!

所谓的进程,在运行的时候,有可能因为运行需要,可能会在不同的队列中,在不同的队列中,所处的状态是不一样的

在运行队列:状态为R,在等待队列中:状态为S或者D


阻塞和唤醒的概念:

阻塞:进程因为某些条件不就绪,导致自己被放到了等待队列中 唤醒:一个进程等待某种资源,条件就绪只会,把自己放到运行队列中


**D 深度休眠状态(Disk sleep)**有时候也叫不可中断睡眠(深度睡眠)状态 在这个状态的进程通常会等待IO的结束

进程处于D状态,不可以被杀掉!

操作系统可以杀掉处于D状态的进程吗? 不可以!


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

注意:使用kill命令可以列出当前系统所支持发出的信号 kill -l

image-20220411093919297

X死亡状态(dead):回收进程资源 = 回收进程相关的内核数据结构 + 代码和数据.

这个状态只是一个返回状态,当一个进程的退出信息被读取后,该进程所申请的资源就会立即被释放,该进程也就不存在了,你不会在任务列表里看到这个状态


Z (zombie)-僵尸状态 僵尸状态(Zombies)是一个比较特殊的状态

当一个进程将要退出的时候:

在系统层面,该进程曾经申请的资源并不是立即被释放,而是要暂时存储一段时间,以供操作系统或是其父进程进行读取,如果退出信息一直未被读取,则相关数据是不会被释放掉的,一个进程若是正在等待其退出信息被读取,那么我们称该进程处于僵尸状态,即:当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵死(尸)进程

为什么要有僵尸状态? 辨别退出死亡原因 -> 进程退出的信息 -> 也是数据 -> 在task_struct保存

所以一个进程退出的时候:先进入僵尸状态,把退出信息写在task_struct (PCB)中,供系统的父进程读取


什么是退出码?

例如:我们经常写代码时候在最后的位置 return 0,这个0实际就是退出码,返回给操作系统,告诉操作系统代码执行结束

在Linux中怎么查看退出码?

echo $? ->可以获取最近一次进程退出时的退出码

例子: 目前创建进程主要有两种方式,./cmd(可执行程序)run command(运行指令),所以运行指令或者运行可执行程序,都会有退出码

image-20220411173411147


为什么要存在僵尸状态

需要辨别退出/死亡原因,把进程退出的信息(例如退出码)暂时被保存在其进程控制块当中,在Linux中就是写入到task_struct中,供系统/父进程读取, 僵死进程会以僵尸状态保持在进程表中,并且会一直在等待父进程读取退出状态代码

所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态


进程状态的验证:

1.运行状态R:

只需要写一个死循环即可,什么都不执行

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

image-20220411174512150


2.睡眠状态S:

死循环打印

#include<stdio.h>
int main()
{
    while(1)
    {
        printf("I am running\n");
    }
    return 0;
}

image-20220411174713757


此时也是死循环,但是为什么多了一条打印的代码,就从R状态变成了S状态?

  • 打印到显示器上,显示器是外设,很慢,IO(输出输出设备)等待外设就绪是要花时间的.而CPU太快了,挂起->运行然后再次挂起->运行特别快,状态切换很快,可能会看到R状态和S状态切换的情况,如下面图片 进程虽然给人感受一直在运行,实际上相当长的时间都在休眠
  • 刚才验证R状态时,只写了一个空语句,因为这样没有IO,不用等待,排队CPU资源即可

我们可以看到,大多数处于睡眠状态,但仍有少部分在运行状态

image-20220411175103686


3.暂停状态T

kill -l 列出当前系统所支持发出的信号

image-20220411180218950


暂停进程: 如何验证:kill -l 向特定进程发信号

image-20220514144506294


所以:给进程发送19号信号,就可以暂停程序了 kill -19 进程的PID

image-20220411180208911


恢复进程: 给进程发送18号信号 kill -18 进程的PID

image-20220411180420091

但是此时,我们不能使用ctrl+c终止进程了, 我们也可以发现,进程状态从S+ 变成了S 因为:我们的暂停和继续->让进程变成了后台运行


那我们应该怎么干掉程序呢? 给进程发送9号信号

image-20220411202429556


前台进程和后台进程

前台进程: ./可执行程序 默认就是前台进程 , 在前台运行的程序状态就是S+ 在运行的时候,输入指令无效, 可以使用 ctrl+c 结束进程

后台进程:./可执行程序 &才是后台进程,状态就是S 在运行的时候,可以执行指令,不可以使用 ctrl+c 结束进程, 退出进程要给进程发送9号信号 kill -9 pid


关于僵尸进程

首先:我们写一个监控命令行脚本(Shell脚本)

while :; do ps axj | head -1 && ps axj | grep myproc | grep -v grep; sleep 1; echo "########################"; done

我们需要演示的是: 父子进程执行一段时候后,我们让子进程退出,父进程一直执行 即:当子进程退出了,父进程还在执行,但是父进程没有读取子进程的退出信息,那么子进程就会进入僵尸状态

image-20220411220946245

如果没有人检测和回收(由父进程来做),该进程退出就进入Z状态,僵尸进程会以终止状态保持在进程表中,等待父进程读取退出状态代码,会造成内存泄漏


  • 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了.可父进程如果一直不读取,那子进程就一直处于Z状态
  • 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说, Z状态一直不退出, PCB一直都要维护
  • 那一个父进程创建了很多子进程,就是不回收,就会造成内存资源的浪费. 因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间

孤儿进程

  • 子进程先退出而父进程没有对子进程的退出信息进行读取,称该进程为僵尸进程
  • 若是父进程先退出,那么将来子进程进入僵尸状态时就没有父进程对其进行处理,此时该子进程就称之为孤儿进程, 若是一直不处理孤儿进程的退出信息,那么孤儿进程的PCB就会一直存在,此时就会造成内存泄漏.因此,当出现孤儿进程的时候,孤儿进程会被1号init进程领养,此后当孤儿进程进入僵尸状态时就由int进程进行处理回收

例子:

image-20220411222459623

image-20220411222732121

孤儿进程的PPID变成1号进程的PID 孤儿进程被1号进程init领养,资源由init进程回收


进程优先级

基本概念

  • cpu资源分配的先后顺序,就是指进程的优先权(priority).
  • 优先权高的进程有优先执行权利.配置进程优先权对多任务环境的linux很有用,可以改善系统性能.还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能

权限:是能和不能的问题 优先级:是先后的问题

问题:为什么会有优先级的问题? 因为系统资源太少,本质是分配资源的一种方式.Linux中的优先级数据,值越小优先级越高


查看系统进程

在linux或者unix系统中,用ps –al命令则会类似输出以下几个内容:

image-20220411223857201

我们很容易注意到其中的几个重要信息,有下 :

  • UID : 代表执行者的身份

image-20220411224251459

  • PID : 代表这个进程的代号
  • PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
  • PRI :代表这个进程可被执行的优先级,PRI的值越小越早被执行(优先级越高)
  • NI :代表这个进程的nice值 (优先级的修正数据)

PRI和NI

  • PRI代表进程的优先级(priority),通俗点说就是进程被CPU执行的先后顺序,该值越小进程的优先级别越高
  • NI代表的是nice值,其表示进程可被执行的优先级的修正数值

不同点:

  • 需要强调一点的是,进程的nice值不是进程的优先级,但是进程的nice值会影响到进程的优先级变化.

  • nice值是进程优先级的修正修正数据

    • PRI(new) = PRI(old) + NI,在Linux中,PRI(old)默认为80,即PRI = 80 + NI
    • 若NI值为负值,那么该进程的PRI将变小,即其优先级会变高
  • NI的取值范围是-20至19,一共40个级别

    • 问:为什么nice值处于一个较小的范围

      因为优先级再怎么设置,也只能是一种相对的优先级,不能出现绝对的优先级,否则会出现严重的进程"饥饿问题" 所以这也是调度器存在的价值:较为均衡的让每个进程享受到CPU资源

什么是饥饿问题? 即让某个进程长时间得不到系统资源


用top命令更改已存在进程的nice值:

我们可以看到PRI通常都是80,NI值默认为0

image-20220412213340719


top命令就相当于Windows操作系统中的任务管理器,它能够动态实时的显示系统当中进程的资源占用情况


修改nice值:

方法1: 输入top -> 进入top后按r–>输入进程PID–>输入nice值 ->按enter键 -> 按q退出

因为nice值是有范围的,你输入的数字尽管再大/再小,最多也只能取19/-20


使用renice命令更改已存在进程的nice值:

方法: renice nice值 进程的PID 注意:如果nice值是负数,则需要sudo提升权限

image-20220412214405108


其他概念
  • 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的
    • 为了高效完成任务,更合理竞争相关资源,便具有了优先级,调度器通过优先级确定进程谁先谁后执行
  • 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
    • 如:关闭qq 不会影响微信的运行
  • 并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
  • 并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间内,让多个进程都得以推进,称之为并发

并行和并发的区别:

并行:任何时候,都有两个以上的进程在跑 并发:在一个时间段内,多个进程同时被推进

并行和并发是可以同时存在的,

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

芒果再努力

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值