cen's blog cen's blog
首页
  • 编程文章

    • Markdown使用
    • C语言程序的运行
  • 学习笔记

    • C++学习
    • C++数据结构
    • MySQL
    • Linux
    • 网络编程
算法
  • Git
  • ProtoBuf
  • 分类
  • 标签
  • 归档
关于
GitHub (opens new window)

cen

十年饮冰,难凉热血
首页
  • 编程文章

    • Markdown使用
    • C语言程序的运行
  • 学习笔记

    • C++学习
    • C++数据结构
    • MySQL
    • Linux
    • 网络编程
算法
  • Git
  • ProtoBuf
  • 分类
  • 标签
  • 归档
关于
GitHub (opens new window)
  • Linux环境搭建
  • 基本指令
  • 权限
  • Linux基础开发工具
  • 进程概念
    • 冯诺依曼体系
    • 操作系统
    • 进程概念
    • 描述进程-PCB
      • task_struct
      • task_struct 内容分类
    • 组织进程
    • 查看进程
      • 系统目录查看
      • ps 命令查看
    • 获取进程的 PID 和 PPID
    • 创建进程
      • fork 创建子进程
      • if 分流
    • 进程状态
      • 运行状态-R
      • 浅度睡眠状态-S
      • 深度睡眠状态-D
    • 僵尸进程-Z
    • 孤儿进程
    • 进程优先级
      • 引入
      • PRI 和 NI
      • 查看进程优先级
    • 环境变量
      • 常见环境变量
      • 环境变量相关命令
    • 进程地址空间
      • 定义
      • 内存映射
  • 进程控制
  • 基础IO流
  • 动态库和静态库
  • 进程通信
  • 进程信号
  • 多线程
  • 线程安全
  • 生产者消费者
  • 线程池
  • 高级IO
  • 多路转接epoll
  • Reactor
  • Linux
cen
2025-02-11
目录

进程概念

# 冯诺依曼体系

我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系:

冯诺依曼体系结构

# 操作系统

操作系统的定位:

简单来说,操作系统就是一款进行软硬件资源管理的软件。

设计操作系统的目的:

  1. 与硬件进行交互,管理所有的软硬件资源。
  2. 为用户程序(应用程序)提供一个良好的执行环境。

# 进程概念

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

可执行程序本质上是一个文件,是放在磁盘上的。当我们双击这个可执行程序将其运行起来时,本质上是将这个程序加载到内存当中了,因为只有加载到内存后,CPU 才能对其进行逐行的语句执行,而一旦将这个程序加载到内存后,我们就不应该将这个程序再叫做程序了,严格意义上将应该将其称之为进程。 进程是操作系统分配资源的基本单位,它包含了程序执行所需的各种资源信息,如代码、数据、系统资源等。每个进程在系统中都是独立存在的,拥有自己的地址空间和运行状态。进程是程序执行的一个实例,通过进程,我们可以实现多任务的并发执行,提高系统的利用率和响应速度。如果有太多加载进来的程序,此时,操作系统就需要进行管理(先描述,后组织)。

# 描述进程-PCB

# task_struct

PCB 实际上是对进程控制块的统称,在 Linux 中描述进程的结构体叫做 task_struct。 task_struct 是 Linux 内核的一种数据结构,它会被装载到 RAM(内存)里并且包含进程的信息。

进程 = 内核数据结构(task_struct) + 进程对应的磁盘代码

# task_struct 内容分类

task_struct 就是 Linux 当中的进程控制块,task_struct 当中主要包含以下信息:

  • 标示符: 描述本进程的唯一标示符,用来区别其他进程。
  • 状态: 任务状态,退出代码,退出信号等。
  • 优先级: 相对于其他进程的优先级。
  • 程序计数器(pc): 程序中即将被执行的下一条指令的地址。
  • 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
  • 上下文数据: 进程执行时处理器的寄存器中的数据。
  • I/O 状态信息: 包括显示的 I/O 请求,分配给进程的 I/O 设备和被进程使用的文件列表。
  • 记账信息: 可能包括处理器时间总和,使用的时钟总和,时间限制,记账号等。
  • 其他信息。

# 组织进程

可以在内核源代码里找到它。所有运行在系统里的进程都以 task_struct 链表的形式存在内核里。

# 查看进程

# 系统目录查看

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

比如:要获取 PID 为 2565167 的进程信息,你需要查看 /proc/2565167 这个文件夹

# ps 命令查看

使用ps aux可以打印出当前所有进程的信息。

比如:要获取 PID 为 2565167 的进程信息,你需要查看进行以下命令:

[cen@VM-8-2-opencloudos proc]$ ps aux | head -1 && ps aux | grep 2565167
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
cen      2565167  0.0  0.1  13856  1808 pts/2    S+   15:54   0:00 ./test
cen      2567165  0.0  0.0 222012  1096 pts/3    S+   15:59   0:00 grep --color=auto 2565167
1
2
3
4

# 获取进程的 PID 和 PPID

进程 id(PID) 父进程 id(PPID)

#include<iostream>

#include <unistd.h>
using namespace std;

#include<sys/types.h>

int main() {
  while(1) {
    sleep(20);
    cout << "我是一个进程:" << "PID:" << getpid() << " PPID:" << getppid() << endl;
  }
  return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

我是一个进程:PID:2578545 PPID:2577206

[cen@VM-8-2-opencloudos ~]$ ps aux | head -1 && ps aux | grep test
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
cen      2578545  0.0  0.1  13856  1892 pts/4    S+   16:30   0:00 ./test
cen      2579086  0.0  0.0 222012  1180 pts/5    S+   16:31   0:00 grep --color=auto test
[cen@VM-8-2-opencloudos ~]$ ps aux | head -1 && ps aux | grep 2577206
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
cen      2577206  0.0  0.2 226016  5144 pts/4    Ss   16:27   0:00 -bash
cen      2579349  0.0  0.0 222012  1120 pts/5    S+   16:32   0:00 grep --color=auto 2577206
1
2
3
4
5
6
7
8

由此,命令行上启动的命令,它的父进程一般都是 bash

# 创建进程

# fork 创建子进程

fork 函数创建子进程:fork 是一个系统调用级别的函数,其功能就是创建一个子进程。(函数执行前,只有父进程;函数执行后,父进程 + 子进程) 返回值:

  • 如果子进程创建成功,在父进程中返回子进程的 PID,而在子进程中返回 0。
  • 如果子进程创建失败,则在父进程中返回 -1。

实际上,使用 fork 函数创建子进程,在 fork 函数被调用之前的代码被父进程执行,而 fork 函数之后的代码,则默认情况下父子进程都可以执行。需要注意的是,父子进程虽然代码共享,但是父子进程的数据各自开辟空间(采用写时拷贝)并且通过返回值的不同,来让父子进程执行不同的代码。

#include<iostream>
#include <unistd.h>
using namespace std;
#include<sys/types.h>
int main() {
  fork();
  cout << "我是一个进程:" << "PID:" << getpid() << " PPID:" << getppid() << endl;
  return 0;
}
1
2
3
4
5
6
7
8
9
[cen@VM-8-2-opencloudos ~]$ ./test
我是一个进程:PID:2607219 PPID:2577206
我是一个进程:PID:2607220 PPID:2607219
1
2
3

分析:运行 cpp 程序,父进程调用 fork(),产生一个子进程,fork()调用后,父子两个进程分别执行 cout 语句,输出各自的 PID 和 PPID。 例如:父进程开始,PID:2607219 PPID:2577206(bash 的 PID),子进程开始,PID:2607220 PPID:2607219(父进程的 PID)。

# if 分流

据返回值的不同来让父子进程执行不同的代码,从而做不同的事。

#include<iostream>

#include <unistd.h>
using namespace std;

#include<sys/types.h>


int main() {
    pid_t id = fork();
    if(id == 0) {
        while(1) {
          cout << "子进程..." << endl;
          sleep(2);
      }
    }
    else if(id > 0) {
        while(1) {
          cout << "父进程" << endl;
          sleep(2);
        }
    }
    else
        // fork error
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

输出结果:父进程、子进程循环打印。

# 进程状态

Linux 操作系统的源代码当中对于进程状态的定义:

/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char * const task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 运行状态-R

一个进程处于运行状态(running),并不意味着进程一定处于运行当中,运行状态表明一个进程要么在运行中,要么在运行队列里。也就是说,可以同时存在多个 R 状态的进程。

# 浅度睡眠状态-S

一个进程处于浅度睡眠状态(sleeping),意味着该进程正在等待某件事情的完成,处于浅度睡眠状态的进程随时可以被唤醒(在等待资源或条件成立时,可以被信号唤醒并中断其等待状态),也可以被杀掉(这里的睡眠有时候也可叫做可中断睡眠(interruptible sleep))。

# 深度睡眠状态-D

一个进程处于深度睡眠状态(disk sleep),表示该进程不会被杀掉,即便是操作系统也不行,只有该进程自动唤醒才可以恢复。该状态有时候也叫不可中断睡眠状态(uninterruptible sleep),通常发生在进程等待某些关键资源或执行不能被中断的操作时,处于这个状态的进程会等待 IO 的结束。

例如,某一进程要求对磁盘进行写入操作,那么在磁盘进行写入期间,该进程就处于深度睡眠状态,是不会被杀掉的,因为该进程需要等待磁盘的回复(是否写入成功)以做出相应的应答。(磁盘休眠状态)

# 僵尸进程-Z

一个进程在退出的时候,不能立即释放对用的资源,保存一段时间后,让父进程或 OS 来进行读取,如果没有回收,子进程的进程描述符仍然会保留在系统中,此时的状态就是僵尸状态。

僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入 Z 状态。

僵尸进程的处理方法:

  • 使用 wait()或 waitpid()系统调用:父进程可以通过这些系统调用来等待子进程的终止,并获取子进程的终止状态。当父进程调用这些系统调用时,子进程的状态信息会被完全清除,从而不再是僵尸进程。
  • 使用信号处理:当子进程终止时,会发送 SIGCHLD 信号给父进程。父进程可以设置一个信号处理函数,在该函数中调用 wait()或 waitpid()来处理子进程的终止状态。

# 孤儿进程

如果父进程先退出,子进程就称之为“孤儿进程”。孤儿进程被1号init进程领养,退出的时候被 init 进程回收,因此,即使原始父进程结束,孤儿进程仍然会在系统中运行,直到它们自己正常结束或被终止。 如果一个进程变成孤儿,会自动变成后台进程,init 进程会周期性地调用 wait()或类似的系统调用来回收已结束的孤儿进程的资源,并释放它们在进程表中所占用的条目。

# 进程优先级

# 引入

cpu 资源分配的先后顺序,就是指进程的优先权(priority)。优先权高的进程有优先执行权利。配置进程优先权对多任务环境的 linux 很有用,可以改善系统性能,还可以把进程运行到指定的 CPU 上,这样一来,把不重要的进程安排到某个 CPU,可以大大改善系统整体性能。

# PRI 和 NI

执行ps -l来查看系统进程:

F S   UID     PID    PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
4 S  1001  365554  365551  0  80   0 -  2063 do_wai pts/0    00:00:00 bash
0 R  1001  365795  365554  0  80   0 -  2736 -      pts/0    00:00:00 ps
1
2
3

其中:

  • UID : 代表执行者的身份
  • PID : 代表这个进程的代号
  • PPID :代表这个进程是由哪个进程发展衍生而来的,即父进程的代号
  • PRI :代表这个进程可被执行的优先级,值越小越早被执行
  • NI :代表这个进程的 nice 值

理解: NI 就是我们所要说的 nice 值,其表示进程可被执行的优先级的修正数值,PRI 值越小越快被执行,那么加入 nice 值后,将会使得 PRI 变为:PRI(new) = PRI(old)(80) + nice,所以,调整进程优先级,在 Linux 下,就是调整进程 nice 值(PRI(old)默认等于 80,nice 其取值范围是-20 至 19,一共 40 个级别)。

# 查看进程优先级

  • ps -al

使用 ps -al 命令查看该进程优先级的信息

  • top 通过 top 命令更改进程的 nice 值,top 命令就相当于 Windows 操作系统中的任务管理器,它能够动态实时的显示系统当中进程的资源占用情况。

具体:按 R,输入更改进程的 UID,进行更改 nice 值,最后按 Q 退出

# 环境变量

环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数,环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性。

# 常见环境变量

  • PATH : 指定命令的搜索路径
  • HOME : 指定用户的主工作目录(即用户登陆到 Linux 系统中时,默认的目录)
  • SHELL : 当前 Shell,它的值通常是/bin/bash
  • ...
  1. 查看环境变量方法: echo $NAME NAME:你的环境变量名称
  2. 代码获取环境变量的方法: cout << getenv("PATH") << endl;或者通过第三方变量 environ 获取
	extern char **environ;  // 一个环境变量指针数组,指向HOME、SHELL等等
	int i = 0;
	for(; environ[i]; i++){
	    cout << environ[i] << endl;
 	}
1
2
3
4
5

# 环境变量相关命令

  1. echo: 显示某个环境变量值
  2. export: 设置一个新的环境变量
  3. env: 显示所有环境变量
  4. unset: 清除环境变量
  5. set: 显示本地定义的 shell 变量和环境变量

# 进程地址空间

# 定义

进程地址空间是 Linux 操作系统为每个进程分配的一个独立的虚拟内存空间。这个空间是进程在内存中的逻辑表示,它包含了程序执行所需的各种资源和数据。页表则是 Linux 操作系统实现进程地址空间与物理内存映射的关键数据结构。通过页表,Linux 能够确保每个进程都拥有自己独立的地址空间,实现进程的隔离和保护。

进程地址空间就类似于一把尺子,尺子按照刻度被划分为各个区域(内核空间约 1GB,其余部分占 3GB)。 例如代码区、堆区、栈区等。而在结构体 mm_struct 当中,便记录了各个边界刻度,例如代码区的开始刻度与结束刻度,如下图所示:

在结构体 mm_struct 当中,各个边界刻度之间的每一个刻度都代表一个虚拟地址, 这些虚拟地址通过页表映射与物理内存建立联系。由于虚拟地址是线性增长的,所以虚拟地址又叫做线性地址。

进程地址空间里面不是物理地址,而是虚拟地址(线性地址)。物理地址用户一概是看不到的,是由操作系统统一进行管理的(通过页表映射:进程 -- 映射 --> 物理地址)

注意点:

  1. 虚拟内存:每个进程都有一个虚拟地址空间,这个空间的大小通常远大于物理内存的大小。操作系统使用虚拟内存技术,将进程的虚拟地址空间映射到物理内存和磁盘上的交换空间(swap space)。
  2. 页面映射:虚拟地址空间被划分为多个固定大小的页面(通常是 4KB),每个页面可以映射到物理内存的一个页面,或者被交换出去存放在磁盘上。当进程访问某个页面时,如果该页面不在物理内存中,操作系统会触发一个页面错误(page fault),然后将该页面从磁盘加载到物理内存中。
  3. 写时复制(Copy-on-Write):在 fork()调用后,子进程和父进程的地址空间是共享的,但使用了写时复制技术。这意味着,除非其中一个进程试图写入其地址空间中的某个页面,否则这两个进程仍然共享相同的物理内存页面。一旦有写入操作发生,操作系统会为该页面创建一个新的物理内存副本,并将写入操作应用到该副本上,以确保两个进程不会相互干扰。
  4. 地址空间保护:操作系统会确保每个进程只能访问其自己的地址空间,而不能直接访问其他进程的地址空间,这保证了进程之间的独立性。
  5. 动态内存分配:进程可以在运行时动态地请求和释放内存。操作系统会管理这些请求,并在物理内存中为进程分配和回收页面。

# 内存映射

页表是操作系统中用于实现虚拟内存管理的重要数据结构。它建立起了虚拟地址与物理地址之间的映射关系,使得进程可以使用连续且统一的虚拟地址空间,而实际上这些虚拟地址在物理内存中可能是分散且不连续的。 具体来说,页表将虚拟内存划分为固定大小的页面(通常为 4KB),并将每个虚拟页面的地址映射到物理内存中的某个页帧。每个页表项包含了虚拟页面的页号以及对应的物理页帧号,以及其他一些属性信息,如访问权限、是否驻留在内存中等。

以二级页表为例:

  1. 页表将虚拟内存划分为固定大小的页面(通常为 4KB),并拆分为 3 部分:页目录索引(10bit),页表索引(10bit)和页内偏移(12bit)
  2. 每个进程有一个页目录,其物理地址保存在 CPU 的 CR3 寄存器中,CPU 取出页目录索引 * 4(每个条目 4 字节),加上 CR3 中的页目录基址,找到对应的页目录项 (PDE),PDE 里存储的是下级页表的物理地址
  3. 从找到的 PDE 中取出页表的物理地址,CPU 取出页表索引 * 4,加上页表物理地址,找到对应的页表项 (PTE),PTE 里存储的就是进程需要的物理页框号 (PPN)
  4. 将 PTE 中的物理页框号 * 4KB,得到物理页的起始地址,再加上虚拟地址中的页内偏移,就得到了最终的物理地址
上次更新: 2026/04/14, 21:59:44
Linux基础开发工具
进程控制

← Linux基础开发工具 进程控制→

最近更新
01
位运算
04-21
02
单例模式
04-14
03
分治和归并
04-14
更多文章>
Theme by Vdoing | Copyright © 2024-2026 京ICP备2020044002号-3 京公网安备11010502056119号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式