在这里插入图片描述
在这里插入图片描述

.

个人主页:晓风飞
专栏:数据结构|Linux|C语言
路漫漫其修远兮,吾将上下而求索



进程

为什么要存在内存??

数据流动的本质是拷贝
数据在计算机体系结构中流动,CPU 不直接与外设打交道。CPU 需要数据时,只能从内存中读取;写数据时,也只能写入内存。

存储分级问题
外设速度太慢,与 CPU 之间存在巨大的速度代差,因此 I/O 操作的效率相对较低。

内存的作用
c++编译形成可执行程序(代码+数据)(文件)程序运行前,必须先加载到内存中(这是体系结构的规定,从纯硬件角度出发)。内存为计算机提供了更高的性价比,提升了整体运行效率。

操作系统

操作系统是一个管理软硬件资源的软件。

广义操作系统:操作系统内核 + 核心应用软件

狭义操作系统:操作系统内核

Linux 内核主要包含:

进程管理

文件管理

内存管理

驱动与设备管理

进程

什么是进程?

教材定义:运行起来的程序就是进程,加载到内存中的程序叫做进程。
更准确的理解:进程 = 内核数据结构(task_struct)+ 程序的代码和数据。可以类比为:学生 = 学籍信息 + 本人。
用户让操作系统执行某个任务、加载某个程序,都会被转换成进程。一切指令执行、工具执行、软件执行,本质上都是进程在运行。进程就是用户要做什么!

PCB(进程控制块)

PCB 内部包含了描述进程的所有属性。操作系统通过“先描述,再组织”的方式来管理进程(类似于找工作:进程是求职者,PCB 是简历,CPU 是面试官,操作系统是 HR 调度)。

在 Linux 中输入 top 命令可以查看进程,类似于 Windows 的任务管理器。
os内部可以同时存在多个进程
在这里插入图片描述

操作系统如何管理进程?

操作系统必须对进程进行管理,管理方式是“先描述,再组织”。每个进程在内核中都有一个对应的内核数据结构——PCB(Process Control Block,进程控制块)

struct task_struct
{
	进程属性+链接字段
}

进程的前身我们叫做程序, 程序在没有加载到内存之前,以文件的形式存储在磁盘上(冯·诺依曼原理)。程序加载到对应内存中才会变成进程

当程序加载到内存时,操作系统不仅会加载代码和数据,还会在内核中创建一个 task_struct)包含了描述当前进程所有属性的结构体,也包含了程序在内存中的代码和数据,操作系统创建进程。结构体来描述该进程。

如果内存中存在多个进程,操作系统会将所有 PCB 连接起来,形成一个链表。CPU 调度时,会遍历链表,选择优先级最高的进程来执行。操作系统对进程的管理,本质上就是对链表进行增删查改。

示例:getpid

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>

int main()
{
        pid_t id = getpid();
        while(1)
        {
                printf("hello, i am a proces , pid: %d\n",id);
                sleep(1);
                fflush(stdout);
        }
        return 0;

}

对应的Makefile

yproc:code.c
        gcc -o $@ $^
.PHONY:clean
clean:
        rm -f myproc
~                     

运行 ./myproc 后,可以看到当前进程的 PID(如 2367)。getpid() 本质上是从 task_struct 中获取进程的 PID 字段。
在这里插入图片描述
在这里插入图片描述

struct task_struct
{
	pid_t pid;
}

ctrl+c本质是终止进程,终止后,下次再运行操作系统给与的pid值不同

在这里插入图片描述

查看进程的做法

1.process-/proc 2命令
linux一切皆文件,数字目录本身是特定进程对应的pid
我们在一个端口运行程序,另一个端口查看进程
在这里插入图片描述

如果这时候把进程的可执行程序删除,会不会对进程暂时有影响吗?
我们输入rm proc删除掉正在执行的程序
这时候在进进程里ls下,可以发现可执行程序属性冒红发现被(deleted)删除了
在这里插入图片描述

进程的 CWD 如果指向一个被删除的目录,进程依然可以正常运行(内核会持有目录的引用计数,不会立刻释放),但此时你用相对路径创建文件会失败,因为对应的目录已经不存在了。

注意
在这里插入图片描述

进程的 CWD(当前工作目录)

在哪里?
CWD 存储在进程文件系统信息结构体 fs_struct 中,并由进程控制块 task_struct 内的指针所管理。
子进程会继承父进程的 CWD。
为什么需要 CWD?

实现 “工作目录隔离”,避免路径冲突

不同进程可以有不同的 CWD,比如你在两个终端里,分别在dirA和dirB下运行同一个程序,它们打开的config.txt会分别对应dirA/config.txt和dirB/config.txt,互不干扰。

“文件不存在时进程帮忙创建”,本质也是基于 CWD

比如你用fopen(“new.txt”, “w”)创建文件,内核会基于进程的 CWD,把相对路径解析成绝对路径,再创建文件。如果没有 CWD,内核不知道文件该创建在哪个目录下。

chdir更改当前进程的工作路径
进程可以通过 int chdir(const char *path); 修改自己的 CWD,但这个修改只会影响当前进程和它后续创建的子进程,不会影响父进程(比如终端)。
在这里插入图片描述
#getcwd获取当前进程的工作路径
我在 /home/dzh/work目录下gcc编译程序

#include <stdio.h>    // 必须有 #
#include <unistd.h>
#include <sys/types.h>

int main()
{
    char pwd[128];
    getcwd(pwd, 128);
    printf("before: %s\n", pwd);
    chdir("/home/dzh/work");
    getcwd(pwd, 128);
    printf("after: %s\n", pwd);

    pid_t id = getpid();
    FILE *fp = fopen("log.txt", "w");
    (void)fp;

    while(1)
    {
        printf("hello, I am a process, pid: %d\n", id);
        sleep(1);
    }
    return 0;
}

这时候可以看到程序当前路径变成了home/dzh
在这里插入图片描述

2命令
ps查看当前目录进程

ps ajx | grep procmy
ps ajx 看 父子进程关系、PID、PPID、PGID、SID(偏进程关系)
ps aux | grep procmy
ps aux 看 CPU、内存占用、运行时间、用户(偏资源占用)

在这里插入图片描述

实用命令
ps ajx |head -1 && ps ajx | grep procmy
先打印表头,再搜索你的进程
这样你看结果时,每一列是什么意思一目了然

ps ajx | head -1 # 只输出第一行(标题头)
&& # 并且(前面成功才执行后面)
ps ajx | grep procmy # 搜索你的进程 procmy

在这里插入图片描述

bash

ash 命令行解释器本身也是一个进程,它以死循环的方式运行,负责接收用户输入的命令并创建子进程来执行。通过 ps ajx 可以查看进程间的父子关系。

当你登录 Linux 后,系统给你启动的 -bash 进程,就是你当前终端的父进程:
查看你的 bash 进程 PID
echo $$
在这里插入图片描述

查看它的父进程(通常是终端/sshd)
ps -ef | grep $PPID
在这里插入图片描述

它的工作就是接收你输入的命令,创建子进程来执行。

kill

kill -9 进程
可以kill掉bash,会发现掉线

类似bash,它是如何创建字进程的呢?
fork
创建子进程
在这里插入图片描述

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>

int main()
{
    printf("fork 之前:我是一个进程:pid: %d, ppid: %d\n", getpid(), getppid());

    fork();

    printf("fork 之后:我是一个进程:pid: %d, ppid: %d\n", getpid(), getppid());

    sleep(1);
    return 0;
}

pid在这里插入图片描述

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
int main()
{
        pid_t id = fork();
        if(id == 0)
        {
                while(1)
                {
                        printf("读书...\n");
                        sleep(1);
                }
        }
        else if(id > 0)
        {
                while(1)
                {
                        printf("种地...\n");
                        sleep(1);
                }
        }
        return 0;

在这里插入图片描述

可以看到两个进程同时在运行
在这里插入图片描述

进程状态

子进程以父进程为模板进行创建,
父进程创建子进程,代码默认是被子进程共享的
子进程后续默认只能执行父进程fork之后的代码

问题1:为什么给子进程返回0,给父进程返回子进程的pid

子进程返回 0:子进程知道自己是谁,不需要通过返回值来获取 PID(可以通过 getpid() 获取),返回 0 只是告知它是子进程。

一个父进程可以创建多个子进程(1:n 关系),父进程需要知道每个子进程的 PID,才能对它们进行管理(比如 wait() 等待、kill() 终止)。如果父进程也返回 0,它就分不清哪个是哪个子进程了。
就像家长有多个孩子,每个孩子只需要知道 “我是孩子”,但家长必须知道每个孩子的学号,才能点名、管理。

问题2:为什么同一个函数,会返回两次

a,如果函数都准备return了,这个函数的核心工作做完了吗?

常规函数:工作没做完,就不会走到 return;一旦 return,函数就结束了,只返回一次。
但是fork() 的核心工作是创建一个新进程:在父进程中,将 task_struct 复制一份,修改属性(PID、PPID 等),并将新进程放入调度队列。
在这里插入图片描述
新进程创建完成后,两个进程都会继续执行 fork() 的剩余逻辑:父进程将子进程 PID 写入自己的变量并返回,子进程将 0 写入变量并返回。因此,fork() 不是同一个函数被调用了两次,而是两个进程各自执行了该函数的 return 部分。

问题3:为什么同一个变量,id,即 == 0,又大于0?

核心在于进程独立性与写时复制(Copy-on-Write)。

fork() 时,子进程会复制父进程的虚拟地址空间,因此父子进程的变量 id 在虚拟地址上是相同的,但它们映射到不同的物理内存。

写时复制机制:刚 fork() 完成时,父子进程共享同一块物理内存(只读)。当任意一方需要修改数据时,系统会复制一份新的物理内存给修改方,之后双方的变量就完全独立了。

因此,父进程的 id 和子进程的 id 是两个不同的物理内存变量,只是虚拟地址相同,它们的值可以不同且互不影响。

在这里插入图片描述

在这里插入图片描述
这个地址不可能是物理地址-虚拟地址
在这里插入图片描述
反回的本质,就是写入,本质不是同一个变量,只不过是虚拟地址相同,物理内存中,其实是分开的

进程状态

进程状态,本质就是task——struct内部的一个整型变量
站在操作系统原理角度,解释进程状态:运行,阻塞,挂起
在这里插入图片描述
FIFO:调度算法
进程处于运行状态
可是:task_struct他不是,属于双链表?怎么能还属于调度队列呢?
内核链表的实现
int a =10
4个字节,&a,标识一个地址??类型决定大小
结构体呢?
c语音对任何类型,开辟空间的时候,变量的地址在数字上等于开辟的众多字节中地址最小的那个数字
在这里插入图片描述

调度队列

在单CPU系统中,每一个CPU都必须有一套自己的调度队列。调度队列本质上就是一个队列结构,把所有处于就绪态的进程的PCB以某种数据结构连接起来。只要一个进程的PCB位于这个调度队列中,我们就称它处于R状态(运行状态)——注意,R状态并不代表进程正在CPU上运行,而是代表它随时可以被调度

内嵌式双链表

传统的链表节点通常会包含数据域,例如:

struct node {
    int data;
    struct node *prev, *next;
};

但内核中的链表节点不包含任何业务属性,只包含前驱和后继指针:

struct list_head {
    struct list_head *prev, *next;
};

那么如何将链表节点嵌入到进程控制块(PCB)中呢?Linux的做法是:在PCB结构体内部定义一个list_head类型的成员。例如:

struct task_struct {
    // ... 很多字段
    struct list_head run_list;  // 用于挂入调度队列
    struct list_head wait_list; // 用于挂入等待队列
    // ...
};

关键技巧:已知结构体内某个成员的地址,如何反推出整个结构体的起始地址?通过计算成员在结构体中的偏移量。内核提供了container_of宏:

#define container_of(ptr, type, member) \
    ((type *)((char *)(ptr) - offsetof(type, member)))

这样就可以通过链表节点的指针,找到整个PCB的起始地址。

利用这种技术,一个PCB可以同时属于多个队列:全局进程链表、每个CPU的运行队列、设备的等待队列等。这就实现了灵活的组织。
在这里插入图片描述
在这里插入图片描述

偏移量

求对象d在obj中的偏移量
因为系统是64位取地址占8字节,强转(int)取地址是4字节,所以用(long long)

在这里插入图片描述

d的偏移量是16
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
遍历用偏移量找到pid

内核为什么要这么做
1.对内核对象进行管理,更具有通用性
2struct task_struct
{
//进程其他属性
struct list_head link;
struct list_head queue_link
struct list_head hash;
}
一个进程插入多个结构
一个struct task_struct就可以属于双链表,也可以属于调度队列,未来还可以属于任何结构

阻塞状态:

比如c语言中的scanf,输入时,用户没有输入
在这里插入图片描述

操作系统管理硬件,也要先描述在组织,所有它所描述的对应硬件结构体里,也可以包含进程相关的链接字段
当一个进程正在运行,比如执行scanf,发现底层的硬件没有就绪,就会把进程pcb从cpu调度中剥离下来,放到比如键盘的等待队列中,进程就不被调度了,就阻塞了,当硬件准备好了,操作系统又会把等待队列中的把对应的等待进程重新放回运行队列里
进而进行调用读取

进程状态

基本状态

在操作系统学科中,进程有三种基本状态:运行态就绪态阻塞态。此外还有挂起态

  • 运行态:进程正在CPU上执行。
  • 就绪态:进程已经准备好,只等CPU调度。
  • 阻塞态:进程因为等待某种资源(如键盘输入、磁盘I/O)而主动放弃CPU。
  • 挂起态:当内存资源严重不足时,操作系统会把部分进程的代码和数据交换到磁盘的swap分区,以释放内存空间。挂起可以发生在阻塞态或就绪态上。这个过程对用户透明,操作系统不会主动告诉用户“我把你的进程挂起了”。

进程状态本质上是 task_struct 中的一个整型变量。从操作系统原理的角度,进程的基础状态主要有三种:就绪态(等待 CPU)、运行态(正在占用 CPU)、阻塞态(等待 I/O 或事件)。当系统内存紧张时,还会将某些进程的代码和数据换出到磁盘,形成挂起态(又可细分为静止就绪和静止阻塞)。

阻塞状态

例如在 C 语言中执行 scanf,如果用户没有输入,硬件设备(如键盘)就未就绪。

操作系统管理硬件也遵循“先描述,再组织”的原则,硬件的结构体中会包含与进程相关的链接字段。

当进程执行 scanf 发现底层硬件未就绪时,操作系统会将进程的 PCB 从 CPU 调度队列中移除,放入该硬件(如键盘)的等待队列中。此时进程不再被调度,进入阻塞状态。

当硬件准备就绪后,操作系统会将等待队列中的进程重新放回运行队列,继续执行。

阻塞的本质

当一个进程执行到scanf等待键盘输入时,如果键盘没有数据,进程就会进入S状态(可中断睡眠)。这是Linux中的一种阻塞状态。

它的底层过程是:

1. 进程正在CPU上运行,调用scanf。
2. 操作系统发现键盘设备尚未就绪(没有数据可读)。
3. 操作系统将当前进程的PCB从调度队列中摘下。
4. 将该PCB挂入键盘设备描述结构体的等待队列中。
5. 进程状态从R改为S。
6. 当用户敲击键盘,键盘产生中断,驱动程序获知设备就绪。
7. 驱动程序将等待队列中的PCB摘下,重新放回调度队列。
8. 进程状态从S改回R。
9. CPU调度该进程,继续执行scanf,读取数据。

总结:所谓的阻塞,就是进程在等待某种资源。它等待什么资源,就把自己的PCB放到那个资源的等待队列里。

3. 挂起状态

核心概念:用时间换取内存空间。当系统物理内存紧张时,操作系统会将暂时无法运行的进程的代码段、数据段从物理内存移至磁盘的 Swap 分区 暂存,仅保留 PCB。待条件满足后,再将数据从磁盘调入内存,加入调度队列等待 CPU 执行。

内存紧张时,系统频繁做进程换入换出,本质就是用 IO 时间成本换取内存空间,以此腾出运行空间。
即便开启阻塞挂起、运行挂起,内存依旧严重不足时,系统就会执行杀进程操作,直接终止占用资源多的进程,彻底释放内存。
日常电脑、手机软件无故闪退,大多就是系统内存耗尽,主动清理杀掉进程导致。

两种挂起区分
阻塞挂起:进程处于等待阻塞状态时,提前将代码数据换到磁盘分区,等待条件满足后,再调回内存、加入调度队列等待运行。
就绪挂起:进程在就绪调度队列排队,还未轮到 CPU 执行,就先把数据转出磁盘,轮到调度再调入内存执行。
挂起 = 数据换出存磁盘,激活 = 数据换回内存,频繁读写磁盘外设,会大幅拖慢系统运行速度,这就是多开软件设备卡顿的核心原因。
补充知识
操作系统安装时,会自动划分交换分区,专门用来存放被挂起转出的进程数据。Windows、Linux 系统都可自行查看磁盘分区,直观看到该分区空间。

挂起分类:

阻塞挂起:进程因等待外设响应、系统资源或信号等进入阻塞等待队列,内存不足时,其代码与数据被换出至 Swap 分区释放内存。

就绪挂起:进程已准备就绪,在就绪队列中等待 CPU 调度。内存压力大时,系统会提前将闲置的就绪进程置换到 Swap 分区,为活跃进程腾出内存。

Swap 分区细节:

作用:充当“内存备胎”,实现逻辑内存扩容;统一存放被挂起进程的代码与数据;缓解物理内存满载导致的系统运行阻塞。

弊端:底层依赖磁盘 I/O,读写速度远低于物理内存。分区过大易导致系统过度依赖 Swap,频繁触发换入换出操作,消耗系统资源,降低整机效率,使进程唤醒加载耗时增加,出现明显卡顿。

大小建议:

小内存设备:Swap 容量 = 物理内存容量
常规办公设备:Swap 容量 = 物理内存的 1/2
16G 及以上大内存设备:配置 2~4G 即可

服务器场景:主流云服务器默认关闭 Swap 分区,以节省磁盘资源、避免高频 I/O 损耗,保障高性能稳定运行。

系统弊端:进程频繁换入换出产生海量磁盘 I/O,拉高系统负载;闲置进程唤醒需二次加载数据,启动和响应速度变慢;日常多开软件卡顿、后台程序掉线,核心原因往往是内存爆满触发进程挂起置换。

内存耗尽应急机制(OOM) :当进程挂起和 Swap 全部用尽后内存依然严重不足时,操作系统会启动 OOM 进程查杀机制,自动筛选高内存占用、低优先级的后台进程强制终止,彻底释放其占用的全部内存资源,以保障系统核心进程稳定运行。

实际表现:移动端 APP 无故闪退、后台应用被清理;服务端高并发场景业务进程宕机、网站服务崩溃。

理论状态与 Linux 实际状态的区别:教材理论中的进程状态通常更完整,包括新建态、就绪态、运行态、阻塞态、挂起态等,划分细致。而 Linux 实际内核实现中的状态分类会略有不同,更为具体。

挂起状态的补充

挂起状态在Linux中并没有单独的状态码显示。为什么?打个比方:你家里很穷,你上大学的生活费是父母借钱凑的,但父母不会每次给你钱时都说“这是借来的”。操作系统也一样,它把进程的代码和数据换出到swap分区,但不会在状态上体现出来。用户看到的仍然是S或R状态。这是Linux对用户的友好隐藏。

整体流程总结:

内存充足:所有进程常驻物理内存,CPU 直接调度执行。
内存紧张:闲置的阻塞或就绪进程触发挂起,数据存入 Swap 分区释放内存。
进程就绪:从 Swap 调取数据重回内存,加入就绪队列等待调度。
内存爆满:挂起机制失效,系统自动查杀多余进程,兜底保障系统运行。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐