重庆建筑证书查询网站,移动应用开发网站,视频链接生成网站,网站备案的作用1.进程创建 我们在之前的文章中介绍过进程创建的方法#xff0c;可以通过系统调用接口fork来创建新的进程。 fork在创建完新的子进程之后#xff0c;返回值是一个pid#xff0c;对于父进程返回子进程的pid#xff0c;对于子进程返回0。fork函数后父子进程共享代码#xff…1.进程创建 我们在之前的文章中介绍过进程创建的方法可以通过系统调用接口fork来创建新的进程。 fork在创建完新的子进程之后返回值是一个pid对于父进程返回子进程的pid对于子进程返回0。fork函数后父子进程共享代码即二者执行的是同一份代码。不同的是对于数据二者相互独立各有一份于是对于父进程的数据修改不会影响到子进程在修改时进行写时拷贝将父子数据独立起来。在fork函数内进程已经被创建进程之间相互独立各自返回各自的返回值父进程返回子进程PID子进程返回0并向下继续执行代码。因而两个进程进入到了不同的分支语句中。 因为有了进程和地址空间的基础知识的铺垫我们来系统的梳理一下进程创建还有写时拷贝问题 ①在fork调用后创建出一个新的进程即现在存在两个进程他们的关系是父子进程通过fork的返回值来区分。 ②一个进程在内存中的基本内容我们可以暂时简单简化地理解为进程的PCBPCB中圈定的一个进程地址空间以及一个用于映射真实物理地址的页表。对于创建的子进程而言它为了和父进程独立所以将这三部分都拷贝了一份给自己。因此此时父进程和子进程的进程地址空间和页表是完全一致的共用同一份物理内存中的代码和数据。 ③在子进程拷贝页表时会将所有内容修改为只读属性这是为了写时拷贝做准备。 ④当父子进程的一方想要对数据进行修改则会通过页表映射修改物理内存内容但是此时由于只读属性触发了系统错误。于是发生了缺页中断经过系统检测判定为要发生写时拷贝。于是就会申请内存、拷贝内容、修改页表然后再恢复执行。
2.进程终止和等待
2.1 进程正常终止 进程的正常终止有着多种方式 ①在main函数中使用return语句这样可以结束main函数从而结束进程并返回状态码。 ②void exit(int status) exit是一个C标准库函数它可以在代码的任何地方结束进程并且会完成诸如刷新缓冲区、关闭文件描述符等清理操作然后返回状态码。 ③void _exit(int status) _exit是一个系统调用接口它用于直接终止进程返回状态码但不会完成清理操作。 辨析 对于以上三种退出方式return用于函数返回当作用于main函数时则会结束主进程并返回一个值。在执行main函数中的return时C语言标准库会隐式调用exit()函数来处理程序的退出。 exit则是可以用在代码的任何位置和return相比如果不在main函数中return只能返回结束当前函数来直接结束整个进程与此同时会完成清理操作。 而_exit()和exit一样可以直接结束进程但是不会完成如刷新缓冲区的清理操作。这是因为_ exit是一个比exit更底层的接口而缓冲区作为语言缓冲区在其层次之上。如果使用_exit结束一个进程会造成资源泄露但是进程结束操作系统会自动完成进程的资源回收工作所以实际不会出现资源泄露的问题。
2.2 进程等待 我们在之前介绍过如果父进程不对结束的子进程进行处理那么子进程将会成为僵尸进程其PCB始终占据着内存导致内存泄漏。这种情况直到父进程主动回收子进程或者父进程结束后子进程变成孤儿进程被1号进程领养后处理才结束。所以使用父进程妥善处理已经结束的子进程是很有必要的。
2.2.1 进程等待的方法 ①pid_t wait(int *status); wait函数帮助父进程获取子进程的退出信息它会等待任意一个子进程结束结束的子进程pid作为wait函数的返回值交给父进程而退出码则会通过输出型参数status带回父进程。如果在调用wait函数时子进程并未退出那么就会将父进程阻塞在其内部直到子进程结束。
#includecstdio
#includestdlib.h
#includesys/types.h
#includesys/wait.h
#includeunistd.hint main()
{pid_t id fork();if(id0){//子进程int cnn 5;while(cnn--){printf(我是子进程我的pid是%d\n,getpid());sleep(1);}exit(87);}else if(id0){//父进程int status;printf(我是父进程我的pid是%d\n,getpid());pid_t ret wait(status);if(ret 0){printf(%d 成功退出退出码为%d\n,ret,WEXITSTATUS(status));}}return 0;
}②pid_t waitpid(pid_t pid, int *status, int options); 另外一个函数接口waitpid相比于wait具有更加丰富的功能。 参数 pid可以传递指定要等待进程的pid或者也可传参为-1来等待任意一个子进程和wait功能相同。 status同样为一个输出型参数带出进程的退出信息。 options选择等待的可选功能项。如传参为0表示阻塞等待传参为WNOHANG时表示非阻塞等待此时需要自己调用非阻塞接口完成轮询检测。 返回值 0 表示等待成功返回值即为对应子进程的pid 0 表示等待成功但是子进程暂未退出 0 则说明等待失败。 #includecstdio
#includestdlib.h
#includesys/types.h
#includesys/wait.h
#includeunistd.hint main()
{pid_t id fork();if(id0){//子进程int cnn 5;while(cnn--){printf(我是子进程我的pid是%d\n,getpid());sleep(1);}exit(87);}else if(id0){//父进程int status;printf(我是父进程我的pid是%d\n,getpid());while(true){pid_t ret waitpid(id,status,WNOHANG);if(ret 0){printf(%d 成功退出退出码为%d\n,ret,WEXITSTATUS(status));break;}else if(ret0){printf(进程未退出\n);//其他工作sleep(2);}else {printf(等待失败);break;}}}return 0;
}2.2.2 理解退出码 辨析错误码和退出码 错误码我们过去经常见到错误码通常是指errno变量中的值它表示特定操作如系统调用或库函数发生错误的原因。errno是一个全局变量当出现错误时会自动将错误码存储在errno中不同的值代表着不同的错误信息。我们可以通过perror和strerror来查看错误信息。 perrorvoid perror(const char *s); 打印输入的参数字符串此时errno对应的错误信息 strerrorchar *strerror(int errnum); 打印指定错误码传入参数的错误信息 对于退出码它是在进程结束后返回的一个退出状态信息表示程序的执行结果一般约定0为成功而非0为出现错误至于退出码和原因的对应关系没有固定的要求。 错误码是全局变量是在进程执行出现错误后自动为errno赋值其本质上还是进程自己内部的事情。而退出码则是子进程向父进程“汇报”的方式是两个进程间的交互。退出码在子进程结束时通过main函数的return或者exit交付给父进程父进程用wait的status接收。 status status并非一个简单的int类型的数字对于不同的退出方式status的内容是不一样的此处简单讨论status的低16位。 当程序是执行完成并退出时视为正常终止此时16位的高8位是真正的退出码因此要拿到错误码需要对status右移8位可以采取WEXITSTATUS(status)宏来优雅完成。因此可以看出退出码的范围是0~255的。 当程序异常如被信号终止那么此时仍然会记录退出信息放在status中如下图具体细节将在信号部分系统解释。 3.进程程序替换 我们创建新的进程使用的都是fork但是我们会发现fork创建的子进程和父进程执行的是同样的代码区别仅仅是不同分支。为了使得子进程可以去执行新的程序我们可以通过exec函数将当前进程的代码和数据由新的程序进行替换从而启动新的程序。
3.1 exec函数 exec函数分为如上几种根据其名字我们就可以推断出其含义和使用方法。 l表示列表list即表示该接口的参数是以列表的形式可变参数传入的 v与list相对表示数组vector即表示该接口的参数是以数组argv的方式传入的 p表示可以不使用路径具体要替换的可执行程序通过PATH环境变量寻找 e表示可以使用新的环境变量环境变量在参数最后由数组传入 对于exec的使用还有一些要点需要强调 (1)execl的基本使用方法
#includecstdio
#includeunistd.hint main()
{execl(/usr/bin/ls,ls,-a,-l,--color);
}exec函数本质是从磁盘中找到可执行程序然后加载到内存中覆盖调用exec函数的代码和数据从而执行这个可执行程序。如上所示path参数应该指定为具体替换的可执行程序之后的可变参数即为执行这个可执行程序的参数表示如何执行这个可执行程序。 如上这段代码就完成了ls程序的替换后续的可变参数实际上也就是命令行参数了。 (2)execv的基本使用方法
#includecstdio
#includeunistd.hint main()
{char* const argv[] {(char*)ls,(char*)-a,(char*)-l,(char*)--color,nullptr};execv(/usr/bin/ls,argv);
} 对于vector的传参方式需要使用一个数组其中是命令行参数。和之前介绍过的命令行参数列表argv一样最后一个元素一定是nullptr。 (3)execvpe的基本使用方法
#includecstdio
#includeunistd.h
#includesys/wait.h
#includesys/types.h
#includestdlib.hint main()
{pid_t id fork();if(id0){//子进程char* const argv[] {(char*)Show,nullptr};char* const env[] {(char*)ONE1,(char*)TWO2,(char*)THREE3};execvpe(./show,argv,env);exit(1);}pid_t rid waitpid(id,nullptr,0);if(rid0){printf(等待成功\n);}return 0;
}我们在上述的代码中使用了execvep所以需要手动将环境变量表传参在正常情况下环境变量表是继承自父进程的但是在这种手动传递的情况下进程替换后的show程序打印环境变量表内容就拿到了我们提供的环境变量表于是对于替换后的程序而言环境变量表就是这个env了。 于是可以总结出对于一个进程环境变量的来源
①父进程创建子进程子进程会拷贝父进程的环境变量表。 a.可以通过extern char** environ声明环境变量表后访问环境变量表获取。 b.通过main函数的参数env也可以拿到环境变量的字符串数组访问方式和参数列表相同。 c.可以通过系统调用接口getenv(环境变量名)的方法获得指定环境变量的内容。
②在进程替换时使用env参数传递新的环境变量表这样替换后的进程拿到的环境变量表就是这张env了。
③通过putenv()函数也可以完成对环境变量的新增操作。 (4) 在(3)的代码中我们还可以发现实际上这段代码是一个多进程的方式进行的进程替换。fork出子进程后exec进行写时拷贝将子进程的页表指向的物理内存加载入show程序的代码数据然后从新程序的main函数开始执行。 (5) 函数名中包括p的函数表示可以不使用路径具体要替换的可执行程序通过PATH环境变量寻找于是有execl(ls,ls,-a,-l,--color); 其中第一个ls是执行的程序而第二个ls是命令行参数列表。 (6)我们发现exec函数都有返回值int实际上当进程成功替换后并没有返回值因为进程替换成功了代码数据全部被覆盖了。所以虽说返回-1是失败实际上一旦返回了值就肯定失败了。 (7)对于如上所示的execl、execlp、execle、execv、execvp、execvpe都是被封装后的C标准库接口都是真正的系统调用——execve封装过来的。根据传参方式和需求灵活选择即可。 4.shell模拟实现 shell其实就是一个命令解释器是用户和计算机与计算机系统交互的一个途径。用户输入的指令被shell获取然后进行处理解析调用对应的程序。 我们一直在强调一件事就是所有的指令实际都是可执行程序可以通过which找到指令程序所在的路径。所以shell并不生产程序它只是个程序的搬运工是一个调用者。 考虑shell的工作模式我们大致可以将其分为四步进行。 在此之前需要先对环境变量进行初始化。我们在自己的shell中模拟了一个环境变量列表在shell进程被父进程创建后shell实际上有一张真正的环境变量列表。而我们创建的这个环境变量列表则是方便我们理解与传参的一份模拟。
#includeiostream
#includecstdio
#includecstdlib
#includecstring
#includestring
#includeunistd.h
#includesys/types.h
#includesys/wait.husing namespace std;const int basesize 1024; //字符串长度上限
const int argvnum 64; //命令行列表个数上限
const int envnum 64; //环境变量个数上限char* gargv[argvnum]; //命令行参数列表
int gargc 0; //命令行参数个数
char* genv[envnum]; //环境变量列表以此自己定义的表来模拟是为系统真正的环境变量表int lastcode 0; //上个进程的退出码void InitEnv()
{extern char** environ;int i 0;while(environ[i]){genv[i] (char*)malloc(strlen(environ[i])1);strncpy(genv[i], environ[i], strlen(environ[i])1);i;}genv[i] nullptr;
}int main()
{InitEnv(); //初始化环境变量从父shell中拷贝获取char buffer[basesize];while(true){// 1. 打印命令行提示符PrintCommandLine(); // 2. 获取用户命令if(!GetCommandLine(buffer)){continue;}// 3. 分析命令ParseCommandLine(buffer); if (BuildCommand()){continue;}// 4. 执行命令ExecuteCommand();}return 0;
}
4.1 打印命令行提示符 命令行提示字符串我们常见实际上由用户名、主机名、工作路径组合而成。 注意点 ①对于用户名和主机名我们可以直接从环境变量中获取调用getenv()函数即可。 ②对于工作路径如果直接从环境变量获取我们会发现在cd命令之后取到的路径不发生任何改变。这是因为环境变量PWD并不会主动根据我们的工作路径的变化而变化而是需要靠shell去维护的。当使用了cd命令调用了chdir()函数此时修改了工作路径但这个工作路径的修改是发生在进程也就是shell进程的PCB中的。于是我们在获取工作路径时需要通过getcwd()函数来获得此时真正的工作路径。 当获得了工作路径后我们还需要对环境变量中PWD的更新负起责任通过putenv接口即可修改环境变量表中的内容。但是我们自己实现的shell中也模拟了一份环境变量表这个表就需要自己手动修改环境变量了。
string GetUserName()
{string USER getenv(USER);return USER.empty() ? None : USER;
}string GetHostName()
{string HOST getenv(HOSTNAME);return HOST.empty() ? None : HOST;
}string GetPwd()
{//通过getcwd取得工作路径然后以此为pwd//同时需要对环境变量表更新//putenv更新的是进程真正的环境变量表//自己创建的一个模拟的genv需要手动更新char pwd[basesize];char pwdenv[basesize];if(nullptr getcwd(pwd, sizeof(pwd))) return None;snprintf(pwdenv, sizeof(pwdenv),PWD%s, pwd);putenv(pwdenv); //int putenv(const char *string); 修改或新建环境变量int i 0;while(strncmp(PWD,genv[i],4)!0) i;strncpy(genv[i],pwdenv,strlen(pwdenv)1);return pwd;//在这种情况下是从环境变量中读取PWD而cd命令使用chdir改变的是进程的工作路径只修改了PCB中的cwd而不修改环境变量表//string pwd getenv(PWD);//return pwd.empty() ? None : pwd;
}string GetSimpleDir()
{string pwd GetPwd();size_t pos pwd.rfind(/);if(pos string::npos)return pwd;if(pos 0) ///return pwd;else return pwd.substr(pos1);
}string MakeCommandLine()
{//[xlz44847localhost home]$ //[用户名主机名 工作目录]提示符char command_line[basesize];snprintf(command_line, basesize, [%s%s %s]# ,GetUserName().c_str(), GetHostName().c_str(), GetSimpleDir().c_str());return command_line;
}void PrintCommandLine()
{printf(%s, MakeCommandLine().c_str());fflush(stdout); //刷新缓冲区
}
4.2 接收用户输入命令 对于用户输入命令使用fgets这个对空格符不敏感的接收方式来接收。
bool GetCommandLine(char* buffer)
{char* ret fgets(buffer, basesize, stdin);//fgets在遇到换行符和文件结尾时停止会读入换行符if(!ret)return false;buffer[strlen(buffer)-1] \0;if(strlen(buffer) 0) return false;return true;
}
4.3 解析命令 对于用户输入命令实际就是执行程序的命令行参数列表所以我们要做的就是将这个命令字符串进行分割组成一个命令行参数列表。 void ParseCommandLine(char* buffer)
{memset(gargv, 0, sizeof(gargv));gargc 0;const char* sep ;gargv[gargc] strtok(buffer, sep);while((bool)(gargv[gargc] strtok(nullptr, sep)));gargc--;
}
4.4 执行命令
4.4.1 外部命令 执行一个命令我们一般会通过创建子进程的方式来完成。使用execvpe进行进程替换然后进行差错处理。
//外部命令由子进程执行
bool ExecuteCommand()
{pid_t id fork();if(id 0) return false;if(id 0){//子进程execvpe(gargv[0], gargv, genv);exit(1);}int status 0;pid_t rid waitpid(id, status, 0);if(rid 0){if(WIFEXITED(status)) //WIFEXITED:判断子进程是否正常退出//正常退出指通过exit或到达主程序结尾而结束与之相对的是由信号进行终止{lastcode WEXITSTATUS(status); //WEXITSTATUS:获得子进程的退出码}else {lastcode 178;}return true;}return false;
}
4.4.2 内建命令 有一些命令交给子进程是无法完成任务的。比如cd指令修改shell进程的工作路径这种指令实际上是对shell这个进程自身的变化操作。当创建子进程后自然无法对父进程进行任何操作了所以这种命令需要shell自己调用函数来完成。 其中对于echo而言将其作为内建命令的原因是因为echo $?打印上一次退出码这种指令就无法通过子进程完成因此也将其作为内建命令。
//內建命令shell来执行
bool BuildCommand()
{if(strcmp(gargv[0], cd) 0){if(gargc 2){chdir(gargv[1]);lastcode 0;}else {lastcode 1;}return true;}else if(strcmp(gargv[0], export) 0){if(gargc 2){int i 0;while(genv[i]);genv[i] (char*)malloc(strlen(gargv[1])1);strncpy(genv[i],gargv[1],strlen(gargv[1])1);genv[i] nullptr;lastcode 0;}else {lastcode 2;}return true;}else if(strcmp(gargv[0], env) 0){for(int i 0; genv[i]; i){printf(%s\n, genv[i]);}lastcode 0;return true;}else if(strcmp(gargv[0], echo) 0){if(gargc 2){if(gargv[1][0]$){//echo $?if(gargv[1][1]?){printf(%d\n,lastcode);lastcode 0;}//echo $PATHelse {int i 0;string cmp gargv[1];cmp cmp.substr(1);cmp ;while(strncmp(cmp.c_str(),genv[i],cmp.length())!0) i;printf(%s\n,genv[i][cmp.length()]);lastcode 0;}}//echo xxxelse {printf(%s\n,gargv[1]);lastcode 0;}}else {lastcode 3;}return true;}return false;
}