深入理解进程切换:操作系统多任务的核心引擎
进程切换是操作系统实现多任务并发的核心技术,通过在不同进程间快速切换CPU执行权,使单核处理器能"同时"运行多个程序。关键点包括:1) 进程切换触发时机(主动放弃或被动剥夺);2) 核心数据结构(PCB保存进程状态);3) 完整切换步骤(保存/恢复上下文、切换页表等)。Linux采用统一任务模型和CFS调度器优化进程切换性能。虽然切换过程带来直接开销(寄存器保存、页表切换等)和
目录
深入理解进程切换:操作系统多任务的核心引擎
在计算机世界中,我们经常看到多个程序"同时"运行——浏览器播放视频、编辑器处理文本、下载管理器在后台工作。这背后的魔法就是进程切换,它是现代操作系统的核心技术。
1. 什么是进程切换?
进程切换(Context Switching)是操作系统将CPU的执行权从一个正在运行的进程转移到另一个就绪进程的过程。
这是实现多任务操作系统的核心技术,让单个CPU能够"同时"运行多个程序。
直观理解
想象CPU是一个舞台,进程是演员:
• 每个演员轮流上台表演(运行)
• 导演(操作系统)在合适的时机更换演员
• 观众(用户)感觉所有节目都在同时进行
为什么需要进程切换?
// 没有进程切换的世界:一个程序阻塞,整个系统卡死
while(1) {
// 如果这个读取操作很慢,整个系统都会等待
data = read_from_network(); // 阻塞操作
process_data(data);
}
进程切换解决了三大核心问题:
- 实现多任务并发:单核CPU"同时"运行多个程序
- 提高资源利用率:当一个进程等待I/O时,CPU可以执行其他进程
- 保证系统响应性:确保用户交互能及时得到处理
2. 进程的基础概念
2.1 进程的四大特性
竞争性
// 系统中众多进程竞争有限的CPU资源
进程A(编辑器) ↔ 进程B(浏览器) ↔ 进程C(音乐播放器)
↓ ↓ ↓
竞争CPU时间片 竞争CPU时间片 竞争CPU时间片
// 解决方案:优先级调度
nice -n -5 ./high_priority_app // 高优先级
nice -n 10 ./low_priority_app // 低优先级
独立性
// 每个进程有独立的虚拟地址空间
进程A地址空间: 0x0000-0xFFFF → 物理内存区域X
进程B地址空间: 0x0000-0xFFFF → 物理内存区域Y
// 进程崩溃互不影响
进程A访问非法内存 → 仅进程A崩溃
进程B继续正常运行
并行 vs 并发
// 并行:真正的多核同时执行
CPU核心0: [进程A] ← 同时运行
CPU核心1: [进程B] ← 同时运行
CPU核心2: [进程C] ← 同时运行
// 并发:单核上的时间分片
时间轴: [A:10ms]→[B:10ms]→[C:10ms]→[A:10ms]...
↑ 宏观同时,微观交替
2.2 进程的状态模型
新建 → 就绪 ↔ 运行 → 终止
↕
阻塞
- 运行:进程正在CPU上执行
- 就绪:进程已准备好,等待CPU分配
- 阻塞:进程等待I/O操作完成
- 新建/终止:进程的创建和销毁状态
3. 进程切换的完整机制
3.1 触发时机
主动放弃
// 进程自愿让出CPU
sleep(2); // 等待时间
read(file_fd, buffer, size); // 等待磁盘I/O
wait(&status); // 等待子进程
sem_wait(&semaphore); // 等待信号量
被动剥夺
// 操作系统强制切换
时间片用完 → 时钟中断 → 调度器介入
更高优先级进程就绪 → 抢占当前进程
硬件中断完成 → 唤醒等待进程
3.2 核心数据结构
进程控制块(PCB)
struct task_struct {
// 进程标识
int pid;
int state; // 运行状态
// 上下文信息
struct context context;
// 内存管理
mm_struct *mm; // 内存描述符
pgd_t *pgd; // 页表基址
// 调度信息
int priority;
long counter; // 时间片计数器
int nice;
// 资源信息
files_struct *files; // 打开文件表
// ...
};
上下文结构
struct context {
// 通用寄存器
unsigned long rax, rbx, rcx, rdx;
unsigned long rsi, rdi, rbp, rsp;
// 控制寄存器
unsigned long rip; // 程序计数器
unsigned long cs, ds; // 段寄存器
unsigned long eflags; // 标志寄存器
// 浮点寄存器状态
struct fpu_state fpu;
};
3.3 进程切换的详细步骤
中断发生
↓
保存当前进程上下文
↓
更新当前进程PCB状态
↓
调度器选择新进程
↓
切换地址空间(页表)
↓
恢复新进程上下文
↓
跳转到新进程执行
具体实现代码:
// 保存上下文
void save_context(struct task_struct *prev) {
// 保存通用寄存器
prev->context.rax = get_rax();
prev->context.rbx = get_rbx();
prev->context.rsp = get_rsp();
prev->context.rip = get_return_addr();
// 保存浮点状态(惰性保存)
if (prev->used_math)
save_fpu(&prev->fpu);
}
// 恢复上下文
void restore_context(struct task_struct *next) {
// 切换地址空间
write_cr3(next->pgd);
// 恢复寄存器
set_rax(next->context.rax);
set_rbx(next->context.rbx);
set_rsp(next->context.rsp);
// 恢复浮点状态
if (next->used_math)
restore_fpu(&next->fpu);
}
// 完整的上下文切换
void context_switch(struct task_struct *prev,
struct task_struct *next) {
// 1. 保存前一个进程状态
save_context(prev);
// 2. 更新进程状态
prev->state = TASK_READY;
next->state = TASK_RUNNING;
current = next;
// 3. 切换内存空间
switch_mm(prev->mm, next->mm, next);
// 4. 恢复新进程状态
restore_context(next);
// 5. 更新统计
switch_count++;
prev->context_switches++;
}
4. Linux中的进程切换实现
4.1 统一的任务模型
Linux不严格区分进程和线程,都是task_struct:
// 通过clone标志控制资源共享程度
clone(CLONE_VM | CLONE_FS, 0); // 线程方式(共享地址空间和文件系统)
clone(SIGCHLD, 0); // 进程方式(不共享)
4.2 CFS完全公平调度器
struct sched_entity {
struct load_weight load;
struct rb_node run_node;
u64 vruntime; // 虚拟运行时间
// ...
};
// 总是选择vruntime最小的任务
static struct task_struct *pick_next_task_fair(void) {
struct sched_entity *se = __pick_first_entity(cfs_rq);
return task_of(se);
}
4.3 切换时机
// 主要发生在以下路径:
1. 中断返回路径 (iret)
2. 系统调用返回 (sysret)
3. 显式调用schedule()
4. 阻塞操作中
5. 性能分析与优化
5.1 进程切换的开销
直接开销:
| 操作 | 大致周期数 | 说明 |
|---|---|---|
| 保存/恢复寄存器 | 100-500 | 通用寄存器、浮点寄存器 |
| 切换页表 | 200-1000 | 写入CR3,TLB处理 |
| 调度算法 | 100-500 | 选择下一个进程 |
| 缓存失效 | 500-2000+ | 指令和数据缓存失效 |
间接开销可能比直接开销大数倍:
// 缓存局部性完全失效
新进程的:
- 指令不在I-Cache中
- 数据不在D-Cache中
- 页表不在TLB中
- 分支预测历史不匹配
5.2 实际监控方法
# 查看系统级上下文切换
$ vmstat 1
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
1 0 0 123456 78900 456789 0 0 25 30 500 1200 20 5 75 0 0
# cs列:每秒上下文切换次数
# 进程级监控
$ pidstat -w -p 1234 1
Linux 5.4.0-xx-generic (host) 06/12/2023 _x86_64_ (4 CPU)
11:30:00 AM UID PID cswch/s nvcswch/s Command
11:30:01 AM 1000 1234 50.00 20.00 my_program
# 使用perf进行性能分析
$ perf stat -e context-switches,cpu-migrations ./my_app
5.3 优化技术
线程技术
// 线程切换开销远小于进程切换
优势:
- 共享地址空间 → 无页表切换
- 共享资源 → 上下文更小
- 通信方便 → 共享内存
pthread_create(&thread, NULL, worker, NULL);
惰性上下文切换
// 延迟保存浮点寄存器
// 只有在新进程使用浮点时才保存旧进程的浮点状态
void lazy_fpu_save(struct task_struct *prev) {
if (prev->used_math && !prev->fpu_saved) {
save_fpu(prev);
prev->fpu_saved = 1;
}
}
协程/用户态线程
// 完全在用户空间切换,无内核介入
void coroutine_switch(coroutine_t *from, coroutine_t *to) {
// 只保存调用者保存的寄存器
save_callee_saved_registers(from->regs);
restore_callee_saved_registers(to->regs);
// 切换栈
switch_stack(from->stack_ptr, to->stack_ptr);
}
6. 环境变量与进程执行环境
6.1. 环境变量基础概念
6.1.1 什么是环境变量?
环境变量是操作系统用来指定运行环境参数的动态值,具有全局特性,可以被所有子进程继承。
核心特性:
- 全局性:在整个会话中有效
- 继承性:父进程的环境变量被子进程继承
- 动态性:可以在运行时修改
- 持久性:可以通过配置文件永久保存
6.1.2 环境变量 vs 本地变量
# 本地变量(只在当前shell有效)
MY_VAR="hello"
echo $MY_VAR # 输出: hello
# 子进程无法访问本地变量
bash -c 'echo $MY_VAR' # 输出: (空)
# 环境变量(全局有效,可被子进程继承)
export MY_VAR="hello"
bash -c 'echo $MY_VAR' # 输出: hello
6.2 常见环境变量详解
6.2.1 系统核心环境变量
| 变量名 | 作用 | 示例值 |
|---|---|---|
| PATH | 命令搜索路径 | /usr/bin:/bin:/usr/local/bin |
| HOME | 用户主目录 | /home/username |
| USER | 当前用户名 | username |
| SHELL | 当前shell | /bin/bash |
| PWD | 当前工作目录 | /home/username/projects |
| LANG | 系统语言 | en_US.UTF-8 |
| TERM | 终端类型 | xterm-256color |
1. PATH - 命令搜索路径
__ 作用原理__
# 当输入命令时,系统按PATH中的顺序搜索可执行文件
$ echo $PATH
/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games
# 执行命令时的搜索过程:
ls → [/usr/local/bin/ls] → [/usr/bin/ls] → [/bin/ls] ✅
__ 实际应用__
# 查看PATH
echo $PATH
# 添加自定义路径到PATH
export PATH=$PATH:/home/user/my_tools
# 或者添加到开头(优先搜索)
export PATH=/home/user/my_tools:$PATH
# 临时为单个命令添加PATH
PATH=$PATH:/custom/path command_name
# 查看命令所在路径
which ls # 输出: /bin/ls
whereis ls # 输出: ls: /bin/ls /usr/share/man/man1/ls.1.gz
__ 故障排查__
# 命令找不到的常见原因
$ hello
-bash: hello: command not found
# 解决方法1:使用完整路径
./hello # 当前目录
/home/user/projects/hello # 绝对路径
# 解决方法2:添加到PATH
export PATH=$PATH:$(pwd) # 添加当前目录
hello # 现在可以执行了
2. HOME - 用户主目录
__ 基本使用__
# 查看HOME目录
echo $HOME # 输出: /home/username
# ~ 是 HOME 的快捷方式
echo ~ # 输出: /home/username
cd ~ # 切换到主目录
cd # 同上,默认切换到HOME
# 实际应用
cp file.txt ~/Documents/ # 复制到主目录下的Documents
ls ~/Downloads # 列出下载目录
用户差异
# 不同用户的HOME不同
$ whoami
john
$ echo $HOME
/home/john
$ sudo su - root
# echo $HOME
/root
__ 在编程中的应用__
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pwd.h>
int main() {
// 方法1: 通过环境变量
char *home1 = getenv("HOME");
printf("通过环境变量: %s\n", home1);
// 方法2: 通过系统调用
char *home2 = getpwuid(getuid())->pw_dir;
printf("通过系统调用: %s\n", home2);
// 方法3: 标准定义(POSIX)
char home3[256];
snprintf(home3, sizeof(home3), "%s", getenv("HOME"));
printf("标准方式: %s\n", home3);
return 0;
}
3. USER - 当前用户名
__ 基本使用__
# 查看当前用户
echo $USER # 输出: john
whoami # 输出: john
# 实际应用场景
if [ "$USER" = "root" ]; then
echo "Running as root, be careful!"
else
echo "Running as $USER"
fi
__ 相关环境变量__
# 用户相关的其他变量
echo $USER # 当前用户名
echo $LOGNAME # 登录名(通常与USER相同)
echo $UID # 用户ID
echo $GROUPS # 用户所属组
# 示例输出
# USER=john
# LOGNAME=john
# UID=1000
# GROUPS=1000,4,24,27,30,46,116,1001
4. SHELL - 当前Shell
__ 查看和识别__
# 查看当前SHELL
echo $SHELL # 输出: /bin/bash
# 查看所有可用shell
cat /etc/shells
# 输出示例:
# /bin/sh
# /bin/bash
# /usr/bin/bash
# /bin/rbash
# /usr/bin/rbash
# /bin/dash
# /usr/bin/dash
# 查看当前shell进程
echo $$ # 输出当前shell的PID
ps -p $$ # 查看当前shell的详细信息
__ 切换Shell__
# 临时切换
/bin/zsh # 切换到zsh
exit # 返回原来的shell
# 永久更改默认shell
chsh -s /bin/zsh # 更改当前用户的默认shell
# 需要输入密码,下次登录生效
5. PWD - 当前工作目录
__ 使用示例__
# 查看当前目录
echo $PWD # 输出: /home/user/projects
# 与命令对比
pwd # 输出: /home/user/projects
echo $PWD # 输出: /home/user/projects
# 在脚本中的使用
#!/bin/bash
echo "脚本在目录: $PWD 中执行"
cd /tmp
echo "现在切换到: $PWD"
编程应用
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
// 方法1: 通过环境变量
char *pwd_env = getenv("PWD");
if (pwd_env) {
printf("当前目录(PWD): %s\n", pwd_env);
}
// 方法2: 通过系统调用
char cwd[1024];
if (getcwd(cwd, sizeof(cwd)) != NULL) {
printf("当前目录(getcwd): %s\n", cwd);
}
// 比较两者是否一致
if (pwd_env && strcmp(pwd_env, cwd) == 0) {
printf("PWD和实际目录一致\n");
} else {
printf("PWD和实际目录不一致!\n");
}
return 0;
}
6. LANG - 系统语言和区域设置
__ 语言和编码设置__
# 查看当前语言设置
echo $LANG # 输出: en_US.UTF-8
# 查看所有区域设置
locale # 显示所有区域设置
# 输出示例:
# LANG=en_US.UTF-8
# LC_CTYPE="en_US.UTF-8"
# LC_NUMERIC="en_US.UTF-8"
# LC_TIME="en_US.UTF-8"
# ...
# 可用区域设置
locale -a # 列出所有可用的区域设置
设置语言环境
# 临时设置英文
export LANG=en_US.UTF-8
# 临时设置中文
export LANG=zh_CN.UTF-8
# 永久设置(添加到 ~/.bashrc)
echo 'export LANG=en_US.UTF-8' >> ~/.bashrc
echo 'export LC_ALL=en_US.UTF-8' >> ~/.bashrc
__ 相关环境变量__
# 完整的区域设置变量
echo $LANG # 默认区域设置(后备)
echo $LC_ALL # 覆盖所有区域类别
echo $LC_CTYPE # 字符分类和大小写转换
echo $LC_COLLATE # 排序和正则表达式
echo $LC_MONETARY # 货币格式
echo $LC_NUMERIC # 数字格式
echo $LC_TIME # 日期和时间格式
echo $LC_MESSAGES # 消息和界面语言
7. TERM - 终端类型
__ 终端类型识别__
# 查看终端类型
echo $TERM # 输出: xterm-256color
# 常见终端类型
# xterm - 基本X11终端
# xterm-256color - 支持256色的xterm
# linux - Linux控制台
# vt100, vt220 - 传统终端模拟
# screen - GNU screen会话
# screen-256color- 支持256色的screen
关键要点:
- PATH:控制命令搜索路径,影响命令执行
- HOME:用户工作基准目录,影响文件操作
- USER/LANG/TERM:影响用户界面和国际化
- PWD:反映当前工作状态
6.3 环境变量管理命令
6.3.1 查看环境变量
# 查看所有环境变量
env
printenv
# 查看特定环境变量
echo $PATH
printenv PATH
echo ${PATH}
# 查看所有变量(包括本地变量)
set
# 查看变量是否存在
echo ${VAR:-"默认值"} # 如果VAR不存在则使用默认值
6.3.2 设置环境变量
# 设置临时环境变量(当前会话有效)
export MY_VAR="value"
export PATH=$PATH:/custom/path
# 一次性设置(仅对当前命令有效)
MY_TEMP="temp" command_name
# 设置本地变量(不导出到环境)
MY_LOCAL="local_value"
# 变量引用和扩展
export NAME="John"
echo "Hello, $NAME" # 输出: Hello, John
echo "Hello, ${NAME}!" # 输出: Hello, John!
6.3.3 删除和修改环境变量
# 删除环境变量
unset MY_VAR
# 修改变量值
export PATH="/new/path:$PATH" # 添加到开头
export PATH="$PATH:/new/path" # 添加到结尾
# 字符串操作
export NAME="John Doe"
echo ${NAME% *} # 输出: John (删除空格后的部分)
echo ${NAME#* } # 输出: Doe (删除空格前的部分)
echo ${NAME/J/Joh} # 输出: John Doe (替换)
6.4 环境变量的作用
环境变量为进程提供执行环境配置:
# 查看环境变量
echo $PATH
echo $HOME
# 设置环境变量
export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
export CUSTOM_CONFIG="production"
6.5 在代码中访问环境变量
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[], char *envp[]) {
// 方法1: main函数的第三个参数
printf("环境变量:\n");
for (int i = 0; envp[i] != NULL; i++) {
printf("%s\n", envp[i]);
}
// 方法2: environ全局变量
extern char **environ;
for (int i = 0; environ[i] != NULL; i++) {
printf("%s\n", environ[i]);
}
// 方法3: getenv获取特定变量
char *path = getenv("PATH");
char *home = getenv("HOME");
printf("PATH: %s\n", path);
printf("HOME: %s\n", home);
return 0;
}
6.6 环境变量的全局属性
# 环境变量可以被子进程继承
export MY_VAR="parent_value"
./child_program # 子进程可以读取MY_VAR
# 普通变量不会被继承
MY_TEMP="temp_value"
./child_program # 子进程看不到MY_TEMP
昇腾计算产业是基于昇腾系列(HUAWEI Ascend)处理器和基础软件构建的全栈 AI计算基础设施、行业应用及服务,https://devpress.csdn.net/organization/setting/general/146749包括昇腾系列处理器、系列硬件、CANN、AI计算框架、应用使能、开发工具链、管理运维工具、行业应用及服务等全产业链
更多推荐

所有评论(0)