1. 前言
进程只运行的程序,由汇编语言,数据,资源,状态,虚拟计算机组成。
unix将运行程序分为 创建进程fork,加载二进制exec
1. exec
将二进制程序加载到内存,并开始新程序的执行。
一次成功的exec 会对进程有如下改变:
- 改变地址空间和进程映像
- 任何未决信号丢失
- 进程捕捉信号回到默认动作,因为信号处理程序已经消失在进程地址空间。
- 内存任何锁定丢弃
- 多数线程属性回到默认值
- 多数进程统计数据重置
- 与进程内存有关的任何东西,包括映射的文件,会被丢弃
- 单独存在于用户空间的任何东西,包括C链接库的功能,例如atexit()的行为,会被丢弃
而进程许多特性不会改变:
进程ID,父进程ID,优先级,所有者所有组。
打开的文件可以跨exec被继承,意味新程序可以访问原进程打开的所有文件,假设其直到相应文件描述符的值。
若不期望这行为,可以在exec前先关闭文件,也可以通过fcntl指示内核自动完成。
2. fork
创建新进程来运行与当前相同的映像
父子进程几乎完全一样,除了
- 子进程pid
- 子进程ppid
- 子进程资源统计数重置为0
- 任何未决信号被清除
- 所取得任何文件锁定不会被子进程继承
2.1 写时复制
fork时,linux不会复制父进程地址空间,而是写时才复制页面。
由于写时复制,若立即执行exec,便不会浪费复制。
2.2 vfork
vfork是写时复制之前出现的,以解决立即执行exec动作,导致fork期间浪费地址空间的复制。
vfrok通过暂停父进程,直到子进程终止或执行exec。
在过渡期间父子进程通向地址空间和页表项,因此子进程不得修改地址空间中任何内存的内容。
由于有了写时复制,且vfork有bug,所以不要使用
3. exit
终止进程。
EXIT_SUCCESS EXIT_FAILURE 被定义为可移植的成功和失败。
在终止进程前,c链接库会依次执行下面的shutdown步骤
- 调用atexit 注册的任何函数
- 刷新已打开的标准IO流
- 移除任何由tmpfile() 创建的临时文件。
4. wait
unix设计决定,若子进程先于父进程死亡,则子进程向父进程发信号SIGCHILD,且内核让子进程进入特殊状态,僵尸进程。
进程只会保留最小骨架,等待父进程打听它的状态。
只有在父进程获得终止子进程的信息后,子进程才正式结束。
使用wait获得已终止子进程的信息。
5. system
生成一个进程并等待其终止,用于运行shell script。
通常目的是获得命令返回值。
执行成功时,返回值就是命令的返回状态,如同wait所提供的状态,因此可用 WEXITSTATUS 获得命令的结束码。
命令执行期间 SIGCHILD 被阻挡,SIGINT SIGQUIT被忽略。
由于SIGINT SIGQUIT被忽略,所以你可能需要检查子进程的结束状态
ret = system("ls -l");
if (WIFSIGALED(ret) &&
(WTERMSIG(ret) == SIGINT ||
WTERMSIG(ret) == SIGQUIT))
exit(0);
也可以实现不忽略信号的system
int my_system(const char *cmd)
{
int status;
pid_t pid;
pid = fork();
if (pid == -1)
return -1;
else if (pid == 0) {
const char *argv[4];
argv[0] = "sh";
argv[1] = "-c";
argv[2] = cmd;
argv[3] = NULL;
execv("/bin/sh", argv);
exit(-1);
}
if (waitpid(pid, &status, 0) == -1)
return -1;
else if (WIFEXITED(status))
return WEXITSTATUS(status);
return -1;
}