在centos上做网站九四玩手游代理平台

当前位置: 首页 > news >正文

在centos上做网站,九四玩手游代理平台,wordpress前端接口,wordpress框架结构目录 什么是进程#xff1f; 浅谈进程排队 简述进程属性 进程属性之进程标识符 进程操作之进程创建 初识fork fork返回值 原理角度理解fork fork的应用 进程属性之进程状态 再谈进程排队 进程状态 运行状态 阻塞状态 挂起状态 Linux下的进程状态 “R”(运行状…目录 什么是进程 浅谈进程排队 简述进程属性 进程属性之进程标识符 进程操作之进程创建 初识fork fork返回值 原理角度理解fork fork的应用 进程属性之进程状态 再谈进程排队 进程状态 运行状态 阻塞状态 挂起状态 Linux下的进程状态 “R”(运行状态) “S”(浅度睡眠状态) “D”(深度睡眠状态) “T\t”(暂停状态) “Z”状态与“X”状态 什么是进程 如果要了解进程是什么我们得从程序开始说起 程序就是我们写的源代码进行编译和链接后形成的可执行程序在Windows下就是以.exe为后缀的文件 到这里我们了解了什么是程序而接下来要讨论的内容就是程序是如何运行的 首先从硬件的角度也就是冯诺依曼体系结构我们可以了解到一个数据从输入设备进入被运到CPU再到输出设备在这个中间为了效率考虑所以才会有了一个设备叫做内存这些我在往期博客进行详细说过感兴趣的可以看看  冯诺依曼体系结构 冯诺依曼体系结构设计了一个中间件叫做内存CPU不直接与外设打交道而是与内存打交道外设也不直接与CPU打交道而是直接与内存打交道而程序是一个文件那么接下来的问题就是文件放在哪 这个问题是显而易见的文件肯定是放在硬盘上的而硬盘是一个外设。 程序文件中是包含的一条条二进制的指令这个是一个常识那么既然是指令所以这个指令需要经过CPU的运算吗 答案也是肯定的 所以最后我们的结论出来了程序需要用到CPU所以它在运行的时候一定会被放在内存上这是由冯诺依曼体系结构决定的 接下来的问题是一个程序被运行放到内存上难道内存上只放一个程序吗换句话来说难道同一个时间只能运行一个程序吗 显然不是依据我们的常识可以知道可以有多个程序被放在内存如果内存中只能放一个程序的话那么当我们在运行QQ的时候就不能同时运行音乐软件但实际当中我们是可以的 而我们的操作系统的功能是要对下管理好软硬件资源既然现在有多个程序可以同时在一片内存当中运行所以现在操作系统肯定要对这些程序进行管理什么是管理​​​​​​ 而操作系统怎么管理这些程序呢 答案是先描述、再组织 以Linux操作系统为例Linux是用C语言写的那么C语言当中我们如何描述一个对象呢 实际上如果我们学过C语言就会了解struct也就是结构体类型可以对一个事物进行描述 就好比如果我们要用C语言进行描述一个人这种类型我们可以用结构体包含它的各种各样的属性 比如姓名、性别、年龄、身高、体重等等。。 struct Person {const char* _name;const char* _sex;int _age;//…. } 那么如果我们的Linux操作系统需要管理好这些程序肯定要描述这些程序Linux是用C语言写的所以描述我们可以用到结构体类型而描述在内存中的程序的结构体类型我们称为PCB PCBprocess-control-blco 进程控制块 PCB当中放着在内存中的程序的各种属性我们现在可以粗力度的理解为PCB就是描述程序的 当然PCB当中也一定会有可以找到对应程序的属性这个属性字段我们称为内存指针字段这个内存指针是指向PCB所对应的在内存中的程序的 当有了PCB以后操作系统只需要查看PCB的属性字段就可以知道你这个PCB所对应的程序的属性而不需要跑去看程序本身 所谓的PCB其实是放之四海而皆准也就是Windows当中的描述在内存中的程序的结构体我们可以称为PCBLinux的也可以 但如果具体到Linux操作系统它的PCB具体实现命名为task_struct 现在我们已经可以描述一个程序了接下来我们需要聊聊如何组织的问题 所谓的组织也就是当我们有了很多很多程序的PCB之后如何管理好它们换句话来说就是如果PCB需要删除PCB需要增加、需要查找某个PCB我们如何更方便的实现 其实也就是所谓的数据结构。 在Linux当中我们把一个一个的task_struct看成是一个结点task_struct中包含一个next指针指向下一个task_struct形成一个task_struct的链表此时当我们程序运行结束之后从内存中放回硬盘那么我们只需要对这个链表结点进行删除结点即可如果我们有一个新的程序来到内存创建好task_struct之后我们把这个task_struct插入链表即可 此时操作系统就把对程序的管理变成了对程序所对应PCB对象的管理 到这我们也终于可以聊聊这个标题即什么是进程 所谓的进程不是指的运行期间被放在内存的程序也不是指的PCB(Linuxtask_struct) 而是运行期间被放在内存的程序及其所对应的PCB所组成的一个整体我们称之为进程 当然由于操作系统对PCB的管理是对数据结构的管理所以我们可以更进一步得出如下结论 进程 内核数据结构 可执行程序 在上述中我们谈到了task_struct是描述程序的属性这其实是比较粗浅的理解其实它不仅仅包含程序的属性其实它包含了进程的几乎所有属性 浅谈进程排队 在上述的内容中我们了解到了进程到底是什么并且也了解到了PCB的概念以及操作系统如何管理PCB接下来我们谈一谈所谓的进程排队 一个进程如果需要调度CPU那么CPU肯定只能被一个进程调度因为CPU只有一个哪怕它再快也不能同时处理多个进程所以操作系统会在内存中添加一个运行队列的概念 当一个进程需要调度CPU的时候操作系统会把它所对应的PCB添加到运行队列中我们就假设它是拷贝 若此时再有进程需要调度CPU那么就需要在运行队列中排队所以所谓的进程排队指的是进程的PCB去排队而不是进程的可执行程序去排队 所以我们也能看出所有的对进程的控制和操作都只和进程的PCB有关和进程的可执行程序无关 现在我们已经对进程有了一定的了解了接下来我们将分两部分进行学习 1、PCB内部中的属性信息 2、PCB作为一个整体如何被我们使用 简述进程属性 注意进程属性非常非常多我们无法全部学习只能挑一部分比较重要的内容 在我们详细学习进程属性的之前我们先要知道进程属性到底有哪些做到心中有数 进程属性主要内容 1、标识符描述本进程的唯一标识符用来区分其他进程 2、状态任务状态退出代码退出信号等 3、优先级相对于其他进程的优先级 关于第三点我可以先说说在之前的学习中我们了解过进程排队所谓的优先级就是进程排队时的前后顺序 4、程序计数器程序中即将被执行的下一条指令的地址 关于第四点我可以现在粗力度解释一下接下来我要问一个问题 当你写了一个C代码之后为什么计算机会知道你执行到第几行了呢 实际上在CPU内部存在着一个寄存器eip这个寄存器也叫做pc指针或程序计数器或指令寄存器 当我们写代码的时候一行一行的语句每一个语句都有它所对应的地址 而CPU能完成的工作主要是三个首先取指令取到指令以后分析指令分析完指令之后执行指令而当执行完指令之后就再去取指令循环往复 但此时问题就来了CPU去哪去取指令呢 我们说过cpu内部有一个pc指针这个pc指针实际上存储的就是当前进程正在执行的指令的下一条指令的地址当CPU需要取指令的时候就会从pc指针中取并且更新pc指针使它指向下一条语句 所以此时我们就了解到所谓的循环、判断、函数跳转本质是修改pc指针 5、内存指针包括程序代码和进程相关数据的指针还有和其他进程共享的内存块的指针 对于第五点其实我们之前聊到过当操作系统对进程进行管理的时候管理的其实是进程的PCB但PCB并不是我们需要执行的程序为了帮助我们拿到进程所对应的可执行程序所以才有了这个内存指针 6、上下文数据进程执行时处理器的寄存器中的数据 7、I /O状态信息包含显示I/O请求分配给进程的I/O设备和被进程使用的文件列表 8、记账信息可能包括处理器时间总和使用的时钟数总和时间限制记账号等。 进程属性之进程标识符 所谓的进程标识符其实正如它的名字一样是用来标识一个进程的并且这个进程标识符在操作系统中具有唯一性也就是每一个进程它的进程标识符都不同 就好比人来说为了区分你和其他人那么你肯定是有一个唯一标识你这个人的属性的在现实生活中这个标识就是身份证号码而对于操作系统来说操作系统要管理进程那么肯定要区分不同的进程所以就有了进程标识符 而在Linux操作系统中这个进程标识符我们称为pid 查看进程标识符getpid()和getppid()系统调用 进程标识符我们可以查看但要使用系统调用接口 getpid的参数不用写而getpid的返回值是哪一个进程调用的getpid就返回哪个进程的pid返回值我们可以当成是C语言中的int 如下为demo代码 #include stdio.h#include unistd.hint main() {printf(进程运行pid:%d\n,getpid());while(1);return 0;} 我们知道了它的pid的话现在就可以通过指令查看这个pid对应的可执行程序是否是我们运行的 指令ps axj axj是选项我们暂时先不管只管用就可以了 当我们输入这个指令后可以查找到我们当前运行的所有进程的属性信息 可以看到有很多很多的信息打印出来但这些属性信息我们不知道它是什么但首行有标题可以通过管道打印出首行ps axj | head -1 我们还可以通过grep工具筛选出我们所要的行 上述我们的程序在运行的时候的pid是5044 所以我们可以执行ps axj | head -1 ps axj | grep 5044 可以看到确实是我们运行的进程的pid 当然除了可以这样查看实际上在我们进程被运行期间操作系统会在/proc目录下创建它的pid所对应的目录文件这个文件当中包含了pid所对应进程的属性信息 接下来我补充一个小内容就是其他属性信息我们暂时不管先看这个cwd和exe exe我们比较好理解其实就是我们所对应的可执行程序的路径 主要我们来看看cwdcwd其实是当前进程工作目录 这个有什么用呢我举一个小例子 当我们在学习C语言的fopen函数的时候会经常看到一句话fopen函数如果在当前目录下没有找到这个名字的文件则它会自动创建这个文件其实fopen所说的当前路径也就是这个cwd 这个路径我们是可以更改的 更改当前进程工作路径系统调用chdir() chdir的参数只有一个常量字符串就是我们要把当前路径cwd改成什么路径并且这个系统调用需要包含头文件unistd.h 关于它的返回值如果为0更改成功如果为-1更改失败。 如下为demo代码 #include stdio.h #include unistd.hint main() {const char* path /home/yyf;printf(更改当前路径);chdir(path);FILE* fp fopen(test.txt,w);fclose(fp);return 0; }这个程序我期待的运行结果是它运行完以后在“/home/yyf”目录下创建一个test.txt文件 可以看到运行结果符合我的预期说明如果修改了当前目录cwd那么当使用fopen函数打开创建文件时就会在你修改的目录下查找是否有如果没有则在这个目录下创建一个文件而我这里是以fopen为例其他所有会在当前目录自动创建文件的函数也是同理 前面介绍了getpid系统调用接下来介绍一下getppid系统调用 getpid和getppid是很像的都是获得进程pid但是getpid是获得执行getpid的进程的pid 而getppid是获得执行getppid的进程的父进程pid getppid系统调用的返回值是当前进程的父进程的pid 所谓的父进程就是指的是创建当前进程的进程这一部分在我的Linux基础博客中有涉及父进程(Shell外壳章节) 如下为demo代码 #include stdio.h #include unistd.hint main() {printf(I am child-process pid:%d,My father-process pid:%d\n,getpid(),getppid());return 0; }我们可以发现确实可以获得父进程的pid但是为什么父进程的pid每一次执行的时候都一样为什么子进程的pid每一次执行的时候都不一样呢 要想了解这个我们先去看看我们上面运行的进程所对应的父进程是什么东西 可以发现我们运行的进程的父进程其实是bash也就是我们熟知的命令行解释器 所以如果我们不重启命令行解释器也就是shell那么我们在命令行中使用./的方式创建的进程的父进程的pid是不会变的   进程操作之进程创建 在上述内容中我们了解到了一种进程创建方式即通过命令行创建并且这种创建出来的进程它的父进程一定是bash。 那么我们通过命令行创建的子进程是否可以再创建出子进程呢 换句话说我现在是父亲我的儿子是否还可以有儿子呢 这是肯定的Linux中提供了系统调用fork函数可以创建子进程 初识fork fork函数没有参数fork函数的功能是创建一个子进程 fork函数需要包含unistd.h  接下来我们先看现象如下demo代码 #include stdio.h #include unistd.h #include sys/types.hint main() {printf(before fork : I am a process pid:%d,ppid:%d\n,getpid(),getppid());fork();printf(after fork : I am a process pid:%d,ppid:%d\n,getpid(),getppid());sleep(1);return 0; } 运行结果 before fork : I am a process pid:6670,ppid:2312 after fork : I am a process pid:6670,ppid:2312 after fork : I am a process pid:6671,ppid:6670 从上面的demo代码中我们可以了解到fork执行前的代码只执行了一次而fork之后的代码执行了两次这说明有两个进程都执行了这个after fork这说明创建子进程成功 为什么说创建子进程成功呢 我们注意观察pid和ppid fork之后我们会发现有一个进程的ppid就是一个进程pid说明一个是父进程一个是子进程 并且在上述运行结果中fork之前的一个进程的pid是6670fork之后有一个进程的pid也是6670这说明父进程是fork之前的进程而子进程是由fork之前的进程创建出来的子进程 总之上述所得的结论是fork之后代码共享 fork返回值 接下来我们聊一聊fork的返回值问题 在man手册中对于fork的返回值介绍如下 pid_t fork(void); RETURN VALUE:        On success, the PID of the child process is returned in the parent, and 0 is returned in the  child.           On  failure,  -1  is returned in the parent, no child process is created, and errno is set appropriately. 上述的意思是 fork如果创建子进程成功那么就把子进程的pid返回给父进程且把0返回给子进程 fork如果创建子进程失败那么就把-1返回给父进程且没有子进程被创建且错误码被设置 通过上述介绍难道它想说的是fork有两个返回值吗我们先看demo代码 int main() {printf(before fork : I am a process pid:%d,ppid:%d\n,getpid(),getppid());pid_t id fork();printf(after fork : I am a process pid:%d,ppid:%d,return val:%d\n,getpid(),getppid(),id);sleep(1);return 0; }运行结果 before fork : I am a process pid:9549,ppid:5710 after fork : I am a process pid:9549,ppid:5710,return val:9550 after fork : I am a process pid:9550,ppid:9549,return val:0 首先上述代码中打印after fork执行了两遍分别是父子进程执行 打印的第二个after fork是子进程第一个after fork是父进程原因是第二个after fork的ppid和第一个after fork的pid相同 第一个after fork的返回值是第二个after fork的pid 第二个after fork的返回值是0 说明对于父子进程来说它们的id是不相同的而id是fork的返回值说明fork的返回值对于父子进程来说不相同父进程拿到的返回值是子进程的pid子进程拿到的返回值是0 从实践中我们看到好像fork确实是有两个返回值的 接下来关于fork的返回值我们需要了解三个问题 1、为什么给父进程返回子进程的pid给子进程返回0呢难道不可以给子进程也返回父进程的pid吗 要搞清楚这个问题我们得搞清楚父子进程得关系是什么 首先父进程创建了子进程这没问题。那么父进程可以创建多个子进程吗 不多说这显然是可以的只需要让父进程多次调用fork就行了嘛。那么父进程如何分清多个子进程呢如果fork的返回值不是子进程的pid的话显然是无法区分的所以fork会给父进程返回子进程的pid。 其次子进程是被一个父进程创建的这没问题。那么子进程可以被多个父进程创建吗 这显然是不行的对比我们现实生活中不会有一个人有多个亲生父亲。 那么既然子进程只有一个父进程那么子进程要找到这个唯一的父进程好找吗显然很好找直接getppid就可以找到父进程。既然好找那么对于子进程来说就不需要有父进程的pid如果给子进程返回父进程的pid那么就是多余的操作。 最终的结论就是父子进程是一对多的关系。父进程无法区分多个子进程所以需要从返回值中获取子进程的pid子进程可以区分自己的父进程所以不需要从返回值中获取父进程的pid 2、fork函数为什么会返回两次 我们都知道fork函数的功能是创建子进程那么接下来的问题是fork是先创建的子进程还是先返回的呢 这个问题的答案相信不难理解肯定是先创建的子进程再进行的返回那么当fork函数return的时候肯定就会执行两次因为return也是语句父子进程都会执行这个语句 3、上述代码id为什么既等于0又大于0 首先要完全理解这一点现阶段是比较难的我们可以初步理解一下 从需求的角度看我们需不需要进程之间会互相影响呢 如果要那么今天我们登录音乐软件、聊天软件当我们把音乐软件的进程杀掉以后聊天软件的进程就也被杀掉了。从常识的角度这显然是不合理的。 所以对于父子进程来说也是如此父进程的改变不能影响子进程的改变。 那么操作系统设计的时候就要考虑这一点进行设计至于怎么设计的我们在下文中说 原理角度理解fork 首先我们说过进程 内核数据结构 可执行程序 那么当fork创建子进程的时候就一定会再创建一个PCB用于描述这个子进程 而现在问题来了我们都说进程内核数据结构可执行程序但现在我们只有一个可执行程序且这个可执行程序是父进程的。那么我们的子进程就没有他的可执行程序那他还是一个进程吗 实际上子进程其实是有可执行程序的他其实与父进程共享并且共享的是父进程创建出子进程之后的代码 接下来的问题是PCB中存放的是用来描述进程的属性那么子进程的PCB中放的属性从哪来呢 实际上既然子进程与父进程共享一段代码那么子进程大部分属性都会以父进程为模板但并不是全部都以父进程为模板有一些属性是子进程自己的比如pid等 最后一个问题还是fork返回值的问题为什么fork的返回值可以为两个不同的值 实际上为了实现父子进程互不影响那么在对变量的存储地址的值进行修改的时候操作系统会重新生成一个不同的内存空间用于子进程对这个变量的修改。这其实是一种写时拷贝。所以从表面上看是同一个变量实际上父子进程写入的不是同一块内存空间。 而返回的本质就是写入。所以才会发生写时拷贝 至于怎么做到同一个变量名不同内存空间。我们需要到虚拟内存时才能了解 fork的应用 在实际使用中我们不会像上面代码那样使用因为创建子进程都是为了让子进程干活的并且一般情况都是父子进程执行不同的逻辑。也就是如下使用 int main() {pid_t id fork();if(id -1){//失败return 1;}else if(id 0){//子进程执行的代码块//…}else {//父进程执行的代码块//…}return 0; } 进程属性之进程状态 再谈进程排队 在前面我们说过进程排队就是把进程的PCB放到CPU的运行队列中进程排队指的是进程的PCB进行排队而不是进程的可执行程序去排队 而接下来我们要聊的是Linux内核下的进程排队实现原理 之前我们说过进程排队是从PCB链表中拷贝结点然后把结点插入到运行队列中 但Linux下的task_struct不是按照这种方式设计的它是在task_struct中专门有一个属性表示运行队列的结点假设运行队列的结点它定义为queueNode如下图 可以看到实际上运行队列的结点被内嵌到了task_struct中 那么接下来的问题是我的task_struct中有队列的结点但我不是要队列结点的信息而是要队列结点所在的task_struct的属性信息如何做到呢 首先要拿到属性信息我们得找到这个task_struct的开头是在哪在开头进行解引用就能找到信息了 我们可以把0号地址当成是一个task_struct的开头然后取出这个里面queueNode所在的地址最后把这个地址减去0号地址就得到了图上的大小转化成代码也就是如下 ((task_struct)0-n)  而我们有了这个值就可以找到所对应的task_struct的开头 假设我这个队列的结点是 q - ((task_struct)0-n)  那么接下来的问题是这样设计的好处是什么呢我直接把整个PCB当成一个结点不好吗 这样设计的好处可以使一个PCB不仅仅是链表结点还可以是队列结点 不管它是什么结点只要算出这个task_struct的开头我就都能进行管理 实际上队列的底层我们就可以用链表实现此时不管是队列结点还是链表结点它们都共用一套增删查改的机制 进程状态 要搞清楚进程状态我们先理解状态决定了什么 实际上状态本质上决定了你后续的动作 就好比我们日常生活中可能会说或听到一些话语我今天学习状态不太好我想休息会。 我今天工作状态不好我想请假 灵感来自于生活显然对于进程来说操作系统会根据进程的状态决定它们的后续动作 大体上对于进程来说状态分为运行状态、阻塞状态、挂起状态 补充 我们之前说过进程排队的概念接下来聊一聊为什么要有进程排队 实际上在大多数的电脑中CPU的数量是比较少的而进程相比CPU则多的多CPU少进程多这其实就是导致了进程排队的原因所以本质上进程排队就是进程为了得到某种资源这种资源可以是硬件资源也可以是软件资源 并且我们需要知道CPU被进程调度不是一直被调度到这个进程结束的而是一个进程调度了一段时间后CPU会把它重新插入运行队列CPU再被运行队列中靠前的调度 上述的一段时间我们称为时间片 运行状态 经过前面的铺垫关于运行状态其实已经可以一两句话概括了 所谓的运行状态就是指一个进程被放到CPU的运行队列中等待CPU调度时的状态 注意这种状态不是只表示进程正在被CPU调度在运行队列等待也是这种状态 阻塞状态 依据我们的常识可以知道计算机中有着各种各样的硬件比如硬盘、网卡、键盘等等 那么操作系统是如何管理这些硬件的呢 答案是先描述、再组织 而既然要描述且这是一个抽象事物所以就肯定要用类或结构体来描述 如下demo代码 struct device {int type; // 1 键盘 , 2 网卡 , 3 显示器 ….//设备的操作方法struct device* next;//假设单链表} 那么device中可以有队列吗显然可以你的task_struct中都有队列我跟你并无区别 OK最后一个问题CPU是硬件吗 很显然依据我们的常识也知道CPU是硬件。 既然是硬件所以操作系统对他的描述也是device 其实在某些时候在运行队列中的进程可能需要等待某种资源就好比键盘资源(scanf) 而此时如果让他继续在运行队列中跑那么其实是一种浪费由于可能有多个进程都需要调用键盘资源而键盘只有一个所以键盘也有自己所对应的队列实际上每个设备都有自己的队列操作系统就把这个需要键盘资源的进程从CPU的队列放到了键盘的队列而此时这个进程的状态就是阻塞状态而当键盘资源已经就绪了操作系统作为硬件的管理者会第一时间知道此时操作系统就会把这个进程从键盘的队列中放回CPU的队列 对于需要等待除CPU以外的软硬件资源的进程我们称为阻塞 挂起状态 首先出现这个状态有一个前提那就是我们内存此时已经比较少了 当操作系统发现内存是比较少的时候此时对于它来说会出现两种处理方式 第一种就是直接崩溃第二种是想办法让内存的空间变得多一点 而操作系统肯定是选择后者那么如何让内存变得多一点呢 操作系统就把目光放在了一些处于阻塞状态的进程这种进程都是为了等待某种资源 但此时它还没有得到这种资源还无法运行那么对于操作系统来说它们此时是对内存空间的浪费于是操作系统就把它们的状态改为挂起状态并且把它们所对应的代码和数据换到了磁盘中 接下来的问题是操作系统把他们的代码和数据放在了磁盘的哪里呢 实际上我们的磁盘上会有一个分区叫swap分区这个是专门给操作系统预留的一片空间而操作系统就把挂起状态的进程的代码和数据放在了这个分区中 而操作系统把挂起状态进程的代码和数据放到swap分区的这一行为我们称为唤出   当内存资源已经稍微好转了或者处于挂起状态的进程需要的资源已经就绪了那么操作系统会把它们从swap分区中再次移动到内存这一行为我们称为唤入 需要注意两点 1、操作系统是把处于阻塞状态的进程的数据和代码放到swap分区它们的PCB并没有被放到swap分区原因是操作系统需要对PCB进行管理如果把PCB放到外设上就无法进行管理了。 补充实际上当进程刚开始创建的时候也是先创建PCB再把可执行程序放到内存也是因为操作系统要先对这个进程做管理就好比玩一些大型游戏的时候虽然这个游戏体积非常大例如80G左右它的体积远远大于我们的内存空间大小但我们也能流畅的玩。实际上是这个游戏进程进到内存的时候只会加载一部分而剩下加载多少内容则放到了进程的PCB当中。所以先创建PCB也是必要的。 2、以上的挂起我们称为阻塞挂起实际上挂起的种类非常多还有运行挂起、创建挂起等实际上挂起与内存资源息息相关换句话来说把操作系统逼急了它什么资源都能唤入。且挂起状态我们并不常见了解一下就好 以上我们讲解了运行、阻塞、挂起状态接下来从教科书的角度再理解一下进程状态 创建状态就是进程的PCB刚被创建出来没有被CPU加载到运行队列中 就绪状态就是这个进程已经被加载到运行队列中正在排队等待调度CPU 执行状态就是这个进程正在使用CPU资源 注意现在主流的操作系统都没有对就绪和执行状态进行区分都把这两个状态进行二合一成运行状态 阻塞状态由于这个进程需要等待其他的软硬件资源才能继续运行所以这个进程被操作系统拿下来放到其他资源的等待队列中 终止状态这个进程的代码已经执行完毕 以上我们说了各种各样的进程状态但实际上上述的都是操作系统学科的进程状态分类换句话来说上述的这些状态在各种操作系统中都符合。 但我们作为一个学习Linux的来说我们肯定要了解一下具体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 */ }; 我们发现在Linux中状态一共分为7种而我们将会对这7种进行介绍 “R”(运行状态) 运行状态其实也就是在CPU运行队列的状态这一点在Linux下也如此 如下为demo代码 #include stdio.hint main() {while(1);return 0; }我们把它运行起来后ps查询一下这个进程  查询结果 [yyfVM-24-5-centos 24.05.24]\( ps axj | head -1 ps axj | grep test  PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND 21990 23848 23848 21990 pts/0    23848 R    1001   0:40 ./test 23733 24093 24092 23733 pts/1    24092 R    1001   0:00 grep --colorauto test 我们看上述STAT对应的就发现test(我所运行的进程)确实为R状态但它后面有个是什么意思呢 实际上如果一个进程状态后面有号表示这个进程在前台运行如果一个进程后面没有号表示这个进程在后台运行当一个进程处于前台运行时我们无法在命令行中使用指令。当一个进程处于后台运行时我们可以在命令行中使用指令。我们暂时只需要知道在后台运行的进程不能用ctrlc的方式结束掉必须使用kill -9 pid的方式进行结束 并且在查询结果中我们会发现grep进程也是R状态这是因为其实grep指令本身就是一个进程而当我们使用grep指令进行过滤的时候他只有处于运行状态才能进行过滤 “S”(浅度睡眠状态) 睡眠状态就是一个进程不在运行队列中处于某个等待队列当中这个进程在等待某种资源实际上睡眠状态也就是阻塞状态的一种 如下demo代码 #include stdio.h #include unistd.h int main() {while(1){printf(hello,process\n);sleep(1);}return 0; }查询结果 [yyfVM-24-5-centos 24.05.25]\) ps axj | head -1 ps axj | grep test  PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND  8646 10083 10083  8646 pts/0    10083 S    1001   0:00 ./test 10153 10359 10358 10153 pts/1    10358 S    1001   0:00 grep –colorauto test 可以看到这个进程我们进行查询时大概率都是S状态有非常非常小的概率是在R状态  这是因为这个进程大部分时间都在等待sleep函数执行完成而且就算我们把sleep注释掉它的状态也大概率是S状态因为printf是要在显示器上进行打印但CPU的速度可比设备快多了所以这个进程大概率都在外设的等待队列中进行排队等待 并且这个状态的进程我们可以通过ctrlc进行终止进程所以我们也把S状态称为可中断睡眠 而既然有可中断睡眠与之对应的就是不可中断睡眠接下来我们开始介绍不可中断睡眠 “D”(深度睡眠状态) 要想了解这个状态首先需要搞懂为什么要有这个状态接下来讲一个案例 注意操作系统的内存严重不足时可能会杀掉进程 首先进程在磁盘上写入数据的时候它肯定是让磁盘自己去写那么当磁盘去写的时候进程在干嘛呢假设没有D状态的话它应该是S状态也就是它正在等待磁盘的处理结果嘛。而此时呢假设内存资源已经严重不足了于是操作系统就需要去内存中找出空间来而此时操作系统检测到了这个处于S状态的进程 操作系统一看它还在这闲着占用着内存又并不工作所以操作系统就把这个进程给杀掉了。但磁盘在写入的时候也发现自己的磁盘空间不足了写入失败于是磁盘拿着这个结果去给进程。但此时进程已经挂掉了磁盘没办法把结果给他且这100M的数据还写入失败了磁盘不知道怎么处理就把这个数据丢了。而用户没有接收到任何的结果以为成功了。所以用户就去磁盘上找这个数据发现这个数据被丢了。并且这个数据还及其重要。所以用户一下子就懵了。 而在上述案例中实际上每一个角色干的事都是合理的 对于操作系统来说在内存严重不足的情况下可以杀掉进程这本来就是它被赋予的权利 对于进程来说进程是一个受害者它履行了它的责任也是合理的 对于硬盘来说硬盘实际上是一个搬运工进程让他干嘛就干嘛与他也没什么关系 每个角色做的事都是合理的但导致的结果却不合理问题的关键就出在操作系统杀掉进程这一步 而Linux设计者为了这方面考虑添加了一个D状态若一个进程是D状态则说明它此时的睡眠等待的资源是相当重要的例如写入磁盘等操作系统不能杀掉他。若一个进程是S状态那说明它的睡眠等待的资源不是很重要例如输入输出等。而上述案例中如果进程被设置为D状态则不会出现这种结果这也解释了为什么Linux下的进程需要有D状态 注意D状态也是阻塞状态因为它也是在等待某种资源 “T\t”(暂停状态) 暂停状态是让进程暂时停下来 对于暂停状态Linux内核中主要分为两种“T”和“t” 我们首先介绍“t”  这一种状态其实是当前进程处于被追踪的时候的暂停就比如当我们用gdb进行调试时这个进程运行到断点处它的状态就会被设置为这个 对于“T”其实它的状态也是一种暂停这个状态是当进程执行一些比较危险的指令或者操作时操作系统会把它设置为这个状态并把进程暂停处理 实际上暂停状态也是为了等待某种资源当gdb调试的时候的t是等待gdb的下一个控制指令发出而T是等待操作系统对这个进程的指令所以暂停状态本质也是阻塞状态 “Z”状态与“X”状态 首先我们先聊Z状态 在刑侦片中经常会出现的一个片段是一个人突然之间发生某种意外倒地不起了而当警察来了之后是不是直接就把尸体直接抬走进行人道处理呢显然不是的警察来到现场以后首先会判断一下这个人是不是真的死了如果这个人真的死了那么警察还会保留现场再调查一下这个死者的死因以及一些后续处理最后才会把这个尸体抬走进行人道处理。 而上述处理是针对一个人死亡时的处理那么对于进程来说呢 首先对于进程来说它的功能就是处理某些工作也就是执行它的代码和数据对于进程来说死亡就相当于它把代码和数据都执行完了但同样的对于这个进程执行完毕进行退出之后还需要知道它死亡(退出)时的情况就比如你是正常退出的还是异常退出的而接收这个情况的对象就是这个进程的父进程父进程就相当于法医。而这个进程的退出情况需要被保存那么保存在哪呢其实也就是保存在这个进程的PCB里而如果父进程还没有读到子进程的退出信息时这个进程就会被操作系统设置为Z状态这个状态也称为僵尸状态 也就是说父进程一定要知道子进程的进程状态如果它处于僵尸状态说明父进程就需要读它的退出信息所以子进程的PCB在退出时不会立马销毁他还需要保存自己的退出信息只有当父进程读完了子进程的退出信息后这个子进程的PCB才能销毁 换句话来说也就是如果父进程一直不读取子进程的退出信息那么子进程是不是一直是僵尸状态呢是的基于这一点我们写出如下demo代码 #include stdio.h #include unistd.h #include stdlib.h int main() {pid_t id fork();if(id 0){//子进程printf(start:child\n);sleep(5);printf(end:child\n);exit(0);}//父进程sleep(50);return 0; }上述代码当打印出end:child之前子进程一直处于睡眠状态当打印完end:child之后由于子进程是直接退出的且父进程没有读取这个子进程的退出码所以它的状态是Z [yyfVM-24-5-centos 24.05.25]\( ps axj | head -1 ps axj | grep test  PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND  8646  9497  9497  8646 pts/0     9497 S    1001   0:00 ./test  9497  9498  9497  8646 pts/0     9497 Z    1001   0:00 [test] defunct 而至于父进程如何读取子进程的退出信息这一点我们在之后的文章会说 注意处于僵尸状态的进程如果不被读取也就意味着它的PCB会一直存在此时会造成内存泄漏 接下来聊一聊X状态 这个状态是真正的死亡状态也就是当父进程读取完处于僵尸状态的子进程后这个子进程就是处于这个状态  而对于上述的内容都是子进程先死亡的情况那如果是父进程先死亡呢于是基于这一点我们得聊聊一个新的补充概念孤儿进程 补充孤儿进程 首先我将设计一个场景就是父进程创建了子进程然后父进程先退出然后我们看看子进程的情况 #include stdio.h #include unistd.h #include stdlib.hint main() {pid_t id fork();if(id 0){//child int cnt 10;while(cnt--){printf(I am child,pid:%d ppid:%d\n,getpid(),getppid());sleep(1);}//子进程运行10s}//fathersleep(3);//父进程运行3s后退出exit(0);return 0; }[yyfVM-24-5-centos 24.05.25]\) ./test  I am child,pid:18950 ppid:18949 I am child,pid:18950 ppid:18949 I am child,pid:18950 ppid:18949 [yyfVM-24-5-centos 24.05.25]$ I am child,pid:18950 ppid:1 I am child,pid:18950 ppid:1 I am child,pid:18950 ppid:1 I am child,pid:18950 ppid:1 I am child,pid:18950 ppid:1 I am child,pid:18950 ppid:1 I am child,pid:18950 ppid:1 可以看到当父进程死亡后子进程会被1号进程领养而1号进程就是操作系统 当这个子进程的父进程死亡时我们称这个进程是孤儿进程  至此本篇内容结束对于进程属性还有许多的内容但由于篇幅有限所以下篇博客我们再继续介绍希望多多三连