目录

Linux第六讲进程控制

Linux第六讲:进程控制

1.进程创建

1.1回顾fork

fork之前已经讲过了,所以这里我们就简单回顾一下fork:

fork:创建子进程,父进程返回父进程的pid,子进程返回0,失败返回-1

总结fork内核(OS)工作原理:

1.为子进程分配PCB和页表,以及内存块

2.将父进程的部分数据(页表数据、PCB数据)拷贝给子进程

3.将子进程添加到系统的进程列表中

4.fork返回,开始调度器调度

#include <unistd.h>
pid_t fork(void);
返回值:⾃进程中返回0,⽗进程返回⼦进程id,出错返回-1

1.2写时拷贝

1.写时拷贝的原理为:

子进程拷贝父进程页表之后,OS会将父进程和子进程所有的代码块的权限设置为只读权限,子进程修改数据时,只读权限,操作系统会发生报错(不是真的报错),有多种情况需要分析,当OS分析,有虚拟地址,有物理地址,访问的数据是数据段的内容,不是代码段的内容,那么就触发写时拷贝,其它情况(缺页中断)就有其它的解决方案

2.写时拷贝的好处有:

减少子进程的创建时间、减少空间浪费

https://i-blog.csdnimg.cn/direct/49dbb0ac0f534f06a5922592e2deeeec.png

2.进程终止

进程退出有三种情况:

1.进程正常执行,结果正常

2.进程正常执行,结果错误

3.进程异常终止

而进程的结果要被父进程拿到,需要靠退出码,退出码为0表示正常,退出码为非0表示不同的出错原因(echo $?可以查看最近一次进程的退出码

https://i-blog.csdnimg.cn/direct/5caa7f79497e4a8a8bc8845a558358ee.png

我们可以使用strerror拿到所有的退出码对应的错误信息:

https://i-blog.csdnimg.cn/direct/232a881d2e2f4a46b75d738a1a3bdb42.png

https://i-blog.csdnimg.cn/direct/5e548e8e5aef404e8be50192a0331d3f.png

2.1exit与_exit

我们不只可以使用return来终止进程,还可以使用exit和_exit来终止进程,他们两个被放在任意位置都可以终止进程,但是它们的区别在于exit是C标准库中的函数,_exit是系统调用的函数,exit里面封装了_exit,调用_exit之前会对缓冲区进行刷新

https://i-blog.csdnimg.cn/direct/61b8ebadc694450a86858e7fcb02e3c6.png

3.进程等待

为什么需要进程等待:

1.子进程退出之后,如果父进程什么也不做,就会导致僵尸进程(只有子进程的PCB),进而导致内存泄漏,而kill -9也无法杀掉该子进程,因为该进程已经是死去的进程了

2.父进程需要关心子进程的执行结果,获取子进程的退出信息

3.这是最重要的一点:也就是需要回收子进程的资源

3.1进程等待的方法(wait和waitpid)

wait: pid_t wait(int* status)

1.等待任意一个子进程的结束,成功后,解决子进程的僵尸问题,并返回子进程的pid,失败,返回-1

2.如果等待子进程,父进程会阻塞在wait调用处

waitpid: pid_t waitpid(pid_t pid, int* status, int options)

1.pid:-1表示等待任意的子进程,>1表示等待特定pid的子进程

2.status:输出型参数,通常只有低16位保存有信息,高8位:退出码(错误码),第7位: core dump,其余7位:退出状态(终止信号),需要注意的是对于退出码和终止信号的提取(位运算或者是宏)

3.options:两个选项:1.0,默认为阻塞调用 2.NOHANG,表示非阻塞调用

https://i-blog.csdnimg.cn/direct/73c6d0734a274a89815ddc23340cbd65.png

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

//函数指针类型
typedef void (*func_t)();

#define NUM 5
func_t handlers[NUM];

//注册
void registerHandler(func_t h[], func_t t)
{
    int i = 0;
    for( ; i<NUM; i++)
    {
        if(h[i] == NULL) break;
    }
    h[i] = t;
    if(i == NUM) return;
}

//下面是任务
void Download()
{
    printf("这是一个下载任务\n");
}
void Flush()
{
    printf("这是一个刷新任务\n");
}
void Log()
{
    printf("这是一个记录日志任务\n");
}

int main()
{
    registerHandler(handlers, Download);
    registerHandler(handlers, Flush);
    registerHandler(handlers, Log);

    pid_t id = fork();
    if(id == 0)
    {
        //子进程
        int cnt = 3;
        while(cnt--)
        {
            printf("我是一个子进程,pid:%d,ppid:%d\n", getpid(), getppid());
            sleep(1);
        }
        //int b = 1/0;
        exit(13);
    }
    //父进程
    while(1)
    {
        int status = 0;
        pid_t rid = waitpid(id, &status, WNOHANG);
        if(rid > 0)
        {
            //printf("wait success: id: %d, exit code: %d, exit singal: %d\n", rid, (status>>8)&0xFF, status&0x7F);
            //printf("wait success: id: %d, exit code: %d, exit singal: %d\n", rid, (status>>8)&0xFF, WIFEXITED(status));
            printf("wait success: id: %d, exit code: %d, exit singal: %d\n", rid, WEXITSTATUS(status), WIFEXITED(status));
            printf("%d\n", status);
        }
        else if(rid == 0)
        {
            int i = 0;
            for( ; handlers[i]; i++)
            {
                handlers[i]();
            }
            printf("本轮调用结束,子进程没有退出\n");
            sleep(1);
        }
        else
        {
            //printf("wait success: id: %d, exit code: %d, exit singal: %d\n", rid, (status>>8)&0xFF, status&0x7F);
            printf("wait unsuccess: rid: %d\n", rid);
        }
    }

    return 0;
}

4.进程程序替换

每一个进程都有它的PCB和它的代码和数据,而进程程序替换的本质就是将需要进行替换的进程的代码和数据与现在正在进行的进程的代码和数据进行替换,所以说,一旦发生了进程程序替换,那么原始的代码和数据就被替换,不复存在了,下面我们讲一下关于进程程序替换相关的exec系列函数,注意:exec系列函数只有出错时才会有返回值,返回值为-1,但是一般不需要作返回值判断,因为如果进行了返回,那么就是出错了:

https://i-blog.csdnimg.cn/direct/601781ed9b1244fabef924b79b010c67.png

4.1自定义shell的编写

4.1.1输出命令行提示符

我们自己的shell中,输入指令之前都会给出一个提示符,所以我们自己实现的shell也要初始命令行提示符:

https://i-blog.csdnimg.cn/direct/4594d3eae7054ee5a397964365d864ff.png

对于这些信息,可以在env中找到:

//输出命令行提示符
#define COMMAND_SIZE 1024 //假设命令行可以传入1024字节内容
#define FORMAT "[%s@%s %s]# " //snprintf输出格式
const char* GetUserName()
{
    const char* ret = getenv("USER");
    return ret == NULL ? "NULL" : ret;
}
const char* GetHostName()
{
    const char* ret = getenv("HOSTNAME");
    return ret == NULL ? "NULL" : ret;
}
const char* GetPWD()
{
    const char* ret = getenv("PWD");
    return ret == NULL ? "NULL" : ret;
}

//向out数组中写入命令行提示字符串
void MakeCommandLine(char* out, int size)
{
    //snprintf(char *str, size_t size, const char *format, ...); 
    snprintf(out, size, FORMAT, GetUserName(), GetHostName(), GetPWD());
}

void PrintCommandPrompt()
{
    char commandline[COMMAND_SIZE];
    MakeCommandLine(commandline, sizeof(commandline));
    printf("%s", commandline); 
}

int main()
{
    //1.输出命令行提示符
    PrintCommandPrompt();

    return 0;
}

4.1.2获取用户输入的命令

我们需要直到用户想要执行什么命令,才能够进行输出

//2.获取用户输入的命令,将输入的命令,填写到数组中
bool GetCommandLine(char* out, int size)
{
    char* c = fgets(out, size, stdin);//不能使用scanf
    if(c == NULL) return false;
    out[strlen(out)-1] = 0;//清理最后一个\n,否则输入之后会自动换行
    if(strlen(out) == 0) return false;
    return true;
}

int main()
{
    while(true)
    {
        //1.输出命令行提示符
        PrintCommandPrompt();
        
        //2.获取用户输入的命令
        char commandline[COMMAND_SIZE];
        if(!GetCommandLine(commandline, sizeof(commandline))) continue;//如果获取失败,重新获取
    }

    return 0;
}

4.1.3命令行分析

知道用户输入什么命令之后,我们需要对用户输入的命令进行解析:

//3.命令行解析
#define MAXARGC 128
char* g_argv[MAXARGC];
int g_argc;
bool CommandPrase(char* commandline)
{
#define SEP " "
    g_argc = 0;
    g_argv[g_argc++] = strtok(commandline, SEP);//strtok的作用为将commandline数组中的内容以SEP字符分割
    while((bool)(g_argv[g_argc++] = strtok(NULL, SEP)));//之后传入null继续分割
    g_argc--;
    return true;
}

//打印输入的命令是否正确
void Print()
{
    for(int i = 0; i<g_argc; i++)
    {
        printf("g_argv[%d]:%s\n", i, g_argv[i]);
    }
}

int main()
{
    while(true)
    {
        //1.输出命令行提示符
        PrintCommandPrompt();
        
        //2.获取用户输入的命令
        char commandline[COMMAND_SIZE];
        if(!GetCommandLine(commandline, sizeof(commandline))) continue;//如果获取失败,重新获取
        
        //3.命令行解析
        CommandPrase(commandline);
        Print();
    }

    return 0;
}

4.1.4指令执行

拿到了指令之后,需要执行指令,执行指令需要用到之前学过的exec系列接口了,那么应该选择哪一种接口呢?1.有了argv表,首先排除带l的接口,2.不关心env,排除e接口,3.不需要传入完整的路径,所以最终选择execvp进行指令的执行:

//4.指令的执行
int Execute()
{
    pid_t id = fork();
    if(id == 0)
    {
        execvp(g_argv[0], g_argv);
        exit(1);
    }
    pid_t rid = waitpid(-1, NULL, 0);
    (void)rid;//防止提示,使用一下
    return 0;
}

int main()
{
    while(true)
    {
        //1.输出命令行提示符
        PrintCommandPrompt();
        
        //2.获取用户输入的命令
        char commandline[COMMAND_SIZE];
        if(!GetCommandLine(commandline, sizeof(commandline))) continue;//如果获取失败,重新获取
        
        //3.命令行解析
        CommandPrase(commandline);
        //Print();

        //4.指令的执行
        Execute();
    }

    return 0;
}

4.1.5问题演示

写出的myshell并没有什么大的问题,但是有的指令并不能正常执行:

https://i-blog.csdnimg.cn/direct/38bbeaa387cb48e0a12c646240cd52c2.png

cd、echo等命令属于内建命令,内建命令需要shell自己处理,所以对于内建命令,必须要进行特殊处理:

4.1.5.1cd内建命令处理
//4.内建命令特殊处理
const char* GetHome()
{
    const char* ret = getenv("HOME");
    return ret == NULL ? "" : ret;
}

bool Cd()
{
    if(g_argc == 1) 
    {
        std::string home = GetHome();
        if(home.empty()) return true;
        chdir(home.c_str());
        return true;
    }
    else
    {
        std::string where = g_argv[1];
        if(where == "~")
        {

        }
        else if(where == "-")
        {
            
        }
        else 
        {
            chdir(where.c_str());
        }
        return true;
    }

    return false;
}

void Echo()
{

}

bool CheckAndExecBuiltin()
{
    std::string cmd = g_argv[0];
    if(cmd == "cd")
    {
        Cd();
        return true;
    }
    else if(cmd == "echo")
    {
        Echo();
        return true;
    }
    else if(cmd == "export")
    {

    }
    else if(cmd == "alias")
    {

    }

    return false;
}

int main()
{
    while(true)
    {
        //1.输出命令行提示符
        PrintCommandPrompt();
        
        //2.获取用户输入的命令
        char commandline[COMMAND_SIZE];
        if(!GetCommandLine(commandline, sizeof(commandline))) continue;//如果获取失败,重新获取
        
        //3.命令行解析
        CommandPrase(commandline);
        //Print();

        //4.对于内建命令,要进行特殊处理
        if(CheckAndExecBuiltin()) continue;//如果是内建命令的话,就不需要下面的指令执行了

        //5.指令的执行
        Execute();
    }

    return 0;
}

https://i-blog.csdnimg.cn/direct/0a61f13534064470ae137c554444cdcf.png

可以看到,确实是切换了路径,但是命令行提示符并没有发生改变,而且使用env进行对比时,env也并没有发生变化,原因是:调用chdir命令之后,并没有对环境变量进行更新,导致拿到的pwd还是旧的环境变量,所以我们需要进行环境变量的更新操作:

char cwd[1024];
char cwdenv[1024];
const char* GetPWD()
{
    //consti char* ret = getenv("PWD");
    const char* ret = getcwd(cwd, sizeof(cwd));//函数作用为将现在的pwd写入到cwd数组中
    if(ret != NULL)
    {
    	//更新环境变量
        snprintf(cwdenv, sizeof(cwdenv), "PWD=%s", ret);
        putenv(cwdenv);
    }
    return ret == NULL ? "NULL" : ret;
}
4.1.5.2环境变量表的实现

shell中有自己的命令行参数表,我们已经实现过了(g_argc, g_argv[]),这张表可以让子进程拿到(因为是全局的),子进程就可以根据拿到的命令行指令执行相应的操作。还应该有一张环境变量表,这样每一个子进程都可以拿到环境变量了,即使不进行环境变量的传递:

//0.环境变量表的实现
#define MAX_ENVS 100
char* g_env[MAX_ENVS];
int g_envs = 0;
//对环境变量表进行初始化
void Init_ENV()
{
    //本来是需要在配置文件中拿到环境变量的,但是我们只需要知道怎么从父进程拿到即可
    extern char** environ;
    memset(g_env, 0, sizeof(g_env));
    g_envs = 0;
    //1.获取环境变量
    for(int i = 0; environ[i]; i++)
    {
        g_env[i] = (char*)malloc(strlen(environ[i])+1);
        strcpy(g_env[i], environ[i]);
        g_envs++;
    }
    g_env[g_envs] = NULL;
    //2.导入环境变量
    for(int i = 0; g_env[i]; i++)
    {
        putenv(g_env[i]);
    }
    environ = g_env;
}
4.1.5.3echo内建命令的实现
int lastcode = 0;//保存最近一次进程的结束码
void Echo()
{
    if(g_argc == 2)
    {
        // echo "hello world"
        // echo $?
        // echo $PATH
        std::string opt = g_argv[1];
        if(opt == "$?")
        {
            std::cout << lastcode << std::endl;
            lastcode = 0;
        }
        else if(opt[0] == '$')
        {
            std::string env_name = opt.substr(1);
            const char *env_value = getenv(env_name.c_str());
            if(env_value)
                std::cout << env_value << std::endl;
        }
        else
        {
            std::cout << opt << std::endl;
        }
    } 
}

//5.指令的执行
int Execute()
{
    pid_t id = fork();
    if(id == 0)
    {
        execvp(g_argv[0], g_argv);
        exit(1);
    }
    int status = 0;
    pid_t rid = waitpid(-1, &status, 0);
    if(rid > 0)
    {
        lastcode = WEXITSTATUS(status);//这里要更新退出码
    }
    return 0;
}

5.自定义shell完整代码实现

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

//0.环境变量表的实现
#define MAX_ENVS 100
char* g_env[MAX_ENVS];
int g_envs = 0;
//对环境变量表进行初始化
void Init_ENV()
{
    //本来是需要在配置文件中拿到环境变量的,但是我们只需要知道怎么从父进程拿到即可
    extern char** environ;
    memset(g_env, 0, sizeof(g_env));
    g_envs = 0;
    //1.获取环境变量
    for(int i = 0; environ[i]; i++)
    {
        g_env[i] = (char*)malloc(strlen(environ[i])+1);
        strcpy(g_env[i], environ[i]);
        g_envs++;
    }
    g_env[g_envs] = NULL;
    //2.导入环境变量
    for(int i = 0; g_env[i]; i++)
    {
        putenv(g_env[i]);
    }
    environ = g_env;
}

//1.输出命令行提示符
#define COMMAND_SIZE 1024 //假设命令行可以传入1024字节内容
#define FORMAT "[%s@%s %s]# " //snprintf输出格式
const char* GetUserName()
{
    const char* ret = getenv("USER");
    return ret == NULL ? "NULL" : ret;
}
const char* GetHostName()
{
    const char* ret = getenv("HOSTNAME");
    return ret == NULL ? "NULL" : ret;
}
char cwd[1024];
char cwdenv[1024];
const char* GetPWD()
{
    //consti char* ret = getenv("PWD");
    const char* ret = getcwd(cwd, sizeof(cwd));//函数作用为将现在的pwd写入到cwd数组中
    if(ret != NULL)
    {
        snprintf(cwdenv, sizeof(cwdenv), "PWD=%s", ret);
        putenv(cwdenv);
    }
    return ret == NULL ? "NULL" : ret;
}

//截取pwd,否则命令行提示的pwd显示太多
std::string DirName(const char* pwd)
{
#define SLASH "/"
    std::string dir = pwd;
    if(dir == SLASH) return "SLASH";
    auto pos = dir.rfind(SLASH);
    if(pos == std::string::npos) return "FALSE";
    return dir.substr(pos+1);
}

//向out数组中写入命令行提示字符串
void MakeCommandLine(char* out, int size)
{
    //snprintf(char *str, size_t size, const char *format, ...); 
    snprintf(out, size, FORMAT, GetUserName(), GetHostName(), DirName(GetPWD()).c_str());
    //snprintf(out, size, FORMAT, GetUserName(), GetHostName(), GetPWD());
}

void PrintCommandPrompt()
{
    char commandline[COMMAND_SIZE];
    MakeCommandLine(commandline, sizeof(commandline));
    printf("%s", commandline); 
    fflush(stdout);
}

//2.获取用户输入的命令,将输入的命令,填写到数组中
bool GetCommandLine(char* out, int size)
{
    char* c = fgets(out, size, stdin);//不能使用scanf
    if(c == NULL) return false;
    out[strlen(out)-1] = 0;//清理最后一个\n,否则输入之后会自动换行
    if(strlen(out) == 0) return false;
    return true;
}

//3.命令行解析
#define MAXARGC 128
char* g_argv[MAXARGC];
int g_argc;
bool CommandPrase(char* commandline)
{
#define SEP " "
    g_argc = 0;
    g_argv[g_argc++] = strtok(commandline, SEP);//strtok的作用为将commandline数组中的内容以SEP字符分割
    while((bool)(g_argv[g_argc++] = strtok(NULL, SEP)));//之后传入null继续分割
    g_argc--;
    return true;
}

//打印输入的命令是否正确
void Print()
{
    for(int i = 0; i<g_argc; i++)
    {
        printf("g_argv[%d]:%s\n", i, g_argv[i]);
    }
}

//4.内建命令特殊处理
const char* GetHome()
{
    const char* ret = getenv("HOME");
    return ret == NULL ? "" : ret;
}

bool Cd()
{
    if(g_argc == 1) 
    {
        std::string home = GetHome();
        if(home.empty()) return true;
        chdir(home.c_str());
        return true;
    }
    else
    {
        std::string where = g_argv[1];
        if(where == "~")
        {

        }
        else if(where == "-")
        {
            
        }
        else 
        {
            chdir(where.c_str());
        }
        return true;
    }

    return false;
}

int lastcode = 0;//保存最近一次进程的结束码
void Echo()
{
    if(g_argc == 2)
    {
        // echo "hello world"
        // echo $?
        // echo $PATH
        std::string opt = g_argv[1];
        if(opt == "$?")
        {
            std::cout << lastcode << std::endl;
            lastcode = 0;
        }
        else if(opt[0] == '$')
        {
            std::string env_name = opt.substr(1);
            const char *env_value = getenv(env_name.c_str());
            if(env_value)
                std::cout << env_value << std::endl;
        }
        else
        {
            std::cout << opt << std::endl;
        }
    } 
}

bool CheckAndExecBuiltin()
{
    std::string cmd = g_argv[0];
    if(cmd == "cd")
    {
        Cd();
        return true;
    }
    else if(cmd == "echo")
    {
        Echo();
        return true;
    }
    else if(cmd == "export")
    {

    }
    else if(cmd == "alias")
    {

    }

    return false;
}

//5.指令的执行
int Execute()
{
    pid_t id = fork();
    if(id == 0)
    {
        execvp(g_argv[0], g_argv);
        exit(1);
    }
    int status = 0;
    pid_t rid = waitpid(-1, &status, 0);
    if(rid > 0)
    {
        lastcode = WEXITSTATUS(status);
    }
    return 0;
}

int main()
{
    Init_ENV();
    while(true)
    {
        //1.输出命令行提示符
        PrintCommandPrompt();
        
        //2.获取用户输入的命令
        char commandline[COMMAND_SIZE];
        if(!GetCommandLine(commandline, sizeof(commandline))) continue;//如果获取失败,重新获取
        
        //3.命令行解析
        CommandPrase(commandline);
        //Print();

        //4.对于内建命令,要进行特殊处理
        if(CheckAndExecBuiltin()) continue;//如果是内建命令的话,就不需要下面的指令执行了

        //5.指令的执行
        Execute();
    }

    return 0;
}