[Linux](9)进程控制:进程创建,进程终止,进程等待,进程程序替换

文章目录

  • fork函数
    • 简介
    • 写时拷贝
    • fork常规用法
    • fork调用失败的原因
  • 进程终止
    • 关于终止的正确认识
    • 进程终止的常见做法
    • 内核做了什么
  • 进程等待
    • 进程等待的必要性
    • 如何等待
      • wait()
      • waitpid()
      • status退出状态
        • 正常退出(代码跑完)
        • 异常退出(代码未跑完,被信号所杀)
        • 通过宏提取退出码
        • 总结
    • 阻塞等待和非阻塞等待
  • 进程程序替换
    • 概念
    • execl()
    • execv()
    • execlp()
    • execvp()
    • execle()
    • 简易shell的实现
    • 环境变量

fork函数

简介

在Linux中,fork是非常重要的函数。它从已经存在的进程中创建一个新的进程,新进程为子进程,原进程为父进程。

头文件:

pid_t fork(void);

返回值:子进程返回0,父进程返回子进程id,出错返回-1

进程调用fork,当控制转移到内核中的fork代码后,内核将:

  • 分配新的内存块和内核数据结构给子进程
  • 将父进程部分数据结构内容拷贝至子进程
  • 添加子进程到系统进程列表当中
  • fork返回,开始调度器调度

写时拷贝

fork之前父进程独立执行,fork之后,父子两个执行流分别执行,两个进程共享同一份代码。写时拷贝写入时进行深拷贝)确保了数据的独立。

❓为什么不在创建子进程的时候就重新开辟空间把数据分开,而要写时拷贝?(写时拷贝的优点)

  1. 对于不用修改的数据,两个进程只需要共用一份就够了。至于数据要不要修改,那就到修改时再说(写时)。
  2. 父进程的数据,子进程不一定全部修改,创建进程时就拷贝会浪费时间和空间。(写时拷贝提高内存管理效率)

fork常规用法

  • 一个父进程希望复制自己,使父子进程同时执行不同的代码段,例如:父进程等待客户端的请求,生成子进程来处理请求。
  • 一个进程要执行一个不同的程序,例如:子进程从 fork 返回后,调用 exec 函数

fork调用失败的原因

  • 系统中有太多的进程
  • 实际用户的进程数超过了限制

进程终止

关于终止的正确认识

这里首先提出两个问题:

  1. C/C++main 函数 return 0,是给谁 return
  2. 为什么 return 0return 其他值可以吗?

进程代码跑完,结果是否正确:正确 return 0,错误应该 return 非零,非零的值不同,表示不同的失败原因。这个非零的值又叫做进程退出码。它表征了进程的退出信息,是需要让父进程读取的(return 给了父进程)。

验证:

写一段代码直接返回一个非零值。

#include 
int main()
{
    return 123;
}
[CegghnnoR@VM-4-13-centos 2022_8_16]$ ./mytest
[CegghnnoR@VM-4-13-centos 2022_8_16]$ echo $?
123
[CegghnnoR@VM-4-13-centos 2022_8_16]$ echo $?
0

运行后 echo $?$? 表示在 bash 中,最近一次执行完毕时,对应进程的退出码。再 echo 一次就变成 0 了。

在使用指令时,也可以通过 echo $? 查看返回值:

[CegghnnoR@VM-4-13-centos 2022_8_16]$ ls
makefile  mytest  mytest.c
[CegghnnoR@VM-4-13-centos 2022_8_16]$ echo $?
0
[CegghnnoR@VM-4-13-centos 2022_8_16]$ ls abc
ls: cannot access abc: No such file or directory
[CegghnnoR@VM-4-13-centos 2022_8_16]$ echo $?
2

ls 执行成功返回 0,执行失败返回 2。


进程终止的常见做法

  1. main 函数中 return

  2. 在自己代码的任意地点,调用 exit() 括号中传入退出码

_exit() 类似,但是有区别:

#include 
#include 
#include 

int main()
{
    for (int i = 0; i < 100; ++i)
    {
        printf("hello world");
        sleep(1);
        _exit(111); // 不显示hello world
        //exit(111); // 显示hello world
        return 10;
    }
    return 123;
}

因为没有 \n,所以 printf 执行完 hello world 不会出现在屏幕上,而是在缓冲区。

  • exit 终止进程,刷新缓冲区

  • _exit 终止进程,不会有任何刷新操作

实际上,_exit 属于系统调用,exit 的底层实现会去调用它。

内核做了什么

进程 = 内核结构 + 进程代码 + 数据

当一个进程不跑了,首先进入Z状态,然后等待父进程回收退出信息,然后将进程设置为X状态,释放内核结构以及进程代码和数据。

其实,操作系统可能不会释放进程的内核数据结构,Linux内部会维护一个废弃的数据结构链表,原来的内核数据结构仅仅设置为无效,出现新的进程,就直接初始化一个废弃的数据结构,节省了开辟空间的消耗。这种操作叫做 slab分派器,废弃的数据结构链表叫做 数据结构缓冲池

进程等待

进程等待的必要性

  • 之前讲过,子进程退出,如果父进程不管不顾,就可能造成僵尸进程内存泄漏的问题。另外,进程一旦变成僵尸状态,就连 kill -9 也杀不死,因为你无法杀死一个已经死去的进程。
  • 父进程派给子进程的任务完成得如何,它有必要获取子进程的退出信息。这也要通过等待的方式。

如何等待

wait()

头文件:

pid_t wait(int* status);

返回值:成功返回被等待进程 pid,失败返回 -1

参数:输出型参数,获取子进程退出状态,不关心可以设置为NULL

例子:

父进程先等待20s,在这20s内把子进程杀掉,20s后父进程等待,回收子进程,打印子进程pid。再过20s父进程死亡,被 bash 回收。

#include 
#include 
#include 
#include 
#include 

int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        //child
       while (1)
       {
          printf("我是子进程,pid: %d,正在运行...\n", getpid());
          sleep(1);
       }
    }
    else
    {
        printf("我是父进程,pid: %d,准备等待子进程\n", getpid());
        sleep(20);
        pid_t ret = wait(NULL);
        if (ret < 0)
        {
            printf("等待失败!\n");         
        }
        else                                
        {
            printf("等待成功,result: %d\n", ret);
        }
        sleep(20);
    }
    return 0;
}

waitpid()

pid_ t waitpid(pid_t pid, int* status, int options);

返回值:

  • 等待子进程成功,返回子进程的 pid

  • 等待失败,返回值 <0

参数:

  • pid:传入一个子进程的 pid 表示指定等待该子进程;-1 表示等待任意进程

  • status:输出型参数,同上

  • options:0 阻塞等待(阻塞状态地等,阻塞状态不止可以等硬件资源,也可以等软件)

status退出状态

输出型参数 status 如何查看?只需要关心它的低16个比特位,分为三部分:

[Linux](9)进程控制:进程创建,进程终止,进程等待,进程程序替换_第1张图片

正常退出(代码跑完)

正常终止只要关注次低8位,表示子进程的退出码。

测试:子进程运行5s后死亡,退出码123,父进程等待子进程死亡,等待成功,打印子进程退出码。

#include 
#include 
#include 
#include 
#include 

int main()
{
    pid_t id = fork();
    if (id == 0)
    {
       //child
       int cnt = 5;
       while (cnt--)
       {
          printf("我是子进程,pid: %d,正在运行...\n", getpid());
          sleep(1);
       }
       exit(123);
    }
    else
    {
        int status = 0;
        printf("我是父进程,pid: %d,准备等待子进程\n", getpid());
        pid_t ret = waitpid(id, &status, 0);
        if (ret > 0)
        {
            printf("等待成功, ret: %d, 所等待的子进程的退出码:%d\n", ret, (status >> 8) & 0xFF);
        }
    }
    return 0;
}

使用位运算(status >> 8) & 0xFF 可以得到次低8位。

结果符合预期:

[CegghnnoR@VM-4-13-centos 2022_8_16]$ ./mytest
我是父进程,pid: 27225,准备等待子进程
我是子进程,pid: 27226,正在运行...
我是子进程,pid: 27226,正在运行...
我是子进程,pid: 27226,正在运行...
我是子进程,pid: 27226,正在运行...
我是子进程,pid: 27226,正在运行...
等待成功, ret: 27226, 所等待的子进程的退出码:123

异常退出(代码未跑完,被信号所杀)

如果异常退出,是因为这个进程收到了特定的信号,比如我们之前常用的 kill -9,其中9就是一个信号。

kill -l 显示的就是所有的信号:

[CegghnnoR@VM-4-13-centos 2022_8_16]$ kill -l
 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
 6) SIGABRT	 7) SIGBUS	 8) SIGFPE	 9) SIGKILL	10) SIGUSR1
11) SIGSEGV	12) SIGUSR2	13) SIGPIPE	14) SIGALRM	15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD	18) SIGCONT	19) SIGSTOP	20) SIGTSTP
21) SIGTTIN	22) SIGTTOU	23) SIGURG	24) SIGXCPU	25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF	28) SIGWINCH	29) SIGIO	30) SIGPWR
31) SIGSYS	34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX

测试:子进程无限循环,父进程阻塞等待。最后我们手动杀死子进程,父进程等待成功,打印退出信号。status & 0x7F 即可获得低7位:

#include   
#include   
#include   
#include   
#include   
  
int main()  
{  
    pid_t id = fork();  
    if (id == 0)  
    {  
       //child  
       while (1)  
       {  
          printf("我是子进程,pid: %d,正在运行...\n", getpid());  
          sleep(1);  
       }  
    }  
    else   
    {  
        int status = 0;  
        printf("我是父进程,pid: %d,准备等待子进程\n", getpid());  
        pid_t ret = waitpid(id, &status, 0);  
        if (ret > 0)  
        {  
            printf("等待成功, ret: %d, 所等待的子进程的退出码:%d, 退出信号:%d\n", ret, (status >> 8) & 0xFF, status & 0x7F);                                                                   
        }
    }
    return 0;
}

结果符合预期:

[CegghnnoR@VM-4-13-centos 2022_8_16]$ ./mytest
我是父进程,pid: 32299,准备等待子进程
我是子进程,pid: 32300,正在运行...
我是子进程,pid: 32300,正在运行...
我是子进程,pid: 32300,正在运行...
等待成功, ret: 32300, 所等待的子进程的退出码:0, 退出信号:9
[CegghnnoR@VM-4-13-centos 2022_8_16]$ kill -9 32300

除了我们手动发信号,平时运行代码遇到错误程序崩溃,其实是由操作系统给进程发了信号。

比如下面,0作除数的情况:

int main()
{
    pid_t id = fork();
    if (id == 0)
    {
    	//child
    	int a = 5/0; //除以0
    }
    else
    {
        int status = 0;
        printf("我是父进程,pid: %d,准备等待子进程\n", getpid());
        pid_t ret = waitpid(id, &status, 0);
        if (ret > 0)
        {
            printf("等待成功, ret: %d, 所等待的子进程的退出码:%d, 退出信号:%d\n", ret, (status >> 8) & 0xFF, status & 0x7F);
        }
    }
    return 0;
}

如果我们不管编译错误,执意运行:

[CegghnnoR@VM-4-13-centos 2022_8_16]$ ./mytest
我是父进程,pid: 3483,准备等待子进程
等待成功, ret: 3484, 所等待的子进程的退出码:0, 退出信号:8

通过查表发现 8 号是 SIGFPE,表示数学相关的异常,如被0除,浮点溢出,等等

再比如,对空指针解引用:

if (id == 0)  
{  
    //child  
    int* a = NULL;  
    *a = 10;                                                                            
} 
[CegghnnoR@VM-4-13-centos 2022_8_16]$ ./mytest
我是父进程,pid: 4795,准备等待子进程
等待成功, ret: 4796, 所等待的子进程的退出码:0, 退出信号:11

信号11 SIGSEGV,表示非法内存访问。

通过宏提取退出码

也可以不用位运算,而通过系统自带的宏来提取退出码。

WIFEXITED(status):查看进程是否正常退出

WEXITSTATUS(status):若正常退出,查看进程的退出码

正确写法:

else                                                                         
{                                                                            
    //parent                                                                 
    int status = 0;                                                          
    printf("我是父进程,pid: %d,准备等待子进程\n", getpid());               
    pid_t ret = waitpid(id, &status, 0);                                     
    if (ret > 0)                                                             
    {                                                                        
        if (WIFEXITED(status))                                               
        {                                                                    
            printf("子进程正常退出,退出码:%d\n", WEXITSTATUS(status));     
        }                                                                    
    }                                                                        
}
总结

常见进程退出:

  1. 代码跑完,结果正确
  2. 代码跑完,结果不正确
  3. 代码没跑完,程序异常

上面讲的正常终止,其实就是前两种情况,异常终止则对应第三种情况。

退出码和退出信号,优先查看退出信号

阻塞等待和非阻塞等待

阻塞等待:父进程在等待子进程时,只要子进程没有结束,父进程就不进行任何操作,像上面的就是阻塞等待。

非阻塞等待:父进程等待子进程时,如果子进程没有结束,那么父进程会接受0来表示子进程还没有结束,然后继续执行父进程代码,不继续等待。

waitpid 的非阻塞等待可以通过 man 手册查询:

RETURN VALUE

waitpid(): on success, returns the process ID of the child whose state has changed; if WNOHANG was specified and one or more child(ren) specified by pid exist, but have not yet changed state, then 0 is returned. On error, -1 is returned.

等待成功子进程结束则返回子进程id,如果指定了选项 WNOHANG,子进程未结束返回0,出错返回-1.

下面是一个基于非阻塞的轮询等待,即每隔一会非阻塞等待一次

#include 
#include 
#include 
#include 
#include 

int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        // 子进程
        while (1)
		{
            printf("这是子进程,pid:%d, ppid:%d\n", getpid(),getppid());
            sleep(5);
            break;
        }
        exit(202);
    }
    else if(id > 0)
    {
        int status = 0;
        // 父进程,基于非阻塞的轮询等待
        while (1)
        {
            pid_t ret = waitpid(-1, &status, WNOHANG);
            if (ret > 0)
            {
                printf("等待成功:%d, exit sig: %d, exit code: %d\n", ret, status & 0x7F, (status >> 8) & 0xFF);
                break;
            }
            else if (ret == 0)
            {
                printf("等待成功,子进程未结束\n");
                sleep(1);
            }
            else
            {
                // 出错
            }
        }
    }
    return 0;
} 
[CegghnnoR@VM-4-13-centos 2022_9_24]$ ./test
等待成功,子进程未结束
这是子进程,pid:8292, ppid:8291
等待成功,子进程未结束
等待成功,子进程未结束
等待成功,子进程未结束
等待成功,子进程未结束
等待成功:8292, exit sig: 0, exit code: 202

要想让父进程在轮询等待的间隔期间做其他事,可以写一个方法集,即用vector存函数指针,然后父进程一遍轮询等待,一边遍历vector数组进行回调函数。

#include 
#include 
#include 
#include 
#include 
#include 
#include 

typedef void (*handler_t)();

// 方法集
std::vector<handler_t> handlers;

void fun1()
{
    printf("hello, 我是方法1\n");
}

void fun2()
{
    printf("hello, 我是方法2\n");
}

void Load()
{
    // 加载方法
    handlers.push_back(fun1);
    handlers.push_back(fun2);
}

int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        while (1)
        {
            printf("这是子进程,pid:%d, ppid:%d\n", getpid(),getppid());
            sleep(5);
            break;
        }
        exit(202);
    }               
    else if(id > 0)
    {
        int status = 0;
        while (1)
        {                                                              
            pid_t ret = waitpid(-1, &status, WNOHANG);
            if (ret > 0)
            {
                printf("等待成功:%d, exit sig: %d, exit code: %d\n", ret, status & 0x7F, (status >> 8) & 0xFF);
                break;
            }    
            else if (ret == 0)
            {        
                printf("等待成功,子进程未结束\n");
                if (handlers.empty()) Load();
                for (auto f : handlers)             
                {     
                    f();
                }
                sleep(1);
            }
            else 
            {
                // 出错
            }
        }
    }
    return 0;
}   
[CegghnnoR@VM-4-13-centos 2022_9_24]$ ./test
等待成功,子进程未结束
hello, 我是方法1
hello, 我是方法2
这是子进程,pid:17949, ppid:17948
等待成功,子进程未结束
hello, 我是方法1
hello, 我是方法2
等待成功,子进程未结束
hello, 我是方法1
hello, 我是方法2
等待成功,子进程未结束
hello, 我是方法1
hello, 我是方法2
等待成功,子进程未结束
hello, 我是方法1
hello, 我是方法2
等待成功,子进程未结束
hello, 我是方法1
hello, 我是方法2
等待成功:17949, exit sig: 0, exit code: 202

进程程序替换

概念

通过fork创建的子进程执行的是父进程的代码片段,如果我们要让子进程执行全新的程序呢?

我们一般在Linux程序设计的时候,往往需要让子进程干两件事情

  1. 让子进程执行父进程的代买片段(服务器代码)
  2. 让子进程执行磁盘中的一个全新的程序(比如shell,想让客户端执行对应的程序,通过我们的进程,执行其他人的进程代码等等)

程序替换的原理:

  1. 将磁盘中的程序,加载到内存结构
  2. 为将要执行程序替换的进程(子进程),重新建立页表映射,使之指向新的程序的数据段和代码段,在这个过程中,父进程和子进程彻底分离,子进程开始执行一个全新的程序。

那么如何把磁盘上的一个新的程序导入到内存中呢,这要靠os来完成,作为程序员可以通过系统调用来完成。

execl()

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

我们如果想执行一个全新的程序,需要做哪几件事情?

  1. 先找到这个程序在哪里(程序在哪?)
  2. 程序可能携带选项进行执行(怎么执行?)
  • path:传入可执行程序的路径
  • arg:参数,命令行怎么写选项的,这个就怎么填,最后必须是NULL,表示参数传递完毕。

执行 /usr/bin 下的 ls ,也就是平时敲的ls命令

#include 
#include 

int main()
{
    //ls -a -i
    printf("此进程pid:%d\n", getpid());
    execl("/usr/bin/ls", "ls", "-l", "-a", NULL);
    printf("执行完毕,此进程pid:%d", getpid());
    return 0;
}
[CegghnnoR@VM-4-13-centos 2022_9_30]$ ./myexec
此进程pid:19513
total 28
drwxrwxr-x  2 CegghnnoR CegghnnoR 4096 Sep 30 20:36 .
drwxrwxr-x 16 CegghnnoR CegghnnoR 4096 Sep 30 20:05 ..
-rw-rw-r--  1 CegghnnoR CegghnnoR   66 Sep 30 20:31 makefile
-rwxrwxr-x  1 CegghnnoR CegghnnoR 8464 Sep 30 20:36 myexec
-rw-rw-r--  1 CegghnnoR CegghnnoR  237 Sep 30 20:36 myexec.c

由结果可以看到,printf("执行完毕,此进程pid:%d", getpid());这条代码没有执行,这是因为该进程在运行到 execl 时被替换了。

execl 函数不用判断返回值,因为只要替换成功了,就不会有返回值, 如果失败,则返回错误码,然后继续向后执行当前程序。

下面这段代码,让子进程去执行ls程序,父进程等待:

#include 
#include 
#include 
#include 

int main()
{
    printf("此为父进程,pid:%d\n", getpid());
    pid_t id = fork();
    if (id == 0)
    {
        // child
        // 让子进程执行全新的程序
        printf("此为子进程,pid:%d\n", getpid());
        execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
        exit(1); // 执行此条语句,说明execl失败了
    }
    // 父进程
    int status = 0;
    int ret = waitpid(id, &status, 0);
    if (ret == id)
    {
        sleep(2);
        printf("父进程等待成功\n");
    }
    return 0;
}
[CegghnnoR@VM-4-13-centos 2022_9_30]$ ./myexec
此为父进程,pid:2473
此为子进程,pid:2474
total 28
drwxrwxr-x  2 CegghnnoR CegghnnoR 4096 Oct  2 10:18 .
drwxrwxr-x 16 CegghnnoR CegghnnoR 4096 Sep 30 20:05 ..
-rw-rw-r--  1 CegghnnoR CegghnnoR   66 Sep 30 20:31 makefile
-rwxrwxr-x  1 CegghnnoR CegghnnoR 8720 Oct  2 10:18 myexec
-rw-rw-r--  1 CegghnnoR CegghnnoR  795 Oct  2 10:18 myexec.c
父进程等待成功

之前我们讲过,子进程和父进程的代码和数据是共享的,只有在需要修改时才会发生写时拷贝,进程替换也属于修改,也会发生写时拷贝,所以子进程的程序替换不会影响父进程。


execv()

也是程序替换函数,和 execl() 只有传参方式的区别

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

execl 后面是可变参数,execv 则是传入一个指针数组。

#include 
#include 
#include 
#include 

int main()
{
    printf("此为父进程,pid:%d\n", getpid());
    pid_t id = fork();
    if (id == 0)
    {
        // child
        // 让子进程执行全新的程序
        printf("此为子进程,pid:%d\n", getpid());
        char* const argv_[] = {(char*)"ls", (char*)"-a", (char*)"-l", (char*)"-i", NULL};
        execv("/usr/bin/ls", argv_);
        //execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
        exit(1); // 执行此条语句,说明execl失败了
    }
    // 父进程
    int status = 0;
    int ret = waitpid(id, &status, 0);
    if (ret == id)
    {
        sleep(2);
        printf("父进程等待成功\n");
    }
    return 0;
}

execlp()

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

file 参数填你想执行的程序名,执行的时候会通过环境变量PATH去搜索。

用 execlp 来替换 ls:

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

execvp()

int execvp(const char *file, char *const argv[]);
char* const argv_[] = {(char*)"ls", (char*)"-a", (char*)"-l", (char*)"-i", NULL};
execvp("ls", argv_);

execle()

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

前面两个参数上面都讲过,最后一个参数 char * const envp[] 的作用是将环境变量传递给目标进程

总结:exec+

l(list):填入多个参数,

v(vector):填入一个指针数组,

p(path):表示直接写程序名,不带p的需要写程序路径。

e(env):表示自己维护环境变量。

简易shell的实现

#include 
#include 
#include 
#include 
#include 
#include 

#define SEP " "
#define NUM 1024
#define SIZE 128

char command_line[NUM];
char* command_args[SIZE];

int main()
{
    while (1)
    {
        // 1.显示提示符
        printf("[张三@我的主机名 当前目录]# ");
        fflush(stdout);
        // 2.获取用户输入
        memset(command_line, '\0', sizeof(command_line)*sizeof(char));
        fgets(command_line, NUM, stdin);
        command_line[strlen(command_line) - 1] = '\0'; // 去除\n
        // 3.字符切分 "ls -a -l -i" -> "ls" "-a" "-l" "-i"
        command_args[0] = strtok(command_line, SEP);
        int index = 1;
        // 为ls命令增加--color=auto选项,给展示的文件名添加颜色
        if (strcmp(command_args[0], "ls") == 0)
            command_args[index++] = (char*)"--color=auto";
        while (command_args[index++] = strtok(NULL, SEP));
        // 创建进程,执行
        pid_t id = fork();
        if (id == 0)
        {
            // child
            // 程序替换
            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);
        }
    }
    return 0;
}
[CegghnnoR@VM-4-13-centos 2022_9_30]$ ./myshell
[张三@我的主机名 当前目录]# ls
makefile  mycmd.cpp  myexec.c  myshell	myshell.c
等待子进程成功:sig: 0, code: 0
[张三@我的主机名 当前目录]# ls -l
total 28
-rw-rw-r-- 1 CegghnnoR CegghnnoR   78 Oct  2 19:03 makefile
-rw-rw-r-- 1 CegghnnoR CegghnnoR   93 Oct  2 14:18 mycmd.cpp
-rw-rw-r-- 1 CegghnnoR CegghnnoR  634 Oct  2 15:22 myexec.c
-rwxrwxr-x 1 CegghnnoR CegghnnoR 8984 Oct  2 19:27 myshell
-rw-rw-r-- 1 CegghnnoR CegghnnoR 1239 Oct  2 19:27 myshell.c
等待子进程成功:sig: 0, code: 0
[张三@我的主机名 当前目录]# ls           
makefile  mycmd.cpp  myexec.c  myshell	myshell.c
等待子进程成功:sig: 0, code: 0

其实目前还存在一个问题:cd.. 无效

[张三@我的主机名 当前目录]# pwd
/home/CegghnnoR/code/2022_9_30
等待子进程成功:sig: 0, code: 0
[张三@我的主机名 当前目录]# cd ..
等待子进程成功:sig: 0, code: 1
[张三@我的主机名 当前目录]# pwd                         
/home/CegghnnoR/code/2022_9_30
等待子进程成功:sig: 0, code: 0

其实问题的原因也很简单,因为我们的程序是给子进程运行的,cd…改变的是子进程的工作目录,子进程运行完就结束了,没有切实地改变父进程的工作目录。

而进程之间是相互独立的,我们并不能让子进程去改变父进程的工作目录,所以像cd这类命令,只能由父进程自己来执行。这类由shell自己执行的命令,称为内建(bind-in)命令

代码如下:

直接定义一个ChangeDir函数,当输入的是cd命令就直接调用这个函数,而不是创建子进程。

chdir 是系统调用接口,可以更改工作目录

#include 
#include 
#include 
#include 
#include 
#include 

#define SEP " "
#define NUM 1024
#define SIZE 128

char command_line[NUM];
char* command_args[SIZE];

int ChangeDir(const char* new_path)
{
    chdir(new_path);
    return 0;
}

int main()
{
    while (1)
    {
        // 1.显示提示符
        printf("[张三@我的主机名 当前目录]# ");
        fflush(stdout);
        // 2.获取用户输入
        memset(command_line, '\0', sizeof(command_line)*sizeof(char));
        fgets(command_line, NUM, stdin);
        command_line[strlen(command_line) - 1] = '\0'; // 去除\n
        // 3.字符切分 "ls -a -l -i" -> "ls" "-a" "-l" "-i"
        command_args[0] = strtok(command_line, SEP);
        int index = 1;
        if (strcmp(command_args[0], "ls") == 0)
            command_args[index++] = (char*)"--color=auto";
        while (command_args[index++] = strtok(NULL, SEP));
        // 如果是cd命令,则直接调用ChangeDir
        if (strcmp(command_args[0], "cd") == 0 && command_args[1] != NULL)
        {
            ChangeDir(command_args[1]);
            continue;
        }
        // 创建进程,执行
        pid_t id = fork();
        if (id == 0)
        {
            // child
            // 程序替换
            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);
        }
    }
    return 0;
}
[张三@我的主机名 当前目录]# pwd
/home/CegghnnoR/code/2022_9_30
等待子进程成功:sig: 0, code: 0
[张三@我的主机名 当前目录]# cd ..
[张三@我的主机名 当前目录]# pwd        
/home/CegghnnoR/code
等待子进程成功:sig: 0, code: 0

环境变量

环境变量的数据,在进程的上下文中

  1. 环境变量会被子进程继承下去,所以它有全局属性
  2. 当我们进行程序替换的时候,当前进程的环境变量非但不会被替换,而且是继承父进程的。

export也是内建命令,如果我们要导出环境变量,需要给父进程提供一个函数,当我们使用export命令时直接调用这个函数即可。

putenv 是系统调用接口,可以用来添加或改变环境变量

char env_buffer[NUM];

void PutEnvInMyShell(char* new_env)
{
    putenv(new_env);
}

int main()
{
    while (1)
    {
        // 1.显示提示符
        // 2.获取用户输入
        // 3.字符切分 "ls -a -l -i" -> "ls" "-a" "-l" "-i"
        // ...
        if (strcmp(command_args[0], "export") == 0 && command_args[1] != NULL)
        {
            strcpy(env_buffer, command_args[1]);
            PutEnvInMyShell(env_buffer);
            continue;
        }
        // 创建进程,执行
        // ...
    }
    return 0;
}

你可能感兴趣的:(Linux,linux,运维,服务器,后端,c语言)