一、退出码:
1、main函数的返回值本质表示:进程运行完成是否是正确的结果,如果不是可以用不同的数字表示不同的出错原因--退出码。
2、(echo)$?---保存最近一次进程退出的退出码
strerror:---返回错误码以及对应错误码描述
errno:保存最近一次执行的错误码
:!man 3 quit---终止一个进程
#include
#include
#include
#include
#include
int main()
{
int ret = 0;
char *p = (char*)malloc(1000*1000*1000*4);
if(p == NULL)
{
printf("malloc error, %d: %s\n", errno, strerror(errno));
ret = errno;
}
else{
//使用申请的内存
printf("malloc success\n");
}
return ret;
}
二、进程退出场景(统一采用进程的退出码来进行判定)
1、
代码运行完毕,结果正确。
代码运行完毕,结果不正确。
代码异常中止,进程退出码无意义。
(进程出现异常,本质上是我们的进程收到了对应的信号。)
2、
exit:从正常运行的程序,执行用户定义的清理函数冲刷缓冲关闭流等。
_exit():直接调用操作系统接口,在进程层面上终止进程。
exit对应的数字就是当前进程对应的退出码。
exit在任意地方被调用,都表示调用进程直接退出,return只表示当前函数返回。
return在其他函数中代表函数结束,在main函数代表进程退出,exit在任意地方都表示进程退出
三、进程等待
是什么?
通过系统调用wait/waitpid,来进行对子进程进行状态检测与回收的功能。
为什么?
僵尸进程无法被杀死。需要通过进程等待来杀掉他,进而解决内存泄露问题。
我们要通过进程等待获得子进程的退出情况。(知道我布置给子进程的任务,他完成的怎么样了?)要么关心,要么不关心--可选的。
#include
#include
#include
#include
#include
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
else if(id == 0)
{
//child
int cnt = 5;
while(cnt)
{
printf("I am child, pid:%d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
cnt--;
sleep(1);
}
exit(0);
}
else{
//parent
while(1)
{
printf("I am father, pid:%d, ppid:%d\n", getpid(), getppid());
sleep(1);
}
}
return 0;
}
//监视进程状态
while :; do ps ajx | head -1 && ps ajx | grep myproc | grep -v grep; sleep 1;echo "-----------------------------------------------------------------"; done
怎么办?
1、父进程通过调用wait/waitpid进行僵尸进程的回收问题。
wait:等待一个进程,直到这个进程的状态被改变(让父进程来等待子进程),默认参数(int* status)设为Null
①单子进程情况
//单子进程情况
#include
#include
#include
#include
#include
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
else if(id == 0)
{
//child
int cnt = 5;
while(cnt)
{
printf("I am child, pid:%d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
cnt--;
sleep(1);
}
exit(0);
}
else{
//parent
int cnt = 10;
while(cnt)
{
printf("I am father, pid:%d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
cnt--;
sleep(1);
}
pid_t ret = wait(NULL);//wait是等待任意一个子进程退出
if(ret == id)
{
printf("wait success, ret: %d\n", ret);
}
}
return 0;
}
②多子进程情况(循环等待)
//多子进程情况
#include
#include
#include
#include
#define N 3
void RunChild()
{
int cnt = 5;
while(cnt)
{
printf("I am Child Process, pid: %d,ppid: %d\n", getpid(), getppid());
sleep(1);
cnt--;
}
}
int main()
{
for(int i = 0; i < N; i++)
{
pid_t id = fork();
if(id == 0)
{
RunChild();
exit(0);
}
printf("create child process: %d sucess\n", id);//这句话只有父进程才会执行
}
sleep(10);
//等待
for(int i = 0; i < N; i++)
{
pid_t id = wait(NULL);
if(id > 0)
{
printf("wait %d sucess", id);
}
}
}
(那如果任意一个子进程都不退出呢)
③阻塞等待:如果子进程不退出,父进程默认在wait的时候,调用这个系统调用的时候,也就不返回,默认叫做阻塞状态。
wait调用会回收僵尸,释放内存泄露问题,wait调用,如果子进程不进行退出,Wait父进程也就不会返回,会一直等待,直到子进程退出。
(那如何处理此类情况呢?)
2、【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:(输出型参数)(int是被当做几部分使用的)
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
options:
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。】
//错误使用范例
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
else if(id == 0)
{
//child
int cnt = 5;
while(cnt)
{
printf("I am child, pid:%d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
cnt--;
sleep(1);
}
exit(1);
}
else{
//parent
int cnt = 10;
while(cnt)
{
printf("I am father, pid:%d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
cnt--;
sleep(1);
}
//pid_t ret = wait(NULL);
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(ret == id)
{
printf("wait success, ret: %d, status: %d\n", ret, status);
}
sleep(5);
}
return 0;
}
②为什么status返回256而不是1呢?
分析:
子进程退出,一共会有3种退出场景。
父进程等待,期望获得子进程退出的哪些信息呢?
1. 子进程代码是否异常?
2. 没有异常结果,exitcode对吗?不对的是因为什么?1 2 3 4->不同的退出码表示不同的出错原因
一共有32个比特位,先不考虑高16位,先考虑低16位。其中低七位代表终止信号,第八个比特位代表是否core dump。次低八位代表退出状态,即子进程退出时的退出码。
若低7位为零,则进程在运行时根本没有收到过信号,则代码没有异常。
方法一:采用位操作对status这样的整形数据进行数据提取
//采用位操作对status这样的整形数据进行数据提取
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
else if(id == 0)
{
//child
int cnt = 5;
while(cnt)
{
printf("I am child, pid:%d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
cnt--;
sleep(1);
}
exit(1);
}
else{
//parent
int cnt = 10;
while(cnt)
{
printf("I am father, pid:%d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
cnt--;
sleep(1);
}
//pid_t ret = wait(NULL);
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(ret == id)
{
//7F: 0111111
printf("wait success, ret: %d, exit sig: %d, exit code: %d\n", ret, status&0x7F, (status>>8)&0xFF);
}
else
{
printf("wait failed!\n");
}
sleep(5);
}
return 0;
}
方法二:宏操作
//宏操作
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
else if(id == 0)
{
//child
int cnt = 5;
while(cnt)
{
printf("I am child, pid:%d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
cnt--;
sleep(1);
}
exit(1);
}
else{
//parent
int cnt = 10;
while(cnt)
{
printf("I am father, pid:%d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
cnt--;
sleep(1);
}
//pid_t ret = wait(NULL);
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(ret == id)
{
//7F: 0111111
//printf("wait success, ret: %d, exit sig: %d, exit code: %d\n", ret, status&0x7F, (status>>8)&0xFF);
if(WIFEXITED(status))
{
printf("进程是正常跑完的,退出码%d\n",WEXITSTATUS(status));
}
else
{
printf("进程出异常了\n");
}
}
else
{
printf("Wait failed!\n");
}
sleep(5);
}
return 0;
}
③waitpid的本质是其进程的内核数据结构,并且将进程Z状态改成X状态。
操作系统在等待的时候也会检测当前进程等待是否正确。
无论创建单个进程还是创建一批进程,保证子进程状态PCB能够回收,防止内存泄露,可以获得退出状态。
子进程main函数的返回值表征当前进程退出结果是否正确,此退出码将来会被父进程获取,通过该退出码决定子进程任务完成的好坏,如果执行失败,父进程则有其他策略。所以一个命令的退出结果能被bash拿到。
整个linux的进程结构实际上是一个多叉树结构,父进程永远都对自己直系的子进程负责。(隔代爷孙关系不负责)。
pid_t waitpid(pid_t pid, int *status, int options);
其中options:
默认0称为阻塞等待方式
非阻塞(WNOHANG)轮询
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
else if(id == 0)
{
//child
int cnt = 5;
while(cnt)
{
printf("I am child, pid:%d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
cnt--;
sleep(1);
}
exit(1);
}
else{
//parent
int cnt = 10;
while(cnt)
{
printf("I am father, pid:%d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
cnt--;
sleep(1);
}
//pid_t ret = wait(NULL);
int status = 0;
while(1) //轮询
{
pid_t ret = waitpid(id, &status, WNOHANG);//非阻塞
if(ret > 0)
{
//7F: 0111111
//printf("wait success, ret: %d, exit sig: %d, exit code: %d\n", ret, status&0x7F, (status>>8)&0xFF);
if(WIFEXITED(status))
{
printf("进程是正常跑完的,退出码:%d\n",WEXITSTATUS(status));
}
else
{
printf("进程出异常了\n");
}
break;
}
else if(ret < 0)
{
printf("Wait failed!\n");
break;
}
else
{
//ret == 0
printf("子进程还没有退出,再等等...\n");
}
}
sleep(5);
}
return 0;
}
进程把所有的子进程创建出来,也就理所当然的由创建者对他所创建的对象负责,将他所创建的所有子进程退出。通过进程等待保证父进程是多个进程中最后退出的进程,可以保证将所有曾经创建出的子进程资源全部释放。
四、程序替换(exec系列)
1. 单进程版--最简单的看看程序替换
#include
#include
#include
int main()
{
printf("before: I am a process, pid: %d, ppid: %d\n",getpid(), getppid());
//这类方法的标准写法
execl("/usr/bin/ls","ls","-a","-l",NULL);
printf("after: I am a process, pid: %d, ppid: %d\n",getpid(), getppid());
return 0;
}
2.谈进程替换的原理
基本原理:新的可执行代码和数据,替换原先的代码和数据,并从零开始执行。(并没有创建新进程,id并未改变)
由于写时拷贝技术以及进程相互的独立性,子进程程序替换并不影响父进程。
写时拷贝不仅仅在数据上实现,也在代码上实现。
去替换有没有创建新的进程?
程序替换没有创建新的进程,只进行进程的程序代码和数据的替换工作。
为什么after代码不去执行呢?
程序替换之后,老数据的代码已被替换,后续代码不会被执行。
补充:
① 现象:程序替换成功之后,exec*后续代码不会被执行,替换失败才可能执行后续代码。exec*函数只有失败返回值,没有成功返回值
② 小知识:Linux中形成的可执行程序是有格式的,ELF可执行程序的表头,可执行程序的入口地址就在表中。
③进程程序替换后,该进程对应的PCB,进程地址空间以及页表等数据结构没有发生改变。
3. 多进程版--验证各种程序替换接口
execl:
第一个参数:必须是全路径,以绝对或相对的路径。
第二个参数:按照命令行传参的方式传参。
execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
execlp:
PATH:execlp自己会在自己默认的环境变量中查找。
execl("ls", "ls", "-a", "-l", NULL);
execv:
第二个参数:以字符串指针数组传参
char *const myargv[] = {"ls", "-l", "-a", NULL};
execv("/usr/bin/ls", myargv);
execvp:
char *const myargv[] = {"ls", "-l", "-a", NULL};
execv("ls", myargv);
总结:在Linux中所有的进程一定是别人的子进程
在命令行当中,所有的进程都是bash的子进程
所以所有的进程在启动的时候都是通过exec来启动执行的
所以exec承担的是加载器的效果。(内存申请,外设访问等)
exec*不仅能够执行系统命令,还可以执行我们自己的命令。
execl("./otherExe", "otherExe", NULL);
在makefile自顶向下扫描时,所对应的伪目标(第一个目标文件)all依赖于xx,由于没有依赖方法,所以依赖方法不执行。
##Makefile
.PHONY:all
all:otherExe mycommand
otherExe:otherExe.cpp
g++ -o $@ $^ -std=c++11
mycommand:mycommand.c
gcc -o $@ $^ -std=c99
.PHONY:clean
clean:
rm -f mycommand otherExe
脚本语言就是文本文件,bash会对文本文件中的文本行进行读取,边读取,边执行。
无论我们的可执行程序还是脚本,为什么能跨语言调用呢?
因为所有语言运行起来,都是进程!只要是进程,就可以被调用!
execle:
最后一个参数:环境变量
#include
#include
#include
#include
#include
int main()
{
extern char **environ;//系统默认环境变量
putenv("PRIVATE_ENV=666666");//新增环境变量方法一:父进程的地址空间中直接putenv
pid_t id = fork();
if(id == 0){//child
printf("before: I am a process, pid: %d, ppid: %d\n",getpid(), getppid());
char *const myargv[]={"otherExe","-a","-b""-c",NULL};
char *const myenv[]={"MYVAL=1111","MYPATH=/usr/bin/XXX",NULL};//自定义环境变量
execle("./otherExe","otherExe","-a","-w","-v",NULL,environ);
execle("./otherExe","otherExe","-a","-w","-v",NULL,myenv);//新增环境变量方法二:putenv后传递给父进程 注:采用的策略是覆盖,而不是追加
printf("after: I am a process, pid: %d, ppid: %d\n",getpid(), getppid());
exit(1);
}
//father
pid_t ret = waitpid(id, NULL,0);
if(ret > 0) printf("wait success, father pid: %d, ret id: %d\n", getpid(), ret);
sleep(5);
return 0;
}
环境变量是什么时候给进程的?执行的时候给进程的。
环境变量也是数据,当我们创建子进程的时候,环境变量就已经被子进程继承下去了。所以程序替换中,环境变量信息不会被替换。
环境变量在不断给自己的每一个子进程下沉的时候,每一个子进程可以给自己定义put属于自己的环境变量,put后的环境变量会被后续的子进程再继承。
所以我如果想给子进程传递环境变量,该怎么传递?
1. 新增环境变量(父进程的地址空间中直接putenv或者putenv后传递给父进程)
2. 彻底替换(直接定义)
五、自定义shell
#include
#include
#include
#include
#include
#include
#include
#include
#define LEFT "["
#define RIGHT "]"
#define LABLE "#"
#define DELIM " \t"
#define LINE_SIZE 1024
#define ARGC_SIZE 32
#define EXIT_CODE 44
int lastcode = 0;
int quit = 0;
extern char **environ;
char commandline[LINE_SIZE];
char *argv[ARGC_SIZE];
char pwd[LINE_SIZE];
// 自定义环境变量表
char myenv[LINE_SIZE];
// 自定义本地变量表
const char *getusername()
{
return getenv("USER");
}
const char *gethostname()
{
return getenv("HOSTNAME");
}
void getpwd()
{
getcwd(pwd, sizeof(pwd));
}
void interact(char *cline, int size)
{
getpwd();
printf(LEFT"%s@%s %s"RIGHT""LABLE" ", getusername(), gethostname(), pwd);
char *s = fgets(cline, size, stdin);
assert(s);
(void)s;//为了使编译器能编过(防止告警)
// "abcd\n\0"
cline[strlen(cline)-1] = '\0';
}
int splitstring(char cline[], char *_argv[])
{
int i = 0;
argv[i++] = strtok(cline, DELIM);
while(_argv[i++] = strtok(NULL, DELIM));
return i - 1;
}
void NormalExcute(char *_argv[])
{
pid_t id = fork();
if(id < 0){
perror("fork");
return;
}
else if(id == 0){
//让子进程执行命令
//execvpe(_argv[0], _argv, environ);
execvp(_argv[0], _argv);
exit(EXIT_CODE);
}
else{
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid == id)
{
lastcode = WEXITSTATUS(status);
}
}
}
int buildCommand(char *_argv[], int _argc)
{
if(_argc == 2 && strcmp(_argv[0], "cd") == 0){
chdir(argv[1]);
getpwd();
sprintf(getenv("PWD"), "%s", pwd);
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", lastcode);
lastcode=0;
}
else if(*_argv[1] == '$'){
char *val = getenv(_argv[1]+1);
if(val) printf("%s\n", val);
}
else{
printf("%s\n", _argv[1]);
}
return 1;
}
// 特殊处理一下ls(内建命令)
if(strcmp(_argv[0], "ls") == 0)
{
_argv[_argc++] = "--color";
_argv[_argc] = NULL;
}
return 0;
}
int main()
{
while(!quit){
// 1.
// 2. 交互问题,获取命令行, ls -a -l > myfile / ls -a -l >> myfile / cat < file.txt
interact(commandline, sizeof(commandline));
// commandline -> "ls -a -l -n\0" -> "ls" "-a" "-l" "-n"
// 3. 子串分割的问题,解析命令行
int argc = splitstring(commandline, argv);
if(argc == 0) continue;
// 4. 指令的判断
// debug
//for(int i = 0; argv[i]; i++) printf("[%d]: %s\n", i, argv[i]);
//内建命令,本质就是一个shell内部的一个函数
int n = buildCommand(argv, argc);
// 5. 普通命令的执行
if(!n) NormalExcute(argv);
}
return 0;
}