赞
踩
fork 是代码创建进程的一种方式,它从已存在的进程中创建一个新进程。原进程为父进程,新进程叫做子进程。
#include <unistd.h>
pid_t fork(void);
简而言之,fork 之后,就有两个代码相同的进程,且它们都运行到相同的地方。从此便开始分道扬镳,各自独立。
一般数据和代码是共享的,只有一份。代码一般不改,修改的都是数据。
当数据被修改时,系统便为修改者单独创建一份空间,以供其使用,其他进程仍旧使用原变量。
子进程的页表由父进程的拷贝而来,父子进程的页表中的地址映射是相同的。当写入数据时,操作系统底层会自动将数据,拷贝一份到新空间,再重新建立修改方的页表映射,将虚拟地址映射到新空间处。
进程创建完毕,就到了进程的终止。
进程运行终止无非有三种结果:
$ echo $? # 输出最近一次进程的退出码
一般进程运行正确返回值为 0 0 0 代表 s u c c e s s success success ;进程运行错误,返回非零值作为错误码,一个错误码对应着一个错误信息。
**退出码只能确定程序运行完毕结果是否正确。**当程序异常退出时,此时返回值就没有意义了。
main函数return返回,则进程退出;其他函数返回,只是函数运行结束。
#include <stdlib.h>
void exit(int status);
EXIT_SUCCESS
和EXIT_FIALURE
。#include <unistd.h>
void _exit(int status);
_exit
只强制终止进程,不进行进程的后续收尾工作。
_exit
是exit
的子函数,只负责退出程序。_exit 没有清理函数和冲刷缓冲区关闭IO流的任务。创建进程本质上是系统中多出一个进程,就要多出进程相关的数据结构。
故进程退出也就意味着释放为其开辟的内核数据结构,如进程控制块、地址空间、页表及相关映射关系和为程序开辟的空间等等。
主函数return
、exit
、_exit
的方式都是正常退出,除此之外还有异常退出的情况。这个我们放到进程信号处讨论。
子进程是为了帮助父进程完成某种任务的,而父进程通常需要获得子进程运行的结果。
父进程使用wait
,waitpid
函数来等待子进程完成任务并退出,这个过程就叫做进程等待。
如果父进程不进行等待,子进程就会一直保持僵尸状态,造成内存泄漏。换句话说**,父进程调用waitpid
接口等待子进程,就可以避免僵尸进程的产生,避免内存泄漏**。
当一个进程进入僵尸状态时,就变得“刀枪不入”,发送kill -9
信号也无济于事,因为进程已经死去。所以最好由父进程等待的方式解决问题。
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
等待成功返回所终止的子进程 PID,等待失败则返回 − 1 -1 −1。
pid_t id = fork(); if (id == 0) { // child int cnt = 5; while (cnt--) { printf("child proccess[%d] running, cnt:%d\n",getpid(), cnt); sleep(1); } } else { // parent sleep(10); // 此处父进程不进行等待会直接退出,子进程就是僵尸进程 pid_t ret = wait(NULL); if (ret > 0) printf("parent proccess wait[%d] seccess\n", ret); else printf("parent proccess wait failed\n"); }
pid_t waitpid(pid_t pid, int *status, int options);
组成 | 说明 |
---|---|
返回值 | 正常返回时,返回收集信息的子进程 PID,返回–1表示等待错误。 |
参数 pid | pid 为指定进程ID,等待的是指定的某个进程,若设为–1,则等待的是任意一个进程。 |
参数 status | status 是输出型参数,父进程传入该变量,借此获取进程退出的结果。 |
参数 option | option 为等待方式。设0代表默认行为,即阻塞等待,设为WNOHANG ,代表非阻塞等待。 |
进程正常结束依靠返回值传递信息,而进程异常终止需要
status
向父进程返回。
进程异常终止往往是因为其收到了某种信号。故可以通过进程是否收到信号或收到何种信号,判定进程是否异常终止或异常原因。
此时需要进程返回两种信息,一是退出码,二是信号。
status
一个整数是退出码和信号的结合。其中
32
32
32个比特位目前只使用最低的
16
16
16比特位,高位暂时不管。
code dump
标志暂时不管。当下标 [ 8 , 15 ] [8,15] [8,15] 的次低8位为0时,说明没有收到信号,当次低8位不为0时,再去考虑 [ 0 , 6 ] [0,6] [0,6] 的低7位。
pid_t id = fork(); if (id == 0) { //child int cnt = 3; while (cnt--) { printf("child proccess[%d] running, cnt:%d\n", getpid(), cnt); sleep(1); } exit(0); } else { // parent int status = 0; pid_t ret = waitpid(id, &status, 0); if (ret > 0) { printf("waited[%d], code:%d, signal:%d\n", ret, status >> 8, status & 0x7f); } else { printf("parent proccess wait failed\n"); } } // FEDC BA98 7654 3210 // | // status - 0000 0000 0000 0000 xxxx xxxx | xxxx xxxx // 0xff 1111 1111 | // ox7f 0111 1111
实际开发中不要写易错的位操作,直接使用宏即可。
WIFEXITED
判断进程是否正常退出,正常退出返回1,否则返回0。WEXITSTATUS
返回进程退出码。WTERMSIG
返回进程退出信号。if (ret > 0)
{
if (WIFEXITED(status)) // 判断进程是否正常终止
printf("exit code:%d\n", WEXITSTATUS(status)); // 返回进程退出码
else
printf("exit signal:%d\n", WTERMSIG(status)); // 返回进程退出信号
}
参数option
意为等待方式。设为0代表默认行为,即阻塞等待,也可设置为WNOHANG
,代表非阻塞等待。
等待方式指父进程等待子进程的方式。而阻塞和非阻塞,指父进程等待时所处的状态。
非阻塞等待时,不会等待子进程结束再返回,此时返回值有三种情况:
0
,代表没有子进程退出,则需要再次等待;0
,表示退出的子进程PID,父进程等待成功。0
,等待失败。pid_t id = fork(); if (id == 0) { //child int cnt = 3; while (cnt--) { printf("child proccess[%d] running, cnt:%d\n", getpid(), cnt); sleep(1); } exit(0); } else { int status = 0; /* 轮询检测方案 */ while (1) { pid_t ret = waitpid(id, &status, WNOHANG); if (ret == 0) { // 1.子进程并未结束 printf("parent do other things\n"); } else if (ret > 0) { // 2.子进程退出成功 printf("wait[%d] seccess, code:%d, signal:%d\n", ret, (status >> 8) & 0xff, status & 0x7f); break; } else { // 3. 等待失败 printf("parent wait failed\n"); break; } sleep(1); } }
用户层,用户传入参数status
给waitpid
为获取进程退出结果。
进程退出时,系统会将退出码和退出信号分别写入进程PCB中的两个变量exit_code
和exit_signal
中。
内核层,父进程在子进程僵尸状态时将 PCB 中的退出码和信号和父进程传入的status_p
进行运算:
*status_p |= exit_code << 8;
*status_p |= exit_signal;
父进程的status_p
就获得了子进程退出结果,再通过waitpid
接口返回给用户层。
如果想让子进程执行一个“全新的程序”,可以使用进程程序替换。
只将内存中子进程的代码数据替换成新程序的代码数据,并修改部分页表映射,就可以让子进程执行新的程序。
这种进程不变,仅仅替换当前进程的代码和数据的技术就叫做进程的程序替换。
进程替换的原理比较简单,但程序替换这个概念是非常重要的。
程序的本质就是一个文件,程序文件包含程序代码和数据。将程序的代码和数据加载到当前进程的对应内存空间上。可能会因代码变化而稍调页表数据,但这不是重点。
重点是用旧进程的壳子套入新程序的代码数据,“旧瓶装新酒”,并没有创建任何的进程。
系统调用是以exec
开头的函数,称为exec*
系列函数。
#include <unistd.h>
extern char **environ;
int execl (const char* path, const char* arg, ...);
int execv (const char* path, char* const argv[]);
int execlp (const char* file, const char* arg, ...);
int execvp (const char* file, char* const argv[]);
int execle (const char* path, const char* arg, ..., char* const envp[]);
int execve (const char* path, char* const argv[], char* const envp[]);
int execvpe(const char* file, char* const argv[], char* const envp[]);
int main()
{
printf("proc[%d] is running\n", getpid());
execl("/usr/bin/ls", "ls", "-al", NULL); // 进程程序替换
printf("do other things..\n");
printf("do other things..\n");
printf("do other things..\n");
}
进程程序替换后,进程的代码数据全部会被替换,原程序中替换函数调用之后的代码不会被执行。
程序替换的本质就是把程序代码和数据加载到当前进程的上下文中。系统中程序的运行加载,也都是使用execl
程序替换函数的。
进程替换会将程序的所有代码都替换掉,若想之后的代码不被替换,可以创建子进程进行程序替换。
int main()
{
if (fork() == 0) { //child -- 进行程序替换
//exec*
exit(1);
}
//parent -- 等待子进程
waitpid(-1, NULL, 0);
printf("parent proc waited success\n");
}
父子进程的代码是共享的,子进程程序替换时,修改了代码也会发生写时拷贝。
程序替换一旦成功,原程序的后续代码就不会被执行。也就是说,exec*
函数执行成功就不需要返回值,只有调用失败了才会有返回值。
进程程序替换虽然有多种接口,根据参数的不同,名称也跟着变化。但本质都是一类的。
名称组成 | 解释 |
---|---|
l 列表list | 使用可变参数列表传递命令行参数 |
v 数组vector | 使用数组传递命令行参数 |
p 环境变量PATH | 在环境变量PATH 中自动查找可执行程序的路径 |
e 环境变量env | 传递给新程序自行维护的环境变量env |
int execl (const char* path, const char* arg, ...);
int execv (const char* path, char* const argv[]);
arg,...
是可变参数列表,就是执行目标程序所需的命令行参数,以字符串的形式传递。最后要带上空NULL
,标识字符串数组的结尾。argv
是命令行参数数组,也就是把命令行参数和NULL
放到一个数组中,以数组的形式作参数。本质和arg,...
一样的。execl("/usr/bin/ls", "ls", "-l", "-a", NULL);
const char* const argv[] = {
"ls",
"-a",
"-l",
NULL
};
execv("/usr/bin/ls", (char** const)argv);
int execl (const char* path, const char *arg, ...);
int execlp (const char* file, const char *arg, ...);
path
时,指的是目标程序文件所在的全路径。file
时,函数名带p
表示会在环境变量PATH
中寻找命令,故参数file
只要传文件名即可。execl("/usr/bin/ls", "ls", "-l", "-a", NULL);
execlp("ls", "ls", "-l", "-a", NULL);
int execle(const char* path, const char* arg, ..., char* const envp[]);
int execve(const char* path, char* const argv[], char* const envp[]);
execle
是为程序替换的新程序传递环境变量。
新程序使用系统定义变量extern char** environ
可以获得这些环境变量。当然,自定义环境变量会将系统环境变量覆盖。
所有接口的本质都相同,做出多种版本只是为使用方便。以上所有进程程序替换函数都是库函数,都是系统调用函数execve
的代码复用。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。