文章目录
1.进程2.task_stuct的属性2.1 标示符2.1.1 PID2.1.2 PPID2.1.3 创建子进程 2.2 状态2.2.1 进程的状态2.2.2 Linux下进程的状态 2.3 优先级2.4 进程切换(上下文数据、程序计数器 )2.5 Linux的进程调度算法
1.进程
在没有正式学习进程之前,通过理解操作系统的管理,我们就可以知道操作系统是怎么进行进程管理的了。
很简单,先把进程描述起来,再把进程组织起来!那什么叫进程呢?
课本概念:程序的一个执行实例,正在执行的程序等
内核观点:担当分配系统资源(CPU时间,内存)的实体。
但是看完上面两句话我还是不懂进程是什么意思。
操作系统为了管理进程,必须得描述进程,描述进程就要有结构体(课本上称之为进程控制块,PCB process control block,Linux操作系统下的PCB是: task_struct )
,有结构体就可以连接起来,连接起来就可以将管理进程转化为数据结构的增删查改操作。
我们先对进程简单建一下模:
所以, 进程 = 内核的数据结构(task_struct)+程序的代码和数据 ,调度运行进程,本质就是让进程控制块task_struct进行排队!
那么task_struct中存放的都是什么呢?
2.task_stuct的属性
task_struct是Linux内核的一种数据结构,它会被操作系统在RAM(内存)里创建,并且里面包含着进程的信息。
task_ struct内容分类:
标示符:描述本进程的唯一标示符,用来区别其他进程。状态:任务状态,退出代码,退出信号等。优先级: 相对于其他进程的优先级。程序计数器:程序中即将被执行的下一条指令的地址(pc指针)。上下文数据:进程执行时处理器的寄存器中的数据。内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。其他信息为了查看相关属性,我们先写一个简单的程序
当程序编译好以后,我们 ./myproc运行它,根据冯诺依曼体系结构的设计,它会被加载到内存中。此时它就不叫程序了,而应该叫做进程。
那么我们应该如何查看该进程的属性呢?
命令:ps axj ,用于显示系统中当前运行的进程的信息,包括进程ID(PID)、父进程ID(PPID)、CPU使用率、内存使用率等 ps(process status)
此时我们我们发现有很多进程在运行,所以我们可以使用grep命令过滤一下,并且使用head命令显示文件的第一行。
可使用反向过滤,去除grep myproc 进程
ps ajx | head -1 && ps axj | grep myproc | grep -v grep
因此,将程序运行起来,本质上就是在系统中启动了一个进程,此时进程可分为两种
执行完就退出的进程,例如指令 ls mkdir等一直不退出,除非用户主动关闭 - - 常驻进程2.1 标示符
2.1.1 PID
PID 标示符: 描述本进程的唯一标示符,用来区别其他进程。
每次运行我们的程序,它的PID都是不同的。
在程序中,可以通过调用getpid()函数
获取当前进程的PID
当前我已经知道了这个进程的PID,如果我想终止该进程怎么办呢?
在程序运行处,直接Ctrl + C使用kill命令,该命令可向进程发送信号。该命令的编号是9,所以我们直接可以:kill -9 + PID
此时,该进程就被杀掉了
查看进程信息时,除了使用ps命令外,如果我想看到进程更多的信息,那我应该怎么查呢?
linux下,一切皆文件,因此进程的信息也是以文件的方式保存的。因此在linux系统中,存在一个特殊的目录proc
proc目录中以数字命名的目录,这个数字就是指定进程的PID,每一个目录就代表一个进程,里面包含进程的所有信息。
这也就意味着,当我们运行一个程序后,操作系统就会立即在proc目录下,创建一个以该进程PID命名的目录,目录中存放进程的所有信息,方便上层去查看。进程结束后,该目录就被删除了,不存在了,是实时更新的。
不知道大家有没有一个疑问,频繁的创建、删除、修改文件会不会影响系统的效率?
注意:proc文件不是磁盘级别的文件,它是内存级别的文件,因此不会影响
我们进程的PID为2947,可以发现它确实存在于proc目录中。
因此,我们如果要查看进程的详细信息,可以使用ls /proc/PID -l
,此时就显示出了该进程文件下的所有信息,即进程的信息。这时显示的比我们使用ps显示的就详细的多。
在这些信息中,我们重点关注exe 和cwd。
在下面的代码中,我们打开了一个文件,此时文件不存在,它会自动创建一个文件,但它怎么知道在哪里创建文件呢?
运行程序我们会发现,test.txt文件和我们的进程 myproc创建在同一个位置了
所以,它应该是在“当前工作目录下”创建了文件test.txt,此时的cwd就是当前进程的cwd,将要创建的文件直接拼接在cwd路径后,即可创建对应文件。如果指定了绝对路径,则会在指定路径下创建。
在linux系统中,提供了一个系统级的接口chdir()
,可供我们改变进程的cwd。
2.1.2 PPID
在Linux系统中,启动之后,新创建任何进程的时候,都是由父进程创建的!(父进程让操作系统建的)
getppid()函数可以获得父进程的id。
我们多次运行程序可以发现,子进程的id会变化,但是父进程的id没有改变。
那么父进程是谁呢?- - bash(命令行解释器,linux中是bash)
在命令行中,执行命令/程序的本质是:一个叫做bash的进程,创建了子进程,由子进程执行我们的代码!
那么子进程是如何创建的呢?
2.1.3 创建子进程
如何创建子进程?fork函数
:用来创建子进程。
该函数的返回值非常特殊,如果进程创建成功,它会返回子进程的PID给父进程(给父进程返回子进程的pid是为了方便父进程管理孩子),返回0给子进程;如果失败,返回-1给父进程。
创建成功它为什么会返回两个值呢?我们写个代码来看一下
经过fork后,该程序就会有两个执行分支,因此第二条printf会执行两次。
而且我们可以发现,第三个printf的父进程是执行第二条printf的进程,因此二三条printf是父子进程。
第二条就是执行该程序的进程,第三条就是程序中又创建的子进程。
而且我们可以发现,父进程可以有多个子进程,而子进程只能有一个父进程。所以,linux系统中,所有的进程都是树形结构!
对于fork的返回值,我们使用下面的代码来验证一下:
在调用fork函数创建子进程后,我们的if 和else两个条件同时满足了,而且两个死循环同时在跑,这是为什么呢?
这里就证明了,在fork以后,有两个循环同时在跑,那么它就一定是有两个执行流,分别叫做父进程和子进程。
那fork为什么会有两个返回值呢?
因为fork后,会有两个进程,两进程为父子关系。一般而言,代码是会共享的,但是数据只有一份。
通过下面的代码我们可以发现,父进程与子进程二者不是同一个gval,也就是说它俩各自有一个gval,
为什么父子进程代码共享,但是数据各自私有一份呢?- - 进程是相互独立的,对各进程之间运行时互不影响,即便是父子
。
对于代码,二者都是只读的;对于数据,各自私有一份(使用写时拷贝的思想)
对于接收fork返回值的id,它也是数据,因此父子进程中会各有一份,所以我们的if和else这两个条件都会走,两个进程各自运行。
fork是一个系统调用调用函数,创建子进程的时候,需要先拷贝父进程的task_struct,然后调整部分属性,最后在链入到进程列表中。在这个函数执行return语句之前,父子进程绝对都已经存在了,那两个进程执行各自执行依次return语句,返回两个值也就是理所当然的了。
如何创建多进程呢?一个程序中多次调用fork函数即可。并且在父进程中可以收集到子进程的pid,方便管理。
2.2 状态
2.2.1 进程的状态
相信学习过操作系统的伙伴,对进程的理解就是上图这些东西,但是你真的理解了吗?上图是宏观的描述了操作系统的状态,对于不同的操作系统,它们都满足上面的状态,但是不同的操作系统的处理方式又有区别。
在真正理解进程的状态之前,我们补充几点知识:
并发和并行并发
:在单CPU的计算机中,并不是把当前进程执行完毕以后再执行下一个,而是给每个进程分配一个时间片,基于时间片,进行轮换调度,轮换调度的过程就叫做并发。
并行
:多个进程在多个CPU下分别、同时运行,叫做并行。
Linux/Windows等民用操作系统,都是分时操作系统
(给每个进程分配一个时间片,当时间片耗尽时,就必须从CPU上剥离下来,然后把另一个进程放上去)。分时操作系统的特点:调度任务最求公平。
实时操作系统
:任务一旦执行,从开始到结束尽量、优先的执行完毕。
对于每一个CPU,操作系统都要给其提供一个叫做运行队列的东西。每次执行了一个进程,就是将进程的task_struct链入到运行队列中,CPU在处理进程时,只需要到运行队列中去取队头的task_struct。
当一个进程处于运行队列当中时,我们就称该进程为运行状态。
计算机大多数情况下都是在做IO的操作(外设的访问),比如获取键盘上的数据,执行scanf。
但是有时候键盘迟迟没有按下,进程也就获取不数据,但是CPU不会一直等该进程,此时该进程就会被设置为阻塞状态
,等待对应的底层硬件准备好,该进程才会被重新放到运行队列中。
那阻塞状态具体是怎么做的呢?
由于操作系统也需要管理底层的硬件(先描述,再组织),所以它清楚的知道你硬件此时是什么状态(到底有没有数据),如果没有就该进程就会阻塞,那该进程去哪里“阻塞”呢?- - 去外部设备上,这里的外部设备指的是OS所管理的外设的PCB。
因此,等CPU的就叫做运行状态,等外设的叫做阻塞状态。
阻塞期间,进程不会被调度,但进程对应的PCB与代码和数据也是会占用内存的,此时该部分内存是被白白浪费的。
当内存资源严重不足时,操作系统为了保证整个系统的安全,会将进程的代码和数据换出到磁盘中;当进程不阻塞时,OS会再将进程对应的代码和数据换入内存中。
磁盘中会有一块分区,专门进行换入和换出工作,该分区叫做交换分区(swap分区),是一种用时间换空间的策略。
当进程处于阻塞状态,并且操作系统将其代码和数据换出到磁盘中,此时被换出的进程就处于阻塞挂起状态
。
2.2.2 Linux下进程的状态
上面我们已经知道了操作系统宏观上的状态解释,那在具体的Linux系统下又是什么样的呢?
我们看看Linux内核源代码怎么说
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 和 S 状态 R:运行状态S:休眠状态(阻塞等待状态),可中断睡眠(浅睡眠,即可直接被kill掉) D 状态 D:disk sleep,也是阻塞等待的一种状态(不可中断睡眠,深度睡眠),专门为磁盘设计的状态。
为了保护访问磁盘的进程(保护数据),禁止操作系统“杀掉”该进程,该进程就处于D状态
T 状态T:暂停进程。
在认识T状态前,我们先掌握两个kill 18和19号。
使用kill -19,就可以让指定进程暂停掉,此进程的状态就变成了T。
使用kill -18,就可以让指定进程继续执行,此进程的状态就变成了S。为什么不是S+了呢?而且我发现无法使用Ctrl+C杀掉该进程了。只能使用kill -9 命令了。
为什么不是S+了呢?因为当一个进程被暂停又恢复后,它就变到后台去运行。
t 状态t:tracing stop,遇到断点,进程就被暂停掉了。此时该进程就是被追踪状态,断点处停下来,此时进程的状态就是t.
gdb后,会有一个gdb进程,遇到断点,gdb调试进程的状态就是t了。
X 与 Z状态Z:僵尸状态
X:死亡状态,即进程结束。
先介绍linux下的一条命令:echo $ ?
,显示最近一条进程的退出信息(执行状态,0为正常退出,非0为异常退出)
为什么一个进程要返回执行信息呢?- - 通过进程的执行结果,告诉父进程/操作系统,我把任务执行的怎么样。
那什么是僵尸状态呢?
当一个进程退出时,它的代码和数据会被释放掉;但是它的task_struct还存在,task_struct中存着该进程的退出信息(执行信息)。当一个进程退出并且父进程没有读取到子进程退出的返回代码时
就会产生僵尸进程。僵尸进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程则进入僵尸状态,僵尸状态一直不退出,PCB就要一直维护,直到父进程回收。那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,此时就会造成内存泄漏(系统级的)。 为了验证僵尸状态,我们写一个测试代码
//循环查询命令while :; do ps axj | head -1;ps axj | grep test; sleep 1;done
当我们的程序运行10秒后,子进程就结束了,但是依然可以看到子进程的信息,而且它的状态变成了Z,处于了僵尸状态。
孤儿进程当子进程退出,父进程存在时,叫做僵尸状态。那如果反过来,父进程退出,子进程存在呢?
验证一下
当父进程被杀掉,子进程存在时,可以发现子进程的ppid变成了1。
我们top一下,发现1是系统。
因此,当父进程退出时,系统会领养子进程,以便接收子进程的退出信息,回收进程。这个被领养的进程就叫做孤儿进程
。孤儿进程运行在后台
2.3 优先级
优先级是什么?获得某种资源的先后顺序。
为什么要有优先级因为资源有限。
优先级是怎么做的呢?task_struct中有一个优先级属性priority,里面包含特定的几个int类型的变量表示优先级。Linux中,优先级数字越小,优先级越高。
在linux中,一共有两种数字来维护一个进程的优先级
PRI:当前进程的优先级,默认是80。NI:nice,优先级的修正数据。最终优先级 = PRI + NI
上图中的UID是用来:标识该进程是谁启动的。
那如何修改优先级呢?
修改优先级,只能通过nice来修改的。
方式:用top命令更改已存在进程的nice:输入top,进入top后按“r”–>输入进程PID–>输入nice值
nice值的取值范围是:[-20,19]
最终优先级是:使用默认pri(80),然后加上nice值,并不是使用上一次的pri,所以上图中的pri是99和60。因此调整优先级习惯称作:优先级的重置。 2.4 进程切换(上下文数据、程序计数器 )
当一个进程的时间片到了,它就需要被切换。Linux是基于时间片,进行调度轮转的。
但是当一个进程的时间片到时,它并不一定能够执行完毕,可以在任何地方被重新调度切换。
那如何切换呢?
当一个进程被切换走的时候,需要保存该进程执行到了哪里;当进程被切换回来时,需要从上次执行到的地方恢复执行。即进程切换时,它“从哪里来,回哪里去”,那如何记录它从哪来到哪去呢?当一个进程运行的时候,会有很多的临时数据,这些临时数据都是保存到CPU的寄存器中的,例如eip、ir。
这些寄存器中,是进程执行时瞬时状态的信息数据,即:上下文数据
。
虽然CPU有很多寄存器,但是它只有一套寄存器,是被多个进程共享使用的。如果有多个进程同时运行时,会频繁的更新寄存器中的数据。
如果不预先将寄存器中当前进程①的数据保存,则会被其它进程②覆盖掉,当进程①又回来时,不知道从哪里开始了,此时就无法完成进程的调度与切换。
因此,进程切换的核心:进程上下文数据的保存和恢复。
那将寄存器中进程的上下文信息预先保存到哪里呢?
当前进程的PCB中的一个任务状态段中,也就是在内存中
2.5 Linux的进程调度算法
下图是Linux2.6内核中进程队列的数据结构
一个CPU拥有一个runqueue
,如果有多个CPU就要考虑进程个数的负载均衡问题。
优先级
普通优先级:100~139(我们都是普通的优先级,想想nice值的取值范围,[-20,19],刚好20个)实时优先级:0~99 活动队列 时间片还没有结束的所有进程都按照优先级放在该队列nr_active: 总共有多少个运行状态的进程 queue[140]:一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度,所以,数组下标就是优先级!相同优先级的进程在同一个数组下标中,使用类似于hash桶的思想挂在同一个桶中
从该结构中,选择一个最合适的进程,过程是怎么的呢?
从0下表开始遍历queue[140]找到第一个非空队列,该队列必定为优先级最高的队列拿到选中队列的第一个进程,开始运行,调度完成!遍历queue[140]时间复杂度是常数!但还是太低效了借助位图
bitmap[5]
:一共140个优先级,一共140个进程队列,为了提高查找非空队列的效率,就可以用5*32个比特位表示队列是否为空(即一次检查一个int,可一次跳跃32个比特位)。这样,便可以大大提高查找效率。 过期队列 过期队列和活动队列结构一模一样
过期队列上放置的进程,都是时间片耗尽的进程/新建进程当活动队列上的进程都被处理完毕之后,对过期队列的进程进行时间片重新计算 active指针和expired指针active指针永远指向活动队列 expired指针永远指向过期队列
可是活动队列上的进程会越来越少,过期队列上的进程会越来越多,因为进程时间片到期时一直都存在的。在合适的时候,只要能够交换active指针和expired指针的内容,就相当于有具有了一批新的活动进程! 为什么进程会是运行起来的程序呢?因为进程会被调度,被切换。