目录
1.进程创建
1.1依旧鞭尸fork
1.1.1fork做什么
众所周知,在Linux中fork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,原进程为父进程。
进程调用fork,当控制转移到内核中的fork代码后,内核会这么做:
1——>分配新的内存块和内存数据给子进程。
2——>将父进程部分数据结构内容拷贝给子进程。
3——>添加子进程到系统进程列表当中。
4——>fork返回,开始调度器调度。
当进程调用fork之后,就会有二进制代码完全相同的两个进程。而且他们都运行到相同的地方。但每个进程都可以开始他们自己的旅程,看下面这段代码
int main( void )
{
pid_t pid;
printf("Before: pid is %d\n", getpid());
if ( (pid=fork()) == -1 )perror("fork()"),exit(1);
printf("After:pid is %d, fork return %d\n", getpid(), pid);
sleep(1);
return 0;
}
运⾏结果:
[root@localhost linux]# ./a.out
Before: pid is 43676
After:pid is 43676, fork return 43677
After:pid is 43677, fork return 0
可以看到有三行输出,一行before和两行after,43676打印了before接着打印了after,但是43677没有打印before这是为什么?看下图
所以fork之前父进程独立执行,fork之后,父子进程执行流轮流进程。执行的顺序完全由调度器决定。
1.1.2fork的返回值
返回给父进程 子进程的PID,返回给子进程 0。
1.为什么要这样?!
一个父进程可以创建多个子进程,但一个子进程只有一个父进程。对于子进程来说,父进程不需要被识别,通过PPID就能找到父进程。对于父进程来说 子进程才需要被失败,因为父进程创建子进程目的是为了让子进程工作,父进程只有知道子进程的PID才能很好的对该子进程委派任务。
2.为什么有两个返回值?
fork函数内部执行retrun之前,子进程就已经创建完毕了,所以return这个语句,父进程和子进程都会执行一次,这就是为啥会有两个返回值。
1.1.3写时拷贝
通常,父子代码共享,父子在不写入时,数据也是共享的。当任意一方试图写入,就会以写时拷贝的方式拷贝一份副本然后进行修改。具体见下图
因为写时拷贝,所以父子进程得以彻底分离,完成了进程独立性的技术保证。
1.1.4fork的意义
1.一个进程希望复制自己,使子进程执行不同代码段。例如父进程等待客户端请求,生成子进程来处理请求。
2.一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数(进程替换)。
1.1.5fork失败原因
fork函数创建子进程可能也会失败,具体原因如下:
1.系统中有太多进程,内存资源不足,创建子进程失败。
2.实际用户的进程数目超过了显示,子进程创建失败,比如PID就是有限的。
2.进程终止
进程退出的场景有三:
我们最希望的:代码运行完毕,结果正确。
帮助我们提升的:代码运行完毕,结果错误。
我们不想看到的:代码异常终止(进程崩溃)。
2.1进程退出码
2.1.1为什么main函数要返回0
众所周知,main函数是代码的入口。实际上,main函数只是用户级别代码的入口,main函数也是被其他代码调用的。比如在VS2013中main函数就是被一个名为__tmainCRTStartup的函数所调用,而__tmainCRTStartup又是通过加载器被操作系统调用。
既然main函数是被操作系统间接调用的,那么main函数调用结束后就应该给操作系统返回相应的退出信息。这个就是退出码,在main函数中就是返回值0,返回0了就代表代码运行完毕,结果正确。返回其他的就是出问题了。
2.1.2退出码概念
2.1.2.1查看退出码echo $?
进程运行结束后,可以通过 echo $? 命令查看进程最近一次退出码
翻了一段陈年老代码,运行!
查看!退出码是0,说明main函数也是顺利进行了。
2.1.2.2翻译错误退出码strerror
C语言中我们可以通过strerror函数,利用错误退出码,找到错误信息。
通过下面代码我们可以看到不同退出码对应的退出信息。可以发现退出码非0,就代表着各种错误。
ps:退出码都有对应的 字符串解释,帮助用户确定执行失败的原因。而这些退出码具体代表什么含义都是人为规定的,不同环境下相同退出码代表的执行失败原因可能不同。
Linux Shell主要退出码
2.2进程正常退出
2.2.1 return退出
参照2.1.1
2.2.2 exit退出
exit退出进程也是常用的方法,exit可以在代码的任何地方退出进程,并在退出前做一堆准备工作:
1.执行 用户通过atexit和on_exit定义的 清理函数
2.关闭所有打开的流,所有缓存数据均被写入。
3.调用_exit函数终止进程。
2.2.3 _exit退出
使用_exit函数并不常用,但也可以作为终止进程的一种方法,但相较于exit ,_exit并不会做任何收尾工作。
比如将上面代码exit替换成_exit进程终止后,缓冲区信息不会被打印,
代码演示exit and _exit
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]#
2.2.4 return exit _exit 区别与联系
区别:
只有在main函数中的return起到退出进程的作用,其他任何函数的return都不会终止进程,而exit和_exit可以在代码任何位置退出进程。
exit相较于_exit会处理进程退出的收尾工作。
1.执行 用户通过atexit和on_exit定义的 清理函数
2.关闭所有打开的流,所有缓存数据均被写入。
3.调用_exit函数终止进程。
联系:
return num ==调用 exit(num)
调用main函数运行结束后,会将返回值传给exit,然后调用exit函数。
2.3进程异常退出
异常退出的进程不会有退出码,所有进程结束后应该优先判断退出是否异常,再确定用不用退出码。
向进程发送信号导致进程异常退出:
例如,在进程运行过程中向进程发生kill -9信号使得进程异常退出,或是使用Ctrl+C使得进程异常退出等。
代码错误导致进程运行时异常退出:
比如除0错误,和访问野指针,会触发硬件级别的错误,除0:CPU状态寄存器会出现溢出,访问野指针:即将访问的虚拟地址在页表中找不到对应的映射,或者是建立的映射关系只有只读权限。像这样的问题,最终都是转换成一些硬件级别的信号来给操作系统。
3.进程等待
3.1如何理解
3.1.1是什么,怎么做
通过系统调用接口 wait/waitpid ,来对子进程进行状态检查和回收的功能,避免产生僵尸进程。
3.1.2必要性
1.避免内存泄漏
子进程退出,父进程如果不读取子进程的退出信息,子进程会变成僵尸进程。僵尸进程是一个死掉的进程,即使使用kill -9命令也无法将其杀死,因为无法杀死一个死掉的进程。所以必须通过进程等待解决问题。
2.父进程通过进程等待的方式,回收子进程资源,获取子进程退出情况。
如此父进程才能知道,子进程运行是否完成,结果对还是不对,或者是否正常退出。
3.2等待方法
3.2.0获取子进程status
下面提到的两种方法都会有一个输出型参数:status.
status 参数是 wait() 系统调用的一个 输出型参数,用于获取子进程的退出状态信息。它是一个指向 int 的指针(int*),如果传递NULL,表示不关心子进程的退出状态信息。否则系统会将子进程的终止状态写入该地址,将子进程退出信息反馈给父进程。
status不能简单当作整型看待,可以看作位图看待,具体如下图(只研究低16比特位):
正常退出,高8位表示进程退出状态,也就是退出码。
被信号所杀,低7位表示终止信号,第8位是core dump标志。
我们可以通过位操作,获得这些信号,比如:
exitCode = (status >> 8) & 0xFF; //退出码exitSignal = status & 0x7F; //退出信号
系统中定义了两个宏,来获取退出码和退出信号
WIFEXITED(status):判断子进程是否正常退出。
WEXITSTATUS(status):当WIFEXITED(status)为真时,获取子进程退出码。
3.2.1wait
阻塞父进程,等待任意一个子进程终止。
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int* status);
返回值:
成功返回被等待进程pid,失败返回-1。
参数:
输出型参数,获取⼦进程退出状态,不关⼼则可以设置成为NULL
例如父进程可以一直等待子进程,等待到子进程退出后读取退出信息。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
pid_t id = fork();
if(id == 0){
//child
int count = 10;
while(count--){
printf("I am child...PID:%d, PPID:%d\n", getpid(), getppid());
sleep(1);
}
exit(0);
}
//father
int status = 0;
pid_t ret = wait(&status);
if(ret > 0){
//wait success
printf("wait child success...\n");
if(WIFEXITED(status)){
//exit normal
printf("exit code:%d\n", WEXITSTATUS(status));
}
}
sleep(3);
return 0;
}
3.2.2waitpid
更灵活的版本,可以等待指定子进程。
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: 默认为 0 ,表⽰阻塞等待若设置为WNOHANG: 进入非阻塞模式, 若 pid 指定的⼦进程没有结束,则 waitpid() 函数返回 0 ,不予以等待。若正常结束,则返回该⼦进程的ID 。
如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
如果不存在该子进程,则立即出错返回。
3.3非阻塞轮询
上述所给的例子中,如果子进程一直不退出,父进程就会一直等待子进程结束,在这期间不做任何事,这称为阻塞等待。(通过这我们也可以知道,阻塞并不只发生在向硬件发送请求等待硬件状态准备好,也可以发生在父进程等待子进程结束获取状态)
实际上我们可以让父进程并不一直等待,在子进程退出之前可以做点别的事,等到子进程真正退出了再获取退出信息,处理对应逻辑。
做法很简单,就是使用waitpid,将第三个参数传WNOHANG,这样等待进程如果没有结束,就直接返回0,不等了,去干别的事。如果正常结束,就返回子进程PID。
例如,我们可以通过while函数,每隔一段时间就waitpid一下,看下有没有结束,如果发现没有结束就去干别的事,结束了就处理子进程结束后剩余的代码逻辑。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if (id == 0){
//child
int count = 3;
while (count--){
printf("child do something...PID:%d, PPID:%d\n", getpid(), getppid());
sleep(3);
}
exit(0);
}
//father
while (1){
int status = 0;
pid_t ret = waitpid(id, &status, WNOHANG);
if (ret > 0){
printf("wait child success...\n");
printf("exit code:%d\n", WEXITSTATUS(status));
break;
}
else if (ret == 0){
printf("father do other things...\n");
sleep(1);
}
else{
printf("waitpid error...\n");
break;
}
}
return 0;
}
输出结果如下(*^_^*)
3.3.0子进程对父进程数据并发备份 代码模型
3.4多进程创建与等待 代码模型
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t ids[10];
for (int i = 0; i < 10; i++){
pid_t id = fork();
if (id == 0){
//child
printf("child process created successfully...PID:%d\n", getpid());
sleep(3);
exit(i); //将子进程的退出码设置为该子进程PID在数组ids中的下标
}
//father
ids[i] = id;
}
for (int i = 0; i < 10; i++){
int status = 0;
pid_t ret = waitpid(ids[i], &status, 0);
if (ret >= 0){
//wait child success
printf("wiat child success..PID:%d\n", ids[i]);
if (WIFEXITED(status)){
//exit normal
printf("exit code:%d\n", WEXITSTATUS(status));
}
else{
//signal killed
printf("killed by signal %d\n", status & 0x7F);
}
}
}
return 0;
}
运行后发现,父进程同时创建了这些子进程,然后依次读取退出子进程的退出信息
4.进程替换
fork()之后父子进程各自执行 父进程的部分代码,如果想要子进程执行一个全新的程序,就需要进程替换来完成!
进程替换是通过特定的接口,加载磁盘上的一个全新的程序(代码和数据),加载到调用进程的地址空间。
4.1替换原理
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的PID并未改变。
4.2替换函数
其实有六种以exec开头的替换函数,统称exec函数:
# 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
所以exec函数只有出错的返回值,没有成功的返回值。
4.2.1命名解释
这些函数原型看起来很容易混,但只要理解后缀含义就很好理解:
l (list):表示参数采用列表
v (vector):参数使用数组
p (path):表示能自动搜索环境变量
e (env):表示自己维护环境变量。
实际上只有execve是真正的系统调用,其他五个函数最终调用的都是execve(execve在man2,其他在man3)说明其他五个函数其实是对execve系统调用进行了封装以满足不同用户的不同调用场景。
4.2.2函数解释
1.int execl(const char *path, const char *arg, ...);
第一个参数是程序执行的路径,第二个参数是一个可变参数列表,表示你要如何执行这个程序,以NULL结尾。
比如执行ls程序
execl("/usr/bin/ls", "ls", "-a", "-i", "-l", NULL);
2.int execlp(const char *file, const char *arg, ...);
第一个参数是执行程序的名字,其他同上
执行ls程序例子:
execlp("ls", "ls", "-a", "-i", "-l", NULL);
3.int execle(const char *path, const char *arg, ...,char *const envp[]);
第三个参数是你自己设置的环境变量,以NULL结尾。
例如,你设置了MYVAL环境变量,在mycmd程序内部就可以使用该环境变量。
char* myenvp[] = { "MYVAL=2021", NULL };
execle("./mycmd", "mycmd", NULL, myenvp);
4.int execv(const char *path, char *const argv[]);
第二个参数是一个指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾。
比如执行ls程序(反复鞭尸bushi)
char* myargv[] = { "ls", "-a", "-i", "-l", NULL };
execv("/usr/bin/ls", myargv);
5.int execvp(const char *file, char *const argv[]);
执行ls程序
char* myargv[] = { "ls", "-a", "-i", "-l", NULL };
execvp("ls", myargv);
6.int execve(const char *path, char *const argv[], char *const envp[]);
例如,你设置了MYVAL环境变量,在mycmd程序内部就可以使用该环境变量。
char* myargv[] = { "mycmd", NULL };
char* myenvp[] = { "MYVAL=2021", NULL };
execve("./mycmd", myargv, myenvp);
5.实现Shell命令行解释器
shell也就是命令行解释器,其运行原理就是:当有命令需要执行时,shell创建子进程,让子进程执行命令,而shell只需等待子进程退出即可。
思考下面这个shell典型的互动:
[root@localhost epoll]# ls
client.cpp readme.md server.cpp utility.h
[root@localhost epoll]# ps
PID TTY TIME CMD
3451 pts/0 00:00:00 bash
3514 pts/0 00:00:00 ps
用下图的时间轴来表示事件发生次序。其中时间从左向右。shell由标识符为bash的方块代表,它随时间流逝从左向右移动。shell从用户读入字符串“ls”。shell建立一个新的进程,然后在那个新的进程中运行ls程序并等待那个进程结束。
然后shell读取新的一行输入,建立一个新的进程,在这个进程中运行程序 并等待这个进程结束。所以要写一个shell,需要循环以下过程:
1.获取命令行
2.解析命令行
3.建立一个子进程(fork)
4.替换子进程(execvp)
5.父进程等待子进程退出(wait)
根据这些思路,和我们前面学的技术,就可以实现一个shell了。
代码实现如下:
myshell.h
1 #ifndef __MYSHELL_H__ 2 #define __MYSHELL_H__ 3 #include<iostream> 4 #include<stdlib.h> 5 #include<string.h> 6 #include<cstdio> 7 #define ARGS 64 8 extern char *gargv[]; 9 void Debug(); 10 void InitGlobal(); 11 void PrintCommandPrompt(); 12 bool GetCommandString(char cmd_str_buff[],int len); 13 bool ParseCommandString(char cmd[]); 14 void ForkAndExec(); 15 bool BulitInCommandExec(); 16 #endif
myshell.cc
1 #include"myshell.h" 2 #include<iostream> 3 #include<stdlib.h> 4 #include<string.h> 5 #include<string> 6 #include<cstdio> 7 #include<unistd.h> 8 #include<sys/types.h> 9 #include<sys/wait.h> 10 //命令行参数表,故意定义为全局 11 //用来存命令 12 char *gargv[ARGS]={NULL}; 13 int gargc=0; 14 char pwd[1024];//保存当前shell进程的工作路径 15 int lastcode=0; 16 17 void Debug() 18 { 19 printf("hello shell!\n"); 20 } 21 static std::string GetUserName() 22 { 23 std::string username=getenv("USER"); 24 return username.empty()?"None":username; 25 } 26 static std::string GetHostName() 27 { 28 std::string hostname=getenv("HOSTNAME"); 29 return hostname.empty()?"None":hostname; 30 } 31 static std::string GetPwd() 32 { 33 //getenv("PATH")获取的是环境变量的路径,chdir时不会改> 变进程环境变量表,也就是chdir不会改变环境变量pwd,需要sh ell自己更新环境变量的值 34 //std::string pwd = getenv("PWD"); 35 //return pwd.empty()?"None":pwd; 36 char temp[1024]; 37 getcwd(temp,sizeof(temp)); 38 //更新pwd 39 snprintf(pwd,sizeof(pwd),"PWD=%s",temp); 40 //设置环境变量 41 putenv(pwd); 42 //设置命令行显示格式 43 std::string pwd_lable=temp; 44 const std::string pathsep="/"; 45 auto pos=pwd_lable.rfind(pathsep); 46 if(pos==std::string::npos) 47 { 48 return "None"; 49 } 50 51 pwd_lable=pwd_lable.substr(pos+pathsep.size()); 52 53 return pwd_lable; 54 } 55 static std::string GetHomePath() 56 { 57 const char* home= getenv("HOME"); 58 return home==nullptr?"/":home; 59 } 60 //输入提示符 61 void PrintCommandPrompt() 62 { 63 64 std::string user =GetUserName(); 65 std::string hostname=GetHostName(); 66 std::string pwd=GetPwd(); 67 printf("[%s@%s %s]# ",user.c_str(),hostname.c_str(), pwd.c_str()); 68 } 69 //获取键盘输入 70 bool GetCommandString(char cmd_str_buff[],int len) 71 { 72 if(cmd_str_buff==NULL||len<=0)return false; 73 char *res=fgets(cmd_str_buff,len,stdin); 74 if(res==NULL)return false; 75 // ls -a -l\n -> ls -a -l 76 cmd_str_buff[strlen(cmd_str_buff)-1]=0; 77 return true; 78 } 79 //解析命令 80 bool ParseCommandString(char cmd[]) 81 { 82 83 if(cmd==NULL)return false; 84 #define SEP " " 85 gargc = 0; //别忘重置 86 87 //"ls -a -l"->"ls""-a""-l" 88 gargv[gargc++]=strtok(cmd,SEP); 89 90 while((bool)(gargv[gargc++]=strtok(NULL,SEP))); 91 //回退一次命令行参数个数,把NULL删了 92 gargc--; 93 return true; 94 95 } 96 void InitGlobal() 97 { 98 gargc=0; 99 memset(gargv,0,sizeof(gargv)); 100 101 } 102 void ForkAndExec() 103 { 104 pid_t id=fork(); 105 if(id<0) 106 { 107 perror("fork"); 108 return; 109 } 110 else if(id==0) 111 { 112 //子进程 113 execvp(gargv[0],gargv); 114 exit(0); 115 } 116 else 117 { 118 //父进程 119 int status=0; 120 pid_t rid=waitpid(id,&status,0); 121 if(rid>0) 122 { 123 lastcode=WEXITSTATUS(status); 124 } 125 } 126 } 127 //shell执行内建命令 128 bool BulitInCommandExec() 129 { 130 std::string cmd=gargv[0]; 131 bool ret=false; 132 if(cmd=="cd") 133 { 134 if(gargc==2) 135 { 136 std::string target=gargv[1]; 137 if(target=="~") 138 { 139 ret=true; 140 chdir(GetHomePath().c_str()); 141 } 142 else 143 { 144 ret=true; 145 chdir(gargv[1]); 146 } 147 } 148 else if(gargc==1) 149 { 150 ret=true; 151 chdir(GetHomePath().c_str()); 152 } 153 else 154 { 155 //BUG 156 } 157 } 158 else if(cmd=="echo") 159 { 160 if(gargc==2) 161 { 162 std::string args=gargv[1]; 163 if(args[0]=='$') 164 { 165 if(args[1]=='?') 166 { 167 printf("lastcode:%d\n",lastcode); 168 lastcode=0; 169 ret = true; 170 } 171 else 172 { 173 const char*name=&args[1]; 174 printf("%s\n",getenv(name)); 175 lastcode=0; 176 ret=true; 177 } 178 } 179 else{ 180 printf("%s\n",args.c_str()); 181 ret=true; 182 } 183 } 184 } 185 return ret; 186 }
main.cc
1 #include"myshell.h" 2 #define SIZE 1024 3 4 int main() 5 { 6 char commandstr[SIZE]; 7 while(true) 8 { 9 //0.初始化操作 10 InitGlobal(); 11 //1.输入命令行提示符 12 PrintCommandPrompt(); 13 //2.获取用户输入的命令 14 if(!GetCommandString(commandstr,SIZE))cont inue; 15 //3.解析 16 ParseCommandString(commandstr); 17 //4.检查命令,内建命令,要让shell自己执行 18 if(BulitInCommandExec()) 19 { 20 continue; 21 } 22 //5.执行命令,让子进程来执行 23 ForkAndExec(); 24 25 } 26 return 0; 27 }
此篇完