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

进程探秘:从 PCB 到 fork 的核心原理之旅

前言

       在操作系统的世界里,“进程” 是一个贯穿始终的核心概念。无论是我们日常打开的浏览器、运行的代码,还是后台默默工作的服务,本质上都是一个个 “进程” 在操作系统的调度下有序运行。理解进程,是掌握操作系统工作机制、走进并发编程世界的第一步。
       本文将从最基础的 “进程是什么” 讲起,带你逐层揭开进程的神秘面纱:从描述进程的核心数据结构 PCB(进程控制块),到 Linux 内核中具体的task_struct;从如何查看进程的标识符(PID)、父进程 ID(PPID),到通过ps命令和/proc文件系统窥探进程的实时状态;最终聚焦于进程创建的核心系统调用fork,解析它如何 “一分为二” 生成子进程,以及那些看似反直觉的返回值背后的底层逻辑。
       无论你是刚接触操作系统的初学者,还是想夯实基础的开发者,这篇文章都将为你搭建起理解进程的 “知识骨架”,为后续深入学习进程调度、通信、同步等内容铺好基石。

目录

1. 基本概念

1. 概念理解

1.2 描述进程-PCB

1.3 task_ struct

2. 进程查看

2.1getpid获取标识符

2.2 ps 和/proc 获取进程信息

2.3  getppid()获取父进程pid

3. 进程创建

3.1 系统调用创建进程-fork

3.2 fork的返回值


1. 基本概念

1. 概念理解

课本概念:程序的一个执行实例,正在执行的程序等
内核观点:担当分配系统资源(CPU时间,内存)的实体。

换个方式理解:

进程=内核数据结构对象+自己的代码和数据

Linux下:进程=PCB(task_struct)+代码和数据 

对进程的管理就变成了对构建的数据结构进行增删查改。

1.2 描述进程-PCB

进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
课本上称之为PCB(process control block),Linux操作系统下的PCB是: task_struct
task_struct-PCB的一种
在Linux中描述进程的结构体叫做task_struct。
task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。
进程的所有属性,就可以直接或者间接通过task_struct找到。

1.3 task_ struct

内容分类
标示符: 描述本进程的唯一标示符,用来区别其他进程。
状态: 任务状态,退出代码,退出信号等。
优先级: 相对于其他进程的优先级。
程序计数器: 程序中即将被执行的下⼀条指令的地址。
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
I∕O状态信息: 包括显示的I/O请求,分配给进程的I∕O设备和被进程使用的文件列表。
记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
其他信息
struct task_struct {volatile long state;    // 进程状态(运行、睡眠等)struct thread_info *thread_info;  // 指向线程信息结构pid_t pid;              // 进程标识符struct mm_struct *mm;   // 指向内存描述符struct mm_struct *active_mm;  // 当前使用的内存描述符struct list_head tasks; // 用于链接所有进程的双向循环链表节点 [^1]struct sched_entity se; // 调度实体unsigned int time_slice; // 时间片// ... 其他字段省略
};
组织进程
可以在内核源代码里找到它。所有运行在系统里的进程都以task_struct链表的形式存在内核里。

2. 进程查看

我们历史上执行的所有指令,工具,自己的程序,运行起来,全部都是进程!!!

2.1getpid获取标识符

获取当前进程的唯一标识符(Process ID,简称 PID)。PID 是操作系统分配给每个正在运行的进程的一个正整数值,用于唯一标识和管理进程。 

  1#include <stdio.h>2 #include <unistd.h>3 #include <sys/types.h>4 int main(){5     while(1){6     sleep(1);7     printf("我是一个进程!我的pid:%d \n",getpid());                                                                                                                                                          8     }9     return 0;10 }

2.2 ps 和/proc 获取进程信息

ps aux:以用户为中心的详细进程快照

USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.1 168504 13080 ?        Ss   08:00   0:02 /sbin/init
hu       12345  0.0  0.0   4320   720 pts/0    S+   10:30   0:00 ./a.out

ps axj:以进程关系为中心的输出(包含进程组和会话信息)

ps axj 输出格式侧重进程间的关系,包含进程组 ID(PGID)、会话 ID(SID)、控制终端(TTY)等字段,适合分析进程的层级关系(如父子进程、进程组、会话)。

选项含义

  • a:同 ps aux,显示所有用户的进程。
  • x:同 ps aux,显示无控制终端的进程。
  • j:以作业控制格式输出,增加进程组 ID(PGID)、会话 ID(SID)、控制终端 ID(TTY)等与进程关系相关的字段。
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND0     1     1     1 ?           -1 Ss       0   0:02 /sbin/init1234  5678  5678  5678 pts/0     5678 S+    1000   0:00 ./a.out

 ps axj | grep 是一个组合命令,用于在 ps axj 的输出中筛选包含特定关键词的进程信息。

kill - 9+进程pid 可以杀死进程,也可以用ctrl C

进程的信息可以通过 /proc 系统文件夹查看 

/proc 是一个特殊的虚拟文件系统(procfs),它并非存储在磁盘上,而是动态反映系统内核和进程的实时状态。通过访问 /proc 下的文件和目录,你可以查看或修改内核参数、进程信息、硬件状态等。

 

进程启动,查看,着重关注cwd和exe文件 ,一般是在当前路径下生成可执行文件,cwd是当前路径。我们可以用chdir改变当前进程的工作目录。 

改变进程的当前工作目录:调用 chdir 后,进程后续的相对路径操作都将基于新的目录。

影响文件操作:例如,若当前目录为 /home/hu,执行 chdir("/tmp") 后,打开文件 test.txt 实际访问的是 /tmp/test.txt

2.3  getppid()获取父进程pid

每次重新启动进程 ,进程pid会变,但是父进程ID没变。

 

命令行解释器bash本身就是一个进程。

每次登录服务器时,操作系统会给每一个登录用户分配一个bash. 

上面是bash打印的字符串,然后卡住等待,等待输入命令给bash

回想我们的程序,都可以先printf再scanf

3. 进程创建

3.1 系统调用创建进程-fork

#include <stdio.h>3 #include <unistd.h>4 #include <sys/types.h>5 int main(){6     printf("父进程开始执行,pid:%d\n",getpid());7     fork();8     printf("进程开始运行,pid:%d\n",getpid());                                                                                                                                                                9 }

 刚开始只有一个执行流,fork创建进程之后,有两个执行流,所以后面的printf会有两个,且结果id不一样。子进程执行父进程之后的代码。

 在仅创建子进程时,子进程没有自己的代码和数据,因为目前,没有程序新加载。子进程执行父进程之后的代码。

3.2 fork的返回值

 

fork会有两个返回值。

子进程PID返回给父进程,0返回给子进程,失败的话-1返回给父进程 

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main(){printf("父进程开始执行,pid:%d\n",getpid());pid_t id=fork();if(id<0){perror("fork");return 1;}else if(id==0){//childwhile(1){sleep(1);printf("我是一个子进程!我的pid:%d ,我的父进程pid:%d\n",getpid(),getppid());                                                                                                                     }}else{while(1){sleep(1);printf("我是一个父进程!我的pid:%d ,我的父进程pid:%d\n",getpid(),getppid());}}//    printf("进程开始运行,pid:%d\n",getpid());return 0;}

根据ID的判断执行了两个部分程序 

 

不免产生一个疑惑?。

为什么fork给父子返回各自不同的返回值?

一个父进程可以有多个子进程,父:子=n:1;将子进程的pid返回给父进程方便父进程管理区分不同的子进程,用于标识新创建的子进程;

为什么一个函数会返回两次?

一个函数return xxx了,它的核心功能就完成了。fork创建子进程,申请新的pcb,拷贝父进程的pcb给子进程,子进程pcb放到进程列表中甚至放到调度队列中,return是条语句,是个函数,是共用的,最后父子进程都会执行return语句。

函数 “返回两次” 的本质:进程复制 + 指令指针共享

fork() 的核心是内核为当前进程创建了一个几乎完全相同的副本。

为什么一个变量id==0又>0? 导致 if 与else同时成立?(以后解释,当学习到虚拟地址空间会说明)

进程具有独立性,父子进程相互独立。父子进程的数据结构独立;代码是共享只读的,不可修改的;数据是写时拷贝,父子一方修改数据时,OS会把数据拷贝一份,目标进程修改这个拷贝。

#include <stdio.h>3 #include <unistd.h>4 #include <sys/types.h>5 int val=520;6 int main(){7     printf("父进程开始执行,pid:%d\n",getpid());8     pid_t id=fork();9     if(id<0){10         perror("fork");11         return 1;12     }13     else if(id==0){14         //child15         while(1){16             sleep(1);17             printf("我是一个子进程!我的pid:%d ,我的父进程pid:%d,val:%d \n",getpid(),getppid(),val);18             val+=10;19         }20     21     }22     else{23         while(1){24              sleep(1);25              printf("我是一个父进程!我的pid:%d ,我的父进程pid:%d,val:%d\n",getpid(),getppid(),val);                                                                                                         26           }27     28     }29 //    printf("进程开始运行,pid:%d\n",getpid());30    return 0;31 }

结束语

         到这里,我们已经走完了进程基础知识的探索之旅。从抽象的 “进程概念” 到具体task_struct结构体,从getpid、ps等工具的使用,到fork创建进程的底层逻辑,我们不仅认识了进程的 “外貌”(如何查看信息),更触摸到了它的 “骨架”(PCB 的核心作用)和 “诞生方式”(fork 的特殊机制)。
        这些知识看似基础,却是理解操作系统并发能力的关键 —— 毕竟,所有复杂的多任务场景,追根溯源都是一个个进程在 PCB 的 “记录” 下,通过调度器的协调有序运行的结果。
接下来,你可能会好奇:进程是如何被调度的?多个进程之间如何通信?fork创建的子进程为何能共享代码却拥有独立内存?这些问题,我们将在后续的内容中继续探索。 


文章转载自:
http://amphibology.dmyyro.cn
http://burble.dmyyro.cn
http://boo.dmyyro.cn
http://bilbo.dmyyro.cn
http://bumbo.dmyyro.cn
http://advertisement.dmyyro.cn
http://cagoule.dmyyro.cn
http://cerebrosclerosis.dmyyro.cn
http://brushability.dmyyro.cn
http://chiaroscuro.dmyyro.cn
http://agrobiology.dmyyro.cn
http://asleep.dmyyro.cn
http://cerated.dmyyro.cn
http://abbreviate.dmyyro.cn
http://biracial.dmyyro.cn
http://ammonal.dmyyro.cn
http://anglicism.dmyyro.cn
http://biter.dmyyro.cn
http://ambulate.dmyyro.cn
http://amphipod.dmyyro.cn
http://backslash.dmyyro.cn
http://biscuit.dmyyro.cn
http://amianthus.dmyyro.cn
http://aginner.dmyyro.cn
http://blepharoplasty.dmyyro.cn
http://choosey.dmyyro.cn
http://adventist.dmyyro.cn
http://brasilein.dmyyro.cn
http://chronon.dmyyro.cn
http://blowzy.dmyyro.cn
http://www.dtcms.com/a/280434.html

相关文章:

  • Lang3
  • Spring Ioc Bean 到底是什么
  • 朝鲜升级供应链恶意软件XORIndex,再次瞄准npm生态系统
  • 从springcloud-gateway了解同步和异步,webflux webMvc、共享变量
  • 四种高效搭建SpringBoot项目的方式详解
  • 基于UDP/IP网络游戏加速高级拥塞控制算法(示意:一)
  • SpringBoot 实现 Redis读写分离
  • 【PTA数据结构 | C语言版】根据前序序列重构二叉树
  • npm install failed如何办?
  • 【10】MFC入门到精通——MFC 创建向导对话框、属性页类、属性表类、代码
  • centos 安装java 环境
  • FreeRTOS学习笔记——总览
  • 【Docker基础】Dockerfile构建与运行流程完全指南:从原理到实践优化
  • CentOS 8-BClinux8.2更换为阿里云镜像源:保姆级教程
  • 【第二章自定义功能菜单_MenuItemAttribute_顶部菜单栏(本章进度1/7)】
  • Rust基础-part5-引用
  • 【jvm|基本原理】第四天
  • 游戏行业中的恶梦:不断升级的DDoS攻击
  • 深入理解C++11 std::iota:从原理到实践
  • UDP和TCP的主要区别是什么
  • 17. 什么是 webSocket ?
  • 力扣 hot100 Day45
  • ZYNQ千兆光通信实战:Tri Mode Ethernet MAC深度解析
  • Keepalived双机热备概述
  • 基于深度学习的LSTM、GRU对大数据交通流量分析与预测的研究
  • omniparser v2 本地部署及制作docker镜像(20250715)
  • 从浏览器到服务器:TCP 段的网络传输之旅
  • 设计模式二:策略模式 (Strategy Pattern)
  • 云计算如何提高企业的数据安全性和隐私保护
  • 我会秘书长杨添天带队赴杭州融量农业发展有限公司考察调研