linux(5) 进程
前言今天我们来聊聊进程^^ ,我们会先从冯诺依曼体系中认识到内存这个东西然后我们会聊一聊操作系统的管理。接着我们将会开始有侧重的聊一聊进程中PCB的一些内容最后我们将综合上文所有内容来聊一聊进程被CPU调度让它动起来正文冯诺依曼体系我在这里画了一张图这张图演示的是数据在输入后如何被计算机进行处理这途中会经过这些硬件那么就引出了计算机的组成输⼊单元包括键盘, ⿏标扫描仪, 写板等中央处理器(CPU)含有运算器和控制器等输出单元显⽰器打印机等存储器内存是什么这里的存储器就是指内存也是我们接下来重点要说的我们上面的图中有输入设备有输出设备输入和输出是相对于谁而言内存为何存在这时我们发现一个问题我们发明计算机是为了处理数据那么为什么还要有内存这个东西我们大可以像下面这样设计这样直接把输入交给CPU再进行输出不是更加简洁更加快吗可惜答案并非如此小demo这里在网上找了一张图我们用这张图来看这张图自上而下效率越来越低而价格同时也是越来越低所以一般磁盘这些存储空间就很大效率不快毫秒相反寄存器这些存储空间小效率快约1纳秒而内存效率还行约100纳秒所以可以发现寄存器和磁盘的效率差距极大内存缩小差距所以我们可以发现如果输入输出假设是磁盘直接和CPU打交道CPU速度极快所以整个流程的速度由磁盘主导那么就会很慢很慢因为寄存器与磁盘速度差距过大那么如何解决答案是引入内存。在这张图中有一个速度还行的内存在中间那就带来了两个好处①. 内存本身速度远超磁盘即使不做别的只是把数据放在内存给cpu读写就已经快成千上万倍②. 预加载机制程序访问的数据往往是连续重复使用的后续再次使用时直接命中内存会更加快所以我们就引入了内存之后就出现一个结论总结所有设备都只能直接和内存打交道操作系统OS概念狭义上的理解操作系统就是操作系统内核广义上的理解操作系统是操作系统内核加上程序设计目的对下进行管理进程管理文件管理驱动管理内存管理对上服务人为用户提供一个良好的操作环境图理解细谈管理假如武斌当上了校长他现在要管理学校里面的几千个学生有一天学校里要选一个打篮球很厉害的学生去代表学校参数武斌就要从这几千个人里面找一个打篮球最厉害的武斌很头疼.......总不能一个一个来谈话吧所以武斌找到了他的下属班主任他让这些班主任去搜集学生的信息然后最后把信息都录入到电脑上然后呢一个一个看嘛好像武斌眼睛看瞎了也看不完呢所以武斌就想到可以定义一个结构体来把学生的信息录入进去来描述这些学生嗯.....录完后条理多了不过还是很难找啊所以武斌就想到一个办法把描述好的学生结构体用链表组织起来然后通过一个简单的比较大小接口遍历了一下链表武斌就找到了那个打篮球最好的学生~所以通过这个小故事我们得出以下结论①上级管理下级不需要直接见面只需管理它的数据即可②在管理的时候我们通常要先描述事物的属性再选择合适的数据结构把他们组织起来这种先描述再组织的思维非常重要细说这张图片接下来我们来看看这张图那这里操作系统就相当于校长驱动程序就相当于班主任而硬件就相当于学生操作系统不可能直接和硬件打交道就像校长不可能挨个找几千个学生而驱动程序收集好硬件信息进行描述生成结构体交由操作系统管理这时我们把操作系统驱动程序硬件看成一个整体学校而我总有需求来让操作系统给我做点事那我直接动底层我直接掀开硬盘把东西写进去太危险了所以呢既然操作系统是硬件的管理者那么在操作时就先要问它的意见所以它为了解决我们的诉求就给自己开放了一些接口系统调用接口我们通过这个来对底层进行操作但是直接用系统调用接口太难了吧我是普通人我不会所以就有人继续对系统调用接口进行封装从而诞生了更易操作的用户操作窗口来方便我们的使用举个例子就是校园开放了一些校园办事窗口系统调用接口来给我们解决问题但是啊我是一个六十岁老大爷我不太会这时就有服务人员用户操作窗口来帮助我在它的帮助下我用户就完成了我的诉求命令小问题那么我的操作系统如何对进程进行管理先描述再组织进程概念进程内核数据结构(PCB) ⾃⼰的程序代码和数据PCB我们说先描述再组织那么PCB是什么呢就是我们对程序代码和数据的描述官方一些的语言是进程信息被放在⼀个叫做进程控制块的数据结构中可以理解为进程属性的集合在linux中PCB叫做task_struct显然task_struct是操作系统管理进程的主要所以我们来讲讲它task_struct:内容标⽰符: 描述本进程的唯⼀标⽰符⽤来区别其他进程。pid状态: 任务状态退出代码退出信号等优先级:相对于其他进程的优先级。内存指针: 包括程序代码和进程相关数据的指针还有和其他进程共享的内存块的指针上下⽂数据: 进程执⾏时处理器的寄存器中的数据记账信息: 可能包括处理器时间总和使⽤的时钟数总和时间限制记账号等其他信息声明我们主要讲解上面被标红的三项标示符本质linux中的每一个进程操作系统都会分配一个唯一非负整数编号即为pid作用操作系统管理进程通过pid定位进程就像校长管学生通过学号演示我们写了一段代码#includestdio.h 2 #includeunistd.h 3 int main() 4 { 5 while(1) 6 { 7 printf(奥拉星牛逼我的pid是:%d\n,getpid()); 8 sleep(1); 9 } 10 }我们把它编译之后开始运行我们打开分屏[tsxVM-0-10-centos ~]$ ps axj | head -1;ps axj | grep code PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 13708 21756 21756 13708 pts/0 21756 S 1001 0:00 ./code 14287 21986 21985 14287 pts/1 21985 S 1001 0:00 grep --colorauto code这几列分别是PPID: 父进程id谁创建了当前进程填谁PID: 当前进程自身idPGID: 进程组idSID: 会话idTTY: 控制终端TPGID: 前台进程组idSTAT: 进程状态有是前台进程没有则是后台一会细讲UID: 用户idTIME: 进程累计占用cpu时间COMMAND: 启动进程的命令上面那个是code但同时我们用grep命令这也是个进程所以它也显示出来了内存指针一句话总结用来指向进程对应的代码和数据记账信息一句话总结用来统计进程占用系统资源的各项数据内核依靠它完成统计调度资源限制小说明由于操作系统它要满足理论在所有操作系统中都受用那么就一定会抽象一些而且还会和我们详细的操作系统有出入所以状态这里我们分成两部分讲解①. 操作系统的定义②. linux中的定义操作系统状态操作系统中这里在网上找了一个图那么这里我们如何理解呢就绪状态资源全部准备完毕只差cpu调度这里和linux中不一样我们一会细分运行状态从就绪态被调度正在CPU上执行命令这里和linux中不一样我们一会细分阻塞状态我们先创建一段代码#includestdio.h 2 int main() 3 { 4 int a1; 5 scanf(%d,a); 6 printf(%d,a); 7 return 0; 8 }我们可以看到在我们运行之后进程需要键盘上的数据所以它就卡住了那么底层的原理就是就比如这张图task_struct1对应的进程需要键盘输入那么我就从当前的runqueue上断开从而来到了硬件的等待队列中所以区分阻塞状态与运行状态为什么是运行状态一会在linux状态中细说就是看当前我进程的task_struct在哪个队列之下排队在硬件等待队列中排队那么就是阻塞状态挂起状态挂起状态是什么呢如果我们在进程运行时内存突然不够了那么为了腾出内存我们会把wait_queue中的进程的内存和数据放在swap分区之中这种状态叫阻塞挂起状态直到我要被CPU调度我才会从swap分区中把我的代码和数据取出来。那如果把wait_queue中的内存和数据挂起以后内存还是不够呢那么我们就会把runqueue中的内存和数据挂起这叫就绪挂起状态那再极端些都挂起后内存还是不够呢 那么CPU就要杀进程了小结那么操作系统中的进程状态就讲完了接下来我们讲讲linux中的进程状态linux状态R运⾏状态running:并不意味着进程⼀定在运⾏中它表明进程要么是在运⾏中要么在运⾏队列⾥。只要一个进程的task_struct在runqueue中那么这个进程它就是运行状态#includestdio.h 2 #includeunistd.h 3 int main() 4 { 5 while(1) 6 { 7 printf(奥拉星牛逼\n); 8 } 9 }我运行一下就成了这样奥拉星牛逼 奥拉星牛逼 奥拉星牛逼 奥拉星牛逼 奥拉星牛逼 奥拉星牛逼 奥拉星牛逼 奥拉星牛逼 奥拉星牛逼 奥拉星牛逼 奥拉星牛逼 奥拉星牛逼 奥拉星牛逼然后我看一下状态PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 13708 27314 27314 13708 pts/0 27314 S 1001 0:00 ./code 14287 27354 27353 14287 pts/1 27353 S 1001 0:00 grep --colorauto code [tsxVM-0-10-centos ~]$那么此时我查看进程状态不对吧居然是S?这里是有原因的打印输出本质是向硬件执行io写操作终端窗口接受字符速度跟不上死循环打印速度此时进程放弃CPU资源变成s状态写出完成之后排完队后变成R状态等待它写入时候继续变成S状态所以呢我们把代码改成[tsxVM-0-10-centos ~]$ cat code.c #includestdio.h #includeunistd.h int main() { while(1) { ; } }再次查看PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 13708 28373 28373 13708 pts/0 28373 R 1001 0:11 ./code 14287 28445 28444 14287 pts/1 28444 S 1001 0:00 grep --colorauto code就是R状态了前台进程与后台进程只需记住一点前台进程可以读取键盘输入后台进程不行S睡眠状态sleeping):意味着进程在等待事件完成这⾥的睡眠有时候也叫做可中断睡眠比如说[tsxVM-0-10-centos ~]$ cat haha.c #includestdio.h int main() { int a1; scanf(%d,a); printf(%d,a); return 0; } [tsxVM-0-10-centos ~]$ ./haha我一查看[tsxVM-0-10-centos ~]$ ps axj | head -1;ps axj | grep haha PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 13708 29535 29535 13708 pts/0 29535 S 1001 0:00 ./haha 14287 29738 29737 14287 pts/1 29737 S 1001 0:00 grep --colorauto haha是睡眠状态那么我[tsxVM-0-10-centos ~]$ ps axj | head -1;ps axj | grep haha PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 13708 29535 29535 13708 pts/0 29535 S 1001 0:00 ./haha 14287 29738 29737 14287 pts/1 29737 S 1001 0:00 grep --colorauto haha [tsxVM-0-10-centos ~]$ kill -9 29535 [tsxVM-0-10-centos ~]$ ^C我把这个进程杀了[tsxVM-0-10-centos ~]$ ./haha Killed它能被杀掉所以叫做浅度睡眠D磁盘休眠状态Disk sleep有时候也叫不可中断睡眠状态在这个状态的进程通常会等待IO的结束。T停⽌状态stopped可以通过发送 SIGSTOP 信号给进程来停⽌T进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运⾏。那么我们说过后台进程不能从键盘读数据有个办法可以让进程在后台运行[tsxVM-0-10-centos ~]$ ./haha [1] 31517 [tsxVM-0-10-centos ~]$就是在后面加此时我们再查看状态PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 13708 31517 31517 13708 pts/0 13708 T 1001 0:00 ./haha 14287 31650 31649 14287 pts/1 31649 S 1001 0:00 grep --colorauto haha我们发现TT这个状态呢一般是出现问题的时候会出现的做对的事情叫休眠做错的事情叫暂停同时[tsxVM-0-10-centos ~]$ ps axj | head -1;ps axj | grep code PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 13708 2598 2598 13708 pts/0 2598 S 1001 0:00 ./code 14287 2660 2659 14287 pts/1 2659 S 1001 0:00 grep --colorauto code [tsxVM-0-10-centos ~]$ kill -19 2598 [tsxVM-0-10-centos ~]$ ps axj | head -1;ps axj | grep code PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 13708 2598 2598 13708 pts/0 13708 T 1001 0:00 ./code 14287 2831 2830 14287 pts/1 2830 S 1001 0:00 grep --colorauto code [tsxVM-0-10-centos ~]$ kill -18 2598 [tsxVM-0-10-centos ~]$ ps axj | head -1;ps axj | grep code PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 13708 2598 2598 13708 pts/0 13708 S 1001 0:00 ./code 14287 2897 2896 14287 pts/1 2896 S 1001 0:00 grep --colorauto codekill -19 pid可以让进程停止kill -18 pid可以让进程继续不过前台进程会变为后台进程不能ctrlc结束只能用kill -9杀死Z僵尸状态zombie子进程被杀了或者结束以后代码和数据被释放PCB需要维持此时变成僵尸进程。代码如下#includestdio.h 2 #includeunistd.h 3 int main() 4 { 5 pid_t idfork(); 6 if(id0) 7 { 8 while(1) 9 { 10 printf(我是子进程我的pid是%d,我的父亲pid是%d\n,getpid(),getppid()); 11 sleep(1); 12 } 13 } 14 else{ 15 while(1) 16 { 17 printf(我是父进程我的pid是%d\n,getpid()); 18 sleep(1); 19 } 20 } 21 return 0; 22 }接下来我们进行一些操作[tsxVM-0-10-centos ~]$ ps axj | head -1;ps axj | grep code PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 13708 6312 6312 13708 pts/0 6312 S 1001 0:00 ./code 6312 6313 6312 13708 pts/0 6312 S 1001 0:00 ./code 14287 6392 6391 14287 pts/1 6391 S 1001 0:00 grep --colorauto code [tsxVM-0-10-centos ~]$ kill -9 6313 [tsxVM-0-10-centos ~]$ ps axj | head -1;ps axj | grep code PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 13708 6312 6312 13708 pts/0 6312 S 1001 0:00 ./code 6312 6313 6312 13708 pts/0 6312 Z 1001 0:00 [code] defunct 14287 6674 6673 14287 pts/1 6673 S 1001 0:00 grep --colorauto code [tsxVM-0-10-centos ~]$打印结果如下我们可以看到杀了子进程以后子进程变成z状态其实呢就是等待退出数据被回收被回收以后那么就从z状态变为x状态释放PCBX死亡状态dead这个状态只是⼀个返回状态你不会在任务列表⾥看到这个状态孤儿进程操作如下[tsxVM-0-10-centos ~]$ ps axj | head -1;ps axj | grep code PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 13708 8102 8102 13708 pts/0 8102 S 1001 0:00 ./code 8102 8103 8102 13708 pts/0 8102 S 1001 0:00 ./code 14287 8143 8142 14287 pts/1 8142 S 1001 0:00 grep --colorauto code [tsxVM-0-10-centos ~]$ kill -9 8102 [tsxVM-0-10-centos ~]$ ps axj | head -1;ps axj | grep code PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 1 8103 8102 13708 pts/0 13708 S 1001 0:00 ./code 14287 8373 8372 14287 pts/1 8372 S 1001 0:00 grep --colorauto code [tsxVM-0-10-centos ~]$打印结果如下我是父进程我的pid是8102 我是子进程我的pid是8103,我的父亲pid是8102 我是父进程我的pid是8102 我是子进程我的pid是8103,我的父亲pid是8102 Killed [tsxVM-0-10-centos ~]$ 我是子进程我的pid是8103,我的父亲pid是1 我是子进程我的pid是8103,我的父亲pid是1 我是子进程我的pid是8103,我的父亲pid是1 我是子进程我的pid是8103,我的父亲pid是1 我是子进程我的pid是8103,我的父亲pid是1 我是子进程我的pid是8103,我的父亲pid是1我们发现他的父进程先结束的话那么他就会认systemd为父亲同时变成后台进程它结束后由systemd给它“收尸”从上面的孤儿进程和僵尸进程我们可以发现进程是有独立性的优先级概念首先区分权限和优先级权限能不能优先级能但是什么时候PRI和NI我们输入ps -l 后会出现这些[tsxVM-0-10-centos ~]$ ps -l F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD 0 R 1001 9440 14287 0 80 0 - 38324 - pts/1 00:00:00 ps 0 S 1001 14287 14286 0 80 0 - 29216 do_wai pts/1 00:00:00 bash其中PRI代表这个进程可被执⾏的优先级其值越⼩越早被执⾏NI 代表这个进程的nice值PRI(new)PRI(old)nice80其中nice的取值是从-20到19左右皆闭一共四十个值而PRI的取值则是从60到99当nice值为负值的时候那么该程序将会优先级值将变⼩即其优先级会变⾼则其越快被执⾏所以调整进程优先级在Linux下就是调整进程nice值例[tsxVM-0-10-centos ~]$ ps -al F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD 0 T 1001 1483 13708 0 80 0 - 1054 do_sig pts/0 00:00:00 haha 0 S 1001 10941 13708 0 90 10 - 1054 hrtime pts/0 00:00:00 code 1 S 1001 10942 10941 0 80 0 - 1054 hrtime pts/0 00:00:00 code 0 R 1001 11295 14287 0 80 0 - 38332 - pts/1 00:00:00 ps [tsxVM-0-10-centos ~]$我在改的时候把pid为10941的进程nice改为10那么PRI变为90补充概念①竞争性: 系统进程数⽬众多⽽CPU资源只有少量甚⾄1个所以进程之间是具有竞争属性的为了⾼效完成任务更合理竞争相关资源便具有了优先级②独⽴性: 多进程运⾏需要独享各种资源多进程运⾏期间互不⼲扰③并⾏: 多个进程在多个CPU下分别同时进⾏运⾏这称之为并⾏④并发: 多个进程在⼀个CPU下采⽤进程切换的⽅式在⼀段时间之内让多个进程都得以推进称之为并发我们电脑一般是这个进程切换CPU上下⽂切换其实际含义是任务切换, 或者CPU寄存器切换。当多任务内核决定运⾏另外的任务时, 它保存正在运⾏任务的当前状态, 也就是CPU寄存器中的全部内容。这些内容被保存在任务⾃⼰的堆栈中, ⼊栈⼯作完成后就把下⼀个将要运⾏的任务的当前状况从该任务的栈中重新装⼊CPU寄存器, 并开始下⼀个任务的运⾏, 这⼀过程就是context switch。这里就体现上下文数据的重要性了这里只区分一点寄存器是CPU的只有一套而寄存器内数据是进程私有的每一个进程都有其自己的寄存器数据。调度讲了这么多终于来到这里了调度是我们今天要讲的终极问题需要结合我们以上讲的所有概念来说不过在此之前我们还有引入几个小demo①demo1定义一个结构体如果只知道结构体中一个成员的地址还知道这个结构体的类型那么是可以知道这个结构体内所有地址的②.demo2:其实在linux中数据结构有些不同比如说在我们的task_struct中可能是这样的结构那么它就同时成为了两种链表的节点二叉树的节点还可以更多它是把数据结构内嵌到里面这样做还有一个好处就是只需要写一组代码就可以实现该数据结构在所有自定义类型中的使用。正式开始我给大家简单画了一下我们先看array[0]:nr_active: 总共有多少个运⾏状态的进程(用来提速}queue[140]: ⼀个元素就是⼀个进程队列相同优先级的进程按照FIFO规则进⾏排队调度,所以数组下标就是优先级这里有一个哈希映射的关系0-99对应的是实时优先级我们今天并不讨论而100-139对应的是普通优先级PRI40bitmap[5]:⼀共140个优先级⼀共140个进程队列为了提⾼查找⾮空队列的效率就可以⽤5*32个⽐特位表⽰队列是否为空这样便可以⼤ 提⾼查找效率也是提速效率O1过程①.先看nr_active是否为0防止没有进程让我去查找②. 之后遍历位图实时优先级实时优先级跳着走效率高可以跳过一大片无进程的位置,直到找到位图为1的位置n③.通过对应关系再到queue数组中的第n个位置把第一个进程取出来进行调度过期队列此时我们发现有一个问题如果我现在正在调度假如在queue[120],而我一直在[119]新增进程那么剩下的是不是就永远不会被调度了所以为了解决这个问题我们有了过期队列① 过期队列和活动队列结构⼀模⼀样② 过期队列上放置的进程都是时间⽚耗尽的进程③ 当有新进程时插入到过期队列中④. active指针永远指向活动队列expired指针永远指向过期队列⑤ 在合适的时候只要能够交换active指针和expired指针的内容就相当于有具有了⼀批新的活动进程尾终于写完了从两点半开始写到八点多思路的整理是个有趣的过程呢不过我现在真的要休息了明天还要五点半起床去备战奥拉星~~