进程创建
通过调用进程创建一个新的进程,调用进程称为父进程,新的进程称为子进程。
接口介绍:
pid_t fork(void);
功能:
创建一个进程。
返回值:
父进程:返回值大于0,返回的是子进程的pid。
子进程:返回值等于0。
出错,返回值小于0。
注意:
- 子进程通过复制父进程的pcb创建,代码共享,数据独有。
- 子进程并非从头开始运行,而是从创建成功后的下一刻开始运行。
- 父子进程谁先运行不一定,取决于CPU调度算法。
pid_t vfork(void);
功能:
创建子进程,阻塞父进程。
返回值:
父进程:返回值大于0,返回的是子进程的pid。
子进程:返回值等于0。
出错,返回值小于0。
注意:
- 子进程先运行,父进程等到子进程退出或者程序替换后才会运行。
- 父子进程共用同一个虚拟地址空间。同时运行会造成调用栈混乱,因此阻塞父进程。
- 子进程退出后,如果释放资源会导致父进程陷入混乱或错误。
- vfork用处,为解决早期fork创建子进程拷贝数据的缺点,vfork被fork的写时拷贝技术淘汰。
两个接口都是通过在底层调用clone()函数实现的。
下面来看一下fork之前和fork之后的示意图:
当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方,但每个进程接下来就会开始走自己的路。示例如下:
#include <iostream>
#include <unistd.h>
#include <stdio.h>
int main(){
std::cout << "before: " << getpid() << std::endl;
pid_t pid = fork();
if(pid < 0){
perror("fork error");
return 0;
}
else if(pid == 0){
// 子进程
std::cout << "This is child process: " << getpid() << std::endl;
}
else{
// 父进程
std::cout << "This is parent process: " << getpid() << std::endl;
}
sleep(1);
return 0;
}
这里看到了三行输出,一行fork之前的,两行fork之后的。进程13047先打印before消息,然后再打印parent process这条信息。注意到进程13048没有打印before这条消息。
所以,fork之前父进程独立执行,fork之后,父子两个执行流分别执行。
注意:fork之后,谁先执行完全由CPU调度器决定。
写时拷贝技术
通常,父子进程代码共享,父子进程在不进行写入时,数据也是共享的,但是当任意一方试图写入,便以写时拷贝的方式各自一份副本。
子进程对数据进行修改,就会重新为其申请一块空间并更新页表。这样可以节省资源,提高子进程创建效率,保证数据独有,保证进程间的独立性。
示意图如下:
fork常规用法:
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如:父进程等待客户端的请求,生成子进程来处理请求。
- 一个进程要执行一个不同的程序。例如:子进程从fork返回后,调用exec函数进行程序替换。
fork调用失败的原因:
- 系统中有太多的进程。
- 实际用户的进程数超过了限制。
进程创建代码代码演示:
fork接口:
#include <iostream>
#include <unistd.h>
#include <stdio.h>
int main(){
// 进程创建
pid_t pid = fork();
if(pid < 0){
// 进程创建失败
perror("fork error");
return 0;
}
else if(pid == 0){
// 子进程
std::cout << "This is child process: " << getpid() << std::endl;
}
else{
// 父进程
std::cout << "This is parent process: " << getpid() << std::endl;
}
sleep(1);
return 0;
}
vfork接口:
#include <iostream>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(){
// 进程创建
pid_t pid = vfork();
if(pid < 0){
// 进程创建失败
perror("vfork error");
return 0;
}
else if(pid == 0){
// 子进程
// 子进程先等待5s,看看子进程是否是先执行
sleep(5);
std::cout << "This is child process: " << getpid() << std::endl;
exit(0);
}
else{
// 父进程
std::cout << "This is parent process: " << getpid() << std::endl;
}
sleep(1);
return 0;
}
进程终止
进程退出场景:
- 代码运行完毕,结果正确。
- 代码运行完毕,结果不正确。
- 代码异常终止。
接口介绍:
头文件:stdlib.h
void exit(int status);
头文件:unistd.h
void _exit(int status);
进程常见退出方法:
正常退出:
- 从main函数中return退出。
return退出是一种更常见的退出进程方法,执行return n;等同于执行exit(n),因为main函数运行时会将main函数的返回值当做exit()的参数。 - 调用exit()接口。
exit()是库函数,相当于是对_exit()系统调用接口的封装,在调用_exit()之前还做了其他工作:执行用户通过atexit或on_exit定义的清理函数,关闭所有打开的流,所有的缓存数据均被写入,调用_exit()。
- _exit()系统调用接口。
异常退出:
- 信号终止。
- ctrl + c。
main函数中return,exit、_exit的区别:
- main函数中return和exit()会先刷新缓冲区,然后释放资源。
- _exit()会直接释放资源。
exit()和_exit()代码演示:
#include <iostream>
#include <stdlib.h>
int main(){
std::cout << "hello, world!";
// 刷新缓冲区,然后释放资源退出
exit(0);
}
#include <iostream>
#include <unistd.h>
int main(){
std::cout << "hello, world!";
// 直接释放资源并退出
_exit(0);
}
进程等待
为什么要进行进程等待?
- 我们知道,子进程退出,父进程如果不管不顾,就可能造成僵尸进程的问题,进而造成内存泄漏。
- 子进程一旦成为僵尸进程,就会很难处理,kill -9强杀也没用。
- 而且我们需要知道父进程派给子进程的任务完成的如何,如:子进程运行完成,结果对还是不对,或者是否正常退出。
- 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。
进程等待的接口介绍:
pid_t wait(int *status);
功能:
阻塞等待任意一个子进程的退出。
参数:
status:用于获取子进程的退出返回值。
返回值:
返回子进程的pid,出错返回-1。
pid_t waitpid(pid_t pid, int *status, int options);
功能:
等待指定pid的子进程,pid = -1等待任意子进程,pid > 0等待指定子进程。
参数:
status: 用于获取返回值。
options: 可以将waitpid设置为阻塞或非阻塞。WNOHANG非阻塞。
返回值:
小于0,出错,==0目前没有子进程退出,>0退出的子进程的pid。
注意:
- 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
- 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
- 如果不存在该子进程,则立即报错返回。
阻塞和非阻塞:
- 阻塞:为了完成某个功能,发起调用,如果当前不具备完成条件,一直等待直到完成后返回。
- 非阻塞:为了完成某个功能,发起调用,如果当前不具备完成条件直接报错返回。
获取子进程status:
- wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
- 如果传递NULL,表示不关心子进程的退出状态信息。
- 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
- status不能简单的当做整型来看待,可以当做位图来看待,status是一个32位int型变量,这里我们只研究低16位,高8位用来保存子进程的退出信息,低8位中的最高位是core dump标志(是否要进行核心转储–保存程序异常退出原因),剩下的7位是异常退出原因,判断低7位是否为0就可以判断程序是否是异常退出,为0表示不是异常退出,否则是异常退出,这里有两个封装好了的宏:WIFEXITED(status)为真,表示子进程正常退出,WEXITSTATUS(status)提取子进程的退出码,具体细节如下图:
获取子进程状态示例:
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(){
pid_t pid = fork();
if(pid < 0){
perror("fork error");
return -1;
}
else if(pid == 0){
// 子进程
sleep(10);
exit(10);
}
else{
// 父进程
int status;
// 进程等待
int ret = wait(&status);
// 正常退出
if(ret > 0 && (status & 0x7F) == 0){
// 获取子进程返回状态
printf("child exit code: %d\n", (status >> 8) & 0xFF);
}
// 异常退出
else if(ret > 0){
// 获取子进程异常退出信息
printf("sig code: %d\n", status & 0x7F);
}
}
}
首先来看正常退出,等待10s让其正常退出:
然后我们运行该程序,另开一终端,使用信号kill -9将该进程杀死,看看结果:
waitpid的代码演示:
阻塞等待:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main(){
// 进程创建
pid_t pid = fork();
// 创建失败
if(pid < 0){
perror("fork error");
return -1;
}
else if(pid == 0){
// 子进程
printf("child process is running, pid: %d!\n", getpid());
// 睡眠10s
sleep(10);
exit(1);
}
else{
// 父进程
int status;
// 阻塞等待
int ret = waitpid(-1, &status, 0);
if(WIFEXITED(status) && ret == pid){
// 等待成功
printf("wait child process for 10s success, child return code is: %d\n",
WEXITSTATUS(status));
}
else{
// 等待失败
printf("wait child process failed!\n");
return 1;
}
}
return 0;
}
非阻塞等待:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main(){
// 进程创建
pid_t pid = fork();
if(pid < 0){
perror("fork error");
return -1;
}
else if(pid == 0){
// 子进程
printf("child is run, pid is: %d\n", getpid());
// 休眠10s
sleep(10);
exit(20);
}
else{
// 父进程
int status;
int ret;
// 休眠1ms
usleep(1);
// 非阻塞循环等待
do{
ret = waitpid(-1, &status, WNOHANG);
if(ret == 0){
printf("child process is running...\n");
}
// 休眠2s
sleep(2);
} while(ret == 0);
// 等待成功
if(WIFEXITED(status) && ret == pid){
printf("wait child process 10s success, child process return code is: %d!\n",
WEXITSTATUS(status));
}
// 等待失败
else{
printf("wait child process failed!\n");
return 1;
}
}
return 0;
}
程序替换
使用fork创建的子进程执行的和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数来进行程序替换,当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,重新初始化虚拟地址空间,更新页表信息,从新程序的启动示例开始。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
替换函数:
头文件: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 *file, char *const argv[], char *const envp[]);
上述函数,只有execve是系统调用,其他五个函数最终都调用execve。
函数解释:
- 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
- 如果调用出错则返回-1。
- 所以exec函数只有出错的返回值而没有成功的返回值。
命名理解:
- l(list):表示参数采用列表,命令参数部分必须以","相隔,最后一个命令参数必须是NULL。
- v(vector):参数用数组,命令参数部分必须是一个以NULL结尾的字符串指针数组的头部指针。例如:char* p就是一个字符串的指针,char* pa[]就是数组了,分别指向各个字符串。
- p(path):有p自动搜索环境变量PATH,带p执行文件部分可以不带路径,exec函数会在$PATH中找。
- e(env):表示自己维护环境变量,参数必须带环境变量部分,环境变量部分参数会成为执行exec函数期间的环境变量,比较少用。
exec[l or v][p][e]函数里的参数分为三个部分:
- 执行文件部分。
- 命令参数部分。
- 环境变量部分。
例如要执行一个命令:ls -l /home/sss;执行文件部分就是"/usr/bin/ls";命令参数部分就是"ls","-l","/home/sss",NULL;环境变量部分,这是一个数组,最后的元素必须是NULL。例如:char* env[] = {“PATH=/home/sss”,“USER=sss”,NULL};
函数名 | 参数格式 | 是否带路径 | 是否使用当前环境变量 |
---|---|---|---|
execl | 列表 | 不是 | 是 |
execlp | 列表 | 是 | 是 |
execle | 列表 | 不是 | 不是, 须自己组装环境变量 |
execv | 数组 | 不是 | 是 |
execvp | 数组 | 是 | 是 |
execve | 数组 | 不是 | 不是, 须自己组装变量 |
exec函数代码演示:
- execl函数。
#include <stdio.h>
#include <unistd.h>
int main(){
// 进程创建
pid_t pid = fork();
// 创建失败
if(pid < 0){
perror("fork error");
return -1;
}
else if(pid == 0){
// 子进程
printf("The child process is replaced as ls -a...\n");
// 不带p,不能使用环境变量PATH,执行文件部分需要写全路径
// 不带e,使用系统环境变量
// 带l,参数使用列表,参数部分以,分割
execl("/bin/ls", "ls", "-a", NULL);
}
else{
// 父进程
printf("The parent process!\n");
}
sleep(1);
return 0;
}
- execlp函数。
#include <stdio.h>
#include <unistd.h>
int main(){
// 进程创建
pid_t pid = fork();
// 创建失败
if(pid < 0){
perror("fork error");
return -1;
}
else if(pid == 0){
// 子进程
printf("The child process is replaced...\n");
// execlp
// 带p,执行文件部分不需要带路径,可以使用环境变量PATH
// 带l,命令参数部分使用列表,参数之间以,分隔
// 不带e,使用系统环境变量
execlp("ls", "ls", "-a", NULL);
}
else{
printf("The parent process!\n");
}
sleep(1);
return 0;
}
- execle函数。
#include <stdio.h>
#include <unistd.h>
int main(){
const char* envp[] = {"HOME=/hehe", NULL};
// 进程创建
pid_t pid = fork();
// 创建失败
if(pid < 0){
perror("fork error");
return -1;
}
else if(pid == 0){
// 子进程
printf("The child process is replaced...\n");
// 带l,命令参数部分使用列表,参数之间以,分割
// 不带p,执行文件部分需要带路径,不能使用环境变量PATH
// 带e,自己组织环境变量
execle("/bin/ls", "ls", "-a", NULL, envp);
}
else{
printf("This is parent processing!\n");
}
sleep(1);
return 0;
}
- execv函数。
#include <stdio.h>
#include <unistd.h>
int main(){
// 注意参数匹配
char* const argv[] = {
"ls", "-a", NULL
};
// 进程创建
pid_t pid = fork();
// 进程创建失败
if(pid < 0){
perror("fork error");
return -1;
}
else if(pid == 0){
// 子进程
printf("The child process is replaced...\n");
// 不带p,执行文件部分需要带路径,不能使用环境变量PATH
// 带v,命令参数部分使用数组
// 不带e,使用系统环境变量
execv("/bin/ls", argv);
}
else{
printf("This is parent process!\n");
}
sleep(1);
return 0;
}
- execvp函数。
#include <stdio.h>
#include <unistd.h>
int main(){
// 注意参数匹配
char* const argv[] = {
"ls", "-a", NULL
};
// 进程创建
pid_t pid = fork();
// 进程创建失败
if(pid < 0){
perror("fork error");
return -1;
}
else if(pid == 0){
// 子进程
printf("The child process is replaced...\n");
// 带p,执行文件部分不需要带路径,可以使用环境变量PATH
// 带v,命令参数部分使用数组
// 不带e,使用系统环境变量
execvp("ls", argv);
}
else{
printf("This is parent process!\n");
}
sleep(1);
return 0;
}
- execve函数。
#include <stdio.h>
#include <unistd.h>
int main(){
// 注意参数匹配
char* const argv[] = {
"ls", "-a", NULL
};
// 自己组织的环境变量
char* const envp[] = {
"PATH=/hehe", NULL
};
// 进程创建
pid_t pid = fork();
// 进程创建失败
if(pid < 0){
perror("fork error");
return -1;
}
else if(pid == 0){
// 子进程
printf("The child process is replaced...\n");
// 不带p,执行文件部分需要带路径,不可以使用环境变量PATH
// 带v,命令参数部分使用数组
// 不带e,使用系统环境变量
execve("/bin/ls", argv, envp);
}
else{
printf("This is parent process!\n");
}
sleep(1);
return 0;
}
函数和进程之间的相似性
exec/exit就像call/return。
一个C程序由很多函数组成。一个函数可以调用另一个函数,同时传递给它一些参数。被调用的函数执行一定的操作,然后返回一个值。每个函数都有它的局部变量,不同的函数通过call/return进行通信。
这种通过参数和返回值在拥有私有数据的函数间通信的模式是结构化程序设计的基础。Linux鼓励将这种应用于函数之间的模式扩展到程序之间。
一个C程序可以fork/exec另外一个程序,并传给它一些参数。这个被调用的程序执行一定的操作,然后通过exit(n)来返回值。调用它的进程可以通过wait(&ret)来获取exit的返回值。
minishell
根据本篇博客所讲述的内容,我们来实现一个简单的minishell,模拟实现bashell的功能。
首先,我们来看一下bashell的简单原理图:
所以要写一个minishell,需要循环以下流程:
- 获取命令行。
- 解析命令行。
- 创建一个子进程。
- 子进程程序替换。
- 父进程阻塞等待子进程。
代码如下:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <ctype.h>
#include <stdlib.h>
#include <sys/wait.h>
#define MAX_COMMAND 256
#define MAX_ARGV 32
// 保存输入命令行
char command[MAX_COMMAND];
// 保存解析出来的命令及其参数
char* argv[MAX_ARGV];
// 主界面
int interface(){
// 初始化为0
memset(command, 0, MAX_COMMAND);
// 主界面打印
printf("minishell$ ");
// 刷新输出缓冲区
fflush(stdout);
// %[^\n]读取一串带空格的字符串,遇到空格不结束,遇到回车结束
// %*c表示读取一个指定类型的数据,但不保存
if(scanf("%[^\n]%*c", command) == 0){
// scanf返回值为成功读取字符数量
// 为0表示没有字符读取成功
return -1;
}
return 0;
}
// 命令行解析
void commandAnalysis(){
int argc = 0;
char* pb = command;
while(*pb != '\0'){
// 不是空格,是参数,将该参数的指针放入字符指针数组
if(!isspace(*pb)){
argv[argc++] = pb;
// 没有空格,表示是同一个参数,向后跳转
while(!isspace(*pb) && (*pb) != '\0'){
++pb;
}
}
// 是空格,表示一个参数结束,空格位置补'\0'
else{
// 空格位置补'\0'
while(isspace(*pb)){
*pb = '\0';
++pb;
}
}
}
// 最后一个位置补NLLL
argv[argc] = NULL;
}
// 程序替换
void execReplace(){
// 子进程创建
pid_t pid = fork();
// 进程创建失败
if(pid < 0){
perror("fork error");
exit(-1);
}
else if(pid == 0){
// 子进程
// 命令为NULL
if(argv[0] == NULL){
exit(-1);
}
// 程序替换
// 带p,不需要带路径
// 不带e,使用系统环境变量
// 带v,参数使用数组的形式
execvp(argv[0], argv);
}
else{
// 父进程
// 进程等待
waitpid(pid, NULL, 0);
}
}
int main(){
while(1){
// 主界面显示,命令行输入
if(interface() < 0){
continue;
}
// 命令行解析
commandAnalysis();
// 程序替换
execReplace();
}
return 0;
}