1. 进程控制基础概念
在Linux系统中,进程是程序执行的基本单位。理解进程控制是系统管理和程序开发的核心技能之一。每次我们在终端输入命令时,系统都会创建一个新的进程来执行这个命令。比如运行ls -l查看目录内容时,系统就会启动一个专门处理这个命令的进程。
进程与程序不同,程序是静态的指令集合,而进程是动态的执行实体。一个程序可以对应多个进程,就像同一个剧本可以被多个剧团同时演出。每个进程都有自己独立的内存空间、文件描述符和系统资源,操作系统通过进程控制块(PCB)来管理这些信息。
重要提示:Linux系统中第一个进程是init进程(PID为1),现在大多数发行版已改用systemd作为初始化系统。
进程控制主要涉及以下核心操作:
- 创建新进程
- 终止现有进程
- 进程状态监控
- 进程间通信
- 进程优先级调整
2. 进程创建与终止机制
2.1 fork()系统调用原理
在Linux中,创建新进程主要通过fork()系统调用实现。这个调用会创建一个与父进程几乎完全相同的子进程,包括代码段、数据段、堆栈和打开的文件描述符等。有趣的是,fork()调用一次却返回两次 - 在父进程中返回子进程的PID,在子进程中返回0。
#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; }在实际应用中,fork()后通常会立即调用exec系列函数来加载新程序。这种fork-exec模式是Linux启动新程序的标准方式。
2.2 进程终止方式
进程可以通过多种方式终止:
- 正常退出:main()函数返回或调用exit()
- 异常退出:收到未处理的信号(如SIGSEGV)
- 主动终止:调用abort()或收到SIGKILL信号
exit()与_exit()的区别:
- exit()是库函数,会执行清理工作(如刷新I/O缓冲区、调用atexit注册的函数)
- _exit()是系统调用,直接终止进程不做清理
#include <stdlib.h> #include <unistd.h> void cleanup() { printf("执行清理工作...\n"); } int main() { atexit(cleanup); printf("使用exit退出:\n"); exit(0); // 以下代码不会执行 printf("使用_exit退出:\n"); _exit(0); }3. 进程状态监控与管理
3.1 进程状态查看
Linux提供了多种工具来查看进程状态:
- ps命令:显示当前进程快照
ps aux | grep nginx # 查看nginx相关进程- top命令:实时进程监控
top -p 1234 # 监控特定PID的进程- htop命令:增强版top(需安装)
htop --tree # 以树状结构显示进程3.2 进程状态转换
Linux进程有以下几种主要状态:
- R (Running/Runnable): 运行或就绪状态
- S (Interruptible Sleep): 可中断睡眠
- D (Uninterruptible Sleep): 不可中断睡眠
- Z (Zombie): 僵尸进程
- T (Stopped): 停止状态
状态转换示意图:
新建 → 就绪 ↔ 运行 → 终止 ↑↓ | 等待状态3.3 僵尸进程处理
僵尸进程是已终止但父进程尚未调用wait()获取其退出状态的进程。它们不占用内存等资源,但会占用进程表中的位置。处理僵尸进程的方法:
- 父进程调用wait()或waitpid()
pid_t pid = fork(); if (pid > 0) { int status; waitpid(pid, &status, 0); // 等待子进程结束 }- 忽略SIGCHLD信号(不推荐)
signal(SIGCHLD, SIG_IGN);- 杀死父进程(极端情况)
kill -9 父进程PID4. 进程间通信(IPC)机制
4.1 主要IPC方式比较
| 通信方式 | 特点 | 适用场景 | 相关系统调用 |
|---|---|---|---|
| 管道 | 单向字节流,有亲缘关系进程 | 简单数据传递 | pipe(), mkfifo() |
| 消息队列 | 结构化消息,内核维护 | 需要消息边界 | msgget(), msgsnd() |
| 共享内存 | 最高效,需同步机制 | 大数据量交换 | shmget(), shmat() |
| 信号量 | 计数器,用于同步 | 资源访问控制 | semget(), semop() |
| 套接字 | 跨网络,通用性强 | 网络通信 | socket(), bind() |
4.2 管道使用实例
匿名管道示例:
#include <unistd.h> #include <stdio.h> int main() { int fd[2]; pipe(fd); // 创建管道 if (fork() == 0) { close(fd[0]); // 子进程关闭读端 write(fd[1], "Hello", 6); close(fd[1]); } else { close(fd[1]); // 父进程关闭写端 char buf[20]; read(fd[0], buf, sizeof(buf)); printf("收到消息: %s\n", buf); close(fd[0]); } return 0; }命名管道(FIFO)示例:
mkfifo myfifo # 创建命名管道 echo "test" > myfifo & # 后台写入 cat myfifo # 读取内容4.3 共享内存实战
共享内存是最快的IPC方式,适合大数据量交换:
#include <sys/ipc.h> #include <sys/shm.h> #include <stdio.h> int main() { // 创建共享内存段 int shmid = shmget(IPC_PRIVATE, 1024, 0666|IPC_CREAT); // 附加到进程地址空间 char *shm = (char*)shmat(shmid, NULL, 0); sprintf(shm, "共享内存测试"); // 写入数据 // 在另一个进程中读取 printf("共享内存内容: %s\n", shm); // 分离并删除 shmdt(shm); shmctl(shmid, IPC_RMID, NULL); return 0; }5. 进程调度与优先级
5.1 调度策略
Linux支持多种调度策略:
- SCHED_OTHER: 默认分时调度策略(CFS)
- SCHED_FIFO: 先进先出实时调度
- SCHED_RR: 轮转实时调度
- SCHED_BATCH: 批处理调度
- SCHED_IDLE: 极低优先级调度
查看进程调度策略:
chrt -p 1234 # 查看PID为1234的进程调度策略5.2 优先级调整
Linux进程有两个优先级值:
- 静态优先级(100-139):影响时间片分配
- 实时优先级(1-99):仅实时进程使用
调整优先级的方法:
- nice值调整(普通进程)
nice -n 10 command # 启动时设置 renice 5 -p 1234 # 修改运行中进程- chrt命令(实时进程)
chrt -f -p 50 1234 # 设置PID为1234的进程为FIFO实时调度,优先级50- 编程方式
#include <sched.h> struct sched_param param; param.sched_priority = 50; sched_setscheduler(0, SCHED_FIFO, ¶m);5.3 CPU亲和性设置
将进程绑定到特定CPU核心可以提高缓存命中率:
taskset -c 0,1 command # 在CPU0和1上运行命令 taskset -p 0x3 1234 # 将PID为1234的进程绑定到CPU0和1编程实现:
#include <sched.h> cpu_set_t set; CPU_ZERO(&set); CPU_SET(0, &set); // 绑定到CPU0 sched_setaffinity(0, sizeof(set), &set);6. 守护进程创建与管理
6.1 守护进程特点
守护进程(Daemon)是在后台运行的特殊进程,特点包括:
- 脱离控制终端
- 会话组和进程组独立
- 工作目录通常为根目录
- 文件创建掩码设置为0
- 关闭标准文件描述符
6.2 创建守护进程步骤
标准守护进程创建流程:
- 调用fork()创建子进程,父进程退出
- 调用setsid()创建新会话
- 再次fork()确保不是会话首进程
- 更改工作目录到根目录
- 设置umask为0
- 关闭或重定向标准文件描述符
- 处理SIGCHLD信号
示例代码:
#include <unistd.h> #include <stdlib.h> #include <signal.h> #include <sys/stat.h> #include <fcntl.h> void daemonize() { pid_t pid = fork(); if (pid < 0) exit(EXIT_FAILURE); if (pid > 0) exit(EXIT_SUCCESS); // 父进程退出 // 子进程成为新会话组长 if (setsid() < 0) exit(EXIT_FAILURE); // 防止再次获得控制终端 signal(SIGCHLD, SIG_IGN); signal(SIGHUP, SIG_IGN); pid = fork(); if (pid < 0) exit(EXIT_FAILURE); if (pid > 0) exit(EXIT_SUCCESS); // 会话组长退出 umask(0); // 设置文件权限掩码 chdir("/"); // 更改工作目录 // 关闭标准文件描述符 close(STDIN_FILENO); close(STDOUT_FILENO); close(STDERR_FILENO); // 重定向到/dev/null或日志文件 open("/dev/null", O_RDONLY); open("/dev/null", O_RDWR); open("/dev/null", O_RDWR); }6.3 守护进程管理工具
systemd是现代Linux系统管理守护进程的标准工具:
- 创建服务单元文件
# /etc/systemd/system/mydaemon.service [Unit] Description=My Custom Daemon [Service] ExecStart=/usr/local/bin/mydaemon Restart=always User=daemon Group=daemon [Install] WantedBy=multi-user.target- 管理服务
systemctl start mydaemon # 启动服务 systemctl enable mydaemon # 设置开机启动 journalctl -u mydaemon -f # 查看日志7. 进程控制实战技巧
7.1 信号处理最佳实践
信号是进程间通信的异步机制,正确处理信号至关重要:
#include <signal.h> #include <stdio.h> #include <unistd.h> void handler(int sig) { printf("收到信号 %d\n", sig); } int main() { struct sigaction sa; sa.sa_handler = handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; // 自动重启被中断的系统调用 sigaction(SIGINT, &sa, NULL); // 注册Ctrl+C处理 while(1) { pause(); // 等待信号 } return 0; }重要提示:信号处理函数中只能使用异步信号安全函数,如write(),避免使用printf()等非安全函数。
7.2 进程组与会话控制
进程组和会话是Linux进程组织的重要概念:
#include <unistd.h> #include <stdio.h> int main() { pid_t pid = fork(); if (pid == 0) { // 子进程创建新会话 setsid(); printf("子进程: PID=%d, PGID=%d, SID=%d\n", getpid(), getpgid(0), getsid(0)); } else { printf("父进程: PID=%d, PGID=%d, SID=%d\n", getpid(), getpgid(0), getsid(0)); } return 0; }7.3 多进程编程注意事项
- 资源清理:确保子进程退出时释放所有资源
- 信号处理:正确处理SIGCHLD信号避免僵尸进程
- 竞态条件:使用适当的同步机制
- 文件描述符:注意继承和关闭问题
- 内存管理:避免内存泄漏,子进程退出会自动释放
#include <sys/wait.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> int main() { int status; pid_t pid; for (int i = 0; i < 5; i++) { pid = fork(); if (pid == 0) { // 子进程工作 sleep(1); exit(i); // 退出状态 } } // 父进程等待所有子进程 while ((pid = wait(&status)) > 0) { printf("子进程 %d 退出,状态 %d\n", pid, WEXITSTATUS(status)); } return 0; }在实际项目中,我发现使用进程池模式可以有效避免频繁创建销毁进程的开销。通过预先创建一组工作进程和任务队列,主进程只需分发任务,工作进程从队列获取任务执行。这种方式特别适合CPU密集型任务处理。