目录
一、进程的创建
1.1 fork函数
Linux通过fork函数来创建子进程,它从已存在的进程中创建一个新进程,新进程为子进程,而原进程为父进程。
父进程中的fork()调用后返回的是新的子进程的PID。 新进程将复制父进程中的所有代码,并从fork处继续向下执行,就像原进程一样,不同之处在于,子进程中的fork()函数调用后返回的是0, 父子进程可以通过返回的值来判断究竟谁是父进程,谁是子进程。
子进程与父进程共享的内容:
进程的地址空间。
进程上下文、代码段。
进程堆空间、栈空间,内存信息。
进程的环境变量。
标准 IO 的缓冲区。
打开的文件描述符。
信号响应函数。
当前工作路径。
子进程独享的内容:
进程号 PID:PID 是身份证号码,是进程的唯一标识符。
记录锁:父进程对某文件加了把锁,子进程不会继承这把锁。
挂起的信号:这些信号是已经响应但尚未处理的信号,也就是”悬挂”的信号, 子进程也不会继承这些信号。
#include <unistd.h> //使用fork()需要包<unistd.h>头文件
#include <stdio.h>
pid_t fork(void) //返回值:子进程中返回0,父进程中返回子进程的pid,出错返回-1
int main(void)
{
pid_t result;
printf("This is a fork demo!\n\n");
/*调用 fork()函数*/
result = fork();
/*通过 result 的值来判断 fork()函数的返回情况,首先进行出错处理*/
if(result == -1) {
printf("Fork error\n");
}
/*返回值为 0 代表子进程*/
else if (result == 0) {
printf("result value is %d, I am child, pid is %d\n", result, getpid());
}
/*返回值大于 0 代表父进程*/
else {
printf("result value is %d, I am father, pid is %d\n", result, getpid());
}
return 0;
}
二、写时拷贝
因为子进程几乎是父进程的完全复制,所以父子两个进程会运行同一个程序, 但是这种复制有一个很大的问题,那就是资源与时间都会消耗很大,当发出fork()系统调用时,内核原样复制父进程的整个地址空间并把复制的那一份分配给子进程,这种行为非常的耗时。
因此在Linux中引入一种写时拷贝技术(Copy On Write,简称COW)。写时拷贝只会用在需要写入的时候才会复制地址空间,从而使各个进程拥有各自的地址空间,资源的复制是在需要写入的时候才会进行,在此之前,父进程与子进程都是以只读方式共享页面,这种技术使地址空间上的页的拷贝被推迟到实际发生写入的时候。
三、进程终止
3.1 进程终止方式
在Linux系统中,进程终止的常见方式有5种, 可以分为正常终止与异常终止:
正常终止
从main函数返回(return n相当于执行exit(n))。
调用exit()函数终止。
调用_exit()函数终止。
异常终止
ctrl + c终止。
由系统信号终止。
3.2 exit()和_exit()
在Linux系统中,exit()函数定义在stdlib.h中,而_exit()定义在unistd.h中, exit()和_exit()函数都是用来终止进程的,当程序执行到exit()或_exit()函数时, 进程会无条件地停止剩下的所有操作,清除包括PCB在内的各种数据结构,并终止当前进程的运行。
exit最后也会调用_exit, 但在调用exit之前,还做了其他工作:
1. 执行用户通过 atexit或on_exit定义的清理函数。
2. 关闭所有打开的流,所有的缓存数据均被写入
3. 调用_exit由于在 Linux 的标准函数库中,有一种被称作 "缓冲 I/O(buffered I/O)"操作, 其特征就是对应每一个打开的文件,在内存中都有一片缓冲区。每次读文件时, 会连续读出若干条记录,这样在下次读文件时就可以直接从内存的缓冲区中读取; 同样,每次写文件的时候,也仅仅是写入内存中的缓冲区,等满足了一定的条件(如达到一定数量或遇到特定字符等), 再将缓冲区中的内容一次性写入文件。
这种技术大大增加了文件读写的速度,但也为编程带来了一些麻烦。 比如有些数据,程序认为已经被写入文件中,实际上因为没有满足特定的条件,它们还只是被保存在缓冲区内, 这时用_exit()函数直接将进程关闭,缓冲区中的数据就会丢失。 因此,若想保证数据的完整性,就一定要使用exit()函数。
#include <unistd>
void _exit(int status); //status为终止状态,父进程可以通过wait()来获取该值
#include <stdlib>
void exit(int status); //status为终止状态,父进程可以通过wait()来获取该值
四、进程等待
在Linux中,当我们使用fork()函数启动一个子进程时,子进程就有了它自己的生命周期并将独立运行,在某些时候,可能父进程希望知道一个子进程何时结束,或者想要知道子进程结束的状态,甚至是等待着子进程结束,那么我们可以通过在父进程中调用wait()或者waitpid()函数让父进程等待子进程的结束。
4.1 wait()
#include <sys/types.h>
#include <wait.h>
pid_t wait(int* status);
//返回值: 成功返回被等待进程的pid,失败返回-1
//参数: 输出型参数,获取子进程退出状态,不关心则可以设置为NULL
wait()函数在被调用的时候,系统将暂停父进程的执行(阻塞等待),直到有信号来到或子进程结束, 如果在调用wait()函数时子进程已经结束,则会立即返回子进程结束状态值。 子进程的结束状态信息会由输出型参数status返回,与此同时该函数会返子进程的PID, 它通常是已经结束运行的子进程的PID。
wait()函数有几点需要注意的地方:
wait()要与fork()配套出现,如果在使用fork()之前调用wait(),wait()的返回值则为-1, 正常情况下wait()的返回值为子进程的PID。
输出型参数status用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针,但如果我们对这个子进程是如何死掉毫不在意,只想把这个僵尸进程消灭掉,我们就可以设定这个参数为NULL。
Linux系统提供了关于等待子进程退出状态的一些宏定义, 我们可以使用这些宏定义来直接判断子进程退出的状态:
WIFEXITED(status) :如果子进程正常结束,返回一个非零值。
WEXITSTATUS(status): 如果WIFEXITED非零,返回子进程退出码。
WIFSIGNALED(status) :子进程因为捕获信号而终止,返回非零值。
WTERMSIG(status) :如果WIFSIGNALED非零,返回信号代码。
WIFSTOPPED(status): 如果子进程被暂停,返回一个非零值。
WSTOPSIG(status): 如果WIFSTOPPED非零,返回一个信号代码。
4.2 waitpid()
waitpid()函数的作用和wait()函数一样,但它并不一定要等待第一个终止的子进程, 它还有其他选项,比如指定等待某个pid的子进程、提供一个非阻塞版本的wait()功能等。 实际上wait()函数只是 waitpid() 函数的一个特例,在 Linux内部实现wait函数时直接调用的就是waitpid函数。
#include <sys/types.h>
#include <wait.h>
pid_t waitpid(pid_t pid, int* status, int options);
waitpid()函数的参数有3个,下面就简单介绍这些参数相关的选项:
pid:参数pid为要等待的子进程ID,其具体含义如下:
pid < -1:等待进程组号为pid绝对值的任何子进程。
pid = -1:等待任何子进程,此时的waitpid()函数就等同于wait()函数。
pid = 0:等待进程组号与目前进程相同的任何子进程, 即等待任何与调用waitpid()函数的进程在同一个进程组的进程。
pid > 0:等待指定进程号为pid的子进程。
status:与wait()函数一样。
options:参数options提供了一些另外的选项来控制waitpid()函数的行为。 如果不想使用这些选项,则可以把这个参数设为0。
WNOHANG:如果pid指定的子进程没有终止运行,则waitpid()函数立即返回0, 而不是阻塞在这个函数上等待;如果子进程已经终止运行,则立即返回该子进程的进程号与状态信息。
WUNTRACED:如果子进程进入了暂停状态(可能子进程正处于被追踪等情况),则马上返回。
WCONTINUED:如果子进程恢复通过SIGCONT信号运行,也会立即返回(这个不常用,了解一下即可)。
很显然,当waitpid()函数的参数为(子进程pid, status,0)时,waitpid()函数就完全退化成了wait()函数。
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
pid_t pid, child_pid;
int status;
pid = fork(); //创建子进程
if (pid < 0) {
printf("Error fork\n");
}
/*子进程*/
else if (pid == 0) { //fork返回值为0是子进程
printf("I am a child process!, my pid is %d!\n\n",getpid());
/*子进程暂停 3s*/
sleep(3);
printf("I am about to quit the process!\n\n");
/*子进程正常退出*/
exit(0); //使用exit函数退出
}
/*父进程*/
else { //如果fork返回值不为0为父进程
/*调用 wait,父进程阻塞*/
child_pid = wait(&status); //调用wait函数阻塞等待子进程
/*若发现子进程退出,打印出相应情况*/
if (child_pid == pid) {
printf("Get exit child process id: %d\n",child_pid);
printf("Get child exit status: %d\n\n",status);
} else {
printf("Some error occured.\n\n");
}
exit(0);
}
}
五、进程程序替换
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),想要子进程执行其他的任务,往往要调用一种exec函数来替换当前子进程中的代码。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的pid并未改变。
5.1 exec函数
有六种以exec开头的函数,统称exec函数。
#include <unistd.h>
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
这些函数如果调用成功则加载新的程序,不再返回,如果调用失败则返回-1。
这些函数可以分为两大类, execl、execlp和execle传递给子程序的参数个数是可变的,execv、execvp和execve通过数组去装载子程序的参数,无论那种形式,参数都以一个空指针NULL结束,总结来说,可以通过它们的后缀来区分他们的作用:
名称包含 l 字母的函数(execl、execlp和execle)接收参数列表 "list" 作为调用程序的参数。
名称包含 p 字母的函数(execvp 和 execlp)可接受一个程序名作为参数,它会在当前的执行路径和环境变量 "PATH" 中搜索并执行这个程序(即可使用相对路径),名字不包含p字母的函数在调用时必须指定程序的完整路径(即要求绝对路径)。
名称包含 v 字母的函数(execv、execvp 和 execve)的子程序参数通过一个数组 "vector" 装载。
名称包含 e 字母的函数(execve 和 execle)比其它函数多接收一个指明环境变量列表的参数, 并且可以通过参数envp传递字符串数组作为新程序的环境变量, 这个envp参数的格式应为一个以 NULL 指针作为结束标记的字符串数组, 每个字符串应该表示为 "environment = virables" 的形式。
函数名 | 参数格式 | 是否带路径 | 是否使用当前环境变量 |
---|---|---|---|
execl | 列表 | 不是 | 是 |
execlp | 列表 | 是 | 是 |
execle | 列表 | 不是 | 不是,需要自己组装环境变量 |
execv | 数组 | 不是 | 是 |
execvp | 数组 | 是 | 是 |
execve | 数组 | 不是 | 不是,需要自己组装环境变量 |
#include <unistd.h>
int main()
{
char *const argv[] = {"ps", "-ef", NULL};
char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
execl("/bin/ps", "ps", "-ef", NULL);
// 带p的,可以使用环境变量PATH,无需写全路径
execlp("ps", "ps", "-ef", NULL);
// 带e的,需要自己组装环境变量
execle("ps", "ps", "-ef", NULL, envp);
execv("/bin/ps", argv);
// 带p的,可以使用环境变量PATH,无需写全路径
execvp("ps", argv);
// 带e的,需要自己组装环境变量
execve("/bin/ps", argv, envp);
exit(0);
}