Linux篇:进程控制

一、退出码:

1、main函数的返回值本质表示:进程运行完成是否是正确的结果,如果不是可以用不同的数字表示不同的出错原因--退出码。

2、(echo)$?---保存最近一次进程退出的退出码
strerror:---返回错误码以及对应错误码描述
errno:保存最近一次执行的错误码
:!man 3 quit---终止一个进程

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

int main()
{
        int ret = 0;
        char *p = (char*)malloc(1000*1000*1000*4);
        if(p == NULL)
        {
                printf("malloc error, %d: %s\n", errno, strerror(errno));
                ret = errno;
        }
        else{
                //使用申请的内存
                printf("malloc success\n");
        }

        return ret;
}

418ba31e36964d64932b019acfd3c7c7.png

二、进程退出场景(统一采用进程的退出码来进行判定)
1、
代码运行完毕,结果正确。
代码运行完毕,结果不正确。
代码异常中止,进程退出码无意义。
(进程出现异常,本质上是我们的进程收到了对应的信号。)
2、
exit:从正常运行的程序,执行用户定义的清理函数冲刷缓冲关闭流等。
_exit():直接调用操作系统接口,在进程层面上终止进程。
exit对应的数字就是当前进程对应的退出码。
exit在任意地方被调用,都表示调用进程直接退出,return只表示当前函数返回。
return在其他函数中代表函数结束,在main函数代表进程退出,exit在任意地方都表示进程退出
90d7528331ee4d989a46f577cd940df5.png

三、进程等待
是什么?
通过系统调用wait/waitpid,来进行对子进程进行状态检测与回收的功能。
为什么?
僵尸进程无法被杀死。需要通过进程等待来杀掉他,进而解决内存泄露问题。
我们要通过进程等待获得子进程的退出情况。(知道我布置给子进程的任务,他完成的怎么样了?)要么关心,要么不关心--可选的。

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

int main()
{
        pid_t id = fork();
        if(id < 0)
        {
                perror("fork");
                return 1;
        }
        else if(id == 0)
        {
                //child
                int cnt = 5;
                while(cnt)
                {
                        printf("I am child, pid:%d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
                        cnt--;
                        sleep(1);
                }
                exit(0);
        }
        else{
                //parent
                while(1)
                {
                        printf("I am father, pid:%d, ppid:%d\n", getpid(), getppid());
                        sleep(1);
                }
        }
        return 0;
}

c1af1d5da8a9493fa0e68c7f723a1146.png

//监视进程状态
while :; do ps ajx | head -1 && ps ajx | grep myproc | grep -v grep; sleep 1;echo "-----------------------------------------------------------------"; done

怎么办?
1、父进程通过调用wait/waitpid进行僵尸进程的回收问题。
wait:等待一个进程,直到这个进程的状态被改变(让父进程来等待子进程),默认参数(int* status)设为Null

①单子进程情况

//单子进程情况
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

int main()
{
        pid_t id = fork();
        if(id < 0)
        {
                perror("fork");
                return 1;
        }
        else if(id == 0)
        {
                //child
                int cnt = 5;
                while(cnt)
                {
                        printf("I am child, pid:%d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
                        cnt--;
                        sleep(1);
                }
                exit(0);
        }
        else{
                //parent
                int cnt = 10;
                while(cnt)
                {
                        printf("I am father, pid:%d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
                        cnt--;
                        sleep(1);
                }
                pid_t ret = wait(NULL);//wait是等待任意一个子进程退出
                if(ret == id)
                {
                        printf("wait success, ret: %d\n", ret);
                }
        }
        return 0;
}

6f2efb12134f477a92b45be67bfc243d.png

②多子进程情况(循环等待)

//多子进程情况
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>

#define N 3

void RunChild()
{
        int cnt = 5;
        while(cnt)
        {
                printf("I am Child Process, pid: %d,ppid: %d\n", getpid(), getppid());
                sleep(1);
                cnt--;
        }
}
int main()
{
        for(int i = 0; i < N; i++)
        {
                pid_t id = fork();
                if(id == 0)
                {
                        RunChild();
                        exit(0);
                }
                printf("create child process: %d sucess\n", id);//这句话只有父进程才会执行
        }
        sleep(10);
        //等待
        for(int i = 0; i < N; i++)
        {
                pid_t id = wait(NULL);
                if(id > 0)
                {
                        printf("wait %d sucess", id);
                }
        }
}

aabf34b317f14f0aa0a4f54312f13ae0.png

(那如果任意一个子进程都不退出呢)

③阻塞等待:如果子进程不退出,父进程默认在wait的时候,调用这个系统调用的时候,也就不返回,默认叫做阻塞状态。

wait调用会回收僵尸,释放内存泄露问题,wait调用,如果子进程不进行退出,Wait父进程也就不会返回,会一直等待,直到子进程退出。

(那如何处理此类情况呢?)

2、【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:(输出型参数)(int是被当做几部分使用的)
 WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
 WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
 options:
 WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。】

//错误使用范例
int main()
{
        pid_t id = fork();
        if(id < 0)
        {
                perror("fork");
                return 1;
        }
        else if(id == 0)
        {
                //child
                int cnt = 5;
                while(cnt)
                {
                        printf("I am child, pid:%d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
                        cnt--;
                        sleep(1);
                }
                exit(1);
        }
        else{
                //parent
                int cnt = 10;
                while(cnt)
                {
                        printf("I am father, pid:%d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
                        cnt--;
                        sleep(1);
                }
                //pid_t ret = wait(NULL);
                int status = 0;
                pid_t ret = waitpid(id, &status, 0);
                if(ret == id)
                {
                        printf("wait success, ret: %d, status: %d\n", ret, status);
                }
                sleep(5);
        }
        return 0;
}

36d253393fe448ec9851a99e857d5aa2.png

②为什么status返回256而不是1呢?

分析:

子进程退出,一共会有3种退出场景。

父进程等待,期望获得子进程退出的哪些信息呢?
1. 子进程代码是否异常?
2. 没有异常结果,exitcode对吗?不对的是因为什么?1 2 3 4->不同的退出码表示不同的出错原因

一共有32个比特位,先不考虑高16位,先考虑低16位。其中低七位代表终止信号,第八个比特位代表是否core dump。次低八位代表退出状态,即子进程退出时的退出码。
若低7位为零,则进程在运行时根本没有收到过信号,则代码没有异常。

方法一:采用位操作对status这样的整形数据进行数据提取

//采用位操作对status这样的整形数据进行数据提取
int main()
{
        pid_t id = fork();
        if(id < 0)
        {
                perror("fork");
                return 1;
        }
        else if(id == 0)
        {
                //child
                int cnt = 5;
                while(cnt)
                {
                        printf("I am child, pid:%d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
                        cnt--;
                        sleep(1);
                }
                exit(1);
        }
        else{
                //parent
                int cnt = 10;
                while(cnt)
                {
                        printf("I am father, pid:%d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
                        cnt--;
                        sleep(1);
                }
                //pid_t ret = wait(NULL);
                int status = 0;
                pid_t ret = waitpid(id, &status, 0);
                if(ret == id)
                {
                        //7F: 0111111
                        printf("wait success, ret: %d, exit sig: %d, exit code: %d\n", ret, status&0x7F, (status>>8)&0xFF);
                }
                else
                {
                        printf("wait failed!\n");
                }
                sleep(5);
        }
        return 0;
}

c7c7afd1f0194de9b906b36935e73528.png

方法二:宏操作

//宏操作
int main()
{
        pid_t id = fork();
        if(id < 0)
        {
                perror("fork");
                return 1;
        }
        else if(id == 0)
        {
                //child
                int cnt = 5;
                while(cnt)
                {
                        printf("I am child, pid:%d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
                        cnt--;
                        sleep(1);
                }
                exit(1);
        }
        else{
                //parent
                int cnt = 10;
                while(cnt)
                {
                        printf("I am father, pid:%d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
                        cnt--;
                        sleep(1);
                }
                //pid_t ret = wait(NULL);
                int status = 0;
                pid_t ret = waitpid(id, &status, 0);
                if(ret == id)
                {
                        //7F: 0111111
                        //printf("wait success, ret: %d, exit sig: %d, exit code: %d\n", ret, status&0x7F, (status>>8)&0xFF);
                        if(WIFEXITED(status))
                        {
                                printf("进程是正常跑完的,退出码%d\n",WEXITSTATUS(status));
                        }
                        else
                        {
                                printf("进程出异常了\n");
                        }
                }
                else
                {
                        printf("Wait failed!\n");
                }
                sleep(5);
        }
        return 0;
}

b8f20a3f5db842fb9b8ae50236c54be5.png

③waitpid的本质是其进程的内核数据结构,并且将进程Z状态改成X状态。

操作系统在等待的时候也会检测当前进程等待是否正确。

无论创建单个进程还是创建一批进程,保证子进程状态PCB能够回收,防止内存泄露,可以获得退出状态。

子进程main函数的返回值表征当前进程退出结果是否正确,此退出码将来会被父进程获取,通过该退出码决定子进程任务完成的好坏,如果执行失败,父进程则有其他策略。所以一个命令的退出结果能被bash拿到。

整个linux的进程结构实际上是一个多叉树结构,父进程永远都对自己直系的子进程负责。(隔代爷孙关系不负责)。

pid_t waitpid(pid_t pid, int *status, int options);
其中options:
默认0称为阻塞等待方式

非阻塞(WNOHANG)轮询

int main()
{
        pid_t id = fork();
        if(id < 0)
        {
                perror("fork");
                return 1;
        }
        else if(id == 0)
        {
                //child
                int cnt = 5;
                while(cnt)
                {
                        printf("I am child, pid:%d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
                        cnt--;
                        sleep(1);
                }
                exit(1);
        }
        else{
                //parent
                int cnt = 10;
                while(cnt)
                {
                        printf("I am father, pid:%d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
                        cnt--;
                        sleep(1);
                }
                //pid_t ret = wait(NULL);
                int status = 0;
                while(1) //轮询
                {
                        pid_t ret = waitpid(id, &status, WNOHANG);//非阻塞
                        if(ret > 0)
                        {
                                //7F: 0111111
                                //printf("wait success, ret: %d, exit sig: %d, exit code: %d\n", ret, status&0x7F, (status>>8)&0xFF);
                                if(WIFEXITED(status))
                                {
                                        printf("进程是正常跑完的,退出码:%d\n",WEXITSTATUS(status));
                                }
                                else
                                {
                                        printf("进程出异常了\n");
                                }
                                break;
                        }
                        else if(ret < 0)
                        {
                                printf("Wait failed!\n");
                                break;
                        }
                        else
                        {
                                //ret == 0
                                printf("子进程还没有退出,再等等...\n");
                        }
                }
                sleep(5);
        }
        return 0;
}


b216baad484c4e2287b6b85f73421dfd.png

进程把所有的子进程创建出来,也就理所当然的由创建者对他所创建的对象负责,将他所创建的所有子进程退出。通过进程等待保证父进程是多个进程中最后退出的进程,可以保证将所有曾经创建出的子进程资源全部释放。

四、程序替(exec系列)
1. 单进程版--最简单的看看程序替换

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

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

        //这类方法的标准写法
        execl("/usr/bin/ls","ls","-a","-l",NULL);

        printf("after: I am a process, pid: %d, ppid: %d\n",getpid(), getppid());
        return 0;
}

486137cbe37a423ea1cb729595b164b7.png


2.谈进程替换的原理
基本原理:新的可执行代码和数据,替换原先的代码和数据,并从零开始执行。(并没有创建新进程,id并未改变)

0530cf666be54740a9a1d4a165fa6140.png

由于写时拷贝技术以及进程相互的独立性,子进程程序替换并不影响父进程。
写时拷贝不仅仅在数据上实现,也在代码上实现。

去替换有没有创建新的进程?
程序替换没有创建新的进程,只进行进程的程序代码和数据的替换工作。

为什么after代码不去执行呢?
程序替换之后,老数据的代码已被替换,后续代码不会被执行。

补充:

① 现象:程序替换成功之后,exec*后续代码不会被执行,替换失败才可能执行后续代码。exec*函数只有失败返回值,没有成功返回值
② 小知识:Linux中形成的可执行程序是有格式的,ELF可执行程序的表头,可执行程序的入口地址就在表中。
③进程程序替换后,该进程对应的PCB,进程地址空间以及页表等数据结构没有发生改变。


3. 多进程版--验证各种程序替换接口

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

ad042e76f49c4d818df9485460f0db19.png

execl:
第一个参数:必须是全路径,以绝对或相对的路径。
第二个参数:按照命令行传参的方式传参。

execl("/usr/bin/ls", "ls", "-a", "-l", NULL);

execlp:
PATH:execlp自己会在自己默认的环境变量中查找。

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

execv:
第二个参数:以字符串指针数组传参

char *const myargv[] = {"ls", "-l", "-a", NULL};

execv("/usr/bin/ls", myargv);

execvp:

char *const myargv[] = {"ls", "-l", "-a", NULL};

execv("ls", myargv);

总结:在Linux中所有的进程一定是别人的子进程
在命令行当中,所有的进程都是bash的子进程
所以所有的进程在启动的时候都是通过exec来启动执行的
所以exec承担的是加载器的效果。(内存申请,外设访问等)

exec*不仅能够执行系统命令,还可以执行我们自己的命令。

execl("./otherExe", "otherExe", NULL);

 在makefile自顶向下扫描时,所对应的伪目标(第一个目标文件)all依赖于xx,由于没有依赖方法,所以依赖方法不执行。

##Makefile
.PHONY:all
all:otherExe mycommand

otherExe:otherExe.cpp
        g++ -o $@ $^ -std=c++11
mycommand:mycommand.c
        gcc -o $@ $^ -std=c99
.PHONY:clean
clean:
        rm -f mycommand otherExe

脚本语言就是文本文件,bash会对文本文件中的文本行进行读取,边读取,边执行。

无论我们的可执行程序还是脚本,为什么能跨语言调用呢?
因为所有语言运行起来,都是进程!只要是进程,就可以被调用!

execle:
最后一个参数:环境变量

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

int main()
{
        extern char **environ;//系统默认环境变量
        putenv("PRIVATE_ENV=666666");//新增环境变量方法一:父进程的地址空间中直接putenv
        pid_t id = fork();
        if(id == 0){//child
                printf("before: I am a process, pid: %d, ppid: %d\n",getpid(), getppid());
                char *const myargv[]={"otherExe","-a","-b""-c",NULL};
                char *const myenv[]={"MYVAL=1111","MYPATH=/usr/bin/XXX",NULL};//自定义环境变量
                execle("./otherExe","otherExe","-a","-w","-v",NULL,environ);
                execle("./otherExe","otherExe","-a","-w","-v",NULL,myenv);//新增环境变量方法二:putenv后传递给父进程    注:采用的策略是覆盖,而不是追加
                printf("after: I am a process, pid: %d, ppid: %d\n",getpid(), getppid());
                exit(1);
        }
        //father
        pid_t ret = waitpid(id, NULL,0);
        if(ret > 0) printf("wait success, father pid: %d, ret id: %d\n", getpid(), ret);
        sleep(5);
        return 0;
}

环境变量是什么时候给进程的?执行的时候给进程的。

环境变量也是数据,当我们创建子进程的时候,环境变量就已经被子进程继承下去了。所以程序替换中,环境变量信息不会被替换。

环境变量在不断给自己的每一个子进程下沉的时候,每一个子进程可以给自己定义put属于自己的环境变量,put后的环境变量会被后续的子进程再继承。

所以我如果想给子进程传递环境变量,该怎么传递?
1. 新增环境变量(父进程的地址空间中直接putenv或者putenv后传递给父进程)
2. 彻底替换(直接定义)

五、自定义shell

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

#define LEFT "["
#define RIGHT "]"
#define LABLE "#"
#define DELIM " \t"
#define LINE_SIZE 1024
#define ARGC_SIZE 32
#define EXIT_CODE 44

int lastcode = 0;
int quit = 0;
extern char **environ;
char commandline[LINE_SIZE];
char *argv[ARGC_SIZE];
char pwd[LINE_SIZE];

// 自定义环境变量表
char myenv[LINE_SIZE];
// 自定义本地变量表


const char *getusername()
{
    return getenv("USER");
}

const char *gethostname()
{
    return getenv("HOSTNAME");
}

void getpwd()
{
    getcwd(pwd, sizeof(pwd));
}

void interact(char *cline, int size)
{
    getpwd();
    printf(LEFT"%s@%s %s"RIGHT""LABLE" ", getusername(), gethostname(), pwd);
    char *s = fgets(cline, size, stdin);
    assert(s);
    (void)s;//为了使编译器能编过(防止告警)
    // "abcd\n\0"
    cline[strlen(cline)-1] = '\0';
}

int splitstring(char cline[], char *_argv[])
{
    int i = 0;
    argv[i++] = strtok(cline, DELIM);
    while(_argv[i++] = strtok(NULL, DELIM)); 
    return i - 1;
}

void NormalExcute(char *_argv[])
{
    pid_t id = fork();
    if(id < 0){
        perror("fork");
        return;
    }
    else if(id == 0){
        //让子进程执行命令
        //execvpe(_argv[0], _argv, environ);
        execvp(_argv[0], _argv);
        exit(EXIT_CODE);
    }
    else{
        int status = 0;
        pid_t rid = waitpid(id, &status, 0);
        if(rid == id) 
        {
            lastcode = WEXITSTATUS(status);
        }
    }
}

int buildCommand(char *_argv[], int _argc)
{
    if(_argc == 2 && strcmp(_argv[0], "cd") == 0){
        chdir(argv[1]);
        getpwd();
        sprintf(getenv("PWD"), "%s", pwd);
        return 1;
    }
    else if(_argc == 2 && strcmp(_argv[0], "export") == 0){
        strcpy(myenv, _argv[1]);
        putenv(myenv);
        return 1;
    }
    else if(_argc == 2 && strcmp(_argv[0], "echo") == 0){
        if(strcmp(_argv[1], "$?") == 0)
        {
            printf("%d\n", lastcode);
            lastcode=0;
        }
        else if(*_argv[1] == '$'){
            char *val = getenv(_argv[1]+1);
            if(val) printf("%s\n", val);
        }
        else{
            printf("%s\n", _argv[1]);
        }

        return 1;
    }

    // 特殊处理一下ls(内建命令)
    if(strcmp(_argv[0], "ls") == 0)
    {
        _argv[_argc++] = "--color";
        _argv[_argc] = NULL;
    }
    return 0;
}

int main()
{
    while(!quit){
        // 1.
        // 2. 交互问题,获取命令行, ls -a -l > myfile / ls -a -l >> myfile / cat < file.txt
        interact(commandline, sizeof(commandline));

        // commandline -> "ls -a -l -n\0" -> "ls" "-a" "-l" "-n"
        // 3. 子串分割的问题,解析命令行
        int argc = splitstring(commandline, argv);
        if(argc == 0) continue;

        // 4. 指令的判断 
        // debug
        //for(int i = 0; argv[i]; i++) printf("[%d]: %s\n", i, argv[i]);
        //内建命令,本质就是一个shell内部的一个函数
        int n = buildCommand(argv, argc);

        // 5. 普通命令的执行
        if(!n) NormalExcute(argv);
    }
    return 0;
}

 

 

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值