第六部分:Linux进程初识 目录1、冯诺依曼体系2、操作系统3、进程3.1、描述进程的结构3.2、组织进程3.3、查看进程3.4、fork调用初识4、进程状态4.1、一般进程的状态4.2、Linux进程状态4.2.1、R状态4.2.2、S状态4.2.3、D状态4.2.4、T状态4.2.5、t状态4.2.6、X状态4.2.7、Z状态5、孤儿进程6、进程优先级6.1、查看6.2、修改6.3、原理6.4、补充1、冯诺依曼体系我们常见的计算机如笔记本。我们不常见的计算机如服务器大部分都遵守冯诺依曼体系。截至目前我们所认识的计算机都是有一个个的硬件组件组成的运算器对我们的数据进行计算算数运算和逻辑运算。控制器对计算机的硬件流程进行一定的控制。中央处理器CPU由控制器加运算器组成。输入设备例如键盘、鼠标、扫描仪、写板等。输出设备例如显示器、打印机等。注意磁盘本质上即是输入设备又是输出设备。网卡和磁盘类似是输入设备也是输出设备。外设一般就是指输入和输出设备。注意图中的这五个部分都是独立的各个硬件单元必须用线连接起来成为一个整体这个线可以分为两类分别是系统总线和IO总线合称总线。CPU和内存之间的的线是系统总线内存和外设之间的线是IO总线。关于冯诺依曼必须强调几点这里的存储器指的是内存不考虑缓存情况这里的CPU能且只能对内存进行读写不能访问外设 外设要输入或者输出数据也只能写入内存或者从内存中读取。 一句话所有设备都只能直接和内存打交道。上面的三级缓存和寄存器全部集成在现代CPU的内部。2、操作系统任何计算机系统都包含一个基本的程序集合称为操作系统(OS)。笼统的理解操作系统包括1、内核进程管理内存管理文件管理驱动管理。2、其他程序例如函数库shell程序等等。操作系统与硬件交互管理所有的软硬件资源目的是为用户程序提供一个良好的运行环境。在整个计算机软硬件架构中操作系统的定位是一款纯正的“搞管理”的软件。操作系统为了保证自己的数据安全同时也保证给用户提供服务操作系统以接口的方式给用户提供调用的入口来获取操作系统内部的数据。这个接口本质上就是一个函数所有访问操作系统的行为都只能通过系统调用完成。我们把基于系统接口的开发称为系统编程未来要讲到的主要是系统调用接口和用户操作接口、用户这三个部分上的开发。系统调用和库函数在开发角度操作系统对外会表现为一个整体但是会暴露自己的部分接口供上层开发使用这部分由操作系统提供的接口叫做系统调用。 系统调用在使用上功能比较基础对用户的要求相对也比较高所以有心的开发者可以对部分系统调用进行适度封装从而形成库有了库就很有利于更上层用户或者开发者进行二次开发。操作系统是通过先描述、再组织的方式管理计算机的。描述使用struct结构体进行描述组织则使用各种数据结构进行组织。3、进程直观概念程序的一个执行实例正在执行的程序等。内核概念担当分配系统资源CPU时间内存的实体。也有的把进程称之为任务。通俗的讲进程就是一个加载到内存中的程序叫做进程。一个操作系统不仅仅只有一个进程而是有很多进程所以操作系统必须把进程管理起来。3.1、描述进程的结构人辨别和认识一个事物或者对象都是通过属性辨别和认识的。进程属性就被放在一个叫做进程控制块的数据结构中可以理解为进程属性的集合。课本上称之为PCBprocess control blockLinux操作系统下的PCB是:task_struct。task_struct是Linux内核的一种数据结构它会被装载到RAM(内存)里并且包含着进程的信息。任何一个程序在加载到内存形成真正的进程时操作系统都要创建描述进程的结构体对象。进程常见的属性有1、标示符: 描述本进程的唯一标示符用来区别其他进程。2、状态: 任务状态退出代码退出信号等。3、优先级: 相对于其他进程的优先级。4、程序计数器: 程序中即将被执行的下一条指令的地址。5、内存指针: 包括程序代码和进程相关数据的指针还有和其他进程共享的内存块的指针6、上下文数据: 进程执行时处理器的寄存器中的数据。7、IO状态信息: 包括显示的I/O请求,分配给进程的IO设备和被进程使用的文件列表。8、记账信息: 可能包括处理器时间总和使用的时钟数总和时间限制记账号等。9、其他信息。这些都是比较常见的属性实际上task_struct结构体是非常大的里面的属性是非常多的。3.2、组织进程Linux内核中最基本的组织进程的方式就是采用双向链表组织的。因为有很多进程在操作系统中对进程进行管理就变成了对链表进行增删查改链表的每一个节点就是一个进程。此外想要做哪一方面的管理就把对应的进程数据结构对象放到某一个组织的数据结构中。注意所谓的对进程进行管理本质就是操作系统对内核PCB数据结构对象进行管理而不是直接管理的代码和数据。总的来讲进程内核PCB数据结构对象代码和数据。3.3、查看进程进程的信息可以通过 /proc 系统目录查看。例如ls /proc/proc中存放着系统中所有动态的进程信息。例如其中每一个蓝色的目录名都是进程的PID目录中存放了该进程的信息。我们可以查看目录中的内容例如下面的图片仅仅展示了部分信息。其中exe指向的就是该可执行程序cwd指向的是可执行程序的工作目录也就是该可执行程序执行时所处的目录。ps命令语法ps [选项]功能用来查看当前系统进程。常用选项-e显示所有进程。-x显示没有控制终端的进程。-u [username]显示某个用户的进程。-a显示所有的进程包括系统进程和用户进程。-f使用完整格式显示信息包括UID、PID、PPID等。-j显示进程组和会话信息。-l显示长格式包括更多的状态信息。例如ps ajx如我们要查看某个进程的信息例如ps ajx | head -1 ps ajx | grep test01查看test01进程的信息。中间的也可以使用 ; 代替。还可以这样写用来排除掉grep命令的进程信息例如ps ajx | head -1 ; ps ajx | grep test01 | grep -v grep我们可以编写一个小脚本来每隔一秒监视一次test01进程信息例如while :; do ps ajx | head -1 ; ps ajx | grep test01 | grep -v grep ; echo ------------------; sleep 1 ;done我们可以使用kill -9 PID的方式干掉进程关于kill后面再详细讲其中PID是进程的唯一标识符相当于进程的名字PPID是该进程的父进程的的PID。我们每次使用的指令常规指令的父进程就是bash进程。我们可以使用getpid()和getppid()接口获取进程的PID和PPID例如#includestdio.h #include sys/types.h #include unistd.h int main() { printf(pid:%d ppid:%d\n,getpid(),getppid()); return 0; }可以使用man命令来查看这两个接口的信息其中pid_t这个类型本质上是无符号整型。3.4、fork调用初识功能fork是用来创建子进程的#include unistd.h pid_t fork(void);成功的话给子进程返回0给父进程返回子进程的PID。失败的话给父进程返回-1。一般而言fork之后的代码父子共享当父或子进程不修改数据时数据也是共享的。但如果父或子进程对数据进行修改的话数据就不共享了而是各自有一份这是通过写时拷贝的方式实现的内核会给先写入的进程开辟新的空间用来写入然后另一方拥有原本共享的那份数据。所以子进程和父进程数据的独立性导致进程在运行时是具有独立性的。该接口有两个返回值是为了区分不同的进程。本质上fork函数内部返回部分的语句同属于父子进程所以才会看到有两个返回值的现象也就是返回值语句分别被父子进程各执行一次。一般fork之后的代码使用if进行分流。例如#include stdio.h #include sys/types.h #include unistd.h int main() { pid_t ret fork(); if (ret 0) { printf(i am parent:%d\n, getpid()); sleep(10); } else if (ret 0) { printf(i am child:%d\n, getpid()); sleep(5); } else { printf(error\n); } return 0; }注创建子进程成功后谁先运行由调度器决定所以子进程和父进程谁先运行是不确定的。4、进程状态每个进程都有一个叫做时间片的概念这个时间片就是该进程在CPU上运行的时间不管程序是否能够执行完耗尽时间片的时间进程就要从CPU上下来如果该进程执行完了那就结束了如果该进程没有执行完那就会排队等待再次执行。因此在一个时间段内所有的进程都会被执行。进程的调度就是通过进程切换和时间片实现的。一个CPU上的一个核只能执行一个程序因为进程的数量远多于CPU的核这样就会有大量的进程排队依次放到CPU上然后再拿下来我们把这种现象称为进程切换。因为CPU太快直观是感受不到这种进程切换的正因为CPU很快所以才会感觉多个进程似乎是同时执行的。一个CPU的一个核只有一个运行队列因此一个CPU的核只能执行一个程序CPU有几个核就有几个运行队列。4.1、一般进程的状态就绪进程已经加载到内存并且已具备执行条件但由于CPU资源的不足而无法立即执行。就绪队列中保存着所有在等待CPU的进程。运行运行状态的进程是指正在运行的进程或者在运行队列上的进程。以队列的方式对进程进行调度想要运行一个进程首先要把进程的数据结构对象链入到运行队列中进行排队。调度器其实就是函数可以将运行队列作为参数传递进来所以调度器就可以找到所有排队的进程进行调度。所有处于运行队列的进程以及正在运行的进程处于的状态就是运行态。运行态的含义就是进程已经准备好了随时都可以被调度。阻塞进程因为等待某个事件例如 I/O 操作完成而不能继续执行。也就是说进程无法继续执行直到它所等待的事件发生。在操作系统中底层会存在很多硬件在体系结构中所有的外设对于系统来讲无非就是从外设中读取到内存或者从内存写入到外设。操作系统对外设的管理也是先描述、再组织。每一个设备都会有等待队列在特定设备的等待队列上等待的进程就处于阻塞状态。例如以键盘为例当进程等待键盘输入时驱动程序读取键盘发现读到了数据后若发现没有数据就会让在等待队列上的进程一直等待此时就会将等待键盘设备的进程描述对象从键盘设备上拿下来然后进程进入就绪状态最后放入到运行队列中当该进程被调度时自然而然就可以从键盘中获取数据。注进程在等待外部设备时也就是处在等待队列时如果系统资源不足时操作系统会将一些处于等待队列的进程的代码和数据放入到外存中在内存中仅保留进程的PCB这种进程状态就处在下面要说的挂起状态。挂起挂起的进程是指由于某种原因被操作系统主动暂时停止执行的进程可以理解为一种更深层次的阻塞状态。挂起是指进程因操作系统的管理需求被主动暂停并从内存中移除或临时保留。进程的状态和所有信息会保存在磁盘等存储设备中。例如当内存资源不足时为了腾出一些空间会把一些进程的代码和数据放到磁盘中等需要的时候再拿出来。终止进程的执行已经完成已被系统释放相关资源。4.2、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 */ };4.2.1、R状态R运行状态并不意味着进程一定在运行中它表明进程要么是在运行中要么在运行队列里也就是在等待执行。例如注意R的意思是该程序在前台执行其中号就是指前台。R的意思是在后台运行若想要程序在后台执行可以使用的方式让可执行程序在后台执行例如./myproc 另外要说明的是后台执行的可执行程序是没办法使用ctrlc终止的要使用kill来干掉进程才行。4.2.2、S状态S睡眠状态是指一个进程由于等待某个事件的发生而暂时停止的状态这里的睡眠有时候也叫做可中断睡眠状态。可以响应请求可以被kill命令干掉。例如#include stdio.h int main() { int n 0; scanf(%d, n); printf(%d\n, n); return 0; }当执行该程序时运行到scanf函数时该进程就会进入S状态可用命令查看该进程的状态例如4.2.3、D状态D磁盘休眠状态有时候也叫不可中断睡眠状态在这个状态的进程通常会等待磁盘IO完成。这个状态通常是为了确保重要的操作有条不紊地完成。不响应请求不可以被kill命令干掉。4.2.4、T状态T停止状态可以通过发送信号给进程来停止进程。这个被暂停的进程可以通过发送信号让进程继续运行。关于信号后面再说。可以使用kill -19 or -18 PID的方式暂停或启用进程例如4.2.5、t状态t追踪停止状态指的是对进程的跟踪或调试操作的停止。例如在gdb调试可执行程序时在断点处停下来的时候进程就处于该状态。例如4.2.6、X状态X死亡状态这个状态只是一个返回状态我们不会看到这个状态。也就是释放进程占有的各种资源就相当于进程运行结束。该状态很难被看到因为太快了不再演示。4.2.7、Z状态Z僵尸状态是一个比较特殊的状态。当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵尸进程并且会一直在等待父进程读取退出状态代码。所以只要子进程退出父进程还在运行但父进程没有读取子进程状态子进程进入Z状态。一个进程结束时首先进入Z状态再进入X状态。例如#include stdio.h #include sys/types.h #include unistd.h int main() { pid_t id fork(); if (id 0) { printf(parent:%d\n, getpid()); sleep(100); } else if (id 0) { printf(child:%d\n, getpid()); sleep(10); } else { printf(error); } return 0; }等待几秒后查看该进程就可以观察到僵尸进程如下僵尸进程的危害:进程的退出状态必须要通知父进程因为他要告诉关心它的父进程你交给我的任务我办的怎么样了。可父进程如果一直不读取那子进程就一直处于Z状态退出状态本身就是需要用数据进行维护的也属于进程基本信息换句话说Z状态一直不退出PCB一直都要维护那如果一个父进程创建了很多子进程就是不回收就会造成内存资源的浪费因为数据结构对象本身就要占用内存最终就可能会造成内存泄漏关于如何解决这个问题后面再说。注当系统压力很大时系统可能会主动干掉一些系统认为不重要的进程。5、孤儿进程父进程先退出子进程就被称之为孤儿进程孤儿进程的父进程会被改为1号进程该进程就是操作系统也就相当于被一号进程领养了。之所以会这样是因为孤儿进程未来也会退出资源也是要被释放的。例如可以使用下面的代码验证#include stdio.h #include sys/types.h #include unistd.h int main() { pid_t id fork(); if (id 0) { printf(parent:%d\n, getpid()); sleep(10); } else if (id 0) { printf(child:%d\n, getpid()); sleep(100); } else { printf(error); } return 0; }运行结果为注进程之间的父子关系就构成了多叉树的结构在系统中组织进程的数据结构并不是单一的而是同时存在多种数据结构即一个进程同时被多个数据结构组织。 例如一个进程的数据结构对象既是双向链表中的结点也是多叉树的结点。6、进程优先级cpu资源分配的先后顺序就是指进程的优先级。 优先级高的进程有优先执行权利。6.1、查看在linux中用ps –l命令则会类似输出以下几个内容我们很容易注意到其中的几个重要信息有下UID : 代表执行者的身份 UID就是用户的编号可以使用ls -n来查看当前用户的编号即UIDPID : 这个进程的编号。PPID 代表这个进程是由哪个进程发展而来的即父进程的编号。PRI 代表这个进程可被执行的优先级其值越小越早被执行。NI 代表这个进程的nice值该值是进程优先级的修正数据。进程的默认优先级为80nice值的取值范围为-20到-19总共40个级别经过nice值的调整进程优先级的数值变化范围为60到99。总的来说PRI(new)PRI(old)nice这里需要注意的是PRI(old)永远为80而不是上一次的PRI(new)的值。需要强调一点的是进程的nice值不是进程的优先级他们不是一个概念但是进程nice值会影响到进程的优先级变化。可以理解nice值是进程优先级的修正数据。6.2、修改一般情况下不要改动进程的优先级。可以使用top来改变进程的优先级先输入top然后输入r然后输入进程的PID最后输入nice值即可如果要退出top点击q即可。此外还可以使用nice和renice调整优先级如果感兴趣可以自行查阅。例如把该进程的优先级改为了99。6.3、原理在运行队列中有一个isempty对应的比特位表示running中元素是否为空。run指向runningrunning这个数组中放的内容就是指向要运行的进程。wait指向waiting当在调度running中的进程时新来的要被调度的进程会放到waiting这个数组中当running中的进程调度完毕后交换run和wait的值然后继续进行调度。遍历running数组时整体从上到下从左到右。这就进程优先级的原理。6.4、补充竞争性系统进程数目众多而CPU资源只有少量甚至1个所以进程之间是具有竞争属性的。为了高效完成任务更合理竞争相关资源便具有了优先级。独立性多进程运行需要独享各种资源多进程运行期间互不干扰。并行多个进程在多个CPU下分别同时进行运行这称之为并行并发多个进程在一个CPU下采用进程切换的方式在一段时间之内让多个进程都得以推进称之为并发。CPU中是有很多寄存器的系统是可以通过程序计数器也是一个寄存器来得知进程当前执行到哪一行代码函数返回值之所以可以被外界拿到也是因为寄存器。进程高频的数据就会放入寄存器中CPU中寄存器保存的是与进程相关的临时数据也称为进程的上下文。进程在CPU上离开的时候要将自己的上下文数据保存好保存的目的是在未来可能要进行恢复。进程在被切换的时候1、保存上下文 2、恢复上下文。