Linux:进程控制
1.fork()函数初识
在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
#include <unistd.h>
pid_t fork(void);
返回值:自进程中返回0,父进程返回子进程id,出错返回-1
进程调用fork,当控制转移到内核中的fork代码后,内核将:
---分配新的内存块(4GB大小地址空间)和内核数据结构(包括pcb...)给子进程
---将父进程部分数据结构内容拷贝至子进程(子进程继承父进程的环境变量等内容)
---添加子进程到系统进程列表当中(添加pcb...)
---fork返回,开始调度器调度
fork()的常规用法
---一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
---一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
fork()调用失败的原因
---系统中有太多的进程
---实际用户的进程数超过了限制
2.进程终止
2.1进程退出码
echo $?:永远记录最近一个进程在命令行中执行完毕时对应的退出码(?变量名,$?取变量名)
strerror:通过标准错误的标号,获得错误的描述字符串 ,将单纯的错误标号转为字符串描述,方便用户查找错误。
#include<stdio.h>
#include<string.h>
int add(int from,int to)
{
int sum = 0;
for(int i=from;i<to;i++)
{
sum+=i;
}
return sum;
}
int main()
{
//写代码是为了完成某件事情,我如何得知我的任务完成的如何?---进程退出码
for(int i = 0;i<200;i++)
{//打印0-200每返回的数值的情况
printf("%d:%s\n",i,strerror(i));
}
}
//一般而言,退出码,都必须有对应的退出码的文字描述。自定义/使用系统的映射关系
//退出码的意义:0success !0对应失败的每一种原因
2.2进程退出的情况
1.代码执行完毕,结果正确。 ——return 0;
2.代码执行完毕,结果不正确。 ——return !0;//退出码在这个时候起作用查找原因
3.代码没执行完,程序异常,退出码无意义。//除零、野指针、越界...
2.3进程如何退出?
1.从main函数返回。(其它函数返回只是函数调用结束)
2.exit(退出码)结束进程,return 退出码;程序退出。任意地方调用exit程序都会终止。
3._exit() ,终止进程,不会刷新缓冲区;exit在终止进程时,会主动刷新缓冲区。
缓冲区在哪里?
exit在操作系统上的系统调用层,exit在最上层的库函数层;exit调用的时_exit,但是能刷新缓冲区,用户级缓冲区......
#include <unistd.h>
void exit(int status);//库函数--C语言提供的,在系统调用接口之上。
#include <unistd.h>
void _exit(int status);//系统调用--操作系统提供的
参数:status 定义了进程的终止状态,父进程通过wait来获取该值
3.进程等待
当子进程退出时,父进程如果不管不顾,就可能造成僵尸进程的问题,造成内存泄露。当进程一旦变成僵尸状态,就会杀不死,kill -9也不能杀死,因为系统或用户无法杀死一个已经死去的进程。并且,父进程派给子进程的任务完成的如何,我们也需要知道(子进程运行完成是否正常退出,退出码)。父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
返回值:成功返回被等待进程pid,失败返回-1。
参数:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:当正常返回的时候waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
pid:
Pid=-1,等待任一个子进程。与wait等效。
Pid>0.等待其进程ID与pid相等的子进程。
status:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
options:
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
3.1利用wait()回收子进程资源 ---阻塞等待
int main()
{
pid_t id = fork();
if(id == 0)
{
//子进程
int cnt = 10;
while(cnt)
{
printf("我是子进程:%d,父进程:%d,cnt:%d\n",getpid(),getppid(),cnt--);
sleep(1);
}
exit(0);
}
//父进程
sleep(15);
pid_t ret = wait(NULL);
if(id > 0)
{
printf("wait success:%d\n",ret);
}
sleep(5);
}
3.2 利用waitpid(id,status,0)获取子进程退出信息---阻塞等待
---程序正常终止,终止信号为0,退出状态为exit返回的退出码10:
pid_t id = fork();
if(id == 0)
{
//子进程
int cnt = 5;
while(cnt)
{
printf("我是子进程:%d,父进程:%d,cnt:%d\n",getpid(),getppid(),cnt--);
sleep(1);
}
exit(10);//程序正常执行完毕,返回退出码10
}
//父进程
int status = 0;//不是被整体使用,有自己的位图结构
pid_t ret = waitpid(id,&status,0);
if(id > 0)
{
printf("wait success:%d,sig number:%d,child exit code:%d\n",ret,(status & 0x7F),(status>>8) & 0xFF);
}
sleep(5);
---程序异常终止,被信号所杀,返回终止信号,kill -l可查明终止信号对应的原因。
pid_t id = fork();
if(id == 0)
{
//子进程
int cnt = 5;
while(cnt)
{
printf("我是子进程:%d,父进程:%d,cnt:%d\n",getpid(),getppid(),cnt--);
sleep(1);
}
int a = 3/0;//当子进程发生除零错误
exit(10);
}
---使用系统提供的宏通过拿到的进程的status来检测进程是否正常退出,以及退出的状态。
int status = 0;//不是被整体使用,有自己的位图结构
pid_t ret = waitpid(id,&status,0);
if(id > 0)
{
//先检查是否是正常退出
if(WIFEXITED(status))
{
//判断子进程运行结果是否Ok--退出码
printf("exit code:%d\n",WEXITSTATUS(status));
}
else printf("child exit not normal!\n");
}
return 0;
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真(查看进程是否是正常退出);
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码(查看进程的退出码)
当子进程正在运行时,使用kill -9杀死属于异常终止,不会返回退出码。
3.3非阻塞等待--轮询机制
pid_ t waitpid(pid_t pid, int *status, int options);当options不是0,设置成就是非阻塞等待。当返回值-1(出错),==0(子进程没有退出还在运行),>0(子进程退出返回id)的三种情况;几次非阻塞等待不一定子进程退出,所以使用轮询非阻塞等待,直到访问到子进程退出再结束非阻塞等待的检测。
pid_t id = fork();
if(id == 0)
{
//子进程
int cnt = 3;//运行三秒
while(cnt)
{
printf("我是子进程:%d,父进程:%d,cnt:%d\n",getpid(),getppid(),cnt--);
sleep(1);
}
exit(10);
}
//父进程
int status = 0;
while(1)
{
pid_t ret = waitpid(id,&status,WNOHANG);//WNOHANG:非阻塞等待
sleep(1);
if(ret == 0) printf("wait done,but child is running...\n");
else if(ret>0)
{
printf("wait success:%d,sig number:%d,child exit code:%d\n",ret,(status & 0x7F),(status>>8) & 0xFF);
break;
}
else
{
printf("error!");
break;
}
}
return 0;
非阻塞等待时不会占用父进程的所有时间,可以在轮询期间做其它的事情。例:在每次问询子进程还在运行时,可以调用别的函数任务。
进程等待是什么?为什么?怎么办?
进程等待是当子进程运行结束会释放代码和数据,但是pcb保存退出信息等不会回收,需要父进程去回收,一是避免子进程变成僵尸进程造成内存泄露,二是父进程需要拿到子进程得退出信息知道子进程是否正常结束退出码退出状态。父进程可以使用阻塞等待或者非阻塞等待两种方式进行回收。wait是等待任意一个子进程结束就可以进行回收,waitpid传入特定的Id等待特定的进程,同时waitpid的参数options可以使其选择是阻塞等待还是非阻塞等待。
4.进程程序替换
1.创建子进程的目的?
---想让子进程执行父进程代码的一部分。(执行父进程对应的磁盘代码中的一部分)
---想让子进程执行一个全新的程序。(让子进程想办法加载磁盘上指定的程序,执行新程序的代码/数据)->进程的程序替换。
2.快速搭建--写一个简易的代码
--execl替换本进程
--fork创建子进程excel替换子进程
#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);
}
子进程被创建时有了自己的虚拟地址空间和页表,对应指向父进程的物理内存中的代码和数据;但当子进程发生进程程序替换时,会进行写时拷贝,将它原来指向父进程的代码数据拷贝到其它物理内存然后将这些代码数据替换掉,也就是子进程如果原来要执行父进程的一部分现在也不会再执行,程序和代码都被替换成了新的,但都不会影响到父进程。
3.理解原理
程序替换的本质,就是将指定的程序的代码和数据加载到指定的位置!覆盖自己的代码和数据。进程替换的时候,并没有创建新的进程,只是将指定程序的代码数据加载进内存物理地址。在调用execl之后的代码就无法执行了,因为已经被新的代码数据替换了。当execl调用失败时,不会替换成功,就会向后继续执行剩余的代码。一般用perror打印错误,exit终止程序。
4.分别实现对应调用的方式
#include <unistd.h>`
//exec*加载器,exec将程序加载到内存中,程序替换中execve系统调用,其他的都是封装,为了便于使用
int execl(const char *path, const char *arg, ...);//l:list,将参数一个一个传入
int execlp(const char *file, const char *arg, ...);//p:path,如何找到程序的功能,只需要知道文件名函数就可以自己在环境变量中找,疑问?只能在已有的路径下查找吗?
int execle(const char *path, const char *arg, ...,char *const envp[]);//e:环境变量,传入自定义环境变量
int execv(const char *path, char *const argv[]);//v:vector,可以将所有的执行参数,放入数组中,统一传递,而不用进行使用可变参数方案
int execvp(const char *file, char *const argv[]);//v,p
//出错返回-1,成功时没有返回值。不需要,调用成功时,它的返回值也不会被原代码拿上了。
5.应用场景
---1.进程程序替换当前进程
---2.进程程序替换子进程
---3.调用自定义的可执行程序替换
Makefile怎么同时形成多个可执行文件(默认只形成一个且是第一个)
调用系统指令/程序
调用我们自己写的可执行程序。(可以调用其它后端语言:C++/C/python/shell)
printf("processing is running...\n");
pid_t id = fork();
assert(id != 1);
if(id == 0)//子进程
{
sleep(1);
//自定义环境变量
char *const envp[] = {(char*)"MYENV = bfrbfrbfr",NULL};
extern char **environ;
putenv((char*)"MYENV = bfrbfrbfr");//将指定的环境变量导入到系>统中
execle("./mybin","mybin",NULL,environ);
exit(1);//must failed
}
int status = 0;
pid_t ret = waitpid(id,&status,0);
if(ret>0) printf("wait success:%d,sig number:%d,child exit code:%d\n",ret,(status & 0x7F),(status>>8) & 0xFF);
return 0;
6.补充
先执行execl函数还是main()函数? --先用execl函数调用加载器exec*将程序加载进内存,给main函数传参,开始调用main函数。
为什么execl系列函数就算不传参数,子进程也能拿到默认的环境变量---通过初始化过的虚拟地址空间中的命令行参数环境变量区域拿到的,必要时也会将它传递给main函数。
5.实现一个shell命令行
实现一个我们自己的shell(有命令行、能输入指令、回车调用指令输出结果)
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<ctype.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<sys/wait.h>
#include<assert.h>
#include<errno.h>
#define NUM 1024
#define OPT_NUM 64
#define NONE_REDIR 0 //没有重定向
#define OUTPUT_REDIR 1 //输出重定向
#define INPUT_REDIR 2 //输入重定向
#define APPEND_REDIR 3 //追加重定向
#define trimSpace(start) do{\
while(isspace(*start)) ++start;\
}while(0)
char lineCommand[NUM];
char *myargv[OPT_NUM];//指针数组,存储的是命令和选项部分
int lastCode = 0;
int lastSig = 0;
int redirType = NONE_REDIR;//重定向码初始化为零
char *redirFile = NULL;//重定向文件初始化为空
void commandCheck(char *commands)
{
assert(commands);//断言不为空
char *start = commands;//指向命令行
char *end = commands + strlen(commands);//使其指向命令行末尾
while(start<end)//遍历查找是否指令存在重定向
{
if(*start == '>')
{
*start = '\0';//分割前面的指令部分
start++;
if(*start == '>')//判断是不是>>
{
redirType = APPEND_REDIR;//标注为追加重定向
start++;//指向下一个字符
}
else
{
redirType = OUTPUT_REDIR;//标注为输出重定向
}
trimSpace(start);//跳过空格使start指向文件
redirFile = start;//使文件指针指向当前
break;
}
else if(*start == '<')
{
//"cat < file.txt"
*start = '\0';
start++;
trimSpace(start);
redirType = INPUT_REDIR;
redirFile = start;
break;
}
else
{
start++;
}
}
}
int main()
{
while(1)//命令行输入的循环
{
int redirType = NONE_REDIR;//重定向码初始化为零
redirFile = NULL;//重定向文件初始化为空
errno = 0;
//输出提示符
printf("用户名@主机名~当前路径#");
fflush(stdout);
//获取用户输入,输入的时候用户会键入\n
char *s = fgets(lineCommand,sizeof(lineCommand)-1,stdin);//fgets从指定流FILE*中读取\n,n,末尾结束到lineCo中,���功返回line,失败返回空
assert(s!=NULL);
(void)s;
//清除最后一个\n,abcd\n
lineCommand[strlen(lineCommand)-1] = 0;
commandCheck(lineCommand);//检查是否有重定向语句
//字符串切割
myargv[0] = strtok(lineCommand," ");//将字符串以空格切割
int i = 0;
if(myargv[0]!=NULL && strcmp(myargv[0],"ls")==0)//如果是ls命令
{
myargv[i++] = (char*)"——color-auto";//添加颜色
}
//如果没有子串了,strtok--NULL,myargc[end] = NULL;
while(myargv[i++] = strtok(NULL," "));//设置分割的结束符为NULL
//如果是cd命令,不需要创建子进程,让shell自己执行对应的命令,如果交给子进程执行shell的路径还是变不了,像这种不需要让我们的子进程来进行,而是让shell自己执行的命令--内建命令
if(myargv[0]!=NULL && strcmp(myargv[0],"cd")==0)//如果是cd命令
{
if(myargv[1]!=NULL) chdir(myargv[1]);//chdir()改变当前工作目录的函数,myargv[1]cd后面紧跟的要改变的路径
continue;//进入下次循环-->键入下一个指令
}
//创建子进程
pid_t id = fork();
assert(id!=-1);
if(id == 0)
{
switch(redirType)
{
case NONE_REDIR:
break;
case INPUT_REDIR:
{
int fd = open(redirFile,O_RDONLY);
if(fd<0){
perror("open");
exit(errno);
}
dup2(fd,0);
}
break;
case OUTPUT_REDIR:
case APPEND_REDIR:
{
umask(0);
int flags = O_WRONLY | O_CREAT;
if(redirType == APPEND_REDIR) flags |= O_APPEND;
else flags |= O_TRUNC;
int fd = open(redirFile,flags);
if(fd<0){
perror("open");
exit(errno);
}
dup2(fd,1);
}
break;
default:
printf("bug?\n");
break;
}
execvp(myargv[0],myargv);//execvp会在PATH中查找myargv[0]文件,找到后执行
exit(1);//替换失败退出
}
int status = 0;
pid_t ret = waitpid(id,&status,0);//等待成功返回子id,
assert(ret>0);
(void)ret;
lastCode = ((status>>8) & 0xFF);//取次八位
lastSig = (status & 0x7F);//取后七位
}
}