一、进程等待
1. 进程等待的必要性
这个前面是说过的哦。
1、回收子进程资源
。
2、获取子进程的退出信息
。
2. 什么是进程等待
让父进程通过等待的方式,回收子进程的PCB,Z,如果需要,获取子进程的退出信息。
3. 怎么做
//等待任意一个子进程,成功的话返回子进程的pid,失败返回-1
pid_t wait(int* status);//这里status暂时设置为NULL,不关心
父进程调用wait,表示父进程等待任意一个子进程
。
1、如果子进程没有退出,父进程wait的时候,就会阻塞。
2、如果子进程退出了,父进程wait的时候,wait就会返回了,让系统自动解决子进程的僵尸问题。
通过这段代码,就可以验证父进程等待子进程是否成功,成功的话子进程的僵尸状态就没有了。子进程会从阻塞状态变为僵尸状态,最后被父进程回收。
上面只回收了一个子进程,如何回收多进程呢?
多进程中,父进程往往最先创建,最后退出。fork之前,只有一个父进程,fork之后,父进程需要对所有的子进程进行回收。
//pid为-1,表示等待任意进程,pid > 0,等待进程id与pid相等的进程
//status输出型参数,目的是为了带出一些数据
//options为0,阻塞等待,options为WNOHANG,非阻塞等待
//等待成功,返回子进程的pid,等待失败,返回0
//既没有等待成功也没有失败返回0(子进程在运行,暂时未退出)
pid_t waitpid(pid_t pid, int* status, int options);
子进程的退出码为1,可是status为什么是256呢?这里的status不仅仅是退出码。
status是一个int(整型),32个bit,高16位不考虑。剩下的16位,次8位是子进程的退出码
,所以,进程的退出码取值范围是[0,255],低7位是终止信号
,还有一位是core dump(这个后面再讲)。
进程正常终止,终止信号为0
,子进程退出码为1,但是后面有8个0,所以status为256。要想拿到子进程的退出码,exit_code = (status >> 8) & 0xFF
。
上面所述都是基于进程正常终止的情况。那么,如果是进程异常呢?我们都知道,进程异常了,退出码是没有意义的。因此,我们的重点应该是进程为什么会异常?
是因为程序出现了问题,导致OS给你的进程发送信号了。
kill -l //查看所有的进程信号
退出信号没有0号信号。所以我们是如何判断进程是否是正常运行结束呢?status->信号的数字 == 0
。
我们用两个数字来表示子进程的执行情况:
1.进程的退出码
。
2.进程的退出信号
。
那我们要如何拿到进程的退出信号呢?status低7位表示的是进程信号,exit_signal = status & 0x7F
。
但是获取进程退出码,进程退出信号的方式太麻烦,所以操作系统提供了两个宏,WIFEXITED(status),WEXITSTATUS(status),分别用来表示进程是否正常退出(正常退出返回非零值,反之,返回0)和获取进程退出状态码
。
不知道大家有没有疑问呢。waitpid(pid_t pid, int* status, int options)
中 pid > 0表示的是等待与pid相等的子进程,是因为父进程可以拿pid找到该子进程。
可是,如果是-1呢?父进程怎么知道要等待哪一个子进程。那是因为父进程的task_struct里有对于子进程进行管理,等待子进程就看哪一个是僵尸状态就可以了
。
我们可以通过控制代码来验证进程等待的效果。
上面所说都是阻塞等待。现在,该解释什么是非阻塞等待了。
像scanf函数就是典型的阻塞等待,资源没有就绪,就会一直卡住。而非阻塞等待,就是资源没有就绪的情况,它并不会一直卡在哪里,会去做别的事情的同时也会继续等待资源就绪。
例子:明天就是C语言考试,你的好朋友李四是一个学霸,你需要借助李四的笔记来复习应对明天的考试,但是李四也要用笔记来复习。但是他说等他复习好了,就可以把笔记借给你。这时候你是很开心的,但是你也很慌张,因为平时你没有好好学习。所以你就不停的给李四打电话,问他复习好了吗?他说没有,这时候你就把电话挂断了,你就去刷抖音了,刷大学生期末挂科应该怎么办?过了一会儿,你又给李四打电话,李四说还没好呢,你又去做自己的事情了。
这就是非阻塞等待。有时候你会听到非阻塞等待比阻塞等待更高效就是这个原因
。
二、进程程序替换
fork之后,父子进程各自执行父进程代码的一部分,那如果子进程就想执行一个全新的程序呢?
进程的程序替换来完成这个功能
。
程序替换是通过特定的接口,加载磁盘上的一个全新的程序(代码和数据),加载到调用进程的地址空间中。
1. 初识程序替换函数
//pathname 你想执行的程序(路径+文件名)
//arg 我们要执行程序的程序名称
//... 给程序传递的命令行选项,必须以NULL结尾(参数包)
//失败了,返回-1
int execl(const char* pathname, const char* arg, ...);
可以看到,程序是替换成功了的。但是,有一个问题,execl 结束之后,为什么没有打印后续的 printf 呢?
那是因为你的进程已经执行另一个程序的代码了,你自己的代码已经没有了
。
总结:程序替换函数,一旦调用成功,后续代码不再执行,因为已经没有了
。
那如果失败呢?
程序替换,如果成功,不需要也不会有返回值,失败返回 -1。
也就是说exe系列的函数,只要返回,必然失败
。
那么,如果我们想用子进程进行程序替换呢?父子进程代码是共享的,数据以写时拷贝的方式各自私有。如果用子进程程序替换,是不是也影响到了父进程呢?不是说好进程之间具有独立性吗?
我们可以认为,fork之后,父子进程的代码和数据都以写时拷贝的方式各自私有
。这样就不会影响父进程了,子进程会加载新的代码和数据(物理内存上新开一段空间),更改页表与物理内存的映射关系
。
2. 关联linux历史知识
1、命令行上的命令是bash的子进程,那它和bash是共享代码和数据的,但是不同的命令有不同的功能。这是为什么呢?
我们已经知道,子进程是由父进程fork创建出来的,那么父进程不就可以对子进程进行程序替换,进程等待...等操作了吗
。
2、二进制文件,先加载程序到内存,为什么呢?
由冯诺依曼体系结构决定的
。
进程 = PCB(内核数据结构)+ 自己的代码和数据
。
我们都知道,进程是先有数据结构的,在加载代码和数据,甚至是需要的时候在加载(惰性加载),那么你的程序,是如何加载到内存的呢?
加载的本质是为了变成进程,那么是怎么加载的呢?我们使用的exe系列的函数不就是相当于一种“加载器”吗?程序从磁盘上拷贝到内存,不就是硬件到硬件吗,只有操作系统有这个权利,所以加载一定要调用系统调用或者是对系统调用做封装
。
3. 详解程序替换函数
//pathname 依旧是路径+文件名
//argv[] 与execl后半部分的参数一致,只不过是用数组组织起来
int execv(const char* pathname, char* const argv[]);
子进程执行程序替换,我们不需要自己的可执行程序名,因此从命令行参数表下标为1的参数开始。
//执行指定的命令,需要让execlp自己在环境变量PATH中寻找指定的程序
int execlp(const char* file, const char* arg, ...);
int execvp(const char* file, char* const argv[]);
上面执行的都是系统命令,那么,可不可以执行我们自己的命令呢?
execl 执行 mycmd的时候,之所以第二个参数不用带./是因为第一个参数已经表明了路径
,系统已经能够找到该文件了。
//argv[]命令行参数表
//envp[]环境变量表
//这两张表可以是系统的,也可以自定义
int execvpe(const char* file, char* const argv[], char* const envp[]);
execvpe函数传递环境变量表,会默认摒弃旧的环境变量,使用你自己设置的全新的环境变量表
,如果你要用系统提供的,就传入系统的环境变量表。
那如果我们既想使用系统的环境变量表又想使用自定义的呢?
//成功返回0,失败返回非0
int putenv(char* string);//默认的环境变量中新增一项
程序替换函数一共有7个。这六个都是execve衍生出来的。剩下的就不多做介绍了。
三、mini shell
myshell.h
#ifndef _SHELL_H_
#define _SHELL_H_
#include<cstdio>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<sys/wait.h>
#include<ctype.h>
#define MAX 1024
#define ARGS 64
#define NONE_REDIR 0
#define INPUT_REDIR 1
#define OUTPUT_REDIR 2
#define APPEND_REDIR 3
void InitGlobal();
void PrintCommandLinePrompt();
bool GetUserCommand(char usercommand[], int len);
void CheckRedir(char usercommand[]);
bool PraseUserCommand(char usercommand[]);
bool BuiltInCommandExec();
bool ForkAndExec();
#endif
myshell.cc
#include "myshell.h"
int gargc = 0; //解析用户命令存储的数组下标
char* gargv[ARGS] = {NULL};//故意设置为全局的,方便使用,安全起见,保证命令行参数表全局有效
int exit_code = 0;
char pwd[MAX] = {0};
int redir;
std::string filename;
void InitGlobal()
{
gargc = 0;
memset(gargv,0,sizeof(gargv));
redir = NONE_REDIR;
filename = "";
}
static std::string GetUserName()
{
std::string username = getenv("USER");
return username.empty()? "None": username;
}
static std::string GetHostName()
{
char name[256] = {0};
if(gethostname(name, sizeof(name)) == 0)
{
std::string hostname = name;
return hostname;
}
else
{
perror("gethostname failed");
return "None";
}
}
static std::string GetPwd()
{
//std::string pwd = getenv("PWD");
//return pwd.empty()? "None": pwd;
char temp[MAX];
char* ptr = getcwd(temp,sizeof(temp));
//将ptr指针里面的内容和PWD=(这四个字符)一起写到pwd数组里,putenv会在环境变量中查找PWD,然后进行覆盖。
snprintf(pwd, sizeof(pwd), "PWD=%s", ptr);
putenv(pwd);
std::string str = temp;
std::string delim = "/";
int pos = str.rfind(delim);
std::string s = str.substr(pos+delim.size());
return s.empty()? "/": s;
}
std::string GetHomePath()
{
std::string home = getenv("HOME");
return home.empty()? "/": home;
}
void PrintCommandLinePrompt()
{
std::string username = GetUserName();
std::string hostname = GetHostName();
std::string pwd = GetPwd();
printf("[%s@%s %s]$ ",username.c_str(), hostname.c_str(), pwd.c_str());
}
bool GetUserCommand(char usercommand[], int len)
{
if(usercommand == NULL || len <= 0)
return false;
//fgets函数会自动在字符串的末尾加上'\0'
char* res = fgets(usercommand, len, stdin);
if(res == NULL)
{
perror("fgets failed");
return false;
}
//去掉末尾的换行符(\n)
usercommand[strlen(usercommand) - 1] = 0;
return strlen(usercommand) == 0? false: true;
}
#define TrimeSpace(start) do{\
while(isspace(*start)){\
start++;\
}\
}while(0)
//检查是否有重定向
void CheckRedir(char usercommand[])
{
char* start = usercommand, *end = usercommand + strlen(usercommand) - 1;
while(start <= end)
{
if(*start == '>')
{
*start = '\0';
if(*(start+1) == '>')
{
redir = APPEND_REDIR;
start += 2;
TrimeSpace(start);
filename = start;
break;
}
else
{
redir = OUTPUT_REDIR;
start += 1;
TrimeSpace(start);
filename = start;
break;
}
}
else if(*start == '<')
{
redir = INPUT_REDIR;
*start = '\0';
start++;
TrimeSpace(start);
filename = start;
break;
}
else
{
start++;
}
}
}
bool PraseUserCommand(char usercommand[])
{
if(usercommand == NULL)
return false;
//解析
//"ls -l -s" --------> "ls", "-l", "-a"
#define SEP " "
gargv[gargc++] = strtok(usercommand, SEP);
//最后一次解析失败,会存储NULL,但是我们不需要NULL
char* str = strtok(NULL,SEP);
while(str)
{
gargv[gargc++] = str;
str = strtok(NULL, SEP);
}
//#define DEBUG
#ifdef DEBUG
printf("gargc:%d\n",gargc);
for(int i = 0; i < gargc; ++i)
printf("gargv[%d]:%s\n",i,gargv[i]);
#endif
return true;
}
bool BuiltInCommandExec()
{
std::string cmd = gargv[0];
bool ret = false;
if(strcmp(cmd.c_str(), "cd") == 0)
{
if(gargc == 2)
{
char* str = gargv[1];
if(strcmp(str, "~") == 0)
{
ret = true;
const char* home = GetHomePath().c_str();
chdir(home);
}
else
{
ret = true;
chdir(str);
}
}
else if(gargc == 1)
{
ret = true;
chdir(GetHomePath().c_str());
}
else
{
//TODO
}
}
else if(cmd == "echo")
{
if(gargc == 2)
{
std::string args = gargv[1];
if(args[0] == '$')
{
if(args[1] == '?')
{
ret = true;
printf("%d\n",exit_code);
exit_code = 0;
}
else
{
const char* name = &args[1];
printf("%s\n",getenv(name));
ret = true;
}
}
else
{
ret = true;
printf("%s\n",args.c_str());
}
}
}
return ret;
}
bool ForkAndExec()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return false;
}
else if(id == 0)
{
//更改文件描述符指向的文件,从而让子进程只需要执行系统命令就可以达到重定向的功能
if(redir == OUTPUT_REDIR)
{
int output = open(filename.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0666);
dup2(output, 1);
}
else if(redir == INPUT_REDIR)
{
int input = open(filename.c_str(), O_RDONLY);
dup2(input, 0);
}
else if(redir == APPEND_REDIR)
{
int append = open(filename.c_str(), O_CREAT | O_WRONLY | O_APPEND);
dup2(append, 1);
}
else
{
//nothing to do
}
execvp(gargv[0],gargv);
exit(0);
}
else
{
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid > 0)
{
//exit_code = WIFEXITED(status);
exit_code = WEXITSTATUS(status);
}
}
return true;
}
main.cc
#include"myshell.h"
int main()
{
char UserCommand[MAX];
while(true)
{
//打印命令行提示符
PrintCommandLinePrompt();
//获取用户输入的命令
if(!GetUserCommand(UserCommand, sizeof(UserCommand)))
continue;
InitGlobal();
//printf("echo %s\n",UserCommand);
CheckRedir(UserCommand);
//解析用户命令
PraseUserCommand(UserCommand);
//检查内建命令
if(BuiltInCommandExec())
continue;
//执行命令,不能让父进程自己去执行程序替换,否则父进程程序替换之后就结束了,而shell是一个死循环软件,应该让子进程去执行
ForkAndExec();
}
return 0;
}
Makefile
myshell:myshell.cc main.cc
g++ -o $@ $^ -g
.PHONY:clean
clean:
rm -f myshell
运行结果:
至此,我们简易版的mini_shell就完成了。觉得不错的小伙伴给个一键三连吧。