当前位置: 首页 > 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://www.xdnf.cn/news/1125811.html

相关文章:

  • 从零开始的云计算生活——第三十二天,四面楚歌,HAProxy负载均衡
  • 测试tcpdump,分析tcp协议
  • JAVA学习笔记 使用notepad++开发JAVA-003
  • Bootstrap-HTML(七)Bootstrap在线图标的引用方法
  • SELinux 详细解析
  • 【安卓笔记】RxJava之flatMap的使用
  • python原生处理properties文件
  • 第十四章 Stream API
  • 【第二章自定义功能菜单_MenuItemAttribute_顶部菜单栏(本章进度1/7)】
  • 零售企业用户行为数据画像的授权边界界定:合规与风险防范
  • 16、鸿蒙Harmony Next开发:组件扩展
  • RAG实战指南 Day 16:向量数据库类型与选择指南
  • Django+Celery 进阶:动态定时任务的添加、修改与智能调度实战
  • 第三章 OB SQL 引擎高级技术
  • PostgreSQL 数据库中 ETL 操作的实战技巧
  • 深入探讨Hadoop YARN Federation:架构设计与实践应用
  • docker搭建freeswitch实现点对点视频,多人视频
  • 综合网络组网实验(机器人实验)
  • Java 避免空指针的方法及Optional最佳实践
  • 【Linux系统】命令行参数和环境变量
  • 【Java篇】IntelliJ IDEA 安装与基础配置指南
  • 网络安全职业指南:探索网络安全领域的各种角色
  • 蛋白质组学技术揭示超急性HIV-1感染的宿主反应机制
  • HR数字化转型:3大痛点解决方案与效率突破指南
  • 渭河SQL题库-- 来自渭河数据分析
  • 在 SymPy 中精确提取三角函数系数的深度分析
  • Spring Boot - Spring Boot 集成 MyBatis 分页实现 RowBounds
  • MySQL高级篇(二):深入理解数据库事务与MySQL锁机制
  • AutoGPT vs BabyAGI:自主任务执行框架对比与选型深度分析
  • 【PTA数据结构 | C语言版】二叉树层序序列化