1. 进程:Linux系统的灵魂单元
在Linux系统中,进程是程序执行的基本单位,也是操作系统资源分配的最小实体。当我们敲下一条命令或启动一个应用时,系统会为其创建一个独立的进程环境。这个环境包括内存空间、文件描述符、信号处理机制等关键资源。
进程与程序的关系就像菜谱和做菜的过程。程序是静态的指令集合,相当于菜谱上的文字说明;而进程是动态的执行实体,相当于厨师按照菜谱实际烹饪的过程。同一个程序可以对应多个进程(就像多位厨师可以同时使用同一份菜谱),每个进程都有自己独立的执行状态。
Linux内核通过task_struct结构体管理进程的所有信息。这个结构体包含了几十个字段,记录着进程ID、优先级、内存映射、打开文件列表等关键数据。在64位系统上,一个空的task_struct就占用约1.7KB内存,实际运行中的进程通常更大。
提示:可以通过
ps aux命令查看系统当前运行的所有进程,其中VSZ列显示了每个进程占用的虚拟内存大小(包括task_struct和其他数据结构)。
2. fork():魔法般的进程复制
2.1 fork的核心机制
fork()系统调用是Linux创建新进程的主要方式,其独特之处在于它通过复制现有进程来生成新进程。当父进程调用fork()时,内核会执行以下操作:
- 分配新的task_struct结构体
- 复制父进程的内存页表(写时复制机制)
- 继承父进程的文件描述符表
- 设置新的进程ID和父子关系
- 返回两次:父进程得到子进程PID,子进程得到0
这种"一次调用,两次返回"的特性常常让初学者困惑。理解这个机制的关键在于认识到fork()复制了整个进程上下文,包括程序计数器(PC),所以子进程会从fork()返回处继续执行。
#include <unistd.h> #include <stdio.h> int main() { pid_t pid = fork(); if (pid == 0) { printf("这是子进程,PID=%d\n", getpid()); } else { printf("这是父进程,子进程PID=%d\n", pid); } return 0; }2.2 写时复制(COW)优化
早期的Unix实现中,fork()会立即复制父进程的全部内存空间,这在现代系统中效率极低。Linux采用写时复制(Copy-On-Write)技术优化这一过程:
- fork()时只复制页表,不复制实际内存页
- 父子进程共享相同的物理内存页
- 当任一进程尝试修改内存页时,触发页错误
- 内核捕获错误,复制该页并更新页表
- 进程继续执行修改操作
这种机制大幅减少了fork()的开销。例如启动一个500MB的Java应用,传统方式需要立即分配500MB内存,而COW方式可能只需复制几十KB的页表。
注意:虽然COW优化了fork性能,但频繁fork大型进程仍可能导致大量页错误。在内存紧张时,可能触发OOM killer终止进程。
3. exec族函数:进程的华丽变身
3.1 exec的核心作用
fork()创建的子进程默认与父进程运行相同代码,而exec系列函数则让进程脱胎换骨,加载并执行全新的程序。常见的exec变体包括:
- execl():参数列表形式
- execv():参数数组形式
- execle():带环境变量
- execvp():自动搜索PATH
这些函数底层都调用同一个系统调用execve(),其主要操作包括:
- 验证可执行文件权限和格式
- 加载程序头和段信息
- 释放旧地址空间
- 建立新内存映射
- 设置参数和环境变量
- 重置信号处理
- 开始执行新程序入口点
#include <unistd.h> int main() { char *args[] = {"ls", "-l", NULL}; execvp("ls", args); perror("exec失败"); // 只有出错才会执行到这里 return 1; }3.2 文件描述符的继承与关闭
默认情况下,exec后的进程会继承所有打开的文件描述符。这可能导致安全问题或资源泄漏。解决方法有:
- 在exec前手动关闭不需要的fd
- 设置fd的close-on-exec标志(FD_CLOEXEC)
fcntl(fd, F_SETFD, FD_CLOEXEC); // 设置标志在Linux 2.6.23+内核中,还可以通过/proc/sys/fs/nr_open调整系统最大文件描述符数。
4. exit()与进程终止
4.1 正常终止路径
进程可以通过以下方式正常终止:
- 从main()函数return
- 调用exit()或_exit()
- 最后一个线程结束
exit()会执行以下清理工作:
- 调用atexit()注册的函数
- 刷新stdio缓冲区
- 关闭所有打开的文件描述符
- 释放其他资源(如共享内存段)
- 向父进程发送SIGCHLD信号
而_exit()是更底层的系统调用,直接终止进程不做任何清理:
#include <unistd.h> void quick_exit() { _exit(1); // 立即退出,不刷新缓冲区 }4.2 进程状态与僵尸进程
进程终止后,其退出状态(exit status)会被内核保留,直到父进程通过wait()系列函数读取。这段时间内的进程称为"僵尸进程"(Zombie),其特点:
- 已释放大部分资源
- 仅保留task_struct和退出状态
- 在进程列表中显示为"Z"状态
僵尸进程过多会占用有限的PID资源。处理方式包括:
- 父进程正确处理wait()/waitpid()
- 如果父进程先退出,子进程会被init进程(pid=1)接管
- 手动杀死父进程(不推荐)
# 查看僵尸进程 ps aux | grep 'Z'5. 信号与异常终止
5.1 常见终止信号
进程可能因信号而异常终止,常见信号包括:
- SIGTERM(15):礼貌终止请求
- SIGKILL(9):强制立即终止
- SIGSEGV(11):段错误(非法内存访问)
- SIGABRT(6):abort()触发
- SIGINT(2):Ctrl+C中断
信号处理可以通过signal()或更强大的sigaction()设置:
#include <signal.h> void handler(int sig) { printf("收到信号%d\n", sig); _exit(1); } int main() { struct sigaction sa; sa.sa_handler = handler; sigaction(SIGTERM, &sa, NULL); while(1) {} // 无限循环 }5.2 核心转储(Core Dump)
当进程因某些信号(如SIGSEGV)终止时,系统可能生成核心转储文件,包含进程终止时的内存状态。调试步骤:
- 确保ulimit -c不为0
- 复现崩溃
- 用gdb分析core文件
ulimit -c unlimited # 允许生成core文件 gdb /path/to/exe core # 调试分析6. 进程关系与会话
6.1 进程组与会话
Linux进程之间存在复杂的关系网络:
- 进程组:一组相关进程,共享同一个PGID
- 会话:一组进程组,通常对应一个终端会话
- 控制终端:会话可能关联的终端设备
这些关系影响信号传播和作业控制。例如,Ctrl+C会向前台进程组的所有进程发送SIGINT。
6.2 守护进程的创建
守护进程(Daemon)是脱离终端的后台服务进程,创建步骤包括:
- fork()后退出父进程
- setsid()创建新会话
- 再次fork()避免获取控制终端
- 关闭/重定向标准文件描述符
- 更改工作目录到根目录
- 设置umask(0)
#include <unistd.h> #include <stdlib.h> #include <sys/stat.h> void daemonize() { pid_t pid = fork(); if (pid > 0) exit(0); // 退出父进程 setsid(); // 成为会话组长 pid = fork(); if (pid > 0) exit(0); // 再次fork umask(0); chdir("/"); close(STDIN_FILENO); close(STDOUT_FILENO); close(STDERR_FILENO); }7. 现代Linux的进程管理扩展
7.1 线程与轻量级进程
Linux通过轻量级进程(LWP)实现线程,特点包括:
- 共享相同的地址空间
- 有独立的task_struct
- 通过clone()系统调用创建
- 由NPTL(Native POSIX Thread Library)实现
#define _GNU_SOURCE #include <sched.h> #include <stdio.h> int child(void *arg) { printf("线程运行中\n"); return 0; } int main() { char stack[4096]; clone(child, stack+4096, CLONE_VM|CLONE_FS, NULL); return 0; }7.2 cgroups与命名空间
现代Linux通过两种机制增强进程隔离:
控制组(cgroups):限制资源使用
- CPU、内存、IO等配额
- 通过/sys/fs/cgroup配置
命名空间(namespaces):隔离系统视图
- PID、网络、挂载点等隔离
- Docker等容器技术的基础
# 创建内存限制的cgroup mkdir /sys/fs/cgroup/memory/mydemo echo 100000000 > /sys/fs/cgroup/memory/mydemo/memory.limit_in_bytes echo $$ > /sys/fs/cgroup/memory/mydemo/tasks # 将当前shell加入该组