目录
一、进程的创建
1.初识fork函数
a.fork函数的基本介绍:
b.fork函数返回值及其参数 和 头文件:
2.fork函数的原理 :
3.fork函数用法:
4.fork()调用失败的原因
二、进程终止(进程退出)
1.进程退出的场景
a.main()函数的返回值(有两种情况):
b.命令行中获取最近一个进程执行完毕的退出码
2.进程常见的退出方法
I.正常终止/用代码终止一个进程(可以用echo $? 查看退出码)
II.异常退出
三、进程等待
1.进程等待的必要性(为什么要进程等待)
2.进程等待的方法
I.wait方法
II.waitpid()方法
3.获取子进程status
4.宏
5.阻塞等待 和 非阻塞等待(轮询式等待)
阻塞等待 (用wait()实现)
非阻塞等待 (用waitpid()实现)
6.深入理解阻塞等待和非阻塞等待
四、进程替换
1.进程替换的原理
2.进程替换的过程及其代码
过程图示:
代码:
3.exec族函数
五、实现简易minishell
在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程
#include
pid_t fork(void);//pid_t为整形
返回值:子进程中的fork()返回0,父进程中的fork()返回子进程的id (pid),出错时返回 -1
fork前后
pid_t fork(void)
{
1.分配新的内存块和内核数据结构给子进程
2.将父进程部分数据结构内容拷贝至子进程
3.添加子进程到系统进程列表当中
4.fork返回,开始调度器调度}
fork内部
子进程的特点:
1.将父进程的所有数据都 共享或拷贝 到了子进程中。(若子进程不对父进程的数据进行修改的话,父子进程的数据也是共享的。若子进程对父进程的数据进行修改时,会发生写时拷贝,将父进程的数据进行拷贝一份到子进程中)
写时拷贝 :(是一种延时申请技术,可以提高整机内存的使用率)
2.子进程和父进程的所有代码共享
3.由于 程序计数器 和 CPU中存储上下文数据的寄存器 原因,子进程虽然可以共享父进程的所有代码,但是在子进程中是从fork()创建子进程之后的代码开始执行的,fork()创建子进程之前的代码默认已经执行过,不会重复执行,所以子进程会执行子进程创建之后的代码。但是由于fork()进行返回值是在子进程创建之后进行返回的,所以子进程依然会执行fork()的返回值。
父进程用fork派生一个子进程,然后子进程进行进程替换,执行其他代码:
一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数
终端2号:
输入 while :; do ps axj | head -1 && ps ajx |grep myproc -w |grep -v grep; sleep 1; done
可对父子进程进行监控
makefile:
myproc : myproc.c
.PHONY : clean
clean:
rm -f myproc
myproc.c
#include
#include
int main()
{
printf("i am father准备开始fork\n");
pid_t id=fork();
if(id<0)
{
perror("fork");
return 1;
}
else if(id==0)
{
while(1)
{
//child
printf("#############################################\n");
printf("i am child,pid:%d,ppid: %d\n",getpid(), getppid());
sleep(1);
}
}
else if(id>0)
{
while(1)
{
//parent
printf("i am father,pid:%d,ppid: %d\n",getpid(),getppid());
printf("#############################################\n");
sleep(1);
}
}
return 0;
}
·系统中有太多的进程
·实际用户的进程数超过了限制
进程终止:进程终止时,操作系统释放进程中申请的内核数据结构和对应的数据和代码(本质就是释放系统资源,最主要是内存)
·代码运行完毕,结果正确 (main函数的返回值为0则为正确,0:success)
·代码运行完毕,结果不正确(main函数的返回值非0则为不正确,返回值:报错信息)
·代码异常终止(main函数的返回值不具有意义,此时应该去看退出信号)
(main函数的返回值实际上是 进程的退出码,用于表示进程是否是正确返回。)
第一种:若退出码是0,则表示进程正确返回,0:success。
第二种:若退出码为非0,则表示进程不正确返回,并且每个退出码都对应一个报错信息,退出码:报错信息。
意义:将返回值返回给上一进程,用于监控进程的退出状态。出错时方便定位错误。
0:Success
1:Operation not permitted
2:No such file or directory
3:No such process
4:Interrupted system call
5:Input/output error
6:No such device or address
7:Argument list too long
8:Exec format error
9:Bad file descriptor
10:No child processes
11:Resource temporarily unavailable
12:Cannot allocate memory
13:Permission denied
14:Bad address
15:Block device required
16:Device or resource busy
17:File exists
//以上只列出部分
//可以用printf("%s",strerror(退出码));打印退出错误的信息,
//或者用perror直接打印最近一个函数执行完毕之后的错误的退出信息
(ls , kill等等都是进程,都具有退出码)
输入 echo $?
·从main返回 (用return 语句,返回值,并且终止。只有在main函数中return 才是真正的终止)
·调用exit()1.头文件:
是#include
2.返回值及其参数:
void exit(int status-退出码);
3.exit()和return :
和return 语句类似,都是返回值,并且终止;不一样的是,exit()不论在哪个函数中exit()进程都会返回值,并且终止。推荐使用exit()
·_exit() (_exit()和_EXIT()是等价的。)
1.头文件:
是#include
2.返回值及其参数:
void _exit(int status-退出码);
2._exit()和exit()的区别:
_exit()和exit()的区别在于,exit()终止进程时,会将缓冲区的数据先刷新到屏幕上来。而_exit()不会将缓冲区的东西刷新到屏幕上来,而是直接终止。
-------------------------------------------------------------------------------------------------------------------------
exit()和_exit()的区别详解
-------------------------------------------------------------------------------------------------------------------------
i.使用exit()和_exit()对缓冲区的区别:
a. _exit() 不+ \n
b. _exit() + \n
c.exit() 不+\n
d.exit() +\n
-------------------------------------------------------------------------------------------------------------------------
ii.exit()和_exit()的底层区别:
_exit():_exit()是系统调用接口的函数。
exit():而exit()是把_exit()封装在内,并且增加了其他的函数,共同组合成exit()——是一个库函数。
·ctrl + c,信号终止
没有进程等待的危害:(子进程变成无法杀死的“僵尸进程”,并造成内存泄漏)
a.子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
b.另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。僵尸进程:[此处链接正在努力加载中]
有进程等待的好处:(父进程可以获取子进程的退出信息,监控子进程)
c.最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
I.wait方法--一般用于阻塞等待
II.waitpid()方法--用于轮询等待(非阻塞式等待),也可用于阻塞等待,与options传的参数有关
1.头文件:
include
#include
2.返回值及其参数:pid_t wait(int*status);
wait()返回值(两种情况):
第一种:成功返回被等待进程pid(返回值>0时)
第二种:等待失败返回-1 (返回值==-1时)
参数:
status:(输出型参数,获取子进程退出状态,不关心则可以设置成为NULL)
WIFEXITED(status)--子进程退出信号:
若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status)--子进程退出码:1.若WIFEXITED非零(status& 0x7F ==0为真),提取子进程退出码:WEXITSTATUS为0时则为正常退出;WEXITSTATUS>0时则为正常退出,但有错误信息。(查看进程的退出码)
2.若WIFEXITED为零时(status& 0x7F ==0为假),子进程的退出码将没有任何意义,转而应该去查看进程的退出信号。(查看进程的退出信号)
1.头文件:
#include
#include
2.返回值及其参数:pid_ t waitpid(pid_t pid, int *status, int options);
waitpid()返回值:(三种情况)
第一种:当正常返回的时候waitpid返回收集到的子进程的进程ID;(返回值>0时)
第二种:如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;(返回值==0时)
第三种:如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;(返回值==-1时)
参数:-------------------------------------------------------------------------------------------------------------------------
pid:
pid=-1,等待任一个子进程。与wait等效。
pid>0.等待其进程ID与pid相等的子进程。-------------------------------------------------------------------------------------------------------------------------
status: (输出型参数,获取子进程退出状态,不关心则可以设置成为NULL)
WIFEXITED(status)--子进程退出信号:若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status)--子进程退出码:1.若WIFEXITED非零(status& 0x7F ==0为真),提取子进程退出码:WEXITSTATUS为0时则为正常退出;WEXITSTATUS>0时则为正常退出,但有错误信息。(查看进程的退出码)
2.若WIFEXITED为零时(status& 0x7F ==0为假),子进程的退出码将没有任何意义,转而应该去查看进程的退出信号。(查看进程的退出信号)
-------------------------------------------------------------------------------------------------------------------------
options: (默认为0,是阻塞式等待)
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以阻塞等待。若正常结束,则返回该子进程的ID。若options传0的时候,则waitpid()和wait()一样,都是阻塞式等待;
若options传的是WNOHANG的时候,waitpid()是进行非阻塞式等待,该等待是用于 轮询式 的等待
-------------------------------------------------------------------------------------------------------------------------
·wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
·如果传递NULL,表示不关心子进程的退出状态信息。
·否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
·status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究statu低16比特位)
但是实际上status并不是直接将退出码或者是退出信号传出来的,而是在status上的一部分区域存储退出码,一部分区域存储退出信号。(只研究status的低16位比特位)
退出码:存放在status的低15位至低8位之间
( 退出码=(status>>8)&0XFF 或 退出码=(status>>8)& 二进制(1 1 1 1 1 1 1 1))
退出信号:存放在status的低7位( 退出信号=status& 0x7F 或者 退出码=status & 1 1 1 1 1 1 1 )
当退出信号==0时,退出码对应退出错误的信息
当退出信号!=0时,退出码将无意义
·退出码:WEXITSTATUS(status) 等价于 (status>>8)&0XFF
·退出信号:WIFEXITED(status) 等价于 status& 0x7F
代码:
-------------------------------------------------------------------------------------------------------------------------
step1.fork()
pid_t pid=fork();
step2.返回失败的情况
if(pid<0) { printf("error\n"); }
step3.子进程
if(pid==0) { int cnt=5; while(cnt) { sleep(2); printf("我是子进程:%d,pid:%d,ppid:%d\n",cnt--,getpid(),getppid()); } exit(12); }
step4.父进程
if(pid>0) { int status=0;//用于接收退出码 pid_t res=wait(&status);//res返回的是等待成功的子进程的pid if(res>0) { int st=WEXITSTATUS(status); printf("等待子进程退出成功,子进程pid:%d,子进程退出码为:%d:%s\n",res,st,strerror(st)); } else { printf("等待子进程失败\n"); } sleep(3); int cnt=5; while(cnt--) { sleep(1); printf("我是父进程:%d,pid:%d, ppid:%d\n",cnt,getpid(),getppid()); } printf("父进程已退出\n"); }
-------------------------------------------------------------------------------------------------------------------------
效果图:
终端1号(运行程序):
终端2号(监控进程):
(中间不会产生僵尸进程,因为子进程一结束就会通过wait()函数,让父进程回收子进程,回收资源,释放空间。)
(options传WNOHANG用于轮询等待,options传0用于阻塞等待)
(除了if(pid>0)的父进程处代码有所修改,其他处均与阻塞等待相同)
代码:
//在轮询等待时给父进程分配的任务
typedef void (*handler_t)(); std::vector
handlers;//因为namespace 没展开 int arr[10]={0}; void fun_one() { printf("这是一个给数组arr元素全部初始化为1的临时任务\n"); int i=0; int sz=sizeof(arr)/sizeof(arr[0]); for(i=0;i //父进程
if(pid>0) { int quit=0; while(!quit) { int status =0; pid_t res=waitpid(-1,&status,WNOHANG);//以非阻塞的方式等待 if(res>0) { //等待成功&&子进程退出 //printf("等待子进程退出成功,退出码:%d\n",WEXITSTATUS(status)); printf("等待子进程退出成功,退出码:%d\n",WEXITSTATUS(status)); quit=1; } else if(res==0) { sleep(1); //等待成功&&子进程没有退出 printf("进程还在运行中,暂时还没有退出,父进程可以再等一等,处理一下其他事情??\n"); if(handlers.empty()) { LOAD(); } for(auto& it : handlers) { it(); } } else if(res<0) { //等待失败 printf("wait失败\n"); quit=1; } } }
效果图:
可以看到父进程在轮询等待子进程的过程中还完成了两个任务,这就是轮询等待的用法
阻塞等待:父进程会一直wait()子进程(一直被挂起:只有pcb在运行队列中,但没有代码和数据了),等到子进程退出(直到子进程退出才重新被唤醒,代码和数据才重新从磁盘中加载到pcb中),wait()才结束并返回值,若等待成功,则返回子进程pid,若等待失败则返回-1
举个例子:例如,你去坐飞机。飞机靠站机场等待乘客,而你在赶来的机场的路上,但飞机不能做其他事情,这就是 阻塞式等待。在到达起飞时间前,飞机一直在等待你。若最后你成功赶上了飞机,则说明飞机等待成功你成功。若最后你没赶上飞机,则说明飞机等待你失败。这就是 阻塞式等待的两种返回值结果。
非阻塞等待:也叫轮询式等待,若子进程并没有退出,父进程不会停下来等待子进程,而是直接跳过,此时waipid()返回的是0。若子进程退出了,则父进程等待子进程成功,此时waitpid()返回的是子进程的pid。若出现错误而等待失败,则返回-1。
举个例子:假如,国庆节放假,我打算约张三出来吃饭。我打了一通电话给张三,张三说还在化妆,说等一下。我等待5分钟。又过了5分钟,我又打电话给张三,张三说还在挑衣服,等一下。我又等待5分钟。......我不断打电话给张三,张三不断推迟。由我不断打电话询问张三,张三一直没有下楼。而张三在等待的过程中可以刷视频,或者打游戏 或 做其他事情。这也就是轮询式等待(非阻塞等待)。若最后张三做完所有事情,并下楼与我去吃饭,则说明我等待成功。若张三还一直在忙别的事,则我一直是在轮询等待。若B最后突然不想去了,放了我的鸽子,则说明我等待失败。这也就是 轮询式等待的三种返回值结果。
图示:
总结:阻塞vs非阻塞
我们未来编写的代码内容,网络代码,大部分都是IO类的,不断面临阻塞和非阻塞的接口。因为有些网络代码没有函数接口管理,所以都要等待前一个函数执行完之后才能进行下一个,这也相当于是一个阻塞等待。
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变
替换后:
回答两个问题:
1.进程替换,有没有创建新的子进程?
答:没有派生新的子进程,而是更改了子进程的映射关系。
2.如何理解所谓将程序放入内存中?
答:放入就是所谓的加载。运用加载器,从一个硬件(可以是磁盘,也可以是其他硬件),搬运到另一个硬件,由操作系统提供的接口来加载,即exec族函数--加载器,其本质就是加载程序的函数。
首先是不创建子进程,在父进程中作进程替换
//file.cpp
//效果图
接下来是由父进程派生子进程,再由子进程来进程替换
//exec.c
//cmd.c
//效果图
头文件:#include
extern char**environ
种类:
1.int execl(const char* path,const char *arg,...)
解析:path必须传指定文件的路径。arg,...传的是不定个文件的选项,但最后最好要以nullptr结尾。
如:execl("/usr/bin/ls","ls","-a","-l",nullptr);
2.int execlp(const char* file, const char* arg,...)
解析:file可传指定文件路径,如果指定文件具有相应的环境变量PATH,也可直接传文件名。
arg,...传的是不定个文件的选项,但最后最好要以nullptr结尾。
如:execlp("/usr/bin/ls","ls","-a","-l",nullptr);或execlp("ls","ls","-a","-l",nullptr);
3.int execle(const char* path,const char* arg,...,char* const envp[])
解析:path必须传指定文件的路径。arg,...传的是不定个文件的选项,但最后最好要以nullptr结尾。envp[]是自己定义的环境变量的数组,可以将环境变量传过去给替换成的进程。
如: char* const envp[]={"myval=233333";
execle("/usr/bin/ls","ls","-a","-l",nullptr,envp[]);
4.int execv(const char* path, char* const argv[]);
解析:path必须传指定文件的路径。argv传数组元素是不定个文件选项的数组,数组最后一个元素最好是nullptr。
如:const char* argv[]={"ls","-l","-a",nullptr}; execv("/usr/bin/ls","argv[]);
5.int execvp(const char* file,char* const argv[]);
解析:file可传指定文件路径,如果指定文件具有相应的环境变量PATH,也可直接传文件名。argv传数组元素是不定个文件选项的数组,数组最后一个元素最好是nullptr。
如:const char* argv[]={"ls","-l","-a",nullptr};
execvp("ls","argv[]);
6.int execvpe(const char* file,char* const argv[],char* const envp[]);
解析:file可传指定文件路径,如果指定文件具有相应的环境变量PATH,也可直接传文件名。argv传数组元素是不定个文件选项的数组,数组最后一个元素最好是nullptr。
envp[]是自己定义的环境变量的数组,可以将环境变量传过去给替换成的进程。
如:const char* argv[]={"ls","-l","-a",nullptr};
char* const envp[]={"myval=233333";
execvpe("ls","argv[],envp[]);
------------------------------------------------------------------------------------------------------------------------- 有了以上的知识,我们可以自行实现一个简易的minishell了
-------------------------------------------------------------------------------------------------------------------------
代码:
#include
#include
#include
#include
#include
#include
#define NUM 1024
#define SIZE 32
#define SEP " "
//保存完成的命令行字符串
char cmd_line[NUM];
//保存打散之后的命令行字符串
char *g_argv[SIZE];
// shell 运行原理 :通过让子进程执行进程命令,父进程等待&&解析命令
int main()
{
//0.命令行解释器,一定是一个常驻内存的进程,不退出
while(1)
{
//1.打印出提示信息 [LX@localhost minishell]#
printf("[LX@localhost myshell]#");
//由于前面的printf没有\n刷新缓冲区
//,而又进入下一次循环了,来不及打印,需要用fflush强制将缓冲区的数据刷出来到屏幕上
fflush(stdout);
memset(cmd_line,'\0',sizeof cmd_line);
//2.获取用户的键盘输入[输入的是各种指令和选项]: "ls -a -l -i"
if(fgets(cmd_line,sizeof cmd_line, stdin)==NULL)
{
continue;
}
//"ls -a -l -i\n\0"
//将输入完成指令和选项之后敲的确认键换车给清掉
//,因为他也会被当成选项和指令输入cmd_line数组中
//printf("echo: %s\n",cmd_line);
cmd_line[strlen(cmd_line)-1]='\0';
//3.命令行字符串解析: "ls -a -l -i"->"ls" "-a" "-i"
g_argv[0]=strtok(cmd_line,SEP);//第一次调用,要传入原始字符串
int index=1;
if(strcmp(g_argv[0],"ls")==0)
{
g_argv[index++] = "--color=auto";
}
if(strcmp(g_argv[0],"ll")==0)
{
g_argv[0]= "ls";
g_argv[index++]= "-l";
g_argv[index++]= "--color=auto";
}
while(g_argv[index++]=strtok(NULL,SEP));//第二次,如果还要解析原始字符串,传入NULL,strtok是分割字符
//for debug
//for(inedx=0;g_argv[index];index++)
//printf("g_argv[%d]: %s\n",index,g_argv[index]);
//4.TODO,内置命令,让父进程(shell)自己执行命令,我们叫做内置命令,内建命令
//内建命令本质其实就是shell中的一个函数调用
if(strcmp(g_argv[0],"cd")==0)//not child execute, father execute
{
if(g_argv[1]!=NULL)
chdir(g_argv[1]);//cd path,cd ..//chdir是将路径进行更改到g_argv[1]指定的路径中
}
//5.fork()
pid_t id =fork();
//child
if(id==0)
{
printf("下面功能让子进程进行的\n");
//cd cmd, current child path
execvp(g_argv[0],g_argv);//ls -a -l -i
exit(1);
}
//father
int status=0;
pid_t ret =waitpid(id,&status,0);
if(ret>0)printf("exit code: %d",WEXITSTATUS(status));
}
return 0;
}
效果图: