3.3 进程等待示例
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> int code = 0; // 定义一个全局变量code,用于存储子进程的退出码 int main() { pid_t id = fork(); // 创建一个子进程 if(id < 0) { perror("fork"); // 如果创建子进程失败,则输出错误信息 exit(1); // 退出程序,返回状态码1 } else if(id == 0) { // 子进程 int cnt = 5; // 定义一个计数器 while(cnt) { printf("cnt: %d, 我是子进程, pid: %d, ppid : %d\n", cnt, getpid(), getppid()); // 打印子进程的信息 cnt--; sleep(1); // 子进程休眠1秒 } code = 15; // 将全局变量code的值设置为15 exit(15); // 子进程退出,返回退出码15 } else { // 父进程 printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid()); // 打印父进程的信息 int status = 0; // 定义一个变量用于存储子进程的状态 pid_t ret = waitpid(id, &status, 0); // 阻塞式的等待子进程退出 if(ret > 0) { printf("等待子进程成功, ret: %d, 子进程收到的信号编号: %d, 子进程退出码: %d\n",\ ret, status & 0x7F ,(status >> 8) & 0xFF); // 打印子进程的退出信息 printf("code: %d\n", code); // 打印全局变量code的值 } } }
这段代码创建了一个父进程和一个子进程,父进程通过fork()函数创建子进程。子进程会打印自己的信息,并在循环中每秒减少计数器的值,直到计数器为0。然后,子进程将全局变量code的值设置为15,并退出。父进程会打印自己的信息,并使用waitpid()函数阻塞等待子进程退出。当子进程退出后,父进程会打印子进程的退出信息,包括子进程收到的信号编号和退出码,以及全局变量code的值。
第一次运行让其正常终止,所以没有信号编号的返回,正常走exit函数退出,退出码为15,即为我们自己定义的退出码。
第二次运行期间我们使用kill -9 子进程pid
命令终止子进程,信号编号返回9,此时的退出码并无意义,因为程序非正常退出。
还需要注意的是,这里不管以何种方式终止进程,全局变量code始终为0,这是因为子进程和父进程是两个独立的进程,它们有各自独立的内存空间。在子进程中修改全局变量 code 的值,不会影响父进程中的 code 变量。子进程的修改只影响子进程内部的变量,而不会影响父进程的变量。
要实现子进程修改全局变量并使其对父进程可见,可以使用进程间通信机制,例如管道(Pipe)或共享内存(Shared Memory)。这样父子进程之间可以共享一块内存区域,使得修改在两个进程中都可见。
常见的信号编号如下(这里只做了解):
SIGHUP (1): 终端挂起或控制进程终止。
SIGINT (2): 中断信号,通常是Ctrl+C。
SIGQUIT (3): 退出信号,通常是Ctrl+\,会产生核心转储。
SIGILL (4): 非法指令。
SIGABRT (6): 终止信号,通常是abort()函数发出的信号。
SIGFPE (8): 浮点异常。
SIGKILL (9): 强制终止,不能被忽略、阻塞或捕获。
SIGSEGV (11): 段错误,试图访问无法访问的内存。
SIGPIPE (13): 管道破裂。
SIGALRM (14): 定时器超时。
SIGTERM (15): 终止信号,常用于请求进程正常终止。
SIGUSR1 (10): 用户自定义信号1。
SIGUSR2 (12): 用户自定义信号2。
SIGCHLD (17): 子进程状态改变,例如子进程终止时发送给父进程。
SIGCONT (18): 继续执行一个已停止的进程。
SIGSTOP (19): 停止信号,用于停止进程的执行。
SIGTSTP (20): 终端停止信号,通常是Ctrl+Z。
SIGTTIN (21): 后台进程试图从控制终端读取。
SIGTTOU (22): 后台进程试图向控制终端写入。
SIGBUS (7): 总线错误,试图访问不属于你的内存地址。
3.4 进程的阻塞等待方式
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <sys/wait.h> 5 int main() 6 { 7 pid_t id = fork(); 8 if(id == 0) 9 { 10 //子进程 11 printf("子进程开始运行, pid: %d\n", getpid()); 12 sleep(3); 13 } 14 else 15 { 16 //父进程 17 printf("父进程开始运行, pid: %d\n", getpid()); 18 int status = 0; 19 pid_t id = waitpid(-1, &status, 0); //阻塞等待, 一定是子进程先运行完毕,然后父进程获取之后,才退出! 20 if(id > 0) 21 { 22 printf("wait success, exit code: %d\n", WEXITSTATUS(status)); 23 } 24 } 25 return 0; 26 }
运行结果
[kingxzq@localhost Documents]$ ./test1 父进程开始运行, pid: 12554 子进程开始运行, pid: 12555 wait success, exit code: 0
3.5 进程的非阻塞等待方式
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <stdlib.h> 4 #include <sys/wait.h> 5 int main() 6 { 7 pid_t pid; 8 pid = fork(); 9 if(pid < 0){ 10 printf("%s fork error\n",__FUNCTION__); 11 return 1; 12 } 13 else if( pid == 0 ){ //child 14 printf("child is run, pid is : %d\n",getpid()); 15 sleep(5); 16 exit(1); 17 } 18 else{ 19 int status = 0; 20 pid_t ret = 0; 21 do 22 { 23 ret = waitpid(-1, &status, WNOHANG);//非阻塞式等待 24 if( ret == 0 ){ 25 printf("child is running\n"); 26 } 27 sleep(1); 28 }while(ret == 0); 29 if( WIFEXITED(status) && ret == pid ){ 30 printf("wait child 5s success, child return code is :%d.\n",WEXITSTATUS(status)); 31 } 32 else{ 33 printf("wait child failed, return.\n"); 34 return 1; 35 } 36 } 37 return 0; 38 }
运行结果
[kingxzq@localhost Documents]$ ./test child is running child is run, pid is : 13231 child is running child is running child is running child is running wait child 5s success, child return code is :1.
kill命令终止运行结果
[kingxzq@localhost Documents]$ ./test child is running child is run, pid is : 13268 child is running child is running child is running wait child failed, return.
3.6 进程的阻塞等待方式和进程的非阻塞等待方式有什么区别
进程的阻塞等待方式和进程的非阻塞等待方式是两种不同的等待子进程状态变化的方式:
阻塞等待:当父进程调用等待函数(如wait、waitpid等)等待子进程退出时,父进程会一直阻塞(即挂起自己的执行),直到子进程退出或发生其他指定的状态变化。在等待期间,父进程不会继续执行其他任务。
非阻塞等待:当父进程调用非阻塞等待函数(如waitpid函数的使用WNOHANG标志)等待子进程退出时,父进程会继续执行自己的任务,不会被阻塞。父进程会立即返回等待函数,无论子进程的状态是否发生变化。非阻塞等待允许父进程在等待子进程的同时继续执行其他任务。
总之,阻塞等待会导致父进程在等待子进程状态变化期间被挂起,而非阻塞等待允许父进程在等待子进程的同时继续执行其他任务。选择使用哪种等待方式取决于具体的应用场景和需求。
进程程序替换
1.替换原理
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
2.替换函数
这些函数是用于在Linux/Unix操作系统中执行新的程序的系统调用函数。它们的作用是在一个进程内启动一个新的程序执行,取代当前进程的执行。这些函数在C语言标准库头文件<unistd.h>中声明。
下面是对这些函数的简要说明:
execl:
原型:
int execl(const char *path, const char *arg, ...);
功能:用于执行指定路径的可执行文件,第一个参数是要执行的程序的路径,后面的参数是传递给新程序的命令行参数,以NULL为结束标志。
示例:
execl("/bin/ls", "ls", "-l", NULL);
execlp:
原型:
int execlp(const char *file, const char *arg, ...);
功能:类似于execl,但是它会在系统的路径中搜索可执行文件。
示例:
execlp("ls", "ls", "-l", NULL);
execle:
原型:
int execle(const char *path, const char *arg, ..., char *const envp[]);
功能:类似于execl,但是可以指定新程序的环境变量。最后一个参数是一个指向环境变量的指针数组,以NULL为结束标志。
示例:
char *const envp[] = {"PATH=/bin", NULL}; execle("/bin/ls", "ls", "-l", NULL, envp);
execv:
原型:
int execv(const char *path, char *const argv[]);
功能:类似于execl,但是参数传递方式是使用一个指向参数字符串数组的指针。第一个参数是要执行的程序的路径,第二个参数是指向参数字符串数组的指针,以NULL为结束标志。
示例:
char *const argv[] = {"ls", "-l", NULL}; execv("/bin/ls", argv);
execvp:
原型:
int execvp(const char *file, char *const argv[]);
功能:类似于execv,但是它会在系统的路径中搜索可执行文件。
示例:
char *const argv[] = {"ls", "-l", NULL}; execvp("ls", argv);
execvpe:
原型:
int execvpe(const char *path, char *const argv[], char *const envp[])
功能:类似于execv,但是可以指定新程序的环境变量,最后一个参数是一个指向环境变量的指针数组,以NULL为结束标志。
示例:
char *const argv[] = {"ls", "-l", NULL}; char *const envp[] = {"PATH=/bin", NULL}; execve("/bin/ls", argv, envp);
这些函数通常用于在一个进程内部启动一个新的程序,新程序取代当前进程的执行。执行成功则不会返回,失败则会返回-1并设置errno。它们通常用于在C程序中执行其他程序,比如在Shell中运行命令。
看下面的两段代码:
mycmd.c
1 #include <stdio.h> 2 #include <string.h> 3 #include <stdlib.h> 4 5 int main(int argc, char *argv[]) 6 { 7 if(argc != 2) 8 { 9 printf("can not execute!\n"); 10 exit(1); 11 } 12 13 printf("获取环境变量: MY_VAL: %s\n", getenv("MY_VAL")); 14 15 if(strcmp(argv[1], "-a") == 0) 16 { 17 printf("hello a!\n"); 18 } 19 else if(strcmp(argv[1], "-b") == 0) 20 { 21 printf("hello b!\n"); 22 } 23 else{ 24 printf("default!\n"); 25 } 26 27 return 0; 28 }
test.c
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <sys/wait.h> 5 6 #define NUM 16 7 8 const char *myfile = "./mycmd"; 9 10 int main(int argc, char*argv[], char *env[]) 11 { 12 char *const _env[NUM] = { 13 (char *)"MY_VAL=23333333", 14 NULL 15 }; 16 printf("进程开始运行, pid: %d\n", getpid()); 17 sleep(3); 18 char *const _argv[NUM] = { 19 (char*)"ls", 20 (char*)"-a", 21 (char*)"-l", 22 (char*)"-i", 23 NULL 24 }; 25 26 //execl(myfile, "mycmd", "-b", NULL);//调用自己的进程 27 //execl("/usr/bin/ls", "ls", "-a", "-l", NULL);//调用系统ls进程,自己输入字符命令 28 //execlp("./test.py", "test.py", NULL);//调用自建的python进程 29 //execlp("python", "python", "test.py", NULL);//结果同上,调用形式不同 30 //execlp("bash", "bash", "test.sh", NULL); //调用自建的shell进程 31 //execlp("ls", "ls", "-a", "-l", NULL); //调用系统ls进程,自己输入字符命令 32 execle(myfile, "mycmd", "-a", NULL, _env); 33 34 //execv("/usr/bin/ls", _argv); //和上面的execl只有传参方式的区别 35 //execvp("ls", _argv);//调用系统ls进程,输入字符串数组名 36 //execvpe("/usr/bin/ls",_argv,env);//效果同execv,多了一个环境变量参数 37 return 0; 38 }
运行结果:
[kingxzq@localhost Documents]$ ./test 进程开始运行, pid: 18076 获取环境变量: MY_VAL: 23333333 hello a!
3.函数解释
这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
如果调用出错则返回-1
所以exec函数只有出错的返回值而没有成功的返回值。
4.命名理解
这些函数原型看起来很容易混,但只要掌握了规律就很好记
l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量
下图exec函数族 一个完整的例子:
制作简易shell
就像系统中的bash(即shell),完成下面这类操作
用下图的时间轴来表示事件的发生次序。其中时间从左向右。shell由标识为sh的方块代表,它随着时间的流逝从左向右移动。shell从用户读入字符串"ls"。shell建立一个新的进程,然后在那个进程中运行ls程序并等待那个进程结束。
然后shell读取新的一行输入,建立一个新的进程,在这个进程中运行程序 并等待这个进程结束。
所以要写一个shell,需要循环以下过程:
1. 获取命令行
2. 解析命令行
3. 建立一个子进程(fork)
4. 替换子进程(execvp)
5. 父进程等待子进程退出(wait)
代码如下:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/wait.h> #include <sys/types.h> #define NUM 1024 #define SIZE 32 #define SEP " " //保存完整的命令行字符串 char cmd_line[NUM]; //保存打散之后的命令行字符串 char *g_argv[SIZE]; //环境变量的buffer char g_myval[64]; // shell 运行原理 : 通过让子进程执行命令,父进程等待&&解析命令 int main() { extern char**environ;//获取全局环境变量的指针 //0. 命令行解释器,一定是一个常驻内存的进程,不退出 while(1) { //1. 打印出提示信息 [kingxzq@localhost myshell]# printf("[kingxzq@localhost myshell]# "); fflush(stdout); memset(cmd_line, '\0', sizeof cmd_line); //2. 获取用户的键盘输入[输入的是各种指令和选项: "ls -a -l -i"] if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL) { continue; } if (strlen(cmd_line) <= 1) { // 如果只有回车换行符,长度为1 continue;//输入为空重新输入 } cmd_line[strlen(cmd_line)-1] = '\0';//用\0将\n替换 //"ls -a -l -i\n\0" //3. 命令行字符串解析:"ls -a -l -i" -> "ls" "-a" "-i" g_argv[0] = strtok(cmd_line, SEP); //第一次调用,要传入原始字符串 int index = 1; if(strcmp(g_argv[0], "ls") == 0) { g_argv[index++] = "--color=auto";//添加自动颜色 } if(strcmp(g_argv[0], "ll") == 0)//ll本身为ls -l,所以单独添加一个命令 { g_argv[0] = "ls"; g_argv[index++] = "-l"; g_argv[index++] = "--color=auto"; } while(g_argv[index++] = strtok(NULL, SEP)); //第二次,如果还要解析原始字符串,传入NULL if(strcmp(g_argv[0], "export") == 0 && g_argv[1] != NULL)//添加环境变量 { strcpy(g_myval, g_argv[1]); int ret = putenv(g_myval);//输入环境变量 if(ret == 0) printf("%s export success\n", g_argv[1]); continue; } //4.内置命令, 让父进程(shell)自己执行的命令,我们叫做内置命令,内建命令 //内建命令本质其实就是shell中的一个函数调用 if(strcmp(g_argv[0], "cd") == 0) //cd命令调用 { if(g_argv[1] != NULL) chdir(g_argv[1]); //cd path, cd .. continue; } //5. fork() pid_t id = fork(); if(id == 0) //child { printf("下面功能让子进程进行的\n"); printf("child, MYVAL: %s\n", getenv("MYVAL"));//获取我们输入的环境变量MYVAL printf("child, PATH: %s\n", getenv("PATH"));//获取环境变量路径 execvp(g_argv[0], g_argv); // ls -a -l -i exit(1); } //father int status = 0; pid_t ret = waitpid(id, &status, 0); if(ret > 0) printf("exit code: %d\n", WEXITSTATUS(status));//退出码接收 } return 0; }
测试ls命令
测试添加环境变量和cd命令
结语
有兴趣的小伙伴可以关注作者,如果觉得内容不错,请给个一键三连吧,蟹蟹你哟!!!
制作不易,如有不正之处敬请指出
感谢大家的来访,UU们的观看是我坚持下去的动力
在时间的催化剂下,让我们彼此都成为更优秀的人吧!!!