【Linux】深入理解Linux的进程(一)

📅 2026/7/16 19:18:13
【Linux】深入理解Linux的进程(一)
【Linux】深入理解Linux的进程一学校OS书本讲的Linux的进程太哲学。本文将解释子进程根据指令、代码、Linux内核源码剖析Linux的进程状态包你听懂。另外一提本文命令都在云服务器上运行先前我在WSL环境下进行测试发现有部分指令和在传统Linux运行结果不同比如孤儿进程的处理。还是很坑人的这方面所以我也推荐大家现在云服务器或者本机虚拟机、物理机上进行学习冯诺依曼结构在开始前先讲解以下冯诺依曼的计算机结构冯诺依曼结构十分伟大走出了计算机进入家家户户的重要一步另外冯诺依曼结构有助于理解CPU和内存、外设之间的关系所以这里也会讲解一遍。一个计算机是怎么接收数据然后进行处理、发送出去的呢外部信息先通过输入设备把数据传入到内存中然后由CPU对数据进行解密、处理再把数据加密后交给内存内存把数据交给输出设备输出设备把数据传到网络或者显示器等地方。冯诺依曼结构十分厉害的一点就是把内存作为核心都和内存打交道而不是直接把信息交给CPU内存成为CPU的一个缓冲区。进程描述学校讲的PCB完成进程描述功能供CPU找到进程地址进行进程调度我这里会重新讲你不会也无所谓本文将其具象化。​ 在进入主题前我们可以思考一下——如果对一个图书馆的书籍进行管理应该怎么管理保障不会乱。​ 我们可以赋予书籍不同的属性比如一本书可以按题材分类也可以按主旨分类。然后把相同属性的书籍放在一起。放在计算机里我们就需要用到struct对每本书的属性进行记录然后在管理的时候我们可以用到数据结构与算法的知识了。你可以顺序表可以链表可以哈希表可以选择最适合完成业务需求的数据结构完成管理。​ 上述步骤就是先描述指创建结构体对属性进行记录的过程再组织将结构体封装到数据机构中完成管理的过程。​ 事实上你看到这里就已经掌握了计算机管理大部分东西的方法了。我们开始举例说明——你的计算机能够检测到你外设的状态这就用到了先描述再组织操作系统把不同的外设封装成不同的struct然后进行管理。下文提到的进程也是如此。PCB——task_struct​ 我们看一下Linux的进程描述字段部分源码structtask_struct{volatilelongstate;/* -1 unrunnable, 0 runnable, 0 stopped */void*stack;atomic_tusage;unsignedintflags;/* per process flags, defined below */unsignedintptrace;intlock_depth;/* BKL lock depth */structlist_headtasks;structplist_nodepushable_tasks;structmm_struct*mm,*active_mm;pid_tpid;pid_ttgid;structlist_headptraced;structlist_headptrace_entry;};通过源码我们可以知道所谓的PCB进程属性描述字段在Linux下就是task_struct这个结构体描述了进程的所在文件位置、虚拟地址空间分配、文件打开的描述等等关键信息。注意struct list_head ptraced;这个list_head本质就是封装了两个指针的结构体而task_struct有很多list_head。他有什么用呢Linux的task_struct有很多个指针这样可以把一个task_struct放在不同的数据结构、不同的链表、队列里了。而通过偏移量就可以准确找到想要找的元素位置——((task_struct*)0-link1)找到link1的偏移量。(struct task_struct*)((char*)next - x)找到下一个task_struct起始位置地址。这样在不同数据结构下都可以完美找到下一个/上一个进程描述字段在哪了。加载到内存我们写程序的过程是向磁盘写内容的过程那么怎么运行我们写的程序呢结合冯诺依曼结构CPU不能直接和磁盘打交道所以你想要运行需要先把你的进程放到内容中这一过程我们叫做加载。一个完整的进程 task_struct 自己的代码和数据。这个代码和数据就是在内存中。Linux下一切皆文件我们可以通过以下步骤深入理解代码加载到内存的过程首先写一个死循环的程序方便我们查看进程的状态然后完成编译、链接、运行。我这里编译链接生成myprocess文件Linux操作系统给进程命名了一个id我们把这个id叫做进程id或者pid(process id)并且生成这个id相同的文件夹放在/proc目录下所以我们要先查看以下这个id是什么我们新开一个会话运行ps axj | head -1 ; ps axj | grep myprocess查看我们运行的程序信息找到pid这就是我们说的id。然后在/proc找相应的文件夹。详细操作如下可以在你的程序中通过chdir()系统调用修改cwd这里就不演示了。需要注意的内容那么怎么删除你刚刚运行的死循环的程序呢你可以在刚刚运行的会话里输入ctrl c发送信号给进程让他终止kill -9 pid也是一样的效果。不同的是ctrl c只能给当前会话的在前台运行的进程发送终止信号而kill -9可以给其他会话、后台的进程发送终止信号。信号部分会在后面详细讲。/proc是内存级的文件的属性和内容都放在内存但是对于路径查找还是走的文件系统那一套文件系统会出详细解析的文章。子进程子进程指在当前进程下创建的新进程相应的对于子进程这个进程就是父进程。forkfork()命令可以创建进程返回pid_t(long的宏定义)给父进程返回子进程的pid给子进程返回0如果创建进程失败会返回-1。子进程和父进程共享代码和数据当子进程对数据进行修改会发生写时拷贝。以方便节省内存空间另一方面防止子父进程相互影响。写时拷贝指再内存上新开辟空间同时完成页表映射的更新那么问题来了为什么给父进程一个子进程pid给子进程一个0两个返回值不一样通过getppid()函数子进程可以访问到父进程的pid但是父进程不能通过函数查询到子进程的pid如此返回值方便进行管理。fork()创建子进程过程中完成了什么一系列操作向操作系统申请新的task_struct空间父进程把自己task_struct原封不动copy给子进程把子进程task_struct放入runqueuefork()为什么可以返回两次完成创建子进程过程后开始给return id;此时return id;会在父进程执行一遍在子进程执行一遍子进程还会发生写时拷贝自然就可以返回两个值了。子父进程有什么关系子进程共享父进程的代码和数据当子进程对代码和数据有修改会发生写时拷贝。子进程在结束时会把return结果返回给父进程tash_struct结构体也交给父进程回收。进程状态在此之前我们向讲三个系统调用pid_t getpid()和pid_t getppid()这两个系统调用分别是查看当前进程、父进程父进程后文会讲的进程id。先要查看运行的进程的进程状态还需要ps axj | head -1 ; ps axj | grep myprocess命令你可以用while封装一下while :; do ps axj | head -1; ps axj | grep myprocess; sleep 1; done就可以每过一秒查看一次myprocess进程的状态使用ps axj指令查看Linux的进程状态有以下几种(守护进程在网络通信部分讲守护进程也是一种进程状态)RRunning运行态在使用ps命令查看while(1){}程序时可以发现查到的程序大多数在S状态。这是因为CPU处理速度极快而进程在等待外部事件事救护进入S状态CPU实际计算的时间极短绝大多数进程在大部分生命周期都处于等待资源的睡眠状态。需要注意的是Linux没有区分运行态和就绪态Linux统一称其为R状态内核上进程task_struct处在runqueue中等待被CPU调用或者已经被CPU调用。关于runqueue、进程调度算法后文会详细讲解。Ssleep可中断睡眠可中断睡眠阻塞导致的休眠因为一个设备只能由一个进程使用在此设备交给其他进程使用的时候这个进程就会进入阻塞休眠的状态。除了这种情况wait等待其他进程的信号也是S状态。内核上我们上文提到过Linux操作系统把不同设备封装成struct对象那么把需要这个设备的进程放到这个设备的struct结构体中这就是阻塞。当设备空闲要分配给其他进程时从这个struct中寻找下一个分配的进程即可。D不可中断睡眠不可中断睡眠不同于S状态这个状态通常用于磁盘IO比如你在关机的时候有部分进程正在向磁盘中写入东西那么系统就会把这些文件归为D状态D状态不可被外界打断只能由进程自己苏醒这一状态可以保证东西都完美写入到磁盘中。磁盘也很快一般不会吹西安这种状态如果频繁出现这种状态可能是你磁盘老化了情景假设某个进程需要读取数据而这个数据在磁盘上那么首先是需要磁盘驱动程序将磁盘上的数据读取到内核的文件缓冲区这个由磁盘驱动程序将磁盘数据读取到内核文件缓冲区的过程就是磁盘IO而这个过程中当前这个进程是不参与的所以他会等待磁盘IO完成这个他这个等待的过程就是D状态。此时想要杀死D进程就要先处理磁盘IO。需要补充的是磁盘IO不需要CPU参与牢记冯诺依曼结构CPU只会和内存打交道。T暂停因为ctrl z暂停。可以通过jobs指令查看当前处于休眠的任务同时会回显任务号。然后通过kill -9 %任务号指令就可以删除这个休眠的任务。也可以通过kill -SIGCONT %任务号让休眠进程继续进行。或者使用fg %任务号放在前台运行使用bg %任务号放在后台运行。t暂停因为debug暂停ZZombel僵尸状态当子进程完成或者被kill子进程的PCB(tash_struct)此时没有父进程回收就把此时的子进程状态称为僵尸状态。对于僵尸状态如果你kill父进程那么子进程终止的时候就会导致没有父进程回收task_struct为了解决这一问题操作系统会自动把你这个子进程的ppid设为1这个1其实就是bash的进程号。当你子进程运行完毕task_struct就会交给bash回收防止内存泄漏。僵尸状态和孤儿进程有一些区分孤儿进程表示其父进程被杀进程对应的ppid改成bash进程号。僵尸状态表示没有进程回收该进程的tsk_struct和pid号注意了除了这个外其他所有资源都会被OS自行收回只有task_struct和pid没被回收。僵尸状态验证代码#includestdio.h#includeunistd.h#includesys/types.hintgval100;intmain(){printf(父进程开始运行,pid: %d\n,getpid());pid_tidfork();if(id0){perror(fork);return1;}elseif(id0){printf(我是一个子进程 !, 我的pid: %d, 我的父进程id: %d, gval: %d\n,getpid(),getppid(),gval);sleep(5);// childwhile(1){sleep(1);printf(子进程修改变量: %d-%d,gval,gval10);gval10;// 修改printf(我是一个子进程 !, 我的pid: %d, 我的父进程id: %d\n,getpid(),getppid());}}else{// fatherwhile(1){sleep(1);printf(我是一个父进程 !, 我的pid: %d, 我的父进程id: %d, gval: %d\n,getpid(),getppid(),gval);}}printf(进程开始运行,pid: %d\n,getpid());// chdir(/home/whb);// fopen(hello.txt, a);// while(1)// {// sleep(1);// printf(我是一个进程 !, 我的pid: %d, 我的父进程id: %d\n, getpid(), getppid());// }}僵尸状态效果X指结束的进程状态通常情况下看不到因为一进入XPCB被回收就瞬间查看不了此进程的状态。想要验证R状态只需要运行死循环while(1){}即可想要验证S只需要scanf()阻塞住即可。一定要注意验证R状态万万不要涉及文件IO涉及printf scanf之类的都不可以IO会导致进程大多数时间处于S状态你可能会看到S状态。挂起状态挂起状态指现在内存紧张需要把一部分不重要的进程的代码和数据从内存移到磁盘上那么这些本在内存但是移到磁盘上的进程就进入了挂起状态。磁盘上存放这些进程代码和数据的地方就是swap分区。当内存够用了再把swap分区内容移到内存上。在这个过程中页表也会把对应代码和数据的物理地址与磁盘上的逻辑地址像变换。页表就是完成虚拟地址和物理地址的映射关系详细的页表后续会单出一个文章详细的物理地址和虚拟地址变换也会单出一个文章这个状态的相关操作由操作系统完成用户通过ps指令看不到这个状态