zl程序教程

您现在的位置是:首页 >  其他

当前栏目

Linux学习记录——십오 进程控制(2)

2023-04-18 16:44:09 时间


1、程序替换

在之前的父子进程中,创建子进程之前,父进程就有了一些代码,fork创建子进程后,子进程就运行父进程的一部分代码。进程替换则是为了让子进程运行不属于父进程的代码。

1、了解替换

程序替换的命令是exec。

在这里插入图片描述

前两个函数参数里有三个点…,三个点意味着可变参数列表。

  1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <stdlib.h>
  4 
  5 int main()
  6 {
  7     printf("begin.....
");
  8     printf("begin.....
");
  9     printf("begin.....
");
 10     printf("进程开始, PID是: %d
", getpid());                                 
 11 
 12     execl("/bin/ls", "ls", "-a", "-l", NULL);
 13 
 14     printf("end.......
");
 15     printf("end.......
");
 16     printf("end.......
");
 17     return 0;
 18 }

结果如图

在这里插入图片描述

在计算机内部,指令是在磁盘里的,程序运行后,它的pcb就在内存中。当遇到execl后,会把磁盘中的可执行程序替换到当前进程的数据。实际打印出来时,后面的end没有出现,这时候已经替换过了。

2、基本原理

每个进程都有自己的虚拟内存,页表,和映射好的物理内存。进行程序替换时,会把原来在物理内存中映射过去的空间的代码和数据替换为磁盘中可执行程序的代码和数据,这就是程序替换。

那么程序替换有没有创建新的进程呢?它没有创建进程。它只是把一个加载好的新的程序给替换了过来,内存中原有进程的pid并没有变。

对于磁盘中的程序来说,这个程序是直接被加载进了内存中,程序就是通过程序替换加载进内存的。exec等函数可以看做加载器。

操作系统内部会先把数据结构都创建出来,然后再去找程序,这也就是加载。

3、多进程

刚才的程序看不到打印end是因为新的代码和数据替换过去了,老代码已经没有人去执行了,所以就没有执行。所以程序替换只能全局替换,不能局部替换。

  1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <stdlib.h>
  4 #include <sys/wait.h>                                                                                                                                                                                                          
  5 
  6 int main()
  7 {
  8     pid_t id = fork();
  9     if(id == 0)
 10     {
 11         //child
 12         printf("我是子进程: %d
", getpid());
 13         execl("/bin/ls", "ls", "-a", "-l", NULL);
 14     }
 15 
 16     sleep(5);
 17 
 18     //father
 19     printf("我是父进程: %d
", getpid());
 20     waitpid(id, NULL, 0);
 21     return 0;
 22 }

在这里插入图片描述

像上面这样,可以发现,程序替换只能影响当前进程。因为当进行程序替换时,子进程会改变物理内存的数据,系统就会写时拷贝,给子进程另开一个空间,所以它们互不干扰。这样的替换让子进程运行了新代码,所以物理内存的代码区可以发生进程替换。

如果程序替换失败,那么就继续执行老代码,并且会返回-1.

int n = execl("/bin/lssssssss", "lssssssss", "-a", "-l", NULL);
printf("Fail : %d
", n); 
exit(0);

这样写确实会看到-1,不过没有意义,因为失败就会往下进行,成功就不继续了,所以不需要看返回值。

也写一下父进程获取退出码的代码

 20     //father                                                   
 21     int status = 0;                                            
 22     printf("我是父进程: %d
", getpid());                      
 23     waitpid(id, &status, 0);                                   
 24     printf("child exit code: %d
", WEXITSTATUS(status));                      
 25     return 0;  

这样子进程出了什么错误,父进程就会展现出对应的退出码。

4、程序替换接口

execl函数

int execl(const char* path, const char* arg, …);

path是路径,arg就是命令执行的方式,把命令行参数一个个传过来,最后以NULL结尾。

execv函数

int execv(const char* path, char* cont argv[])

argv意味要传数组。

char* const myargv[] = {"ls", "-a", "-l", "-n", NULL};
execv("/bin/ls", myargv);

execlp函数

int execlp(const char* file, const char* arg, …)

execl后面加上p,第一个参数也改为file,这个函数的特点,当我们执行指定程序的时候,只需要传指定程序名即可,系统会在环境变量PATH中查找。

execlp("ls", "ls", "-a", "-l", "-n", NULL);   

execvp函数

int execvp(const char* file, char* const argv[]);

综合上面四个可以看出规律,带p说明函数会去环境变量里找路径,带v则说明要传数组。

execle函数

int execle(const char* path, const char* arg, …, char* const envp[])

除了执行系统命令,还可以运行自己写的程序。

e代表环境变量,可以传自定义的环境变量,但是会影响原有的环境变量。

如果想把自定义变量放到环境变量里面,那么可以先extern char** environ; 然后在数组后面添加上environ。如果要两个都打印,那就putenv(“MYENV=YouCanSeeMe”);括号里保留的是写的自定义变量代码。所以环境变量能够被在全局域就可以靠这个函数来实现。

如果不这样写,也可以在文件外面

export MYENV=YouCanSeeMe
./myproc

因为这时候加入的环境变量在bash里,那么可以给到创建的子进程里,只要execle里面传了environ就行。

execvpe函数

int execvpe(const char* file, char* const argv[], char* const envp[]);

execve函数

int execve(const cjar* filename, char* const argv[], char* const envp[]);

总结

总共7个函数,只有最后一个execve是系统调用的函数,其他6个都是对它的封装。也就是无论调哪一个,内部都是调用了execve这个函数。

2、编写极简的shell(bash)

要先打印出左边的用户,然后后面可以输入命令。

  1 myshell:myshell.c
  2     gcc -o $@ $^ -std=c99
  3 .PHONY:clean
  4 clean:
  5     rm -f myshell   
  1 #include <stdio.h>
  2 #include <string.h>
  3 #include <assert.h>
  4 #include <unistd.h>
  5 #include <sys/types.h>
  6 #include <sys/wait.h>
  7 
  8 #define MAX 1024
  9 
 10 int main()
 11 {
 12     char commandstr[MAX] = {0};
 13     while(1)
 14     {
 15         printf("[zyd@hjdkhs2houqwie iiruoeg]# ");
 16         fflush(stdout);
 17         char* s = fgets(commandstr, sizeof(commandstr), stdin);
 18         assert(s);
 19         (void)s;//保证在release方式发布的时候,因为去掉了assert,所以会出现因s没有被带来的编译警告,这里什么都不做仅充当一次使用
 20         commandstr[strlen(commandstr) - 1] = '';
 21 
 22         pid_t id = fork();
 23         assert(id);
 24         (void)id;
 25         if(id == 0)
 26         {
 27             //child
 28                                                                                                                                                                                                                                
 29         }
 30         int status = 0;
 31         waitpid(id, &status, 0);
 32     }
 33 }
 

fgets可以从特定的标准输入中获取对应的命令行输入,获取之后放到缓冲区里。

在这里插入图片描述

添加进父子进程后,我们继续深入。面对输入进来的指令,我们要做字符串切割,把“ls -a -l” 换成 “ls” “-a” “-l”。

现在整体的代码如下:

 1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <string.h>
  4 #include <assert.h>
  5 #include <unistd.h>
  6 #include <sys/types.h>
  7 #include <sys/wait.h>
  8 
  9 #define MAX 1024
 10 #define ARGC 64
 11 #define SEP " "                                                                                                                                                                                                                
 12 
 13 int split(char* commandstr, char* argv[])
 14 {
 15     assert(commandstr);
 16     assert(argv);
 17     argv[0] = strtok(commandstr, SEP);
 18     if (argv[0] == NULL) return -1;
 19     int i = 1;
 20     while((argv[i++] = strtok(NULL, " ")));
 21     return 0;
 22 }
 23 
 24 void debugPrint(char* argv[])
 25 {
 26     for(int i = 0; argv[i]; i++)
 27     {
 28         printf("%d: %s
", i, argv[i]);
 29     }
 30 }
 31 
 32 int main()
 33 {
 34     char commandstr[MAX] = {0};
 35     char* argv[ARGC] = {NULL};
 36     while(1)
 37     {
 38         printf("[zyd@hjdkhs2houqwie iiruoeg]# ");
 39         fflush(stdout);
 40         char* s = fgets(commandstr, sizeof(commandstr), stdin);
 41         assert(s);
 42         (void)s;//保证在release方式发布的时候,因为去掉了assert,所以会出现因s没有被带来的编译警告,这里什么都不做仅充当一次使用
 43         commandstr[strlen(commandstr) - 1] = '';
 44 
 45         int n = split(commandstr, argv);
 46         if(n != 0) continue;
 47         //debugPrint(argv);
 48         pid_t id = fork();
 49         assert(id >= 0);
 50         (void)id;
 51         if(id == 0)
 52         {
 53             //child
 54             execvp(argv[0], argv);
 55             exit(0);
 56             
 57         }
 58         int status = 0;
 59         waitpid(id, &status, 0);
 60     }
 61 }

对输入的内容进行分割,然后从环境变量中找到对应的命令。子进程做这些,父进程等待就行。

在这里插入图片描述

1、配色方案

在这里插入图片描述

2、pwd

调用程序后,pwd可以查看位置,但是用cd更换位置后,pwd显示的位置没有更新。这是因为虽然cd命令也可以正常分割,但是cd是在子进程里运行的,而父进程并没有相应地改变位置。所以cd要让bash自己去执行,这样的命令也就是内建/置命令。

在这里插入图片描述

我们用chdir命令去改变工作路径。

在这里插入图片描述

3、export和env

环境变量是给bash设置的,也就是当前的父进程,export也是一个内建函数。

在这里插入图片描述

这样还不行,因为env命令会交给父进程执行。在进入整体的while循环前,先
extern char** environ,然后用execvpe函数来交给bash。

在这里插入图片描述

但是还不是正确的。因为现在输入的指令都是给到argv分割的,argv又是读取commandstr,随着一次次输入指令,数组里面的内容一直在变,会覆盖掉之前输入的环境变量,所以需要用户自己维护加入的环境变量。

在这里插入图片描述

在这里插入图片描述

虽然这样可以正常进行,但还是有点问题。当我们查看环境变量的时候,我们想查看的是bash的环境变量,也就是父进程,但现在的程序查的还是子进程的。

在这里插入图片描述

在这里插入图片描述

这样打印出来就很正常了。

在这里插入图片描述

基本上环境变量相关的命令都是内建命令。

4、echo

在进入while循环前就声明一个int类型变量last_exit = 0。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

至此myshell就写完了,基本上常用的操作都能用。

完整代码: https://gitee.com/kongqizyd/linux-beginner/blob/master/myshell.c

结束。