LInux 进程替换(理解系统调用)

目录

一、替换原理

二、替换函数

1、exec函数

2、命名理解

3、返回值

4、使用execl/lp、execv/vp

5、执行自定义命令

Makefile编译多个文件

命令行程序mycmd.c

传入自己的可执行文件

7、子进程都继承父进程环境变量

8、execle/ve修改子进程环境变量

9、exece函数为exec系列函数提供底层实现

三、理解系统调用(接口)

操作系统接口

Shell管理接口

四、内置命令和环境变量的关系

五、模拟实现简易的shell


一、替换原理

  • 当通过fork创建子进程后,子进程会开始执行与父进程相同的程序,但它们可能会执行不同的代码路径。
  • 通常,子进程随后会调用某种exec函数来运行一个全新的程序。这个调用导致子进程的用户空间内的代码和数据被新程序完全替代,并从新程序的入口点开始执行。
  • 值得注意的是,调用exec并不会创建一个新的进程;因此,该进程的标识符(PID)在调用exec之前和之后保持不变。

二、替换函数

1、exec函数

在Linux中,exec函数用于执行其他程序,它会将当前进程的地址空间替换为新程序的地址空间,从而实现程序的替换执行。六种以exec开头的函数,统称exec函数:

#include <unistd.h>`

  1. int execl(const char *path, const char *arg, ...);
  2. int execlp(const char *file, const char *arg, ...);
  3. int execle(const char *path, const char *arg, ...,char *const envp[]);
  4. int execv(const char *path, char *const argv[]);
  5. int execvp(const char *file, char *const argv[]);
  6. int execve(const char *path, char *const argv[], char *const envp[]);

2、命名理解

  • l(list) : 表示参数采用列表
  • v(vector) : 参数用数组
  • p(path) : 有p自动搜索环境变量PATH
  • e(env) : 表示自己维护环境变量

3、返回值

  • 所有这些函数,如果执行成功,都不会返回;它们会替换当前进程的映像,并从新程序的入口开始执行。
  • 如果有错误发生,这些函数会返回-1,不会发生替换。
    • #include <stdio.h>
      #include <unistd.h>
      #include <stdlib.h>
      
      int main()
      {
          printf("进程开始\n");
          execl("/usr/bin/lss","lss","-a","-l",NULL);
          printf("进程结束\n");
          return 0;
      }
      
      [hbr@VM-16-9-centos 6.replace]$ ./myproc
      进程开始
      进程结束
      
  • 这些区别主要在于参数传递方式的不同(列表vs数组)以及是否自动搜索PATH环境变量或手动指定环境变量。

4、使用execl/lp、execv/vp

1. execl(const char *path, const char *arg, ...);

用途:通过指定的路径执行程序,参数以逗号分隔的列表形式直接传递,列表必须以NULL结束。

参数

  • path:要执行的程序的路径。

  • arg:要传递给程序的第一个参数,通常为程序名称。

  • ...:后续参数,以NULL终止。

特点:参数以列表形式传递。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>

int main()
{
    pid_t id=fork();
    if(id==0)
    {
        printf("子进程开始运行,pid:%d\n",getpid());
        execl("/usr/bin/ls","ls","-a","-l",NULL);
        sleep(2);
        exit(1);
    }
    else
    {
        printf("父进程开始运行,pid:%d\n",getpid());
        int status=0;
        pid_t id=waitpid(-1,&status,0);
        if(id>0)
        {
            printf("wait success,exit code:%d\n",WEXITSTATUS(status));
        }
    }
    return 0;
}

[hbr@VM-16-9-centos 6.replace]$ ./myproc
父进程开始运行,pid:7607
子进程开始运行,pid:7608
total 36
789092 drwxrwxr-x 3 hbr hbr 4096 Mar 14 16:59 .
788707 drwxrwxr-x 8 hbr hbr 4096 Mar 13 12:42 ..
789097 -rw-rw-r-- 1 hbr hbr  289 Mar 14 14:55 execl1.c
789093 -rw-rw-r-- 1 hbr hbr   83 Mar 13 12:30 Makefile
789028 -rwxrwxr-x 1 hbr hbr 8664 Mar 14 16:59 myproc
789136 -rw-rw-r-- 1 hbr hbr  765 Mar 14 16:59 myproc.c
789090 drwxrwxr-x 2 hbr hbr 4096 Mar 13 16:16 myshell
wait success,exit code:0

2. execlp(const char *file, const char *arg, ...);

用途:功能与execl相似,但它在环境变量PATH中搜索file,不需要完整路径。

参数:

  • file:要执行的程序名称。

  • arg:第一个参数,通常是程序名称。

  • ...:后续参数,以NULL终止。

特点:PATH搜索文件,参数以列表形式传递。

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

[hbr@VM-16-9-centos 6.replace]$ ./myproc
父进程开始运行,pid:9463
子进程开始运行,pid:9464
total 36
789092 drwxrwxr-x 3 hbr hbr 4096 Mar 14 17:05 .
788707 drwxrwxr-x 8 hbr hbr 4096 Mar 13 12:42 ..
789097 -rw-rw-r-- 1 hbr hbr  289 Mar 14 14:55 execl1.c
789093 -rw-rw-r-- 1 hbr hbr   83 Mar 13 12:30 Makefile
789028 -rwxrwxr-x 1 hbr hbr 8704 Mar 14 17:05 myproc
789136 -rw-rw-r-- 1 hbr hbr  814 Mar 14 17:05 myproc.c
789090 drwxrwxr-x 2 hbr hbr 4096 Mar 13 16:16 myshell
wait success,exit code:0

3. execv(const char *path, char *const argv[]);

用途:通过指定的路径执行程序,参数通过argv数组传递。

参数

  • path:程序的路径。

  • argv[]:一个指针数组,每个指针指向一个参数字符串,数组必须以NULL结束。

特点:参数以数组形式传递。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>

int main()
{
    pid_t id=fork();
    if(id==0)
    {
        printf("子进程开始运行,pid:%d\n",getpid());
        sleep(2);
        char *const _argv[10]={
            (char*)"ls",
            (char*)"-l",
            (char*)"-a",
            (char*)"-i",
            NULL };
        execv("/usr/bin/ls",_argv);
        exit(1);
    }
    else
    {
        printf("父进程开始运行,pid:%d\n",getpid());
        int status=0;
        pid_t id=waitpid(-1,&status,0);
        if(id>0)
        {
            printf("wait success,exit code:%d\n",WEXITSTATUS(status));
        }
    }
    return 0;
}

[hbr@VM-16-9-centos 6.replace]$ ./myproc
父进程开始运行,pid:7607
子进程开始运行,pid:7608//sleep两秒后执行
total 36
789092 drwxrwxr-x 3 hbr hbr 4096 Mar 14 16:59 .
788707 drwxrwxr-x 8 hbr hbr 4096 Mar 13 12:42 ..
789097 -rw-rw-r-- 1 hbr hbr  289 Mar 14 14:55 execl1.c
789093 -rw-rw-r-- 1 hbr hbr   83 Mar 13 12:30 Makefile
789028 -rwxrwxr-x 1 hbr hbr 8664 Mar 14 16:59 myproc
789136 -rw-rw-r-- 1 hbr hbr  765 Mar 14 16:59 myproc.c
789090 drwxrwxr-x 2 hbr hbr 4096 Mar 13 16:16 myshell
wait success,exit code:0

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

用途:功能与execv相似,但它在环境变量PATH中搜索file

参数

  • file:要执行的程序名称。
  • argv[]:参数数组,以NULL结束。

特点:在PATH搜索文件,参数以数组形式传递。

char *const _argv[10]={
            (char*)"ls",
            (char*)"-l",
            (char*)"-a",
            (char*)"-i",
            NULL };
execvp("ls",_argv);

[hbr@VM-16-9-centos 6.replace]$ ./myproc
父进程开始运行,pid:10531
子进程开始运行,pid:10532
total 36
789092 drwxrwxr-x 3 hbr hbr 4096 Mar 14 17:08 .
788707 drwxrwxr-x 8 hbr hbr 4096 Mar 13 12:42 ..
789097 -rw-rw-r-- 1 hbr hbr  289 Mar 14 14:55 execl1.c
789093 -rw-rw-r-- 1 hbr hbr   83 Mar 13 12:30 Makefile
789028 -rwxrwxr-x 1 hbr hbr 8672 Mar 14 17:08 myproc
789136 -rw-rw-r-- 1 hbr hbr  844 Mar 14 17:08 myproc.c
789090 drwxrwxr-x 2 hbr hbr 4096 Mar 13 16:16 myshell
wait success,exit code:0

5、执行自定义命令

Makefile编译多个文件

  • .PHONY:all声明了一个伪目标all,意味着all不是一个真正的文件名,它的作用主要是作为一个便捷的方式来列出所有的默认构建目标。
  • all:exec mycmd定义了一个规则,它指出要构建all时,需要构建execmycmd这两个目标。
  • -o $@指定输出的可执行文件名,其中$@是自动变量,代表当前规则的目标名(execmycmd)。
  • $^是另一个自动变量,代表所有的依赖文件列表(这里分别是exec.cmycmd.c)。
[hbr@VM-16-9-centos 6.replace]$ cat Makefile 
.PHONY:all
all:exec mycmd

exec:exec.c
	gcc -std=c99 -o $@ $^
mycmd:mycmd.c
	gcc -std=c99 -o $@ $^

.PHONY:clean
clean:
	rm -f exec mycmd

[hbr@VM-16-9-centos 6.replace]$ make
gcc -std=c99 -o mycmd mycmd.c

[hbr@VM-16-9-centos 6.replace]$ ll
total 44
-rwxrwxr-x 1 hbr hbr 8664 Mar 14 17:25 exec
-rw-rw-r-- 1 hbr hbr  844 Mar 14 17:08 exec.c
-rw-rw-r-- 1 hbr hbr  289 Mar 14 14:55 execl.c
-rw-rw-r-- 1 hbr hbr  138 Mar 14 17:24 Makefile
-rwxrwxr-x 1 hbr hbr 8456 Mar 14 17:26 mycmd
-rw-rw-r-- 1 hbr hbr  364 Mar 14 17:26 mycmd.c
drwxrwxr-x 2 hbr hbr 4096 Mar 13 16:16 myshell

命令行程序mycmd.c

这是一个简单的命令行程序,它根据传入的参数执行不同的操作。

  • 首先,它检查是否有恰好两个参数(argc!=2),即程序名和一个额外的参数。如果不是,它会打印 "can not execute!" 并退出。
  • 接下来,它比较第二个参数(argv[1])与字符串 "-a" 和 "-b"。如果是 "-a",它打印 "command a";如果是 "-b",它打印 "command b";否则,它打印 "default!"。
  • 最后,程序正常结束。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc,char *argv[])
{
    if(argc!=2)
    {
        printf("can not execute!\n");
        exit(1);
    }
    if(strcmp(argv[1],"-a")==0)
        printf("command a\n");
    else if(strcmp(argv[1],"-b")==0)
        printf("command b\n");
    else
        printf("default!\n");

    return 0;
}

[hbr@VM-16-9-centos 6.replace]$ ./mycmd
can not execute!
[hbr@VM-16-9-centos 6.replace]$ ./mycmd -a
command a
[hbr@VM-16-9-centos 6.replace]$ ./mycmd -b
command b
[hbr@VM-16-9-centos 6.replace]$ ./mycmd -c
default!

传入自己的可执行文件

  • 在这个代码示例中使用的 execl 函数,是用来在当前进程中加载并执行一个新程序的。这个函数需要可执行文件的路径作为参数,而不是源代码文件。
  • 源代码文件不能直接被 execl 执行,因为它们是文本文件,需要先编译成机器可以理解和执行的二进制格式。
  • 当代码中使用 execl(myfile, "mycmd", "-b", NULL); 时,操作系统会查找路径 /home/hbr/linux/process/6.replace/mycmd 指向的可执行文件,加载它到当前进程的内存空间中,并开始执行。
  • 如果 myfile 变量指向的是一个 C 语言源代码文件,操作系统无法执行它,因为它不是二进制的可执行格式。 
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>

//绝对路径和相对路径均可
//const char *myfile="/home/hbr/linux/process/6.replace/mycmd";
const char *myfile="./mycmd";

int main()
{
    pid_t id=fork();
    if(id==0)
    {
        printf("子进程开始运行,pid:%d\n",getpid());
        sleep(2);
        char *const _argv[10]={
            (char*)"ls",
            (char*)"-l",
            (char*)"-a",
            (char*)"-i",
            NULL };
        execl(myfile,"mycmd","-b",NULL);
        exit(1);
    }
    else
    {
        printf("父进程开始运行,pid:%d\n",getpid());
        int status=0;
        pid_t id=waitpid(-1,&status,0);
        if(id>0)
        {
            printf("wait success,exit code:%d\n",WEXITSTATUS(status));
        }
    }
    return 0;
}
  • const char *myfile="./mycmd"; 这行代码定义了一个指向字符串的指针myfile,这个字符串包含了要执行的程序的路径。这里使用的是相对路径,意味着mycmd程序位于当前工作目录下。如果你的程序位于其他位置,你可以通过修改这个字符串来指定正确的路径,无论是绝对路径还是相对路径。

  • execl(myfile,"mycmd","-b",NULL); 这行代码实际上是在调用execl函数,用于在当前进程(这里是子进 程)中执行一个新的程序。execl函数的第一个参数是要执行的程序的路径,这里通过myfile变量传递。第二个参数是程序名,这里是"mycmd",它是传递给新程序的argv[0]的值。接下来的参数是传递给mycmd程序的命令行参数,在这个例子中只有一个"-b"。最后一个参数必须是NULL,标志着参数列表的结束。

[hbr@VM-16-9-centos 6.replace]$ make
gcc -std=c99 -o exec exec.c
gcc -std=c99 -o mycmd mycmd.c
[hbr@VM-16-9-centos 6.replace]$ ./exec 
父进程开始运行,pid:20549
子进程开始运行,pid:20550
command b
wait success,exit code:0

7、子进程都继承父进程环境变量

在操作系统中,当创建一个子进程时,无论是否执行了程序替换(例如,使用exec系列函数),子进程通常会继承父进程的环境变量。这种行为允许子进程访问父进程定义的环境设置,如路径设置、用户信息等。下面分两种情况进行详细讲解:

不执行程序替换的情况

在不执行程序替换的情况下,子进程是父进程的一个副本,除了进程ID等少数属性外。子进程继承了父进程的数据段、代码段、堆、栈以及环境变量等。因此,子进程可以使用和修改它继承的环境变量,这些修改不会影响父进程的环境变量,因为父子进程在内存中是隔离的。

执行程序替换的情况

执行程序替换(例如,通过exec系列函数)意味着子进程将放弃其代码段和数据段,以及堆和栈,转而加载一个新程序。但即便如此,新程序的执行环境仍然继承自父进程的环境变量。exec系列函数允许新程序访问这些环境变量,除非显式地通过调用exec函数的某些变体来改变它们(如execleexecve,这些变体允许调用者指定一个新的环境变量数组)。

为什么这么设计?

这种设计使得环境变量成为了进程间通信的一种简单而有效的方式。父进程可以通过设置环境变量来影响子进程的行为,而不必修改代码。例如,很多程序都会读取PATH环境变量来确定可执行文件的搜索路径,或者读取HOME变量来确定用户的家目录。

8、execle/ve修改子进程环境变量

execle(const char *path, const char *arg, ..., char *const envp[]);

用途:execl类似,但允许指定一个环境变量数组,用于新程序的执行环境。

参数:

  • path:程序的路径。

  • arg:第一个参数,通常是程序名称。

  • ...:后续参数,以NULL终止。

  • envp[]:指向环境变量字符串数组的指针,数组必须以NULL结束。

特点:参数以列表形式传递,可以指定环境变量。

execve(const char *path, char *const argv[], char *const envp[]);

用途:是exec系列函数中最全面的函数,它允许通过路径执行程序,同时指定参数和环境变量。

参数

  • path:程序的路径。

  • argv[]:参数数组,以NULL结束。

  • envp[]:环境变量数组,以NULL结束。

特点:参数和环境变量都以数组形式传递,提供最大的灵活性。

在这个场景中,环境变量MY_VAL的值1024是通过在子进程中使用execleexecve函数传递给新执行的程序mycmd的。这些函数允许你指定一个新的环境变量数组,这个数组会替换掉调用进程的环境变量。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc,char *argv[])
{
    if(argc!=2)
    {
        printf("can not execute!\n");
        exit(1);
    }

    printf("获取环境变量:%s\n",getenv("MY_VAL"));

    if(strcmp(argv[1],"-a")==0)
        printf("command a\n");
    else if(strcmp(argv[1],"-b")==0)
        printf("command b\n");
    else
        printf("default!\n");

    return 0;
}

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>

//const char *myfile="/home/hbr/linux/process/6.replace/mycmd";
const char *myfile="./mycmd";

int main()
{
    pid_t id=fork();
    if(id==0)
    {
        printf("子进程开始运行,pid:%d\n",getpid());
        char *const _env[10]={
            (char*)"MY_VAL=1024",NULL};
        
        execle(myfile,"mycmd","-a",NULL,_env);

        exit(1);
    }
    else
    {
        printf("父进程开始运行,pid:%d\n",getpid());
        int status=0;
        pid_t id=waitpid(-1,&status,0);
        if(id>0)
        {
            printf("wait success,exit code:%d\n",WEXITSTATUS(status));
        }
    }
    return 0;
}

[hbr@VM-16-9-centos 6.replace]$ ./exec 
父进程开始运行,pid:928
子进程开始运行,pid:929
获取环境变量:1024
command a
wait success,exit code:0


 char *const _mycmd[10]={
            (char*)"mycmd",
            (char*)"-a"};
        execve(myfile,_mycmd,_env);

[hbr@VM-16-9-centos 6.replace]$ ./exec 
父进程开始运行,pid:3807
子进程开始运行,pid:3808
获取环境变量:1024
command a
wait success,exit code:0

我们通过打印出环境变量检测是否指定成功

设置环境变量: 在子进程中,通过定义一个包含环境变量定义的字符串数组_env,并将其作为execleexecve函数的最后一个参数传递,你可以设置新进程的环境变量。在这个例子中,_env数组定义了一个环境变量MY_VAL,其值为1024

char *const _env[10]={
    (char*)"MY_VAL=1024",NULL};

使用execleexecve执行程序: 当使用execleexecve函数时,你可以传递上面定义的环境变量数组_env给新程序。这样,新程序就会以这个新的环境变量集合启动。

使用execle:

execle(myfile,"mycmd","-a",NULL,_env);

或使用execve:

char *const _mycmd[10]={
    (char*)"mycmd",
    (char*)"-a"};
execve(myfile,_mycmd,_env);

在新程序中获取环境变量: 在mycmd程序中,使用getenv函数可以获取指定的环境变量的值。如果环境变量存在,getenv会返回指向该环境变量值的指针。

printf("获取环境变量:%s\n",getenv("MY_VAL"));

这样,当mycmd程序执行时,它会打印出获取环境变量:1024,显示了通过execleexecve传递并在新进程中设置的环境变量MY_VAL的值。

execle(myfile,"mycmd","-a",NULL,_env);

[hbr@VM-16-9-centos 6.replace]$ ./exec 
父进程开始运行,pid:928
子进程开始运行,pid:929
获取环境变量:1024
command a
wait success,exit code:0


char *const _mycmd[10]={
            (char*)"mycmd",
            (char*)"-a"};
execve(myfile,_mycmd,_env);

[hbr@VM-16-9-centos 6.replace]$ ./exec 
父进程开始运行,pid:3807
子进程开始运行,pid:3808
获取环境变量:1024
command a
wait success,exit code:0

9、exece函数为exec系列函数提供底层实现

实际上,Linux中的exec系列函数(如execlexecpexecleexecvexecvpexecvpe等)最终都是通过调用execve函数来执行程序替换的。execveexec系列函数中最底层的实现,它直接与Linux内核交互来执行程序替换。其他exec函数提供了不同的接口以方便使用,在内部,它们会转换参数格式以符合execve的要求,然后调用execve

execve函数

execveexec系列函数中唯一一个直接由内核提供的系统调用。它的原型如下

int execve(const char *pathname, char *const argv[], char *const envp[]);
  • pathname是要执行的程序的路径。
  • argv是指向字符串数组的指针,这些字符串构成了传递给新程序的参数列表。
  • envp是指向字符串数组的指针,这些字符串构成了新程序的环境变量列表。

为什么也需要其他exec函数

尽管execve提供了执行程序替换所需的全部功能,但其接口对于日常使用来说可能不够方便或直观。例如,要执行一个程序并传递参数,使用execve需要手动构建参数和环境变量的数组。因此,其他exec函数提供了更简单或更适合特定场景的接口。

  • execlexeclp允许程序员直接在函数调用中列出所有参数,而不是通过数组传递。
  • execvexecvp接受参数数组,但它们不要求手动指定环境变量数组,而是自动使用当前进程的环境。
  • execle提供了一个显式指定环境变量数组的接口。
  • 特别的是,execvpexecvpe版本还会在环境变量中指定的路径列表中搜索程序文件名,而不是要求完整的路径名。

三、理解系统调用(接口)

内部和外部命令体现了Shell作为操作系统管理接口的能力,而操作系统接口(如系统调用)是这些命令实现其功能的基础。Shell通过将用户命令转化为对应的系统调用或内部操作,桥接了用户和操作系统之间的交互。

操作系统接口

  • 操作系统接口主要指的是操作系统提供给用户和程序员的编程接口,这些接口通常是一组系统调用。
  • 系统调用是操作系统的核心提供的服务,允许用户空间的程序执行诸如文件操作、进程控制和网络通信等操作系统级别的操作
  • 例如,当程序执行文件读写操作时,实际上是通过系统调用来实现的,系统调用作为用户程序和操作系统内核之间的桥梁。

Shell管理接口

Shell作为用户与操作系统之间的接口,提供了一种交互式的环境或脚本执行环境,使得用户可以通过命令来操作操作系统。Shell本身并不直接执行操作,而是通过解释命令来调用相应的程序或操作系统功能:

外部命令:这类命令对应于磁盘上的可执行程序,通常由第三方提供。当这类命令被执行时,Shell会创建一个新的子进程来运行对应的二进制文件。这意味着命令执行与Shell本身是隔离的,运行完毕后,控制权返回给Shell。外部命令的示例包括像lsgrep等大多数UNIX/Linux命令。

一些常见的内置命令包括:

  • cd:改变当前工作目录
  • echo:打印文本到标准输出
  • pwd:显示当前工作目录
  • export:设置环境变量

内置命令:这类命令直接由Shell自身实现,并在Shell的上下文中执行。这意味着这些命令可以直接访问和修改Shell的内部状态或环境。由于这些命令需要直接影响Shell本身或者为了效率考虑而不通过创建新进程来执行,它们是作为Shell的一部分实现的。典型的内置命令包括cdexport等,这些命令需要改变当前工作目录或者环境变量,对Shell的运行环境产生直接影响。

一些常见的外部命令包括:

  • ls:列出目录内容
  • grep:搜索文本
  • cat:连接文件并打印到标准输出
  • cp:复制文件或目录

四、内置命令和环境变量的关系

内置命令提供了一种机制来操作和配置环境变量,而环境变量则为内置命令(以及外部命令)的执行提供上下文和配置信息。这种相互作用是shell环境灵活和强大的原因之一。

内置命令和环境变量在shell中扮演着各自独特但互相关联的角色。它们之间的关系主要表现在以下几个方面:

  1. 环境变量配置内置命令行为:许多内置命令的行为可以通过设置特定的环境变量来调整。例如,内置命令echo在打印时,其行为可能会受到IFS(内部字段分隔符)环境变量的影响。

  2. 内置命令管理环境变量:Shell提供了一些内置命令来直接操作环境变量,如exportunset等。使用export可以设置或导出环境变量,以便在当前shell及其子shell中使用。unset命令可以用来删除环境变量。

  3. 环境变量影响命令查找:当执行任何命令时,shell通过PATH环境变量指定的目录列表来查找外部命令的可执行文件。虽然这直接关系到外部命令,但它也说明了环境变量对shell操作的全局影响,包括如何通过修改环境变量来影响内置命令处理外部命令的方式。

  4. 内置命令使用环境变量进行操作:某些内置命令在执行时会使用到特定的环境变量。例如,printenv内置命令(在某些shell版本中可用)可以打印当前的环境变量列表。

  5. 初始化和配置Shell环境:在启动shell时,特定的配置文件(如.bashrc.bash_profile/etc/profile等)会被读取和执行。这些文件中的命令经常包括使用内置命令设置环境变量的语句,以配置用户的shell环境。

五、模拟实现简易的shell

我们来实现一个简单的shell程序,它模拟了Linux shell的基本行为,包括接收命令行输入、解析命令和参数、执行命令,并显示执行结果。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>
#include <sys/types.h>

#define NUM 1024
#define SIZE 32
#define SEP " "//分隔符(必须字符串)
//保存完整的命令行字符串
char cmd_line[NUM];
//保存打散之后的命令行字符串
char *g_argv[SIZE];

//shell运行原理:通过让子进程执行命令,父进程等待&解析命令
int main()
{
    //0.命令行解释器一定是一个常驻内存的进程,不退出
    while(1)
    {
        //1.打印出提示信息[hbr@localhost myshell]#
        printf("[root@localhost myshell]#");
        fflush(stdout);
        memset(cmd_line,'\0',sizeof cmd_line);
        //2.获取用户的键盘输入[输入的是各种指令和选项:"ls -a -l"]
        if(fgets(cmd_line,sizeof cmd_line,stdin)==NULL)
        {
            continue;
        }
        //处理输入后的回车键导致的换行问题
        cmd_line[strlen(cmd_line)-1]='\0';
        printf("echo: %s\n",cmd_line);
        //3.命令行解析 "ls -a -l" -> "ls" "-a" "-l"
        g_argv[0]=strtok(cmd_line,SEP);//第一次调用,传入原始字符串
        int index=1;
        while(g_argv[index++]=strtok(NULL,SEP));//第二次还要解析原始字符串,传入NULL
        // for debug 
       // for(index=0;g_argv[index];index++)
       // {
       //     printf("g_argv[%d]: %s\n",index,g_argv[index]);
       // }
        
       //4.TODO内置命令,让父进程(shell)自己执行的命令叫做内置命令、内建命令
        //cd命令不能在子进程运行
        if(strcmp(g_argv[0],"cd")==0)
        {
            if(g_argv[1]!=NULL) chdir(g_argv[1]);
            continue;
        }
        //5.fork()
        pid_t id=fork();
        if(id==0)
        {
            printf("下面功能让子进程进行的\n");
            execvp(g_argv[0],g_argv);
            exit(1);
        }
        int status=0;
        pid_t ret=waitpid(id,&status,0);
        if(ret>0) printf("exit code:%d\n",WEXITSTATUS(status));
    }
    return 0;
}
  1. 无限循环等待用户输入:程序首先进入一个无限循环,等待用户输入命令。这模拟了shell的常驻内存特性,即shell会一直运行,等待用户输入。

  2. 接收和处理用户输入:使用fgets函数从标准输入(键盘)读取一行命令,并用strtok函数将其拆分为命令和参数。这里的SEP定义为一个空格字符,用作命令和参数之间的分隔符。

  3. 内置命令处理:检查是否输入了cd命令,因为cd是一个内置命令,需要由当前的shell进程(而不是子进程)来执行。如果是cd命令,就调用chdir函数改变当前工作目录。

  4. 创建子进程执行外部命令:通过fork创建一个子进程。在子进程中,使用execvp函数执行用户输入的命令。execvp允许在环境变量PATH中搜索命令的可执行文件,并用提供的参数数组执行该命令。

  5. 父进程等待子进程结束:父进程(即原始的shell进程)使用waitpid函数等待子进程结束,并获取子进程的退出状态。

  6. 命令执行反馈:程序打印出每个命令的执行结果,包括命令输出和退出状态。

测试一下:

[hbr@VM-16-9-centos myshell]$ ./myshell 
[root@localhost myshell]#ls -a -l
echo: ls -a -l
下面功能让子进程进行的
total 28
drwxrwxr-x 2 hbr hbr 4096 Mar 13 16:16 .
drwxrwxr-x 3 hbr hbr 4096 Mar 13 15:16 ..
-rw-rw-r-- 1 hbr hbr   76 Mar 13 15:18 Makefile
-rwxrwxr-x 1 hbr hbr 9128 Mar 13 16:16 myshell
-rw-rw-r-- 1 hbr hbr 2074 Mar 13 16:16 myshell.c
exit code:0
[root@localhost myshell]#pwd
echo: pwd
下面功能让子进程进行的
/home/hbr/linux/process/6.replace/myshell
exit code:0
[root@localhost myshell]#cd ..
echo: cd ..
[root@localhost myshell]#pwd
echo: pwd
下面功能让子进程进行的
/home/hbr/linux/process/6.replace
exit code:0
[root@localhost myshell]#cd /
echo: cd /
[root@localhost myshell]#pwd
echo: pwd
下面功能让子进程进行的
/
exit code:0
[root@localhost myshell]#cd /home/hbr/linux/process/6.replace/myshell
echo: cd /home/hbr/linux/process/6.replace/myshell
[root@localhost myshell]#pwd
echo: pwd
下面功能让子进程进行的
/home/hbr/linux/process/6.replace/myshell
exit code:0
[root@localhost myshell]#^C
[hbr@VM-16-9-centos myshell]$ 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/462401.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Hack The Box-Jab

目录 信息收集 nmap enum4linux 服务信息收集 Pidgin kerbrute hashcat 反弹shell & get user 提权 系统信息收集 端口转发 漏洞利用 get root 信息收集 nmap 端口探测┌──(root㉿ru)-[~/kali/hackthebox] └─# nmap -p- 10.10.11.4 --min-rate 10000 -oA…

Docker----Dockerfile构建微服务镜像

目录 一、关键步骤 二、具体步骤 1、准备后端jar包(这里以java后端演示) 2、编写Dockerfile 3、构建镜像 4、运行镜像容器 5、测试是否成功 一、关键步骤 1、准备后端jar包(这里以java后端演示) 2、编写Dockerfile 3、构建镜像 4、运行镜像容器 5、测试是否成功 二…

Openfeign使用教程(带你快速体验Openfeign的便捷)

文章摘要 本文中将教会您如何快速使用Openfeign&#xff0c;包括Opengfeign的基础配置、接口调用、接口重试、拦截器实现、记录接口日志信息到数据库 文章目录 文章摘要一、Openfeign初步定义二、Openfeign快速入门1.引入maven坐标2.启动类增加EnableFeignClients注解3.定义fei…

从金蝶云星空到钉钉通过接口配置打通数据

从金蝶云星空到钉钉通过接口配置打通数据 对接系统金蝶云星空 金蝶K/3Cloud&#xff08;金蝶云星空&#xff09;是移动互联网时代的新型ERP&#xff0c;是基于WEB2.0与云技术的新时代企业管理服务平台。金蝶K/3Cloud围绕着“生态、人人、体验”&#xff0c;旨在帮助企业打造面…

IDEA连接Mysql失败:下载驱动失败,Failed todownload Cannot download Read timed out

解决&#xff1a; 1. 手动加入jar包 2.选择自己maven仓库中存在mysql-connector 3. 选择完毕后&#xff0c;确定使用&#xff1a; 4. 进行测试连接

【代码随想录】【回溯算法】补day24:组合问题以及组合的优化

回溯算法&#xff1a;递归函数里面嵌套着for循环 给定两个整数 n 和 k&#xff0c;返回 1 … n 中所有可能的 k 个数的组合。 示例: 输入: n 4, k 2 输出: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ] 包含组合问题和组合问题的剪枝优化 class solution:def combine(se…

免费接口调用 招标信息自动抽取|招标信息|招标数据解析接口

一、开源项目介绍 一款多模态AI能力引擎&#xff0c;专注于提供自然语言处理&#xff08;NLP&#xff09;、情感分析、实体识别、图像识别与分类、OCR识别和语音识别等接口服务。该平台功能强大&#xff0c;支持本地化部署&#xff0c;并鼓励用户体验和开发者共同完善&#xf…

git:码云仓库提交以及Spring项目创建

git&#xff1a;码云仓库提交 1 前言 码云访问稳定性优于github&#xff0c;首先准备好码云的账户&#xff1a; 官网下载GIT&#xff0c;打开git bash&#xff1a; 查看当前用户的所有GIT仓库&#xff0c;需要查看全局的配置信息&#xff0c;使用如下命令&#xff1a; git …

旅游管理系统 |基于springboot框架+ Mysql+Java+Tomcat的旅游管理系统设计与实现(可运行源码+数据库+设计文档)

推荐阅读100套最新项目 最新ssmjava项目文档视频演示可运行源码分享 最新jspjava项目文档视频演示可运行源码分享 最新Spring Boot项目文档视频演示可运行源码分享 目录 前台功能效果图 管理员功能登录前台功能效果图 系统功能设计 数据库E-R图设计 lunwen参考 摘要 研究…

羊大师分析羊奶入菜,美味新体验

羊大师分析羊奶入菜&#xff0c;美味新体验 羊奶&#xff0c;这一古老而珍贵的食材&#xff0c;近年来在料理界掀起了一股新风潮。其醇厚的口感和丰富的营养价值&#xff0c;让越来越多的人开始尝试将羊奶融入日常烹饪中&#xff0c;为味蕾带来前所未有的新体验。 在传统的烹饪…

由浅到深认识C语言(5):函数

该文章Github地址&#xff1a;https://github.com/AntonyCheng/c-notes 在此介绍一下作者开源的SpringBoot项目初始化模板&#xff08;Github仓库地址&#xff1a;https://github.com/AntonyCheng/spring-boot-init-template & CSDN文章地址&#xff1a;https://blog.csdn…

湖南麒麟SSH服务漏洞

针对湖南麒麟操作系统进行漏洞检测时&#xff0c;会报SSH漏洞风险提醒&#xff0c;具体如下&#xff1a; 针对这些漏洞&#xff0c;可以关闭SSH服务&#xff08;前提是应用已经部署完毕不再需要通过SSH远程访问传输文件的情况下&#xff0c;此时可以通过VNC远程登录方法&#x…

Arduino IDE配置ESP8266开发环境

一、配置步骤 在Arduino IDE中配置ESP8266开发环境的详细步骤如下&#xff1a; 1.打开Arduino IDE&#xff0c;依次点击“文件”->“首选项”&#xff0c;在“附加开发板管理器网址”一栏添加ESP8266开发板的网址。常用的网址是&#xff1a; http://arduino.esp8266.com/s…

软件测试——接口常见问题汇总

前言 今天我们来聊聊接口设计用例设计&#xff0c;说到这个接口&#xff0c;相信绝大多数的测试员都有遇到过某些棘手的问题&#xff0c;那么今天我们就来总结一下在接口方面会遇到的难题。 一、接口用例设计 接口测试用例可以从功能、性能、安全三方面进行入手&#xff0c;…

【好书推荐-第十二期】《并行计算与高性能计算》

&#x1f60e; 作者介绍&#xff1a;我是程序员洲洲&#xff0c;一个热爱写作的非著名程序员。CSDN全栈优质领域创作者、华为云博客社区云享专家、阿里云博客社区专家博主、前后端开发、人工智能研究生。公众号&#xff1a;洲与AI。 &#x1f388; 本文专栏&#xff1a;本文收录…

[java基础揉碎]多态参数

多态参数 方法定义的形参类型为父类类型&#xff0c;实参类型允许为子类类型 例子: 定义一个员工类, 有名字和工资两个属性, 有年工资的方法 定义一个普通员工继承了员工类 , 重写了年工资的方法 定义一个经理类, 也继承了员工类, 同时经理多以了一个奖金的属性, 重写的年…

GPD<论文精简版>

问题陈述 给定点云数据、机械手的几何参数&#xff0c;抓取位姿检测问题&#xff08; grasp pose detection problem&#xff09;表示为&#xff0c;在抓手闭合情况下&#xff0c;识别抓手的配置的问题。 &#xff1a;机器人工作空间 &#xff1a;三维点云中的一组点&#x…

宠物疾病 与 光线疗法

人类与动物以及大自然是相辅相成的。人离开动物将无法生存&#xff0c;对于动物我们尽力去保护&#xff0c;与大自然和谐稳定生存发展。 生息在地球上的所有动物、在自然太阳光奇妙的作用下、生长发育。太阳光的能量使它们不断进化、繁衍种族。现在、生物能够生存、全仰仗于太…

windbg调试协议wireshark抓包解析插件

把目录下文件复制到如下位置,Wireshark支持版本4.0以上 C:\Program Files\Wireshark\plugins\4.0\kdnet.lua C:\Program Files\Wireshark\gcrypt.dll C:\Program Files\Wireshark\luagcrypt.dll 启动 “C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\windbg.exe” -k …

如何检测无源晶振过驱?晶振过驱怎么办?

无源晶振(Passive Crystal Oscillator)是一种使用晶体元件来生成稳定频率的振荡器&#xff0c;它不像有源振荡器(如时钟芯片)那样需要外部电源。检测无源晶振是否过驱通常需要通过测量其输出波形和频率&#xff0c;与期望的规格进行比较。 如何检测无源晶振过驱&#xff1a; …