学习目标
掌握进程创建
掌握到进程等待
掌握到进程程序替换
模拟实现微型shell,深入认识shell运行原理
掌握到进程终止,认识$?
进程创建
1、fork函数
1)fork声明
#include <unistd.h> pid_t fork(void);
注意:
在父进程中,
fork
函数返回子进程的PID。在子进程中,
fork
函数返回0。如果创建子进程失败,
fork
函数返回-1。
2)fork函数的简单使用
int main() { pid_t pid = fork(); while(1) { if (pid == -1) { // 创建子进程失败(可能由于过多的进程导致) perror("fork failed"); return 1; } else if (pid == 0) { // 子进程逻辑 printf("This is the child process.\n"); } else { // 父进程逻辑 printf("This is the parent process. Child PID: %d\n", pid); } } return 0; }
简要说明:
子进程继承父进程,代码和数据是共享的,但是它们各自独立的执行程序。
子进程从
fork
函数调用的位置开始执行,继续执行之后的代码。通过检查返回值,父进程可以判断当前进程是否是子进程,从而分别执行不同的逻辑。
父进程和子进程是并发执行的,并且它们的执行顺序是不确定的,取决于操作系统的调度算法和优先级策略
3)深入理解写时拷贝
父子代码和数据是共享的,但对物理内存发生修改访问(写入操作、程序替换)时,子进程会在内存产生对应资源,同时也修改了页表的只读属性,保证了进程的独立性,减少了资源冗余。
2、内核对进程创建的管理
给子进程分配新的内存块和内核数据结构(task_struct、mm_struct、页表)
将父进程部分数据结构(环境变量、时间片、epi数据)拷贝至子进程,通过写时拷贝,管理数据和代码的独立或贡献
eip寄存器:程序计数器,保存当前正在执行指令的下一条指令的地址
添加子进程到系统进程列表当中
返回后,开始调度器调度
进程终止
进程终止是指一个正在运行的进程停止执行并结束其生命周期,它会向其父进程发送一个退出状态码,以便父进程可以根据该状态码判断子进程的执行结果,进而执行一系列的清理工作,包括关闭文件、释放内存等。
1、进程终止方式
1)正常终止
程序执行完毕
-
main函数return
return对象:父进程
return 0:正常终止,结果正确
return 非0:正常终止,结果错误
-
exit、_exit
2)异常终止
-
信号终止
kill -l:可以列出系统支持的所有信号及其对应的编号
kill -信号编号 pid:可以给进程发送信号
常见的终止信号:
SIGTERM(15):该信号是默认的终止信号,用于请求进程正常终止。
SIGKILL(9):该信号是不可捕获的终止信号,用于强制终止进程。
SIGINT(2):该信号是中断信号,通常由用户在终端上按下Ctrl+C时发送给前台进程组,用于中断正在运行的进程。
SIGQUIT(3):该信号是退出信号,通常由用户在终端上按下Ctrl+\时发送给前台进程组,用于请求进程终止,并生成核心转储文件。
SIGSTOP(19):该信号用于暂停进程的执行。与SIGKILL不同,被该信号停止的进程可以通过发送SIGCONT信号恢复执行。
-
错误终止
2、exit函数
1)exit声明
#include <unistd.h> void exit(int status);
注意:
整数参数status 是退出状态码,父进程通过进程等待来获取子进程退出状态,了解子进程的执行结果。
exit函数是C语言库中的一个函数,它会在终止进程之前执行一些清理操作,例如关闭文件、刷新缓冲区等。
2)exit简单使用
int main() { // 使用exit函数正常终止进程,并返回状态码 printf("Exiting with status code 0"); exit(0); //echo $?:显示在bash最近一次进程的退出码 // 使用_exit函数立即终止进程,不会执行该行以后的代码 printf("This line will not be executed"); }
3)_exit
_exit函数属于系统调用,用于立即终止进程,不执行任何清理操作。例如关闭文件、刷新缓冲区。
3、内核对进程终止的管理
进程终止后,内核维护了进程的状态信息,一般将进程设置为Z状态,为其父进程返回退出信息状态,清理与进程相关的资源,但可能会对进程PCB数据结构有所保留,方便下次调用
进程等待
进程等待是指一个进程等待另一个进程的终止。父进程常常需要等待子进程的执行完成,以便获取子进程的执行结果或进行进一步处理。等待进程终止可以使用wait或waitpid系统调用,它们会使父进程阻塞,直到指定的子进程终止。在等待过程中,父进程可以获取子进程的退出状态,以便进行后续处理
1、进程等待函数
1)wait
-
声明
#include <sys/types.h> #include <sys/wait.h> pid_t wait(int *status);
参数
status
是一个指向整数的指针,输出型参数,用于存储子进程的终止状态。函数的返回值是子进程的PID,可以通过返回值来判断等待的子进程是哪个进程终止。
如果返回值为-1,表示发生了错误。
-
status
status参数用于获取子进程的退出状态,但不仅限于是整数
高8位表示子进程退出的状态值,如果该子进程是由于接收到信号而退出的,这个值就是0;可以用(status>>8)&0xFF获取
低7位表示导致子进程退出的信号号码,如果该子进程是正常退出的,这个值就是0;可以用(status)&0x7F获取
最低位(第7位)如果是1,表示产生core dump文件,否则没有。
status
可以被以下几个宏用来检测子进程的退出状态:
WIFEXITED(status)
:如果子进程正常结束则返回真;
WEXITSTATUS(status)
:返回子进程的退出状态,只有在WIFEXITED
返回真时才有意义;
WIFSIGNALED(status)
:如果子进程是由于接收到信号而结束则返回真;
WTERMSIG(status)
:返回导致子进程终止的信号的编号,只有在WIFSIGNALED
返回真时才有意义;
WIFSTOPPED(status)
:如果子进程被停止则返回真;
WSTOPSIG(status)
:返回引起子进程暂停的信号的编号,只有在WIFSTOPPED
返回真时才有意义;
WIFCONTINUED(status)
:如果子进程被继续运行则返回真
-
简单使用
int main() { pid_t id=fork(); if(id==0){//子进程 printf("This is child process,pid=%d\n",getpid()); sleep(5); exit(123);//正常终止 } //此时一定是父进程 printf("This is parent process,pid=%d\n",getpid()); printf("Waitting child process to exit\n"); int status=0; pid_t ret=wait(&status);//输出型参数 if(ret){ printf("Waitting sucessed,the exit signal is %d,code is %d\n",status&0x7F,(status>>8)&0xFF); //正常终止时的终止状态或者信号终止时的信号 } // if(WIFEXITED(status)){ //采用宏定义判断 //printf("Waitting sucessed,code is %d\n",WEXITSTATUS(status)); }
上述过程属于父进程属于阻塞等待
在阻塞等待中,进程会暂停执行,直到被等待的事件发生或操作完成。
阻塞等待通常会导致进程的状态变为阻塞状态,进程会等待在某个特定的等待队列中,直到满足等待条件。
当一个进程执行阻塞等待时,它会释放CPU的控制权,将CPU资源让给其他可执行的进程。
一旦被等待的事件发生或操作完成,进程会被唤醒并继续执行。
2)waitpid
-
声明
#include <sys/types.h> #include <sys/wait.h> pid_t waitpid(pid_t pid,int *status,int options)
pid:指定要等待的进程的PID。可以传入以下值:
-1:等待任意子进程。
0:等待与调用进程属于同一个进程组的任意子进程。
正整数:等待指定PID的子进程。
options:指定等待的选项。常用的选项包括:
0:默认选项,表示阻塞等待子进程的状态变化。
WNOHANG:表示非阻塞等待子进程的状态变化
返回值:
当正常终止的时候,返回子进程的pid;
option设置为WNOHANG时,子进程还没有退出则返回0;
发生cuo'wu,则返回-1;
-
简单使用
int main() { pid_t id=fork(); if(id==0){//子进程 printf("This is child process\n"); sleep(20); exit(123);//正常终止 } //此时一定是父进程 printf("This is parent process\n"); int status=0; //轮询检查 while(1) { printf("Waitting child process to exit\n"); pid_t ret=waitpid(-1,&status,WNOHANG);//非阻塞等待 if(ret==0){ printf("The parent process do things\n"); } else if(ret==id){ printf("Waitting sucessed,code is %d\n",WEXITSTATUS(status)); //正常终止时的终止状态 break; } else{ printf("非正常返回\n");//信号终止或者发生错误 } sleep(1); } }
上述过程属于父进程属于非阻塞等待
在非阻塞等待中,进程会以轮询的方式检查被等待的事件是否发生或操作是否完成。
一个进程在非阻塞等待(busy-waiting或轮询)时,其状态通常被视为运行态(Running)
进程会持续地检查等待条件,而不会暂停执行或释放CPU控制权。
如果等待条件尚未满足,进程可以执行其他操作或逻辑,不必一直等待。
2、内核对进程等待的管理
内核在进程阻塞等待期间会将进程置于阻塞状态,并将其从可执行队列中移除,以避免浪费CPU资源。当等待条件满足时,内核会唤醒被阻塞的进程,使其重新开始执行。等待过程中,内核会周期性地检查等待条件是否满足,以确保进程可以及时被唤醒。
进程程序替换
一个正在运行的进程中将当前执行的程序替换为另一个程序的操作,替换原理是先找到执行程序,再选择执行方式。子程序的替换不会影响父进程
1、exec函数
1)声明
#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。
参数:
path/file
:指向要执行的程序文件的路径字符串,该路径可以是绝对路径也可以是相对路径。该程序文件必须是一个可执行文件,即具有可执行权限的文件。同时,有些情况也可以直接使用在环境变量PATH下的可执行的程序文件。
argv
:指向一个以NULL结尾的字符串数组,表示要传递给新程序的命令行参数。数组中的第一个元素通常是新程序的名称,随后是其他参数选项。注意,argv
参数应该是一个指向字符指针的指针,即char *const argv[]
。命名规则:
l(list) : 表示使用参数列表
v(vector) : 表示用数组
p(path) : 表示自动搜索环境变量PATH
e(env) : 表示自己维护环境变量
exec函数众多,但只有execve是系统调用,其他函数是基于此函数的封装
2)简单使用
int main() { pid_t id fork(); if(id==0) { printf("This is child process\n"); char *const env_[] = {(char*)"MYPATH=YouCanSeeMe!!",NULL};//设置继承的环境变量 printf("Prepare the replacement program\n"); execel("./myproc","myproc",NULL, env_);//环境变量的添加时覆盖式的 exit(-1);//只要执行这一行,说明替换失败 } }
exec函数有多种,根据不同的条件,选择合适的函数
2、内核对进程程序替换的管理
当进程请求替换当前程序时,内核会负责加载新的可执行文件到进程的内存中,更新页表,进行上下文切换,分配其他必要的资源。
内核会处理程序替换过程中可能出现的错误。
在程序替换完成后,内核会清理旧的页表,释放相关的资源
3、模拟实现shell
#include <stdio.h> #include <string.h> #include <string.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #define SEP " " #define NUM 1024 #define SIZE 128 char command_line[NUM]; char *command_args[SIZE]; char env_buffer[NUM]; //for test extern char**environ; //对应上层的内建命令--不创建进程,而是调用函数 int ChangeDir(const char * new_path) { chdir(new_path);// return 0; // 调用成功 } void PutEnvInMyShell(char * new_env) { putenv(new_env); } int main() { while(1) { //1. 显示提示符 printf("[输入指令]:"); fflush(stdout);//不换行刷新缓冲区 //2. 获取用户输入 memset(command_line, '\0', sizeof(command_line)); fgets(command_line, sizeof(command_line, stdin); //标准输入stdin, 不要忽视'\n' command_line[strlen(command_line) - 1] = '\0';// 清空\n //3.字符串切分 command_args[0] = strtok(command_line, SEP);//strtok字符串分割的函数 int index = 1; // 给ls命令添加颜色 if(strcmp(command_args[0]/*程序名*/, "ls") == 0 ) command_args[index++] = (char*)"--color=auto"; // strtok 截取成功,返回字符串其实地址 // 截取失败,返回NULL while(command_args[index++] = strtok(NULL, SEP));//以字符串切分 // 4.内建命令cd、export if(strcmp(command_args[0], "cd") == 0 && command_args[1] != NULL) { ChangeDir(command_args[1]); //让调用方进行路径切换 continue; } if(strcmp(command_args[0], "export") == 0 && command_args[1] != NULL) { // 目前,环境变量信息在command_line,会被清空 // 此处我们需要自己保存一下环境变量内容 strcpy(env_buffer, command_args[1]); PutEnvInMyShell(env_buffer); //注意环境变量输入格式 continue; } // 5. 创建进程,执行 pid_t id = fork(); if(id == 0) {//child // 6. 程序替换 execvp(command_args[0], command_args); exit(1); //执行到这里,子进程替换失败 } int status = 0; pid_t ret = waitpid(id, &status, 0); if(ret > 0) { printf("等待子进程成功: sig: %d, code: %d\n", status&0x7F, (status>>8)&0xFF); } } }