Linux——进程控制

目录

一总结

二fork函数

三进程的终止

1先明白

2退出码&&退出信号

3做法

四阻塞等待&&非阻塞等待

1怎么办

2等待函数

3非阻塞等待

五进程程序替换

1分析

2原理

3替换函数

六简易Shell

1打印用户行

2获取用户命令字符串

3分割用户输入字符串

5执行命令

 4检查是否为内建命令

源代码

 


接着上文的进程概念:现在让我们来更深入地理解进程内部的相关知识

一总结

我们在没学过进程时,给它的定义是:进程=PCB + 代码和数据;而现在理解了进程来对它进行进一步的定义:

进程=内核的相关管理数据结构(task_struct + 虚拟地址空间 + 页表) + 代码(共享) + 数据(写时拷贝) 

有了它,再次来理解前面所说的进程之间具有独立性:

进程之间的运行是互不干扰的。父进程创建子进程:子进程继承父进程的代码和数据(只读),一旦发生更改,会发生写时拷贝,保证父子进程之间的独立性!

二fork函数

在前面我们已经对fork函数有了一定的了解,但它的内部是如何实现父进程创建子进程的呢?

我们借助图来解释:

在fork()之前(未执行fork()函数) ,只有父进程在走;在执行fork()时,内部会拷贝(浅拷贝)一份与父进程一样的进程,为子进程:表示子进程在fork()内部已经可以随时准备可以被调度了;

而fork函数为了能更好地管理父子进程(对应关系),设置了不同的返回值:

~ 子进程返回0

~ 父进程返回的是子进程的pid

让我们能很方便来用fork()函数创建子进程来进行学习!

三进程的终止

1先明白

进程的终止是在干什么?    释放曾经的代码和数据?

不准确:释放曾经的代码和数据所占的空间相关内核数据结构

内核数据结构没有释放,就会导致Z状态:僵尸状态

2退出码&&退出信号

平时在写代码时,在代码执行完后最后总要加上 return 0来告诉编译器说我们的代码执行完了;但其实在编译器眼中,return 0还表示你的代码执行的结果是正确的才返回0的。

代码运行完后,一定要返回每个值来保证程序的完整。我们把这个非0返回值叫做:错误码

错误码有它对应的错误描述信息,我们可以打印出来看看:

如果要在Linux中获取进程的退出码,我们可以:

使用echo内建命令,打印的就是bash内部的变量数据(指令大都是bash创建子进程来执行的)

所以:

我们可以认为:进程终止可以总结为三种情况:

代码跑完,结果正确

代码跑完,结果不正确

代码执行时,出现异常提前退出

代码能跑完我们就要关注它对应的退出码来看看结果;

代码没跑完出现异常返回的退出码通常是乱码:编译器在编译运行的时候,崩溃了——OS发现你的进程(代码)做了不该做的事情提前把进程终止;一旦出现这种情况,退出码也就没有了意义

进程出异常要进行终止(OS介入),本质上是因为进程收到OS的退出信号

总结:

进程终止:

1.先确定是否异常;2.不是异常,就一定是代码跑完了,看退出码就行

衡量一个进程退出,只需关注两个数字:退出码,退出信号!

3做法

a. main函数return ,表示进程终止(非main函数return,表示函数结束)

b.调用exit()函数,提前终止进程

c.系统调用_exit(),提前终止进程

这两个函数唯一的区别是:exit在进程退出的时候,会冲刷缓冲区;而_exit不会。

exit在完成执行用户定义的清理函数,冲刷缓存,关闭流等后,也会去调用_exit()来终止进程:这里说的冲刷缓存的缓冲区不是内核缓冲区,而是语言级别的缓冲区(后面写IO时说)

四阻塞等待&&非阻塞等待

前面我们说了:如果子进程退出时父进程不管不顾,子进程对应的状态为Z(僵尸)状态会一直存在,造成内存泄漏

1怎么办

父进程可以通过等待获取子进程的退出信息,知道子进程是由于什么原因退出的;回收系统资源

2等待函数

pid_t wati(int *status)   等待任意一个子进程退出   这里参数暂时不管设为NULL

等待成功时返回子进程的id

~  父进程在等待时当前状态是阻塞等待;如何理解?

父进程在阻塞等待,OS就把它的状态设置为S并把它的PCB链入到子进程的队列中;

当子进程退出时,OS就把子进程队列里的父进程给唤醒过来,wait执行后在返回就得到了子进程的id

子进程本身就是软件,父进程本质是在等待某种软件条件就绪~

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

其中,int *status 是一个输出型参数,与scanf("%d",&ret)的使用一样,不过它的输入是由OS来进行输入,输入的是子进程的退出信息;而在上面我们知道:退出信息有两个:退出码和退出信号

而status便是这两个的结合而成的值,要想知道两个退出值必须对它进行翻译:

0000 0000 0000 0000 0000 0000 0000 0000  红色表示退出信号 蓝色表示退出码

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

void ChildRun()
{
    int *p = NULL;
    int cnt = 5;
    while(1)
    {
        printf("I am child process, pid: %d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
        sleep(1);
        cnt--;
        *p = 100;
    }
}

int main()
{
    printf("I am father, pid: %d, ppid:%d\n", getpid(), getppid());

    pid_t id = fork();
    if(id == 0)
    {
        // child
        ChildRun();
        printf("child quit ...\n");
        exit(123);
    }
    sleep(7);
    // fahter
    //pid_t rid = wait(NULL);
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    if(rid > 0)
    {
        printf("wait success, rid: %d\n", rid);
    }
    else
    {
        printf("wait failed !\n");
    }
    sleep(3);
    printf("father quit, status: %d, child quit code : %d, child quit signal: %d\n", status, (status>>8)&0xFF, status & 0x7F);
}

把status通过&来看到最终的两个信号的值

我们也可以调用系统接口WEXITSTATUS(status)来得到子进程的退出码

 我们只是想让子进程的结果(退出码和退出信号)是否成功告诉给父进程就行,那么我们定义全局变量exit_code,exit_signal来使用能行吗?

答案是:不行!父进程无法看到子进程定义的全局变量

3非阻塞等待

如果子进程没有退出,则父进程就在执行waitpid时进行阻塞等待;那在这期间,能不能让父进程在等待过程中干一些自己的事呢(执行要完成的代码)?

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

返回值有三种情况:

>0 等待成功,子进程退出返回子进程的pid,回收成功

<0 等待失败

==0 检测是成功的,只不过子进程没有退出,需要你进行下一步的等待

我们就可以从返回值为0入手:让父进程在等待时发现子进程还没退出后,来允许父进程做自己的事;

设计:1.while + 在非阻塞等待的时候 + 父进程自己的事  ->非阻塞轮询

           2.将waitpid的options更改为WNOHANG

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

typedef void(*func_t)();

#define N 3
func_t tasks[N] = {NULL};

void LoadTask()
{
    tasks[0] = PrintLog;
    tasks[1] = Download;
    tasks[2] = MysqlDataSync;
}
void HandlerTask()
{
    for(int i = 0; i < N; i++)
    {
        tasks[i](); // 回调方式
    }
}

// fahter
void DoOtherThing()
{
    HandlerTask();
}


void ChildRun()
{
    //int *p = NULL;
    int cnt = 5;
    while(cnt)
    {
        printf("I am child process, pid: %d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
        sleep(1);
        cnt--;
        //*p = 100;
    }
}

int main()
{
    printf("I am father, pid: %d, ppid:%d\n", getpid(), getppid());

    pid_t id = fork();
    if(id == 0)
    {
        // child
        ChildRun();
        printf("child quit ...\n");
        exit(123);
    }
    LoadTask();
    // father
    while(1)
    {
        int status = 0;
        pid_t rid = waitpid(id, &status, WNOHANG); // non block
        if(rid == 0)
        {
            usleep(100000);
            printf("child is running, father check next time!\n");
            DoOtherThing();
        }
        else if(rid > 0)
        {
            if(WIFEXITED(status))
            {
                printf("child quit success, child exit code : %d\n", WEXITSTATUS(status));
            }
            else
            {
                printf("child quit unnormal!\n");
            }
            break;
        }
        else
        {
            printf("waitpid failed!\n");
            break;
        }
    }

五进程程序替换

先来看代码&&现象:

#include<stdio.h>
#include<unistd.h>
int main()
{
    printf("testexec ... begin!");
    execl(“/user/bin/ls” , "ls" , "-l" , "-a" , NULL);
    printf("testexec ... end!\n");
    return 0;
}

执行我们写的程序时,竟然有指令 ls -a -l的功能;execl函数竟然怎么神奇!!

1分析

我们在执行我们的代码时,其中的execl()函数会根据传入的参数来执行起来新的程序;在execl()函数之前,打印信息能够被打印出来;在执行该函数后,后续的代码不见了:因为被execl()函数替换了,向后执行了新的程序(user/bin/ls的可执行程序)

我们要知道:指令能够被执行的原因,是我们它本身就是一个可执行程序(文件)

2原理

以上面代码为例子:当还没有执行execl函数进行替换时,ls的程序(代码和文件)都保存在磁盘中;但进行替换时,本质上就是ls的程序被加载到内存中,把我们excel后的代码完全替换为ls的代码;这样就形成了一个新的程序。./运行可执行程序就是在执行它

那么在进程的程序替换过程里,有没有创建新的进程呢?

没有:调用exec前后该进程的id并未发生改变

3替换函数

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


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 execve(const char *path, char *const argv[], char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);

参数:const char *path  我们要执行的文件名   char *const argv[]   命令行参数表

           const char *arg 命名行中怎么执行就这么传参

          const char *path 传入文件所在的路径

函数之间的规律:

l(list) : 表示参数采用列表
v(vector) : 参数用(命令行参数,环境变量)数组
p(path) : 有p自动搜索环境变量PATH
e(environment) : 表示自己维护环境变量(可自己传环境变量表)

事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册 第2节,其它函数在man手册第3节。这些函数之间的关系如下图所示 :

上面的程序替换,我们替换的都是系统命令,可不可以来替换我们自己写的程序呢?

分别写两个代码:mypragma.cpp里是进行打印工作;testtxe中创建子进程来进行替换工作:

//                             testexe.c


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

int main()
{
    printf("testexec ... begin!\n");
    pid_t id = fork();
    if(id == 0)
    {
        putenv("HHHH=111111111111111111");
        // 我的父进程本身就有一批环境变量!!!, 从bash来
        char *const argv[] = 
        {
            (char*)"mypragma",
            (char*)"-a",
            (char*)"-b",

            NULL
        };
        //char *const envp[] =
        //{
        //    (char*)"HAHA=111111",
        //    (char*)"HEHE=222222",
        //    NULL
        //};
        extern char**environ;
        printf("child pid: %d\n", getpid());
        sleep(2);
        execvpe("./mypragma", argv, environ);
     exit(1);
    }

    // fahter
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    if(rid > 0)
    {
        printf("father wait success, child exit code: %d\n", WEXITSTATUS(status));
    }
    printf("testexec ... end!\n");
    return 0;
}

//                          mypragma.cpp


#include <iostream>
#include <unistd.h>
using namespace std;


int main(int argc, char *argv[], char *env[])
{
    int i = 0;
    for(; argv[i]; i++ )
    {
        printf("argv[%d] : %s\n", i, argv[i]);
    }

    printf("-------------------------------\n");
    for(i=0; env[i]; i++)
    {
        printf("env[%d] : %s\n", i, env[i]);
    }
    printf("-------------------------------\n");

    cout << "hello C++, I am a C++ pragma!: " << getpid() << endl;
    cout << "hello C++, I am a C++ pragma!: " << getpid() << endl;
    cout << "hello C++, I am a C++ pragma!: " << getpid() << endl;
    cout << "hello C++, I am a C++ pragma!: " << getpid() << endl;

    return 0;
}



执行testexe,可以看到myprogma程序的内容可以被替换从而实现出来,且两者的pid是相同的,证明了程序替换确实是不会创建新的进程的!

结论:程序替换可以支持不用的应用场景,语言之间不同也可以进行!

六简易Shell

有了上面的储备知识,我们就可以来制作与XShell类似的Shell了,流程:

1打印用户行

从左向右依次是:(环境变量)username(用户名),hostname(主机名),cwd(所在路径),标识符(#表示超级用户,$表示普通用户)(我们自己设计成 >);将这些信息填写在字符数组中打印出来就可以了。

在环境变量里,cwd的路径是完整的路径;我们要的只是当前路径的目录就行,要对它进行特殊处理:将这个路径往后进行遍历,直到找到’/‘后就停止

#define SIZE 512
#define ZERO '\0'
#define SEP " "
#define NUM 32
#define SkipPath(p) do{ p += (strlen(p)-1); while(*p != '/') p--; }while(0)

char cwd[SIZE*2];
char *gArgv[NUM];
int lastcode = 0;


const char *GetUserName()
{
    const char *name = getenv("USER");
    if(name == NULL) return "None";
    return name;
}

const char *GetHostName()
{
    const char *hostname = getenv("HOSTNAME");
    if(hostname == NULL) return "None";
    return hostname;
}

const char *GetCwd()
{
    const char *cwd = getenv("PWD");
    if(cwd == NULL) return "None";
    return cwd;
}


// commandline : output
void MakeCommandLineAndPrint()
{
    char line[SIZE];
    const char *username = GetUserName();
    const char *hostname = GetHostName();
    const char *cwd = GetCwd();

    SkipPath(cwd);
    snprintf(line, sizeof(line), "[%s@%s %s]> ", username, hostname, strlen(cwd) == 1 ? "/" : cwd+1);
    printf("%s", line);
    fflush(stdout);
}

2获取用户命令字符串

用字符数组来存储用户输入的信息(本质上是字符串),用fgets来接收(不会遇到空格停止)



int GetUserCommand(char command[], size_t n)
{
    char *s = fgets(command, n, stdin);
    if(s == NULL) return -1;
    command[strlen(command)-1] = ZERO;
    return strlen(command); 
}


        char usercommand[SIZE];
        int n = GetUserCommand(usercommand, sizeof(usercommand));
        if(n <= 0) return 1;

3分割用户输入字符串

用户输入的指令中会带有指令的选项,比如:ls -a -l ;我们要对它进行分割就要用到strtoke()函数来使用(按空格来进行一个一个分割字符串)



void SplitCommand(char command[], size_t n)
{
    (void)n;
    // "ls -a -l -n" -> "ls" "-a" "-l" "-n"
    gArgv[0] = strtok(command, SEP);
    int index = 1;
    while((gArgv[index++] = strtok(NULL, SEP))); 
// done, 故意写成=,表示先赋值,在判断. 分割之后,strtok会返回NULL,刚好让gArgv最后一个元素是NULL, 并且while判断结束
}

5执行命令

创建一个子进程来执行程序替换:将分割好的字符数组gArgv用替换函数来执行对应指令的可执行程序;父进程就只需等待子进程退出即可

void ExecuteCommand()
{
    pid_t id = fork();
    if(id < 0) Die();
    else if(id == 0)
    {
        // child
        execvp(gArgv[0], gArgv);
        exit(errno);
    }
    else
    {
        // fahter
        int status = 0;
        pid_t rid = waitpid(id, &status, 0);
        if(rid > 0)
        {
            lastcode = WEXITSTATUS(status);
            if(lastcode != 0) printf("%s:%s:%d\n", gArgv[0], strerror(lastcode), lastcode);
        }
    }
}

 4检查是否为内建命令

写好后,代码基本上能够进行使用了;但在使用时有个问题:凡是涉及到内建命令就会失败,我们要进行单独处理:

以cd为例:但判断gArgv[0]为cd时,为了代码的整洁,重新写个处理指令cd的函数;

要让pwd的路径与cd移动后的路径相同,我们就要对环境变量PWD更改才能做到;

用char* path来接收要更新的路径,用chdir()函数来对当前路径进行更改;

刷新环境变量:用getcwd()函数来获取当前工作路径存储到temp中;单独来维护PWD的环境变量用cwd来维护,将temp的路径写道cwd中再putenv导入环境变量就ok了(此时PWD的路径就用cwd来维护了)

char cwd[SIZE*2];
char *gArgv[NUM];
int lastcode = 0;

const char *GetHome()
{
    const char *home = getenv("HOME");
    if(home == NULL) return "/";
    return home;
}

void Cd()
{
    const char *path = gArgv[1];
    if(path == NULL) path = GetHome();
    // path 一定存在
    chdir(path);

    // 刷新环境变量
    char temp[SIZE*2];
    getcwd(temp, sizeof(temp));
    snprintf(cwd, sizeof(cwd), "PWD=%s", temp);
    putenv(cwd); 
}

int CheckBuildin()
{
    int yes = 0;
    const char *enter_cmd = gArgv[0];
    if(strcmp(enter_cmd, "cd") == 0)
    {
        yes = 1;
        Cd();
    }
    else if(strcmp(enter_cmd, "echo") == 0 && strcmp(gArgv[1], "$?") == 0)
    {
        yes = 1;
        printf("%d\n", lastcode);
        lastcode = 0;
    }
    return yes;
}

源代码

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

#define SIZE 512
#define ZERO '\0'
#define SEP " "
#define NUM 32
#define SkipPath(p) do{ p += (strlen(p)-1); while(*p != '/') p--; }while(0)

// 为了方便,我就直接定义了
char cwd[SIZE*2];
char *gArgv[NUM];
int lastcode = 0;

void Die()
{
    exit(1);
}

const char *GetHome()
{
    const char *home = getenv("HOME");
    if(home == NULL) return "/";
    return home;
}

const char *GetUserName()
{
    const char *name = getenv("USER");
    if(name == NULL) return "None";
    return name;
}
const char *GetHostName()
{
    const char *hostname = getenv("HOSTNAME");
    if(hostname == NULL) return "None";
    return hostname;
}
// 临时
const char *GetCwd()
{
    const char *cwd = getenv("PWD");
    if(cwd == NULL) return "None";
    return cwd;
}

// commandline : output
void MakeCommandLineAndPrint()
{
    char line[SIZE];
    const char *username = GetUserName();
    const char *hostname = GetHostName();
    const char *cwd = GetCwd();

    SkipPath(cwd);
    snprintf(line, sizeof(line), "[%s@%s %s]> ", username, hostname, strlen(cwd) == 1 ? "/" : cwd+1);
    printf("%s", line);
    fflush(stdout);
}

int GetUserCommand(char command[], size_t n)
{
    char *s = fgets(command, n, stdin);
    if(s == NULL) return -1;
    command[strlen(command)-1] = ZERO;
    return strlen(command); 
}


void SplitCommand(char command[], size_t n)
{
    (void)n;
    // "ls -a -l -n" -> "ls" "-a" "-l" "-n"
    gArgv[0] = strtok(command, SEP);
    int index = 1;
    while((gArgv[index++] = strtok(NULL, SEP))); // done, 故意写成=,表示先赋值,在判断. 分割之后,strtok会返回NULL,刚好让gArgv最后一个元素是NULL, 并且while判断结束
}

void ExecuteCommand()
{
    pid_t id = fork();
    if(id < 0) Die();
    else if(id == 0)
    {
        // child
        execvp(gArgv[0], gArgv);
        exit(errno);
    }
    else
    {
        // fahter
        int status = 0;
        pid_t rid = waitpid(id, &status, 0);
        if(rid > 0)
        {
            lastcode = WEXITSTATUS(status);
            if(lastcode != 0) printf("%s:%s:%d\n", gArgv[0], strerror(lastcode), lastcode);
        }
    }
}

void Cd()
{
    const char *path = gArgv[1];
    if(path == NULL) path = GetHome();
    // path 一定存在
    chdir(path);

    // 刷新环境变量
    char temp[SIZE*2];
    getcwd(temp, sizeof(temp));
    snprintf(cwd, sizeof(cwd), "PWD=%s", temp);
    putenv(cwd); // OK
}

int CheckBuildin()
{
    int yes = 0;
    const char *enter_cmd = gArgv[0];
    if(strcmp(enter_cmd, "cd") == 0)
    {
        yes = 1;
        Cd();
    }
    else if(strcmp(enter_cmd, "echo") == 0 && strcmp(gArgv[1], "$?") == 0)
    {
        yes = 1;
        printf("%d\n", lastcode);
        lastcode = 0;
    }
    return yes;
}

int main()
{
    int quit = 0;
    while(!quit)
    {
        // 1. 我们需要自己输出一个命令行
        MakeCommandLineAndPrint();

        // 2. 获取用户命令字符串
        char usercommand[SIZE];
        int n = GetUserCommand(usercommand, sizeof(usercommand));
        if(n <= 0) return 1;

        // 3. 命令行字符串分割. 
        SplitCommand(usercommand, sizeof(usercommand));

        // 4. 检测命令是否是内建命令
        n = CheckBuildin();
        if(n) continue;
        // 5. 执行命令
        ExecuteCommand();
    }
    return 0;
}

以上便是我的学习Linx进程终止所总结的知识点,希望对你有所帮助!

有问题欢迎在评论区里指出,谢谢!! 

 

  • 16
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值