【Linux】进程概念
本文基于Linux内核核心知识,系统讲解进程相关核心概念、底层实现机制及实战操作,涵盖进程管理、状态切换、优先级调度、虚拟地址空间等关键内容,适合Linux编程入门及进阶开发者参考。
一、计算机系统基础:冯诺依曼体系结构
1.1 核心组成与数据流向
现代计算机(笔记本、服务器)均遵循冯诺依曼体系结构,核心组件包括:
- 输入设备:键盘、鼠标、扫描仪等
- 中央处理器(CPU):包含运算器和控制器
- 存储器:特指内存(而非硬盘等外部存储)
- 输出设备:显示器、打印机等
关键原则:所有设备只能直接与内存交互,CPU仅能对内存进行读写操作,外设数据输入输出必须经过内存中转。
1.2 举例理解:QQ聊天的数据流动过程
以QQ聊天为例,数据流向如下:
- 输入阶段:通过键盘输入消息 → 写入内存
- 处理阶段:CPU从内存读取消息数据,进行协议封装等处理后写回内存
- 传输阶段:网卡从内存读取封装后的数据,通过网络发送
- 接收阶段:对方网卡接收数据 → 写入内存
- 输出阶段:CPU读取内存中的接收数据,处理后写回内存 → 显示器从内存读取数据并显示
发送文件时流程类似,仅数据规模更大,需经过分块、校验等额外处理步骤。
二、操作系统:进程管理的核心载体
2.1 操作系统的定位与组成
- 狭义OS:内核(Kernel),负责进程管理、内存管理、文件管理、驱动管理
- 广义OS:内核+外壳程序(shell)、函数库(glibc)、系统级软件等
核心功能:对下管理软硬件资源,对上为应用程序提供稳定的执行环境,是硬件与用户程序之间的中间层。
2.2 操作系统的"管理"本质
管理的核心逻辑可概括为两步:
- 描述被管理对象:通过结构体(如C语言struct)记录对象属性
- 组织被管理对象:通过链表、红黑树等数据结构高效组织对象
例如进程管理:先用task_struct结构体描述进程属性,再用双链表将所有进程组织起来便于调度。
2.3 系统调用与库函数
- 系统调用:操作系统暴露的底层接口,功能基础,使用门槛高
- 库函数:对系统调用的封装,简化开发,如printf封装write系统调用
三、进程核心概念与实战操作
3.1 进程的定义
- 课本概念:程序的一个执行实例,正在执行的程序
- 内核视角:分配系统资源(CPU时间、内存)的基本实体
- 本质:进程 = 内核数据结构(task_struct) + 程序代码和数据
3.2 进程控制块(PCB):task_struct详解
进程的所有属性都保存在PCB中,Linux下的PCB具体表现为task_struct结构体,存储在内存中,核心内容包括:
- 标识符(PID):进程唯一标识
- 状态:进程当前运行状态(运行、睡眠等)
- 优先级:进程抢占CPU的优先级别
- 程序计数器:下一条要执行的指令地址
- 内存指针:指向程序代码、数据及共享内存的指针
- 上下文数据:CPU寄存器中的数据(进程切换时需保存)
- I/O状态信息:已打开文件列表、I/O设备分配情况
所有运行中的进程通过task_struct双链表组织在了你内核中。
3.3 进程相关实战操作
3.3.1 查看进程信息
-
通过/proc文件系统:
/proc/[PID]目录包含对应进程的详细信息 -
使用命令行工具:
bash# 查看所有进程详细信息 ps aux # 查看进程组、会话ID等信息 ps axj # 动态监控进程状态 top
3.3.2 获取进程ID(PID/PPID)
通过getpid()和getppid()系统调用获取进程ID和父进程ID:
c
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main() {
printf("PID: %d\n", getpid()); // 当前进程ID
printf("PPID: %d\n", getppid()); // 父进程ID
return 0;
}
3.3.3 创建进程:fork系统调用
fork是创建进程的核心系统调用,具有以下特性:
- 有两个返回值:子进程返回0,父进程返回子进程PID,出错返回-1
- 父子进程代码共享,数据采用写时拷贝机制(修改时才复制)
基础用法:
c
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main() {
pid_t ret = fork();
if (ret < 0) {
perror("fork failed");
return 1;
} else if (ret == 0) {
// 子进程逻辑
printf("I am child, PID: %d\n", getpid());
} else {
// 父进程逻辑
printf("I am father, PID: %d, Child PID: %d\n", getpid(), ret);
}
sleep(1);
return 0;
}
四、进程状态与特殊进程
4.1 Linux进程状态分类
Linux内核定义的进程状态(task_state_array):
- R(running):运行状态,进程正在运行或在运行队列中
- S(sleeping):可中断睡眠状态,等待事件完成
- D(disk sleep):不可中断睡眠状态,通常等待I/O完成
- T(stopped):停止状态,可通过SIGSTOP/SIGCONT信号控制
- t(tracing stop):追踪停止状态
- X(dead):死亡状态,仅为返回状态,不会出现在任务列表
- Z(zombie):僵尸状态,子进程退出后父进程未读取退出状态
4.2 特殊进程:僵尸进程与孤儿进程
4.2.1 僵尸进程
- 产生原因:子进程退出,父进程未调用wait()读取其退出状态
- 危害:占用PCB资源,导致内存泄漏
- 示例代码:
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
pid_t id = fork();
if (id < 0) {
perror("fork");
return 1;
} else if (id > 0) {
// 父进程睡眠30秒,不读取子进程状态
printf("Parent PID: %d, sleeping...\n", getpid());
sleep(30);
} else {
// 子进程睡眠5秒后退出
printf("Child PID: %d, will exit...\n", getpid());
sleep(5);
exit(EXIT_SUCCESS);
}
return 0;
}
4.2.2 孤儿进程
- 产生原因:父进程提前退出,子进程仍在运行
- 处理机制:孤儿进程会被1号init/systemd进程领养并回收
- 示例代码:
c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
pid_t id = fork();
if (id < 0) {
perror("fork");
return 1;
} else if (id == 0) {
// 子进程运行10秒
printf("Child PID: %d, running...\n", getpid());
sleep(10);
} else {
// 父进程运行3秒后退出
printf("Parent PID: %d, will exit...\n", getpid());
sleep(3);
exit(0);
}
return 0;
}
五、进程优先级与调度
5.1 优先级核心概念
- 作用:决定进程获取CPU资源的先后顺序
- 核心参数:
- PRI:进程基础优先级,值越小优先级越高
- NI(nice值):优先级修正值,范围-20~19
- 计算公式:PRI(new) = PRI(old) + NI
5.2 优先级调整实战
-
使用top命令动态调整:
- 进入top界面,按"r" → 输入进程PID → 输入新的nice值
-
使用命令行工具:
bash# 启动进程时设置nice值 nice -n 5 ./test # 调整已有进程的nice值 renice 10 -p 1234 -
系统调用接口:
c#include <sys/resource.h> // 获取优先级 int getpriority(int which, int who); // 设置优先级 int setpriority(int which, int who, int prio);
5.3 进程调度核心概念
- 竞争性:进程间竞争CPU资源,优先级决定竞争能力
- 独立性:多进程运行时独享资源,互不干扰
- 并行:多个进程在多个CPU上同时运行
- 并发:多个进程在单个CPU上通过切换交替运行
5.4 Linux 2.6内核O(1)调度算法
核心设计:
- 每个CPU对应一个runqueue(运行队列)
- 队列分为活动队列(时间片未用完)和过期队列(时间片耗尽)
- 采用优先级数组(140个优先级)和位图快速查找最高优先级进程
- 调度时间复杂度为O(1),不受进程数量影响
六、环境变量与进程地址空间
6.1 环境变量实战
6.1.1 常见环境变量
- PATH:命令搜索路径
- HOME:用户主目录
- SHELL:当前使用的Shell(默认/bin/bash)
6.1.2 环境变量操作命令
bash
# 查看环境变量值
echo $PATH
# 设置环境变量
export MYENV="hello linux"
# 查看所有环境变量
env
# 清除环境变量
unset MYENV
6.1.3 代码中访问环境变量
c
#include <stdio.h>
#include <stdlib.h>
int main() {
// 方法1:通过main函数第三个参数
// int main(int argc, char *argv[], char *env[])
// 方法2:通过environ全局变量
extern char **environ;
for (int i = 0; environ[i]; i++) {
printf("%s\n", environ[i]);
}
// 方法3:使用getenv获取指定环境变量
printf("PATH: %s\n", getenv("PATH"));
return 0;
}
6.2 虚拟地址空间深度解析
6.2.1 核心认知
- 程序看到的地址是虚拟地址,并非物理内存地址
- 虚拟地址通过页表映射到物理地址,由操作系统和MMU(内存管理单元)完成转换
- 父子进程虚拟地址相同但物理地址不同,实现数据隔离
6.2.2 进程地址空间布局(32位系统)
从高地址到低地址依次为:
- 内核空间(1G)
- 命令行参数与环境变量
- 栈区(向下增长)
- 共享区
- 堆区(向上增长)
- 未初始化数据区(BSS)
- 初始化数据区(Data)
- 代码区(Text,只读)
6.2.3 虚拟地址空间的优势
- 安全性:进程无法直接访问物理内存,避免非法修改
- 地址确定性:程序编译后虚拟地址固定,与物理内存布局无关
- 效率优化:采用延迟分配机制,仅在实际访问时分配物理内存