云计算百科
云计算领域专业知识百科平台

Linux之进程控制

目录

一、进程创建

1.1、fork函数初始

1.2、fork函数返回值

1.3、写实拷贝

1.4、fork常规用法

1.5、fork调用失败的原因

二、进程终止

2.1、进程退出场景

2.2、进程常见退出方法

2.2.1、退出码

2.3.2、_exit函数

2.3.3、exit函数

2.3.4、return退出

三、进程等待

3.1、进程等待必要性

3.2、进程等待的方法

3.2.1、wait方法

3.2.2、waitpid方法

3.2.3、获取子进程status

3.2.4、阻塞与非阻塞等待

3.2.5、为什么要通过系统调用来获取进程退出码

四、进程程序替换

4.1、替换原理

4.2、替换函数

4.2.1、函数解释

4.2.2、命名理解

五、自主Shell命令行解释器

5.1、目标

5.2、实现原理

5.3、源码

5.4、总结


一、进程创建

1.1、fork函数初始

在linux中fork函数是⾮常重要的函数,它从已存在进程中创建⼀个新进程。新进程为⼦进程,⽽原进程为⽗进程。

#include<unistd.h>

pid_t  fork(void);

返回值:子进程中返回0,⽗进程返回⼦进程id,出错返回-1

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

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

当⼀个进程调⽤fork之后,就有两个⼆进制代码相同的进程。⽽且它们都运⾏到相同的地⽅。但每个进程都将可以开始它们⾃⼰的旅程,如下图:

所以,fork之前⽗进程独⽴执⾏,fork之后,⽗⼦两个执⾏流分别执⾏。注意,fork之后,谁先执⾏完全由调度器决定。

1.2、fork函数返回值

  • ⼦进程返回0。
  • ⽗进程返回的是⼦进程的pid。

1.3、写实拷贝

通常,⽗⼦代码共享,⽗⼦在不写⼊时,数据也是共享的,当任意⼀⽅试图写⼊,便以写时拷⻉的⽅式各⾃⼀份副本。具体⻅下图:

详细解释:父进程创建子进程后,更新页表中表示数据访问权限的标志位为只读属性,当父子进程中的某一方写入数据时,触发系统错误,进而引发缺页中断,然后系统就会进行一系列较为复杂的检测,当判定需要发生写实拷贝后,就会开始申请内存,发生拷贝,修改父子页表对应数据的权限为可读可写,然后恢复执行。

意义:因为有写时拷⻉技术的存在,所以⽗⼦进程得以彻底分离离!完成了进程独⽴性的技术保证!

写时拷⻉,是⼀种延时申请技术,可以提⾼整机内存的使⽤率。

1.4、fork常规用法

  • ⼀个⽗进程希望复制⾃⼰,使⽗⼦进程同时执⾏不同的代码段。例如,⽗进程等待客户端请求, ⽣成⼦进程来处理请求。
  • ⼀个进程要执⾏⼀个不同的程序。例如⼦进程从fork返回后,调⽤exec函数。

1.5、fork调用失败的原因

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

二、进程终止

进程终⽌的本质是释放系统资源,就是释放进程申请的相关内核数据结构和对应的数据和代码。

2.1、进程退出场景

  • 代码运⾏完毕,结果正确
  • 代码运⾏完毕,结果不正确
  • 代码异常终⽌
    • 当代码是异常终止时,其实是OS提前发现的代码的问题,然后发送对应的信号终止了该进程。

所有信号:

2.2、进程常见退出方法

正常终⽌(可以通过 echo $? 查看进程退出码):

  • 从main返回
  • 调⽤exit
  • _exit

异常退出:

  • ctrl + c,信号终⽌

2.2.1、退出码

退出码(退出状态)可以告诉我们最后⼀次执⾏的命令的状态。在命令结束以后,我们可以知道命令是成功完成的还是以错误结束的。其基本思想是,程序返回退出代码 0 时表⽰执⾏成功,没有问题。 代码 1 或 0 以外的任何代码都被视为不成功。通过错误码,我们可以让父进程或系统更好的了解当前进程是成功了还是失败了,如果失败了是因为什么失败的。

Linux Shell 中的主要退出码:

  • 退出码 0 表⽰命令执⾏⽆误,这是完成命令的理想状态。
  • 退出码 1 我们也可以将其解释为 “不被允许的操作”。例如在没有 sudo 权限的情况下使⽤ yum;再例如除以 0 等操作也会返回错误码 1 ,对应的命令为 let a=1/0 。
  • 130 ( SIGINT 或 Ctrl C )和 143 ( SIGTERM )等终⽌信号是⾮常典型的,它们属于 128+n 信号,其中 n 代表终⽌码。
  • 可以使⽤strerror函数来获取退出码对应的描述。

示例代码:

1 #include<iostream>
2 #include<string>
3 #include<cstdio>
4 #include<string.h>
5 #include<error.h>
6
7 int main()
8 {
9 printf("before: errno: %d, errstring: %s\\n",errno,strerror(errno));
10
11 FILE *fp = fopen("log.txt","r");
12 if(fp == nullptr)
13 {
14 printf("after: errno: %d, errstring: %s\\n",errno,strerror(errno));
15 return errno;
16 }
17
18 return 0;
19 }

效果:

解释:C语言中提供了可以查看当前错误码的全局变量errno,只要包含errno.h头文件就可以使用,还提供了将错误码转换为对应错误信息的strerror函数。

2.3.2、_exit函数

#include<unistd.h>

void _exit(int status);

参数:status 定义了进程的终⽌状态,⽗进程通过wait来获取该值

说明:虽然status是int,但是仅有低8位可以被⽗进程所⽤。所以_exit(-1)时,在终端执⾏$?发现 返回值是255。

示例代码一:

1 #include<iostream>
2 #include<string>
3 #include<cstdio>
4 #include<string.h>
5 #include<unistd.h>
6
7 void func()
8 {
9 std::cout<<"hello,bit"<<std::endl;
10
11 _exit(10);
12 }
13
14 int main()
15 {
16 func();
17 std::cout<<"进程结束了"<<std::endl;
18
19 return 0;
20 }

效果:

解释:可以看到 "进程结束了"这句话并没有打印,所以得到结论 _exit 方法的作用是结束整个进程。

示例代码二:

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

int main()
{
printf("进程运行结束!\\n");
sleep(2);
_exit(20);

return 0;
}

效果:

解释:从上面代码可以得到结论,_exit 结束进程时,如果缓冲区内有数据,_exit 不会刷新。

2.3.3、exit函数

#include<unistd.h>

void  exit(int status);

exit最后也会调⽤_exit,但在调⽤_exit之前,还做了其他⼯作:

  • 执⾏用户通过 atexit 或 on_exit 定义的清理函数。
  • 关闭所有打开的流,所有的缓存数据均被写⼊。
  • 调⽤_exit。
  • 示例代码一:

    1 #include<iostream>
    2 #include<string>
    3 #include<cstdio>
    4 #include<string.h>
    5 #include<unistd.h>
    6
    7 void func()
    8 {
    9 std::cout<<"hello,bit"<<std::endl;
    10
    11 exit(10);
    12 }
    13
    14 int main()
    15 {
    16 func();
    17 std::cout<<"进程结束了"<<std::endl;
    18
    19 return 0;
    20 }

    效果:

    解释:可以看到 "进程结束了"这句话并没有打印,所以得到结论 exit 方法的作用是结束整个进程。

    示例代码二:

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

    int main()
    {
    printf("进程运行结束!\\n");
    sleep(2);
    exit(20);

    return 0;
    }

    效果:

    解释:我们可以看到 "进程结束了"这句话打印出来了,这里代码我们没有通过 '\\n' 强制刷新缓冲区,所以说明 exit 方法结束进程前会主动帮我们刷新缓冲区。

    2.3.4、return退出

    示例代码:

    1 #include<iostream>
    2 #include<string>
    3 #include<cstdio>
    4 #include<string.h>
    5 #include<unistd.h>
    6
    7 int func()
    8 {
    9 std::cout<<"hello,bit"<<std::endl;
    10
    11 return 10;
    12 }
    13
    14 int main()
    15 {
    16 func();
    17 std::cout<<"进程结束了"<<std::endl;
    18
    19 return 0;
    20 }

    效果:

    解释:return是⼀种更常⻅的退出进程⽅法。执⾏return num等同于执⾏exit(n),因为调⽤main的运⾏时函数会将main的返回值当做 exit 的参数。另外,只有main函数执行 return 语句才会结束进程,其他函数执行 return 语句只是结束当前函数而已。

    三、进程等待

    3.1、进程等待必要性

    • ⼦进程退出,⽗进程如果不管不顾,就可能造成‘僵⼫进程’的问题,进⽽造成内存泄漏。
    • 另外,进程⼀旦变成僵⼫状态,那就⼑枪不⼊,“杀⼈不眨眼”的kill -9 也⽆能为⼒,因为谁也没有办法杀死⼀个已经死去的进程。
    • 最后,⽗进程派给⼦进程的任务完成的如何,我们是需要知道的。如,⼦进程运⾏完成,结果对还是不对,或者是否正常退出。
    • ⽗进程通过进程等待的⽅式,回收⼦进程资源,获取⼦进程退出信息。

    3.2、进程等待的方法

    3.2.1、wait方法

    #include<sys/types.h>

    #include<sys/wait.h>

    pid_t wait(int* status);

    返回值:

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

    参数:

            输出型参数,获取⼦进程退出状态,不关⼼则可以设置成为NULL。

    示例代码:

    1 #include<stdio.h>
    2 #include<unistd.h>
    3 #include<errno.h>
    4 #include<string.h>
    5 #include<stdlib.h>
    6 #include<sys/wait.h>
    7
    8 int main()
    9 {
    10 pid_t id = fork();
    11 if(id < 0)
    12 {
    13 printf("errno: %d,errstring: %s\\n",errno,strerror(errno));
    14 return errno;
    15 }
    16 else if(id == 0)
    17 {
    18 int cnt = 3;
    19 while(cnt)
    20 {
    21 printf("子进程运行中,pid:%d\\n",getpid());
    22 cnt–;
    23 sleep(1);
    24 }
    25 exit(123);
    26 }
    27 else
    28 {
    29 sleep(5);
    30 pid_t rid = wait(nullptr);
    31 if(rid > 0)
    32 {
    33 printf("wait sub process success,rid: %d\\n",rid);
    34 }
    35 else
    36 {
    37 perror("wait fail:");
    38 }
    39 while(true)
    40 {
    41 printf("我是父进程,pid: %d\\n",getpid());
    42 sleep(1);
    43 }
    44 }
    45
    46 return 0;
    47 }

    效果:

    3.2.2、waitpid方法

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

    返回值:

            当正常返回的时候waitpid返回收集到的⼦进程的进程ID;

            如果设置了选项WNOHANG,⽽调⽤中waitpid发现没有已退出的⼦进程可收集,则返回0;

            如果调⽤中出错,则返回-1,这时errno会被设置成相应的值以指⽰错误所在;

    参数:

            pid:

                    pid=-1,等待任⼀个⼦进程。与wait等效。

                    pid>0.等待其进程ID与pid相等的⼦进程。

            status: 输出型参数

            WIFEXITED(status): 若为正常终⽌⼦进程返回的状态,则为真。(查看进程是否是正常退出)

            WEXITSTATUS(status): 若WIFEXITED⾮零,提取⼦进程退出码。(查看进程的退出码)

            options:默认为0,表⽰阻塞等待。

            WNOHANG: 若pid指定的⼦进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该⼦进程的ID。

    • 如果⼦进程已经退出,调⽤wait/waitpid时,wait/waitpid会⽴即返回,并且释放资源,获得⼦进程退出信息。
    • 如果在任意时刻调⽤wait/waitpid,⼦进程存在且正常运⾏,则进程可能阻塞。
    • 如果不存在该⼦进程,则⽴即出错返回。

    示例代码:

    1 #include<stdio.h>
    2 #include<unistd.h>
    3 #include<errno.h>
    4 #include<string.h>
    5 #include<stdlib.h>
    6 #include<sys/wait.h>
    7
    8 int main()
    9 {
    10 pid_t id = fork();
    11 if(id < 0)
    12 {
    13 printf("errno: %d,errstring: %s\\n",errno,strerror(errno));
    14 return errno;
    15 }
    16 else if(id == 0)
    17 {
    18 int cnt = 3;
    19 while(cnt)
    20 {
    21 printf("子进程运行中,pid:%d\\n",getpid());
    22 cnt–;
    23 sleep(1);
    24 }
    25 exit(123);
    26 }
    27 else
    28 {
    29 sleep(5);
    30 pid_t rid = waitpid(id,nullptr,0);
    31 if(rid > 0)
    32 {
    33 printf("wait sub process success,rid: %d\\n",rid);
    34 }
    35 else
    36 {
    37 perror("wait fail:");
    38 }
    39 while(true)
    40 {
    41 printf("我是父进程,pid: %d\\n",getpid());
    42 sleep(1);
    43 }
    44 }
    45
    46 return 0;
    47 }

    效果:

    3.2.3、获取子进程status

    wait和waitpid,都有⼀个status参数,该参数是⼀个输出型参数,由操作系统填充。

    如果传递NULL,表⽰不关⼼⼦进程的退出状态信息。

    否则,操作系统会根据该参数,将⼦进程的退出信息反馈给⽗进程。

    status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16 ⽐特位):

    在 status 的低十六位中,次低八位是退出码,次低七位是退出信号的值。

    示例代码:

    1 #include<stdio.h>
    2 #include<unistd.h>
    3 #include<errno.h>
    4 #include<string.h>
    5 #include<stdlib.h>
    6 #include<sys/wait.h>
    7
    8 int main()
    9 {
    10 pid_t id = fork();
    11 if(id < 0)
    12 {
    13 printf("errno: %d,errstring: %s\\n",errno,strerror(errno));
    14 return errno;
    15 }
    16 else if(id == 0)
    17 {
    18 int cnt = 3;
    19 while(cnt)
    20 {
    21 printf("子进程运行中,pid:%d\\n",getpid());
    22 cnt–;
    23 sleep(1);
    24 }
    25 exit(123);
    26 }
    27 else
    28 {
    29 sleep(5);
    30
    31 int status = 0;
    32 pid_t rid = waitpid(id, &status, 0);
    33
    34 if(rid > 0)
    35 {
    36 if(WIFEXITED(status))
    37 {
    38 //两种获取退出码的方法,第一种还可以获取退出信号
    39 printf("wait sub process success,rid: %d, status code: %d, exit signal: %d\\n", rid, (status>>8)&0xFF, status&0x7F);
    40
    41 printf("wait sub process success,rid: %d, status code: %d\\n", rid, WEXITSTATUS(status));
    42 }
    43 else
    44 {
    45 printf("child process quit error!\\n");
    46 }
    47 }
    48 else
    49 {
    50 perror("wait fail:");
    51 }
    52
    53 while(true)
    54 {
    55 printf("我是父进程,pid: %d\\n",getpid());
    56 sleep(1);
    57 }
    58 }
    59
    60 return 0;
    61 }

    效果:

    3.2.4、阻塞与非阻塞等待

    • 进程的阻塞等待⽅式:

    示例代码:

    #include <iostream>
    #include <vector>
    #include <cstdio>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/wait.h>
    #include <functional>
    #include "task.h"

    enum{
    OK = 0,
    OPEN_FILE_ERROR,
    };

    const std::string gsep = " ";
    std::vector<int> data;

    int SaveBegin()
    {
    std::string name = std::to_string(time(nullptr));
    name += ".backup";
    FILE *fp = fopen(name.c_str(), "w");
    if(fp == nullptr) return OPEN_FILE_ERROR;

    std::string dataStr;
    for (auto d : data)
    {
    dataStr += std::to_string(d);
    dataStr += gsep;
    }
    fputs(dataStr.c_str(), fp);
    fclose(fp);
    return OK;
    }

    void Save()
    {
    pid_t id = fork();
    if(id == 0) // 子进程
    {
    int code = SaveBegin();
    exit(code);
    }
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    if(rid > 0)
    {
    int code = WEXITSTATUS(status);
    if(code == 0) printf("备份成功, exit code : %d\\n", code);
    else printf("备份失败, exit code : %d\\n", code);
    }
    else
    {
    perror("waitpid");
    }
    }

    int main()
    {
    int cnt = 1;
    while(true)
    {
    data.push_back(cnt++);
    sleep(1);

    if(cnt % 10 == 0)
    {
    Save();
    }
    }
    }

    • 进程的非阻塞等待方式:

    如果我们想让父进程在等待子进程的同时还可以做自己的事,不被阻塞住,我们可以通过将waitpid方法的第三个参数传入WNOHANG,即可进入非阻塞等待模式。该方法返回值 > 0 时,等待成功,返回值为目标子进程的pid;当返回值 == 0 时,等待成功,但是目标子进程没有退出,返回值< 0 时,等待失败。

    示例代码:

    task.h:

    #pragma once
    #include <iostream>

    void PrintLog();
    void Download();
    void Backup();

    task.cc:

    #include "task.h"

    void PrintLog()
    {
    std::cout << "Print log task" << std::endl;
    }

    void Download()
    {
    std::cout << "DownLoad task" << std::endl;
    }

    void Backup()
    {
    std::cout << "BackUp task" << std::endl;
    }

    proc.cc

    #include <iostream>
    #include <vector>
    #include <cstdio>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/wait.h>
    #include <functional>
    #include "task.h"

    typedef std::function<void()> task_t;

    void LoadTask(std::vector<task_t> &tasks)
    {
    tasks.push_back(PrintLog);
    tasks.push_back(Download);
    tasks.push_back(Backup);
    }

    int main()
    {
    std::vector<task_t> tasks;
    LoadTask(tasks);

    pid_t id = fork();
    if(id == 0)
    {
    // child
    while(true)
    {
    printf("我是子进程, pid : %d\\n", getpid());
    sleep(1);
    }
    exit(0);
    }

    // father
    while(true)
    {
    sleep(1);
    pid_t rid = waitpid(id, nullptr, WNOHANG);
    if(rid > 0)
    {
    printf("等待子进程%d 成功\\n", rid);
    break;
    }
    else if(rid < 0)
    {
    printf("等待子进程失败\\n");
    break;
    }
    else
    {
    printf("子进程尚未退出\\n");

    // 做自己的事情
    for(auto &task : tasks)
    {
    task();
    }
    }
    }

    }

    Makefile:

    process:process.cc task.cc
    g++ -o $@ $^ -std=c++11

    .PHONY:clean
    clean:
    rm -f process

    3.2.5、为什么要通过系统调用来获取进程退出码

    这是因为如果我们使用全局变量来获取,子进程和父进程的数据在不修改时是共享的,但是一旦修改就会发生写实拷贝,这就导致父进程拿不到修改后的值,可子进程要想通过变量将退出码传递出去,就必须对变量进行修改,所以通过变量的方式行不通,就只能通过系统提供的接口了。

    四、进程程序替换

    fork() 之后,⽗⼦各⾃执⾏⽗进程代码的⼀部分如果⼦进程就想执⾏⼀个全新的程序呢?进程的程序 替换来完成这个功能!程序替换是通过特定的接⼝,加载磁盘上的⼀个全新的程序(代码和数据),加载到调⽤进程的地址空间中!

    4.1、替换原理

    ⽤fork创建⼦进程后执⾏的是和⽗进程相同的程序(但有可能执⾏不同的代码分⽀),⼦进程往往要调⽤⼀种exec函数以执⾏另⼀个程序。当进程调⽤⼀种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执⾏。调⽤exec并不创建新进程,所以调⽤exec前后该进程的id并未改变。

    4.2、替换函数

    其实有六种以exec开头的函数,统称exec函数:

    #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[]);

    4.2.1、函数解释

    • 这些函数如果调⽤成功则加载新的程序从启动代码开始执⾏,不再返回。
    • 如果调⽤出错则返回-1。
    • 所以exec函数只有出错的返回值⽽没有成功的返回值。

    4.2.2、命名理解

    这些函数原型看起来很容易混,但只要掌握了规律就很好记。

    • l(list):表⽰参数采⽤列表。
    • v(vector):参数⽤数组。
    • p(path):有p⾃动搜索环境变量PATH。
    • e(env):表⽰⾃⼰维护环境变量。

    exec调⽤举例如下:

    为了方便演示,所有exec调用都放到了一个程序中了,实际使用时一个子进程根据需要执行某一个就可以。

    myexec.cc:

    1 #include <iostream>
    2 #include <cstdio>
    3 #include <unistd.h>
    4 #include <sys/types.h>
    5 #include <sys/wait.h>
    6
    7 const std::string myenv="HELLO=AAAAAAAAAAAAAAAAAAAA";
    8
    9 extern char **environ;
    10 int main()
    11 {
    12 //在原有环境变量上增加新的
    13 putenv((char*)myenv.c_str());
    14
    15 pid_t id = fork();
    16
    17 if(id == 0)
    18 {
    19 char *const argv[] = {
    20 (char*)"other",
    21 nullptr
    22 };
    23
    24 //(void)argv;
    W> 25 char *const env[] = {
    26 (char*)"HELLO=bite",
    27 (char*)"HELLO1=bite1",
    28 (char*)"HELLO2=bite2",
    29 (char*)"HELLO3=bite3"
    30 };
    31
    32 //允许自己传入环境变量
    33 // 传入的会覆盖继承来的
    34 // 该方法会使用传入的新的环境变量
    35 // 我们可以自己写一个全新的,也可以在原有环境变量后面添加新的项
    36 execvpe("./other", argv, environ);
    37
    38 //可以自动查找环境变量,第一个参数不需要指明路径
    39 execvp(argv[0], argv);
    40
    41 //和execl一样,只是第二个参数变成了数组
    42 execv("/usr/bin/ls", argv);
    43
    44 // execl最后一个参数必须是nullptr
    45 // 第一个参数代表要执行谁
    46 // 后面的可变参数列表表明要怎么执行
    47 execl("/bin/ls", "ls", "-l", "–color", "-a", nullptr);
    48 execl("./other", "other", nullptr);
    49 execl("/usr/bin/python", "python", "test.py", nullptr);
    50 execl("/usr/bin/bash", "bash", "test.sh", nullptr);
    51
    52 //自动查找环境变量,第一个参数不需要指明路径
    53 execlp("ls", "ls", "–color", "-aln", nullptr);
    54
    55 //如果程序替换失败,结束子进程返回错误码 -> 1
    56 exit(1);
    57 }
    58
    59 // father
    60 pid_t rid = waitpid(id, nullptr, 0);
    61 if(rid > 0)
    62 {
    63 printf("等待子进程成功!\\n");
    64 }
    65
    66 return 0;
    67 }

    other.c:

    #include <stdio.h>

    extern char**environ;

    int main()
    {
    for(int i = 0; environ[i]; i++)
    {
    printf("evn[%d]: %s\\n", i, environ[i]);
    }
    }

    test.py:

    #!/usr/bin/python

    print ("hello python")

    test.sh:

    #!/bin/bash

    echo "hello shell"

    Makefile:

    myexec:myexec.cc
    g++ -o $@ $^ -std=c++11

    .PHONY:clean
    clean:
    rm -f myexec

    事实上,只有execve是真正的系统调⽤,其它五个函数最终都调⽤execve,所以execve在man⼿册第2节, 其它函数在man⼿册第3节。这些函数之间的关系如下图所⽰。下图exec函数簇⼀个完整的例⼦:

    五、自主Shell命令行解释器

    5.1、目标

    • 要能处理普通命令。
    • 要能处理内建命令。
    • 要能帮助我们理解内建命令/本地变量/环境变量这些概念。
    • 要能帮助我们理解shell的允许原理。

    5.2、实现原理

    ⽤下图的时间轴来表⽰事件的发⽣次序。其中时间从左向右。shell由标识为sh的⽅块代表,它随着时间的流逝从左向右移动。shell从用户读⼊字符串"ls"。shell建⽴⼀个新的进程,然后在那个进程中运⾏ls程序并等待那个进程结束。

    然后shell读取新的⼀⾏输⼊,建⽴⼀个新的进程,在这个进程中运⾏程序并等待这个进程结束。 所以要写⼀个shell,需要循环以下过程:

    • 获取命令⾏
    • 解析命令⾏
    • 建⽴⼀个⼦进程(fork)
    • 替换⼦进程(execvp)        
    • ⽗进程等待⼦进程退出(wait)

    根据这些思路,和我们前⾯的学的技术,就可以⾃⼰来实现⼀个shell了。

    5.3、源码

    #include <iostream>
    #include <cstdio>
    #include <cstdlib>
    #include <cstring>
    #include <string>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/wait.h>

    using namespace std;

    const int basesize = 1024;
    const int argvnum = 64;
    const int envnum = 64;
    // 全局的命令行参数表
    char *gargv[argvnum];
    int gargc = 0;

    // 全局的变量
    int lastcode = 0;

    // 我的系统的环境变量
    char *genv[envnum];

    // 全局的当前shell工作路径
    char pwd[basesize];
    char pwdenv[basesize];

    string GetUserName()
    {
    string name = getenv("USER");
    return name.empty() ? "None" : name;
    }

    string GetHostName()
    {
    string hostname = getenv("HOSTNAME");
    return hostname.empty() ? "None" : hostname;
    }

    string GetPwd()
    {
    if(nullptr == getcwd(pwd, sizeof(pwd))) return "None";
    snprintf(pwdenv, sizeof(pwdenv),"PWD=%s", pwd);
    putenv(pwdenv); // PWD=XXX
    return pwd;

    //string pwd = getenv("PWD");
    //return pwd.empty() ? "None" : pwd;
    }

    string LastDir()
    {
    string curr = GetPwd();
    if(curr == "/" || curr == "None") return curr;
    // /home/whb/XXX
    size_t pos = curr.rfind("/");
    if(pos == std::string::npos) return curr;
    return curr.substr(pos+1);
    }

    string MakeCommandLine()
    {
    // [whb@bite-alicloud myshell]$
    char command_line[basesize];
    snprintf(command_line, basesize, "[%s@%s %s]# ",\\
    GetUserName().c_str(), GetHostName().c_str(), LastDir().c_str());
    return command_line;
    }

    void PrintCommandLine() // 1. 命令行提示符
    {
    printf("%s", MakeCommandLine().c_str());
    fflush(stdout);
    }

    bool GetCommandLine(char command_buffer[], int size) // 2. 获取用户命令
    {
    // 我们认为:我们要将用户输入的命令行,当做一个完整的字符串
    // "ls -a -l -n"
    char *result = fgets(command_buffer, size, stdin);
    if(!result)
    {
    return false;
    }
    command_buffer[strlen(command_buffer)-1] = 0;
    if(strlen(command_buffer) == 0) return false;
    return true;
    }

    void ParseCommandLine(char command_buffer[], int len) // 3. 分析命令
    {
    (void)len;
    memset(gargv, 0, sizeof(gargv));
    gargc = 0;
    // "ls -a -l -n"
    const char *sep = " ";
    gargv[gargc++] = strtok(command_buffer, sep);
    // =是刻意写的
    while((bool)(gargv[gargc++] = strtok(nullptr, sep)));
    gargc–;
    }

    void debug()
    {
    printf("argc: %d\\n", gargc);
    for(int i = 0; gargv[i]; i++)
    {
    printf("argv[%d]: %s\\n", i, gargv[i]);
    }
    }
    // 在shell中
    // 有些命令,必须由子进程来执行
    // 有些命令,不能由子进程执行,要由shell自己执行 — 内建命令 built command
    bool ExecuteCommand() // 4. 执行命令
    {
    // 让子进程进行执行
    pid_t id = fork();
    if(id < 0) return false;
    if(id == 0)
    {
    //子进程
    // 1. 执行命令
    execvpe(gargv[0], gargv, genv);
    // 2. 退出
    exit(1);
    }
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    if(rid > 0)
    {
    if(WIFEXITED(status))
    {
    lastcode = WEXITSTATUS(status);
    }
    else
    {
    lastcode = 100;
    }
    return true;
    }
    return false;
    }

    void AddEnv(const char *item)
    {
    int index = 0;
    while(genv[index])
    {
    index++;
    }

    genv[index] = (char*)malloc(strlen(item)+1);
    strncpy(genv[index], item, strlen(item)+1);
    genv[++index] = nullptr;
    }
    // shell自己执行命令,本质是shell调用自己的函数
    bool CheckAndExecBuiltCommand()
    {
    if(strcmp(gargv[0], "cd") == 0)
    {
    // 内建命令
    if(gargc == 2)
    {
    chdir(gargv[1]);
    lastcode = 0;
    }
    else
    {
    lastcode = 1;
    }
    return true;
    }
    else if(strcmp(gargv[0], "export") == 0)
    {
    // export也是内建命令
    if(gargc == 2)
    {
    AddEnv(gargv[1]);
    lastcode = 0;
    }
    else
    {
    lastcode = 2;
    }
    return true;
    }
    else if(strcmp(gargv[0], "env") == 0)
    {
    for(int i = 0; genv[i]; i++)
    {
    printf("%s\\n", genv[i]);
    }
    lastcode = 0;
    return true;
    }
    else if(strcmp(gargv[0], "echo") == 0)
    {
    if(gargc == 2)
    {
    // echo $?
    // echo $PATH
    // echo hello
    if(gargv[1][0] == '$')
    {
    if(gargv[1][1] == '?')
    {
    printf("%d\\n", lastcode);
    lastcode = 0;
    }
    }
    else
    {
    printf("%s\\n", gargv[1]);
    lastcode = 0;
    }
    }
    else
    {
    lastcode = 3;
    }
    return true;
    }
    return false;
    }

    // 作为一个shell,获取环境变量应该从系统的配置来
    // 我们今天就直接从父shell中获取环境变量
    void InitEnv()
    {
    extern char **environ;
    int index = 0;
    while(environ[index])
    {
    genv[index] = (char*)malloc(strlen(environ[index])+1);
    strncpy(genv[index], environ[index], strlen(environ[index])+1);
    index++;
    }
    genv[index] = nullptr;
    }

    int main()
    {
    InitEnv();
    char command_buffer[basesize];
    while(true)
    {
    PrintCommandLine(); // 1. 命令行提示符
    // command_buffer -> output
    if( !GetCommandLine(command_buffer, basesize) ) // 2. 获取用户命令
    {
    continue;
    }
    //printf("%s\\n", command_buffer);
    //ls
    //"ls -a -b -c -d"->"ls" "-a" "-b" "-c" "-d"
    ParseCommandLine(command_buffer, strlen(command_buffer)); // 3. 分析命令

    if ( CheckAndExecBuiltCommand() )
    {
    continue;
    }

    ExecuteCommand(); // 4. 执行命令
    }
    return 0;
    }

    5.4、总结

    exec/exit就像call/return。

     ⼀个C程序有很多函数组成。⼀个函数可以调⽤另外⼀个函数,同时传递给它⼀些参数。被调⽤的函数执⾏⼀定的操作,然后返回⼀个值。每个函数都有他的局部变量,不同的函数通过call/return系统进⾏通信。

    这种通过参数和返回值在拥有私有数据的函数间通信的模式是结构化程序设计的基础。Linux⿎励将这种应⽤于程序之内的模式扩展到程序之间。如下图

    ⼀个C程序可以fork/exec另⼀个程序,并传给它⼀些参数。这个被调⽤的程序执⾏⼀定的操作,然后通过exit(n)来返回值。调⽤它的进程可以通过wait(&ret)来获取exit的返回值。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » Linux之进程控制
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!