Linux 多进程、多线程、IO 模型(一)
本文最后更新于:2024年10月11日 晚上
进程
进程的概念
进程描述是一个程序执行过程,当程序执行后,执行过程开始,则进程产生, 执行过程结束, 则进程也就结束。
进程的特点
- 进程是一个独立的可调度的活动, 由操作系统进行统一调度, 相应的任务会被调度到 CPU 中进行执行;
- 进程一旦产生,则需要分配相关资源,同时进程是资源分配的最小单位;
进程和程序的区别
- 程序是静态的,它是一些保存在磁盘上的指令的有序集合,没有任何执行的概念;
- 进程是一个动态的概念,它是程序执行的过程,包括了动态创建、调度和消亡的整个过程
并行执行和并发执行
- 并行执行: 表示多个任务能够同时执行,依赖于物理的支持,比如 cpu 是 4 核心,则可以同时执行 4 个任务
- 并发执行: 在同一时间段有多个任务在同时执行,由操作系统调度算法来实现,比较典型的就是时间片轮转
Linux 系统中管理进程的方式
- 每个进程都需要与其他某一个进程建立父子关系, 对应的进程则叫做父进程;
- Linux 系统会为每个进程分配 id , 这个 id 作为当前进程的唯一标识, 当进程结束, 则会回收;
- 进程的 id 与 父进程的 id 分别通过 getpid() 与 getppid() 来获取
- 例如以下代码就是获取当前进程 id 以及父进程 id: # 进程的地址空间 ## 进程的地址空间
1
2
3
4
5
6
7
8#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
printf(" pid : %d ppid : %d\n",getpid(),getppid());
return 0;
}
- 例如以下代码就是获取当前进程 id 以及父进程 id:
- 一旦进程建立之后, 系统则要为这个进程分配相应的资源, 一般 32
位系统会为每个进程分配 4G 的地址空间;
- 4G 的地址空间结构如下:
- 4G 的进程地址空间主要分为两部分:
- (1)0 - 3G: 用户空间;
- (2)3G - 4G: 内核空间;
- 用户空间又具体分为如下区间:
- stack: 存放非静态的局部变量
- heap: 动态申请的内存
- .bss: 未初始化过的全局变量(包括初始化为 0 的, 未初始化过的静态变量 (包括初始化为 0)
- data: 初始化过并且值不为 0 的全局变量, 初始化过的不为 0 静态变量
- .rodata: 只读变量(字符串之类)
- .text: 程序文本段(包括函数,符号常量)
- 4G 的进程地址空间主要分为两部分:
- 4G 的地址空间结构如下:
- 当用户进程需要通过内核获取资源时, 会切换到内核态运行, 这时当前进程会使用内核空间的资源;
- 用户需要切换到内核态运行时, 主要是通过 系统调用
- 在程序执行过程中,操作系统会分配 4G 的虚拟地址空间
- 虚拟地址空间中的每个地址都是一个虚拟地址
- 虚拟地址:虚拟地址并不代表真实的内存空间, 而是一个用于寻址的编号
- 物理地址:是指内存设备中真实存在的存储空间的编号
- 虚拟地址通过映射的方式建立与物理地址的关联,
从而达到访问虚拟地址就可以访问到对应的物理地址
- 在 cpu 中有一个硬件 MMU(内存管理单元), 负责虚拟地址与物理地址的映射管理以及虚拟地址访问
- 操作系统可以设置 MMU 中的映射内存段
- 在操作系统中使用虚拟地址空间主要是基于以下原因:
- 直接访问物理地址, 会导致地址空间没有隔离, 很容易导致数据被修改
- 通过虚拟地址空间可以实现每个进程地址空间都是独立的,操作系统会映射到不用的物理地址区间,在访问时互不干扰
进程的状态管理
- 进程是动态过程,操作系统内核在管理整个动态过程时会使用了状态机
- 给不同时间节点设计一个状态,通过状态来确定当前的过程进度
- 在管理动态过程时,使用状态机是一种非常好的方式
- 进程的状态一般分为如下:
- 运行态 (TASK_RUNNING): 此时进程或者正在运行,或者准备运行, 就绪或者正在进行都属于运行态
- 睡眠态 (): 此时进程在等待一个事件的发生或某种系统资源
- 可中断的睡眠 (TASK_INTERRUPT): 可以被信号唤醒或者等待事件或者资源就绪
- 不可中断的睡眠 (TASK_UNTERRUPT): 只能等待特定的事件或者资源就绪
- 停止态 (TASK_STOPPED): 进程暂停接受某种处理。例如:gdb 调试断点信息处理。
- 僵尸态(TASK_ZOMBIE):进程已经结束但是还没有释放进程资源
进程的相关命令
- ps:
- -A:列出所有的进程
- -e: 与 -A 功能类似
- -w: 显示加宽可以显示较多的资讯
- -au: 显示较详细的信息
- -aux: 显示所有包含其他使用者的进程 事例:ps -ef | grep “可执行文件名” 根据名称查找指定名字
- top:实时显示相关进程
进程的创建
- 为了提高计算机执行任务的效率,一般采用的解决方案就是能够让多个任务同时进行,这里可以使用并发与并行两种方式:
- 并行: 在 cpu 多核的支持下,实现物理上的同时执行
- 并发: 在有限的 cpu 核心的情况下(如只有一个 cpu 核心), 利用快速交替 (时间片轮转) 执行来达到宏观上的同时执行
- 并行是基于硬件完成,而并发则可以使用软件算法来完成, 在完成任务时,可以创建多个进程并发执行
- 创建进程的函数需要调用 fork() 函数, 则会产生一个新的进程
- 调用 fork() 函数的进程叫做 父进程,产生的新进程则为子进程
fork()函数详解
- 函数头文件:
1
2#include <sys/types.h>
#include <unistd.h> - 函数原型
1
pid_t fork(void);
- 函数功能:创建一个子进程
- 函数返回值:
- 成功: 返回给父进程是子进程的 pid , 返回给子进程的是 0
- 失败: 返回 -1, 并设置 errno
示例: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
pid_t cpid;
cpid = fork();
if (cpid == -1){
perror("fork(): ");
return -1;
}
printf("Hello world.\n");
return 0;
// 输出两个 Hello world
}
示例 : 创建一个子进程,并打印 父进程与子进程的 pid: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
static int idata = 200;
int main(void)
{
pid_t pid;
int istack = 300;
pid = fork();
if (pid < 0)
{
perror("fork():");
exit(-1);
}
else if (pid == 0)
{
idata *= 2;
istack *= 3;
}
printf(" %s %d %d\n",(pid == 0)?("child"):("parent"),idata,istack);
return 0;
}
示例: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
pid_t cpid;
write(STDOUT_FILENO,"Hello",6);
fputs("Hello",stdout);
cpid = fork();
if (cpid == -1){
perror("fork(): ");
return -1;
}
return 0;
}
父子进程执行不同的任务
- 使用 fork() 函数之后,会创建子进程,fork()
之后的代码会在父子进程中都执行一遍
- 如果父子进程执行相同的任务,则正常执行
- 如果父子进程执行不同的任务,则需要利用 fork() 函数返回值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
pid_t cpid;
cpid = fork();
if (cpid == -1){
perror("[ERROR] fork()");
exit(EXIT_FAILURE);
}else if(cpid == 0){
printf("Child process task.\n");
exit(EXIT_SUCCESS);
}else if (cpid > 0){
printf("Parent process task.\n");
}
printf("Child and Process Process task.\n");
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
27
28
29
30
31
32#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void)
{
int cpid;
cpid = fork();
if (cpid == -1){
perror("fork(): ");
exit(EXIT_FAILURE);
}else if (cpid == 0){
printf("The child process < %d > running...\n",getpid());
sleep(2);
printf("The child process < %d > has exited\n",getpid());
exit(EXIT_SUCCESS);
}else if (cpid > 0){
cpid = fork();
if (cpid == -1){
perror("fork(): ");
}else if (cpid == 0){
printf("The child process < %d > running...\n",getpid());
sleep(3);
printf("The child process < %d > has exited\n",getpid());
exit(EXIT_SUCCESS);
}else if (cpid > 0){
}
}
return 0;
} - 在进程结束时,需要释放进程地址空间 以及内核中产生的各种数据结构
- 源的释放需要通过调用 exit 函数或者 _exit 函数来完成
- 在程序结束时,会自动调用 exit 函数
- exit 函数让当前进程退出, 并刷新缓冲区
- exit 函数信息如下:
- 函数头文件:
1
#include <stdlib.h>
- 函数原型
1
void exit(int status);
- 函数功能: 结束进程,并刷新缓冲区
- 函数参数:
- status: 退出状态值
- 在系统中定义了两个状态值 : EXIT_SUCCESS: 正常退出 EXIT_FAILURE: 异常退出, 具体定义在 stdlib.h 中
- status: 退出状态值
- 函数头文件:
示例: 创建一个子进程,让子进程延时 3s 后退出: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17int main(void)
{
pid_t cpid;
cpid = fork();
if (cpid == -1){
perror("[ERROR] fork(): ");
exit(EXIT_FAILURE);
}else if(cpid == 0){
printf("Child Process < %d > running...\n",getpid());
sleep(3);
printf("Child Process < %d > has exited.\n",getpid());
exit(EXIT_SUCCESS);
}else if(cpid > 0){
sleep(5);
}
return 0;
}
- _exit 函数头文件
1
#include <unistd.h>
- 函数原型
1
void _exit(int status);
- 函数参数: status: 进程退出的状态值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17int main(void)
{
pid_t cpid;
cpid = fork();
if (cpid == -1){
perror("[ERROR] fork(): ");
exit(EXIT_FAILURE);
}else if(cpid == 0){
printf("Child Process < %d > running...\n",getpid());
sleep(3);
printf("Child Process < %d > has exited.\n",getpid());
_exit(EXIT_SUCCESS);
}else if(cpid > 0){
sleep(5);
}
return 0;
} - exit 函数与 _exit 函数功能相似, 但有很多不同, 具体如下:
- _exit() 属于系统调用, 能够使进程停止运行, 并释放空间以及销毁内核中的各种数据结构
- exit() 基于_exit() 函数实现, 属于库函数, 可以清理 I/O 缓冲区
示例:验证 exit 函数刷新缓冲区: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
pid_t cpid;
cpid = fork();
if (cpid == -1){
perror("[ERROR] fork(): ");
exit(EXIT_FAILURE);
}else if(cpid == 0){
printf("I/O BUFFER.");
sleep(3);
exit(EXIT_SUCCESS);
}else if(cpid > 0){
sleep(5);
}
return 0;
}