目录
一、前言
二、冯诺依曼体系结构
1.疑问:冯诺依曼体系结构为什么要有内存?
三、操作系统
1.是什么
2.核心功能
3.如何理解“管理”
核心思想:先描述,再组织
4.系统调用和库函数
1.概念
2.关系
四、进程
1.进程是什么
1.操作系统是如何管理进程的
2.什么是PCB
3.task_struct内部都有哪些进程属性?
4.操作系统是如何组织PCB的?
2.怎么查看进程?
1./proc
2.ps命令家族
3.补充:系统调用接口
1.getpid&&getppid
2.chdir
3.getcwd
4.初识fork
返回值问题
疑问:为什么pid既等于0又大于0?
一、前言
从本篇开始,我们就要进入学习【Linux进程】的相关知识,学习目标包括:
1.认识冯诺依曼系统。
2.操作系统概念与定位,理解"管理"。
3.深⼊理解进程概念,了解PCB。
4.学习进程状态,学会创建进程,掌握僵⼫进程和孤⼉进程,及其形成原因和危害。
5.了解进程调度,Linux进程优先级,理解进程竞争性与独⽴性,理解并⾏与并发。
6.理解进程切换,以及Linux2.6 kernel,O(1)调度算法架构。
7.理解环境变量,熟悉常⻅环境变量及相关指令, getenv/setenv函数。
8.理解C内存空间分配规律,了解进程内存映像和应⽤程序区别, 认识虚拟地址空间。
八个学习目标博主会拆分成四篇博客向大家一一交付,本篇博主会向大家介绍前三个目标——冯诺依曼体系结构,操作系统和进程PCB相关内容。
在这篇博客中,我们会从计算机硬件的底层数据流动讲起,探讨操作系统是如何通过“先描述,再组织”的哲学来管理软硬件资源的。随后,我们将视角拉近,去看看进程的“身份证”——PCB,以及如何在 Linux 中通过/proc目录和ps命令来观察它们。
当然,本篇还会重点带大家初识fork()系统调用。很多初学者在刚接触fork()时,都会被它“调用一次,返回两次”的特性,以及pid变量在父子进程中“既等于 0 又大于 0”的现象绕晕。希望通过本篇的拆解,能帮你理清这背后的执行逻辑,为后续更深入的进程控制与管理打下基础。
二、冯诺依曼体系结构
截⾄⽬前,我们所认识的计算机,都是由⼀个个的硬件组件组成输⼊单元:包括键盘, ⿏标,扫描仪, 写板等中央处理器(CPU):含有运算器和控制器等输出单元:显⽰器,打印机等关于冯诺依曼,必须强调⼏点:这⾥的存储器指的是内存不考虑缓存情况,这⾥的CPU能且只能对内存进⾏读写,不能访问外设(输⼊或输出设备)(数据层⾯)外设(输⼊或输出设备)要输⼊或者输出数据,也只能写⼊内存或者从内存中读取。⼀句话,所有设备都只能直接和内存打交道。
1.疑问:冯诺依曼体系结构为什么要有内存?
1.数据在体系结构中的流动:本质就是CPU不直接和I/O设备打交道,交换数据需要内存来当中介。
2.存储分级的问题。
3.因为内存的存在,让计算机在成本不是很高的情况下就拥有了不错的效率。
三、操作系统
1.是什么
广义上,操作系统是控制和管理计算机系统全部软硬件资源、合理组织程序运行、提供用户接口和应用程序运行环境的系统软件集合。
狭义上,操作系统特指内核,即直接运行在硬件之上、常驻内存、负责最核心资源管理和硬件抽象的那部分程序。操作系统内核通常进行进程管理、文件管理,内存管理和驱动与设备管理。下文我们会提到。
总而言之,操作系统是一个进行软件资源管理的软件。
2.核心功能
操作系统通过对软硬件资源进行管理,实现为用户提供良好的运行环境。
对下,与硬件交互,管理所有的软硬件资源。
对上,为用户程序提供一个良好的执行环境。
3.如何理解“管理”
场景:校长对上千名学生进行管理
核心思想:先描述,再组织
先描述(抽象提取):现实世界有各种各样的学生,怎么让计算机认识他们?通过提取关键属性,用代码“描述”出来。
看右上角的代码:定义了
struct Stu结构体。把学生的姓名(name)、学号(number)、性别(sex)、成绩(score)、表现(char 表现[])等作为成员变量。这样,一个复杂的人,在计算机里就变成了一个由各个字段组成的“数据包”。
再组织(建立关联):只有一个个孤立的“学生数据包”是不够的,还需要把他们管理起来。
看左下角的表头和右侧:通过引入
struct Stu *next指针,将一个个独立的学生结构体(张三、李四、王五...)像手拉手一样串连起来。看右中部的图示:画了一个经典的单链表结构(方框代表结构体,箭头代表
next指针)。这就把零散的数据“组织”成了一个整体。
1.描述起来,⽤struct结构体2.组织起来,⽤链表或其他⾼效的数据结构
4.系统调用和库函数
1.概念
系统调用是用户程序请求操作系统内核服务的入口;库函数是用户态代码中可调用的函数,可能封装系统调用,也可能完全在用户态完成计算。
系统调用:
系统调用(System Call)是操作系统内核提供给用户程序的编程接口。
以银行系统为例,对外:银行是一个整体,对用户仅提供银行窗口与银行系统进行交互,为了安全考虑不允许用户直接进入银行系统。这个窗口就是“系统调用接口”,用户程序不能直接操作硬件或内核数据结构,必须通过系统调用“陷入”内核态,请求内核完成受保护的操作。
库:
2.关系
库函数和系统调用不是互斥关系,常见关系是:
库函数可以封装系统调用。
例如 C 程序中调用printf()
printf 是库函数,它最终可能通过write系统调用把数据写到文件描述符。但printf还做了格式化、缓冲区管理等工作,这些是在用户态完成的。
四、进程
1.进程是什么
进程 = PCB + 代码和数据
1.操作系统是如何管理进程的
核心:先描述,再组织
描述:OS内核里使用结构体(PCB)对进程属性进行描述
组织:PCB里有一定的数据结构,通过某种数据结构把进程管理(组织)起来
操作系统管理进程,就是通过创建 PCB来描述它,通过调度 PCB 所在的链表来运行它,通过系统调用和中断来切换它的状态,最后通过销毁 PCB来终结它。一切管理,最终都落脚在对 PCB 及其所在队列的增删查改上。
2.什么是PCB
PCB(Process Control Block,进程控制块)
简单一句话概括:PCB 就是操作系统用来“描述”和“管理”进程的一个数据结构(结构体),结构体内部包含进程的属性信息和链接字段。
如果把一个运行中的程序(进程)比作一个活生生的人,那么PCB 就是这个进程的“身份证”+“档案袋”。在操作系统眼里,没有 PCB 就没有进程。
Linux操作系统中的PCB叫做:task_struct
磁盘中的程序分为代码段和数据段,程序从磁盘加载到内存,操作系统内核就会新建一个PCB,此时静态的程序就变成了动态的进程。
3.task_struct内部都有哪些进程属性?
标识符(PID):描述本进程的唯⼀标⽰符,⽤来区别其他进程。
状态(STATE):任务状态,退出代码,退出信号等。
优先级(PRI): 相对于其他进程的优先级。
程序计数器: 程序中即将被执⾏的下⼀条指令的地址。
内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
上下⽂数据: 进程执⾏时处理器的寄存器中的数据。
I/O状态信息: 包括显⽰的I/O请求,分配给进程的I/O设备和被进程使⽤的⽂件列表。
记账信息:可能包括处理器时间总和,使⽤的时钟数总和,时间限制,记账号等。
其他信息
4.操作系统是如何组织PCB的?
所有运⾏在系统⾥的进程都以task_struct双链表的形式存在内核⾥。
2.怎么查看进程?
查看进程通常有两种做法:
1./proc
proc(process进程):/proc 目录下以纯数字命名的子目录,代表的是当前系统中“存在”的所有进程。
你执行
ls /proc,看到的那些纯数字命名的目录(比如1、1024),每一个数字就是一个进程的 PID。你执行
cat /proc/1234/status,就能看到 PID 为 1234 的进程的状态、内存占用、父子进程 ID。你执行
ls -l /proc/1234/cwd,就能看到这个进程当前的工作目录(这正好对应了你上一个问题里的getcwd系统调用的底层数据来源)。
1.cwd是什么?
cwd(current work directory)指当前进程启动时的工作路径,也就是我们常说的当前路径。
为什么要有?
答:为了支持进程访问文件时,缺省路径的问题。
2.exe是什么?
exe是 Executable 的缩写,代表可执行文件。
这是一个软链接,说明启动这个进程的实际程序是/usr/bin/bash(即 Bash shell)。
2.ps命令家族
ps (Process Status) 是最基础的进程查看工具,它截取的是执行命令那一瞬间的进程状态。
ps aux:最常用。以 BSD 风格列出系统中所有进程,并显示详细的资源占用。
ps -ef:以 System V 风格列出所有进程,显示PPID(父进程 ID)、C(CPU使用率)、STIME(启动时间)、CMD(完整命令行)。
ps -al:只看当前终端下的进程,长格式,侧重调度信息(优先级、等待状态等)。
ps axj:看全系统进程的层级关系(
PGID进程组、SID会话、TPGID控制终端)。
ps命令通常和管道搭配使用!
3.补充:系统调用接口
1.getpid&&getppid
功能:获取当前进程的id或当前进程的父进程的id
注意:
自定义类型pid_t 本质是一个int类型,不过是在头文件里被重定义了。
使用这两个接口的时候要包含头文件<unistd.h>!
2.chdir
功能:修改当前进程的工作目录
3.getcwd
功能:获取当前进程的当前路径
4.初识fork
功能:创建子进程
翻译:子进程和父进程运行在独立的内存空间中。在
fork()的瞬间,两者的内存空间内容相同。其中一个进程进行的内存写入不会影响另一个进程。
返回值问题
fork()是系统编程中最“反直觉”的函数之一,因为它的执行逻辑是:调用一次,返回两次。
| 返回值 | 发生的进程 | 含义与作用 |
|---|---|---|
> 0(大于0) | 父进程 | 返回的是新创建的子进程的 PID(进程ID)。 父进程需要拿到这个 ID,以便后续对子进程进行管理(比如 wait()等待它结束,或者用kill()杀掉它)。 |
== 0(等于0) | 子进程 | 返回 0 表示当前代码正在子进程中运行。 为什么是 0?因为子进程不需要知道自己的 PID(它可以自己调用 getpid()获取),它只需要知道“我是子进程”。 |
==-1 | 出错 | 创建子进程失败。常见原因:系统内存不足,或者当前用户的进程数达到了上限(比如ulimit -u限制)。此时没有子进程产生。 |
疑问:为什么同一个函数会返回两次?
Fork函数核心要做的几件事:
1.创建子进程
2.拷贝PCB
3.修改个性属性数据(例如PID)
4.放入调度队列
—————————此时子进程已经创建好了
5.return(父子进程共享代码块,都要执行return)
场景:
myproc.c文件示例代码:
#include <stdio.h> #include <unistd.h> #include <sys/wait.h> #include <stdlib.h> int main() { pid_t pid; printf("调用 fork() 之前,只有一个进程运行。PID: %d\n", getpid()); printf("----------------------------------------\n"); // 见证奇迹的时刻:调用一次,返回两次 pid = fork(); if (pid < 0) { // 1. 创建失败 perror("fork 失败"); exit(1); } else if (pid == 0) { // 2. 这里是子进程的代码(返回 0) printf("【子进程】我被创建了!\n"); printf("【子进程】fork() 的返回值是: %d\n", pid); printf("【子进程】我的真实 PID 是: %d\n", getpid()); printf("【子进程】我的父进程 PID 是: %d\n", getppid()); exit(0); // 子进程执行完就退出 } else { // 3. 这里是父进程的代码(返回子进程的 PID,大于 0) printf("【父进程】我创建了一个子进程!\n"); printf("【父进程】fork() 的返回值是: %d\n", pid); printf("【父进程】我的真实 PID 是: %d\n", getpid()); wait(NULL); // 父进程等待子进程结束,回收资源 printf("【父进程】子进程执行完毕了。\n"); } return 0; }运行结果:
奇怪的是:同时进入了两个互斥的条件代码块!
疑问:为什么pid既等于0又大于0?
1. 准备阶段(只有父进程)
pid_t pid; // 在父进程的内存里,开辟了一块4字节的空间,叫 pid pid = fork(); // 准备调用 fork此时,世界上只有一个进程(父),内存里只有一个pid变量。
2. 发生奇迹(内核的复制魔法)
当我们调用fork()时,我们上一张图里的“拷贝 PCB”和“分配内存”发生了。
操作系统不仅复制了父进程的代码,还把父进程的整个内存空间(包括那个叫做pid的变量)原封不动地复制了一份给子进程。
这时候,内存里有了两个pid变量:
父进程内存里有一个
pid(假设地址是 0x1000)子进程内存里也有一个
pid(地址也是 0x1000,但它们是两块独立的物理内存)
记住前面
fork手册里的那句红线:两个进程运行在独立的内存空间中。
3. 赋值阶段(分别塞小纸条)
现在,内核开始执行返回操作(也就是图里的return XXX),准备让它们继续往下跑:
针对父进程:内核把父进程内存里的那个
pid变量,填上了子进程的 PID(例如 12345,大于 0)。针对子进程:内核把子进程内存里的那个
pid变量,填上了0。
4. 执行阶段(各回各家,各找各妈)
现在两个进程同时从fork()出来,继续往下执行:
if (pid == 0) { // 只有子进程会走进来,因为子进程内存里的 pid 是 0 } else { // 只有父进程会走进来,因为父进程内存里的 pid 是 12345 (>0) }父进程看自己内存里的pid,是大于 0 的;
子进程看自己内存里的pid,是等于 0 的。
解读(核心原理:写时复制 Copy-On-Write, COW):
1.刚
fork()完,系统为了节省资源,并没有真的把父进程的内存全部拷贝一份给子进程,而是让子进程的虚拟内存指向父进程的物理内存(此时“内容相同”)。2.但是,一旦父进程或子进程尝试修改内存中的变量(比如执行
x = 10),系统内核会立刻拦截这个操作,并给修改内存的那个进程分配一块新的物理内存。