【Linux】深入理解进程(五)(进程终止,进程等待,进程替换,实现shell)
本文探讨了进程终止与等待的相关机制。首先解释了main函数返回值的作用,0表示成功,非0值表示错误,返回值由父进程接收。介绍了Linux中查看退出码的$?指令,并通过代码示例演示了不同错误情况下的退出码。 详细分析了进程退出的三种场景:正常结束(正确/错误)和异常终止。重点讲解了进程等待的必要性,包括wait和waitpid两种方法:wait会阻塞父进程,waitpid支持非阻塞操作。文章通过代码
先考虑一个问题, main函数为什么要有返回值?返回值为什么是0?返回值给谁了?
🚩进程终止
进程退出场景:
- 程序结束,结果正确
- 程序结束,结果错误
- 程序异常结束
怎么判断程序结束后正确与否?
🚩:用return返回值返回退出码,
🚩退出码可以用来判断进程结束状态,0代表success,其他数字代表不同的错误,所以main函数返回0
🚩返给谁了?:进程的父进程
异常就是代码没跑完,所以它的退出码无意义,我们也不用关心,我们要去关注为什么异常
$?
linux指令,返回最近一次进程结束的退出码
int main() {
printf("%s","hello\n");
return 0;
}

注意:$?会替换上次退出码,直接用相当于给了0指令,然后就找不到,
要想查看上次退出码,用 echo $?
int main()
{
int a=10;
a/=0;
printf("%s\n",strerror(errno));
char* mem=(char*)malloc(1000*1000*1000*4);
return 0;
}

136代表浮点数错误
int main()
{
char* mem=(char*)malloc(1000*1000*1000*4);
if(mem==NULL)
{
printf("%d: %s\n",errno,strerror(errno));
}
return 0;
}

查看所有退出码信息
for(int i=0;i<=200;i++)
{
printf("%d:%s",i,strerror(i));
}
我们也可以自己写退出码体系
const char* myerror[]={
"success",
"error1",
"error2",
"error3"
};
exit
作用:直接退出程序,返回退出码
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
using namespace std;
void show()
{
printf("hello\n");
printf("hello\n");
exit(6);
}
int main()
{
show();
exit(9);
return 0;
}

在函数里也直接退出程序了,退出码是6而不是9
_exit
exit实际在执行时还会刷新缓冲区,再执行_exit(),缓冲区会再之后文件细讲
_exit()函数不刷新缓冲区,直接结束程序
#include <unistd.h>
void exit(int status);
int main()
{
printf("hello");
exit(0);
}
运行结果:
[root@localhost linux]# ./a.out
hello[root@localhost linux]#
int main()
{
printf("hello");
_exit(0);
}
运行结果:
[root@localhost linux]# ./a.out
[root@localhost linux]#

🚩进程等待
子进程已结束而父进程还没回收子进程就会造成僵尸进程,所以进程等待是必要的
进程等待:父进程通过wait,waitpid主动回收子进程资源,获取子进程退出码
wait
阻塞调用,暂停父进程的动作,等待任意一个子进程结束
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
// waitpid(-1,&status,0)
status结构:
| 31…16 | 15…8 | 7…0 |
|---|---|---|
| 保留 | 信号值 | 退出码 |
是一个32位图结构,我们只关注低16位,其中低8位存储退出码,高八位存储信号值
正常退出保存退出码,被信号杀死用信号值
如果不在乎怎么退出的,也可以传NULL,
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
using namespace std;
void Runchild()
{
int cnt=3;
while(cnt--)
{
printf("我是一个子进程:pid:%d ppid:%d\n",getpid(),getppid());
}
//exit(10);
}
int main()
{
for(int i=0;i<10;i++)
{
pid_t id=fork();
if(id==0)
{
Runchild();
exit(i);
}
printf("我是父进程:pid:%d ppid:%d\n",getpid(),getppid());
sleep(1);
}
sleep(5);
for(int i=0;i<10;i++)
{
//pid_t id=wait(NULL);
int status=0;
pid_t id=waitpid(-1,&status,0);
printf("wait %d success exit:%d\n",i,WEXITSTATUS(status));
}
return 23;
}
我是父进程:pid:3299 ppid:1974
我是一个子进程:pid:3300 ppid:3299
我是一个子进程:pid:3300 ppid:3299
我是一个子进程:pid:3300 ppid:3299
我是父进程:pid:3299 ppid:1974
我是一个子进程:pid:3301 ppid:3299
我是一个子进程:pid:3301 ppid:3299
我是一个子进程:pid:3301 ppid:3299
我是父进程:pid:3299 ppid:1974
我是一个子进程:pid:3302 ppid:3299
我是一个子进程:pid:3302 ppid:3299
我是一个子进程:pid:3302 ppid:3299
我是父进程:pid:3299 ppid:1974
我是一个子进程:pid:3303 ppid:3299
我是一个子进程:pid:3303 ppid:3299
我是一个子进程:pid:3303 ppid:3299
我是父进程:pid:3299 ppid:1974
我是一个子进程:pid:3304 ppid:3299
我是一个子进程:pid:3304 ppid:3299
我是一个子进程:pid:3304 ppid:3299
我是父进程:pid:3299 ppid:1974
我是一个子进程:pid:3305 ppid:3299
我是一个子进程:pid:3305 ppid:3299
我是一个子进程:pid:3305 ppid:3299
我是父进程:pid:3299 ppid:1974
我是一个子进程:pid:3306 ppid:3299
我是一个子进程:pid:3306 ppid:3299
我是一个子进程:pid:3306 ppid:3299
我是父进程:pid:3299 ppid:1974
我是一个子进程:pid:3307 ppid:3299
我是一个子进程:pid:3307 ppid:3299
我是一个子进程:pid:3307 ppid:3299
我是父进程:pid:3299 ppid:1974
我是一个子进程:pid:3308 ppid:3299
我是一个子进程:pid:3308 ppid:3299
我是一个子进程:pid:3308 ppid:3299
我是父进程:pid:3299 ppid:1974
我是一个子进程:pid:3309 ppid:3299
我是一个子进程:pid:3309 ppid:3299
我是一个子进程:pid:3309 ppid:3299
wait 0 success exit:0
wait 1 success exit:1
wait 2 success exit:2
wait 3 success exit:3
wait 4 success exit:4
wait 5 success exit:5
wait 6 success exit:6
wait 7 success exit:7
wait 8 success exit:8
wait 9 success exit:9
第一个for()循环结束子进程处于僵尸状态,第二for()将它们回收
🚩waitpid
非阻塞状态,也就是父进程在等待子进程时候可以做其他事情,不用特意停下来等待子进程结束
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
返回值等于子进程pid,说明回收成功
返回值等于0,说明子进程没结束,父进程可以做自己事情
- pid如果是-1,等待任意进程,和wait类似
- 如果是0,等待与父进程同一进程组的子进程
- pid如果是大于0,等待pid为该数的进程
status和wait一样, - options如果是0,默认行为,阻塞
- 如果是WNOHANG,如果没有适合的子进程回收,直接返回0
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
using namespace std;
void Runchild()
{
int cnt=3;
while(cnt--)
{
printf("我是一个子进程:pid:%d ppid:%d\n",getpid(),getppid());
sleep(1);
}
//exit(10);
}
int main()
{
pid_t id=fork();
if(id<0)
{
perror("fork");
return -1;
}
else if(id==0)
{
Runchild();
}
else
{
int status=0;
while(1)//轮询
{
int ret=waitpid(id,&status,WNOHANG);
if(ret>0)
{
if(WIFEXITED(status))
{
printf("程序正常 退出码:%d\n",WEXITSTATUS(status));
}
else
{
printf("程序异常\n");
break;
}
}
else if(ret==0)
{
printf("wait---\n");
usleep(60000);
}
else
{
printf("waitfailed\n");
break;
}
}
}
return 23;
}
wait---
我是一个子进程:pid:4118 ppid:4117
wait---
wait---
wait---
wait---
wait---
wait---
wait---
wait---
wait---
wait---
wait---
wait---
wait---
wait---
wait---
wait---
我是一个子进程:pid:4118 ppid:4117
wait---
wait---
wait---
wait---
wait---
wait---
wait---
wait---
wait---
wait---
wait---
wait---
wait---
wait---
wait---
wait---
wait---
我是一个子进程:pid:4118 ppid:4117
wait---
wait---
wait---
wait---
wait---
wait---
wait---
wait---
wait---
wait---
wait---
wait---
wait---
wait---
wait---
wait---
程序正常 退出码:23
waitfailed
🚩进程替换
进程调用exec系列函数会替换程序,该进程的数据代码(物理地址)会被完全替换,执行新程序。调用exec不创建子进程,所以pid不变,环境变量也不变
exec系列有6个函数,
#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
- l(list) : 表示参数采用列表
- v(vector) : 参数用数组
- p(path) : 有p自动搜索环境变量PATH
- e(env) : 表示自己维护环境变量
execl
第一个参数要替换程序的路径,第二个参数程序名称,后续命令行参数,最后以NULL结尾
#include <iostream>
2 #include <unistd.h>
3 #include <stdio.h>
4 using namespace std;
5 int main()
6 {
7 printf("hello world\n");
8 execl("/bin/ls","ls","-a","-l",NULL);
9 printf("hello worldhhh\n");
10 return 0;
11 }
[jib@hcss-ecs-c6ba replace]$ ./replace
hello world
total 28
drwxrwxr-x 2 jib jib 4096 Nov 2 21:52 .
drwxrwxr-x 15 jib jib 4096 Nov 2 21:37 …
-rw-rw-r-- 1 jib jib 66 Nov 2 21:48 makefile
-rwxrwxr-x 1 jib jib 8760 Nov 2 21:52 replace
-rw-rw-r-- 1 jib jib 204 Nov 2 21:52 replace.cpp
可以发现替换函数后面printf没有打印,替换了 ls -a -l NULL
问题:
第一个参数“/bin/ls” , 我们怎么知道替换程序名称的路径呢?
所以有了execlp,第一个参数是程序名称,但目的是找到对应的路径
execlp(“ls”,“ls”,“-a”,“-l”,NULL);

接着:也可以用传字符串数组代替命令行参数,
#include <iostream>
#include <unistd.h>
#include <stdio.h>
using namespace std;
int main()
{
char *const argv[]={"ps","-ef","NULL"};
printf("hello world\n");
//execl("/bin/ls","ls","-a","-l",NULL);
//execlp("ls","ls","-a","-l",NULL);
execvp("ps",argv);
printf("hello worldhhh\n");
return 0;
}
带e的函数需要自己组装环境变量
#include <iostream>
#include <unistd.h>
#include <stdio.h>
using namespace std;
int main()
{
//char *const argv[]={"ps","-ef",NULL};
char *const argv[]={"env",NULL};
char *const envp[]={"PATH=/bin:/usr/bin","hhh=111",NULL};
printf("hello world\n");
//execl("/bin/ls","ls","-a","-l",NULL);
//execlp("ls","ls","-a","-l",NULL);
//execvp("ps",argv);
execve("/usr/bin/env",argv,envp);
printf("hello worldhhh\n");
return 0;
}
[jib@hcss-ecs-c6ba replace]$ ./replace
hello world
PATH=/bin:/usr/bin
hhh=111
注意:环境变量是重置,不是添加
其实其他5个函数最终都会调用execvp,execvp是最终的系统调用
🚩实现myshell
有了以上基础,我们可以做myshell程序,通过程序替换来让系统调用我们输入的指令
#include <iostream>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>
#include <sys/wait.h>
using namespace std;
char commandline[1024];
char pwd[1024];
int quit=0;
char* argv[1024];
int LastNode=0;
char myenv[1024];
void getpwd()
{
getcwd(pwd,sizeof(pwd));
}
void interact(char* _commandline,int size)
{
getpwd();
printf("[%s@%s %s]# ",getenv("USER"),getenv("HOME"),pwd);
char* s=fgets(_commandline,size,stdin);
assert(s);
(void)s;
_commandline[strlen(s)-1]='\0';
}
int splitstring(char* _commandline,char * _argv[])
{
int i=0;
argv[i++]=strtok(_commandline," \t");
while(_argv[i++]=strtok(NULL," \t"));
return i-1;
}
void NormolExcute(char* _argv[])
{
pid_t id=fork();
if(id<0)
{
perror("forkfail");
return;
}
if(id==0)
{
execvp(_argv[0],_argv);
exit(40);
}
if(id>0)
{
int status=0;
pid_t ret=waitpid(id,&status,0);
if(ret==id)
{
LastNode=WEXITSTATUS(status);
}
}
}
int BuildExcute(char* argv[],int argc)
{
if(argc==2&&strcmp(argv[0],"cd")==0)
{
chdir(argv[1]);
getpwd();
sprintf(getenv("PWD"),"%s",pwd);
//setenv("PWD", pwd, 1);
return 1;
}
else if(argc==2&&strcmp(argv[0],"export")==0)
{
strcpy(myenv,argv[1]);
putenv(myenv);
return 1;
}
else if(argc==2&&strcmp(argv[0],"echo")==0)
{
if(strcmp(argv[1],"$?")==0)
{
printf("%d\n",LastNode);
return 1;
}
else if(*argv[1]=='$')
{
char* str=getenv(argv[1]+1);
if(str)
printf("%s\n",str);
return 1;
}
else
{
printf("%s\n",argv[1]);
return 1;
}
}
if(strcmp(argv[0],"ls")==0)
{
argv[argc++]="--color";
argv[argc]=NULL;
return 0;
}
return 0;
}
int main()
{
while(!quit)
{
interact(commandline,sizeof(commandline));
int argc=splitstring(commandline,argv);
if(argc==0)
{
continue;
}
int n=BuildExcute(argv,argc);
//for(int i=0;argv[i];i++)
//{
// printf("%s\n",argv[i]);
//}
if(!n)
NormolExcute(argv);
}
return 0;
}
分为4部分,第一部分输入指令,第二部分分割字符串,第三部分判断是否是内建命令,第4部分执行普通命令
更多推荐



所有评论(0)