Linux中进程控制是编写Linux(或Unix)程序时必备的技能,这篇文章概述了Linux中与进程控制相关的函数调用。 Linux手册中详细地描述了每个系统调用,感兴趣的话可以参考。
获取进程ID
Linux中每个进程都有一个唯一的正数进程ID,通过getpid
以及getppid
可以得到调用进程的PID以及其父进程的PID。
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
pid_t getppid(void);
// 返回:调用者或其父进程的PID
终止进程
进程可以调用exit
函数终止自己,status
表示进程的退出状态,从主程序返回一个数值同样可以设置程序的退出状态。
#include <stdlib.h>
exit(int status);
// 该函数不返回
创建进程
父进程通过调用fork
函数创建一个新的运行的子进程。
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
// 返回:子进程返回0,父进程返回子进程的PID,如果出错,则为-1
fork
函数创建出的新进程会得到与父进程相同的用户级虚拟地址空间,包括代码、数据段、堆、共享库以及用户占,也会获得相同的打开的文件描述符的副本。fork
函数也非常令人迷惑,它只被调用一次,却返回两次,一次在父进程中(返回子进程PID),一次在子进程中(返回0)。因为父进程中的返回值总是不为0(子进程PID或-1),因此可以以此判断程序是在父进程还是子进程中执行。调用fork
的程序往往有如下特点:
- 调用一次,返回两次
- 并发执行
- 相同但是独立的地址空间
- 共享文件
另外,子进程中的fork
依然会创建子进程(即子进程的子进程):
int main() {
printf("hello world!\n"); // only one process
fork();
printf("hello world2!\n"); // tow processes now
fork();
printf("hello world3!\n"); // four processes now
}
上述代码中,第一个fork
被父进程执行,创建了一个子进程CHILD_1;第二个fork
被父进程和子进程CHILD_1执行,创建了子进程CHILD_2以及子进程CHILD_3,因此共创建了三个子进程,加上一个父进程共四个进程。其输出结果如下:
hello world!
hello world2!
hello world2!
hello world3!
hello world3!
hello world3!
hello world3!
回收子进程
终止的子进程需要被父进程回收,否则会成为僵死(zombie)进程。父进程终止时,init
进程(所有进程的祖先)会成为其未结束或回收的孤儿进程的养父。若父进程未回收其僵死进程就终止,那么init
进程会回收它。即便僵死进程没有运行,它也会消耗系统资源,因此长时间运行的程序总是应当回收它的僵死进程。一个进程通过调用waitpid
函数灯带它的子进程终止或停止:
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *statusp, int options);
pid_t wait(int *statusp); // 等价于 waitpid(-1, &status, 0)
// 返回:若成功回收,则为子进程PID,如果 WNOHANG,则为0,其它错误,则为-1
一般情况(options
为0)下,父进程会挂起,直到其等待集合(wait set)中的某个子进程终止,并返回其PID;若等待集合中的某个子进程已经停止,那么父进程会立即返回其PID。返回后,子进程PID已经被回收,内核会删除该子进程的痕迹。waitpid
的三个参数较为复杂,可以参考waitpid获得更为详细的信息:
- pid 指定等待集合成员
- pid>0,等待集合是一个PID=pid的子进程
- pid=-1,等待集合是其所有子进程
- statusp 检查已回收子进程的退出状态,包括
WIFEXITED WEXITSTATUS WIFSIGNALED
等多个值,见参考。 - options 可以用来修改函数行为,这些选项可以通过或运算组合起来
- WNOHANG 若等待集合中的任何子进程都未终止,那么立即返回
- WUNTRACED 挂起调用进程,若等待集合中的一个进程终止或停止,则返回其PID
- WCONTINUED 挂起调用进程,直到等待集合中一个正在运行的进程终止或等待集合中一个被停止的进程收到
SIGCONT
信号重新执行。
休眠进程
想必大家都见过长得差不多的函数:
#include <unistd.h>
unsigned int sleep(unsigned int secs);
// 返回:剩下的要休眠的秒数
int pause(void);
// 返回:-1
sleep
函数会挂起当前进程直到时间到或者被信号中断,而pause
则挂起进程直到进程收到一个信号。
加载并运行程序
execve
函数在当前进程上下文加载并运行一个新程序,与fork
不一样,该函数调用成功后从不返回:
#include <unistd.h>
int execve(const char *filename, const char *argv[], const char *envp[]);
// 如果成功,则不返回,如果错误,返回-1
参数中的argv
表示待执行程序的命令行参数,envp
表示环境变量,与waitpid
一样,该函数也有许多版本,详细信息可以参考(execve)[http://man7.org/linux/man-pages/man2/execve.2.html]。如下代码展示了使用fork
在子进程中通过execve
调用linux下ps
命令(程序未包含错误处理,仅供参考):
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main() {
pid_t pid;
printf("this process will run ps -ef in child process:\n");
if ((pid = fork()) == 0) {
char *nargv[] = {"/bin/ps", "-ef", NULL};
char *nenv[] = {NULL};
execve("/bin/ps", nargv, nenv);
// execve不返回,控制流不会到这里
printf("!!!this printf will not run!!!");
}
// 休眠等待子进程运行
sleep(2);
if (pid == waitpid(-1, NULL, 0))
printf("child process: %ld recycled, exit now...\n", (long)pid);
return 0;
}
上述程序的输出:
this process will run ps -ef in child process:
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 21:31 ? 00:00:00 /init
root 7 1 0 21:31 tty1 00:00:00 /init
eric 8 7 0 21:31 tty1 00:00:00 -bash
eric 94 8 0 22:40 tty1 00:00:00 ./a.sh
eric 95 94 0 22:40 tty1 00:00:00 /bin/ps -ef
child process: 95 recycled, exit now...
可以看到,pid为94的进程创建了子进程95,并在子进程中运行了/bin/ps -ef
命令打印出了系统中的进程信息。
总结
本文讲述了linux中进程控制较为基本的函数,这些函数能够用来创建、休眠、终止、回收进程等。进程控制往往需要与信号(signal)以及进程间通信(IPC InterProcess Communication)如消息队列等配合使用,相关博客也会在接下来的学习过程中写一写。