【Linux】进程控制

目录

1、进程创建

1.2、fork(补充)

1.2、写时拷贝(补充)

2、进程终止

3、进程等待

4、程序替换

5、自己写一个shell


1、进程创建

1.2、fork(补充)

//一次创建一批进程

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

#define N 5

void runChlid()
{
    int cnt = 10;
    while(cnt)  //执行10秒之后自动退出
    {
        printf("I am child: %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);  //子进程结束

                      //父进程往下走

        }  //父进程 再继续去循环  这样就可以一次性创建出5个子进程
    }

    sleep(1000);
    return 0;
}

父子进程或者兄弟进程,谁先运行,完全是由调度器决定的,谁先被放到运行队列里谁就先运行,这是不能被确定的。

1.2、写时拷贝(补充)

补充一个细节问题:OS是如何知道我们此时父子进程共享数据块,此时是需要发生写时拷贝的呢? 

在父进程创建了子进程时候,父进程的数据段就会由原来的读写段被暂时改为只读段,子进程也是只读。只读区不变。所以,不论是父进程还是子进程谁想写,就会触发系统层面的权限问题,但是在权限方面审核的时候,OS会识别出来,历史上这个区域是可以被写入的,只不过是暂时是只读状态。因此这时触发的权限问题,OS不会做异常异常处理,而是将异常转为 将数据拷贝一份,谁写的就将谁的页表重新映射,拷贝完之后在页表中将只读的标签去掉,就可以正常做修改了。

其实,写时拷贝本质上就是用时再用,延迟申请,按需申请!

2、进程终止

问题:为什么以前写代码main函数总是会返回return 0?1?2? 这个值给谁了?为什么要返回这个值?

进程退出场景:a、代码运行完毕,结果正确。(不关心)

b、代码运行完毕,结果不正确。(关心为什么)

c、代码异常终止

输入 echo $? //用来表示最近一个程序退出时的退出码

0 //这个0会被父进程即bash拿到,得到上一个程序的退出码

[]$  ./myproc

.........

[]$  echo $?

11   //假如11是myproc程序的退出码

[]$  echo $?

0   //打印第二遍为什么会变为0?  因为它的上一个是echo命令,echo命令执行是正确的,所以就是0

进程中,谁会关心我运行情况?一般而言是父进程要关心

结果为什么不正确?可以用return 的不同返回值数字,表征不同的出错原因,这些不同的数字就叫作退出码

总而言之,main函数的返回值本质表示:进程运行完成时是否正确的结果,如果不是,可以用不同的数字表示不同的出错原因。

退出码是数字 这些数字是给计算机看的,我们看的应该是一些错误信息。

#include<string.h>

int main()
{
    for(int i = 0; i < 200; i++)
    {
        printf("%d: %s\n", i, strerror(i)); //打印每个数字对应的错误信息
    }
    return 0;
}

系统提供的错误码和错误码描述是有对应关系的。 错误码表示退出原因,错误信息展示具体详细的错误。

那么,我们可不可以自己设计一套退出码体系呢?  答案是:当然可以

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

const char *errString[] = {
    "success",
    "error 1",
    "error 2",
    "error 3",
    "error 4",
    "error 5",
};

int main()
{
    for(int i = 0; i < 200; i++)
    {
        printf("%d: %s\n", i, strerror(i)); //打印每个数字对应的错误信息
    }
    return 0;
}

结果正确或者说不正确,统一会采用进程的退出码来进行判定!!

问题:为什么以前写代码main函数总是会返回return 0?1?2? 这个值给谁了?为什么要返回这个值? 因此,这个问题就可以解决了。

#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 = 1;//并且还可以将错误码转为进程的 退出码,让父进程也知道
    }
    else
    {
        //使用申请的内容
        printf("malloc success\n");
    }

    return ret;
}

[]$  ./myproc

malloc error, 12: Cannot allocate memory

[]$  echo $?

12 //退出码

errno  是C语言给我们提供的一个全局变量。保存最近一次执行的错误码。c语言调用库函数不成功的时候,返回值告诉我们这个函数的情况,结果对不对,至于结果不对的原因,比如当库函数调用失败了,C语言就会将我们的errno全局变量设置成对应的数字,这个数字表示我们当前调用的函数出错时的错误码。 如果连续调函数都失败了,这个errno每次都会被覆盖,它保留的时last error

如果代码出现了异常(第三种情况),退出码还有意义吗??答案是:本质可能代码没有跑完,进程的退出码无意义,我们不关心退出码。

因此,进程退出的时候,我们首先关心有没有发生异常,没出异常就是代码跑完了,再看结果是否是正确的(再看退出码);如果出异常了,比如野指针的时候就相当于你即将要访问一个虚拟地址,这个虚拟地址在页表当中并没有建立对应的映射关系,或者说建立了映射关系它的权限设置成立只读权限,这些问题都会被OS识别,OS就会向目标进程发信号。进程出现异常,本质是我们进程收到了对应的信号(父进程看有没有收到信号)!

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

int main()
{
    printf("hello Linux\n");

    exit(12);  //让一个进程退出 12 这个数字是退出码
                //和return在main函数里是等价的
}
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h> 

void show()
{
    printf("hello show!\n);
    printf("hello show!\n);
    printf("hello show!\n);
    printf("hello show!\n);
    printf("hello show!\n);
    exit(13);
    printf("end show \n");
    printf("end show \n");
    printf("end show \n");

int main()
{
    show();
    printf("hello Linux\n");

    exit(12);  
}

运行结果:

hello show!

hello show!

hello show!

hello show!

hello show!

退出码是13

结论:exit 在任意地方被调用,都表示调用进程直接退出

return在main函数外面,只表示当前函数返回,返回到main函数。

因此,return在其他函数中代表函数返回,在main函数中代表进程退出;而exit在任意地方都代表进程退出!

exit与_exit之间的区别

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

int main()
{
    printf("you can see me!\n");
    sleep(1);

    exit(11);
    //_exit(11);
}

上面的代码exit与_exit的结果是一样的,因为有\n,退出也是一样的。

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

int main()
{
    printf("you can see me!");
    sleep(1);

    exit(11);
    
}

printf没有加\n的时候数据会暂时放到缓冲区当中,所以上面的代码数据并没有立即刷出来,停了一秒,当整个程序退出的时候,系统才会将数据刷出来。

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

int main()
{
    printf("you can see me!");
    sleep(1);

    _exit(11);
    
}

没有打印出来you can see me!

_exit是系统调用接口,程序直接调用系统接口,直接在进程层面就将这个程序控制了,而exit会在应用层将你之前打开的文件..进行刷新(刷新缓冲区),然后exit再调_exit.因为先把结果刷新了(关闭所有打开的流,所有的缓存数据均被写入 )再退出,所以就可以看到数据。

printf一定是先把数据写入缓冲区中,等到合适的时候,再进行刷新。比如说遇到\n 、进程退出。

那么这个缓冲区绝对不在哪里??? 答案是:绝对不在内核里!因为如果在内核里,_exit和exit都会对这个缓存区进行刷新,因为OS不会做任何浪费空间的行为,维护缓冲区就一定要进行刷新,因此不在。它其实在用户区。

3、进程等待

是什么??

通过系统调用wait/waitpid,来对子进程状态进行检测与回收的功能!

为什么??

a、父进程不管子进程,子进程会僵尸,僵尸就会造成内存泄露。

b、僵尸状态的进程不能通过任何操作被杀死,只能等待父进程回收它

a和b是我们必须解决的

c、我们要通过进程等待获得子进程的退出情况,这样是为了知道我布置给子进程的任务,它完成的怎么样了。

c我们要么关心,要么不关心------可选的

怎么办??

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

int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork"); //perror是c语言的系统调用,会将错误码转化成错误码描述,
                         //将对应的函数带上再打印出错误信息
        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;
        whlie(cnt)
        {
            printf("I am father, pid:%d, ppid:%d, cnt: %d\n",getpid(),getppid(),cnt);
            cnt--;
            sleep(1);
        }
        pid_t ret = wait(NULL);
        if(ret == id)//这个id就是要等待的子进程的pid
        {
            printf("wait success, ret: %d\n", ret);
        }
        sleep(5);
    }

    return 0;
}

上面代码的现象:前5s子进程运行,运行5s,子进程变为僵尸状态,父进程运行5s子进程此时还是僵尸进程,之后,父进程回收子进程,然后僵尸进程没有了,父进程单独运行5s

ret返回值刚好也是子进程的pid   那么,我们就看到子进程被回收了。

到目前位置,进程等待是必须的!

wait等待的是任意一个子进程,如果我们有多个子进程呢?

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

#define N 10

void RunChild()
{
    int cnt = 5;
    while(1) //子进程执行5秒
    {
        printf("I am Child Process, pid: %d, ppid: %d\n",getpid(),getppid());
        sleep(1);
        cnt--;
    }
}

int main()
{
    for(int i = 0; i < N; i++) //这个for循环因为执行流的原因,父进程会通过for循环创建一批子进程
    {
        pid_t id = fork(); //使用for循环创建出一批进程
        if(id == 0)
        {
            RunChild();
            exit(0); //子进程退出
        }
        printf("create child process: %d success\n",id);//这句话只有父进程会执行
                                                //这个id值是父进程创建出的子进程的pid
    }

    sleep(10);
    //子进程执行5秒,父进程10秒,现在就要让父进程对子进程进行等待
    for(int i = 0; i < N; i++)
    {
        pid_t id = wait(NULL);  //因为wait每次只等待一个进程退出,所以等待一批进程就要用for循环
        if(id > 0)
        {
            printf("wait %d success\n",id);
        }
    }

    sleep(5);
}

如果子进程不退出,父进程默认在wait的时候,调用这个系统调用的时候不返回,这就叫阻塞状态。进程等待是不是只会等待硬件资源呢?并不是!现在这个进程就是在等待软件。

pid_ t waitpid(pid_t pid, int *status, int options);
返回值,等待一个进程成功了,返回值就是你所等待的那个进程的pid
a、 pid = -1,等待任意一个进程,这样写的话效果与wait一样
       pid > 0,   等待进程id和pid相等的
<1> status是输出型参数:就是能通过函数的参数把值带出来的参数,我给函数传参不是为了给函数什么,而是为了让函数交给我们什么。
<2> int是被当作及部分来使用的   一个int占32位
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>


int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork"); //perror是c语言的系统调用,会将错误码转化成错误码描述,
                         //将对应的函数带上再打印出错误信息
        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);  //子进程退出  退出码是1
    }
    else
    {
        //parent
        int cnt = 10;
        whlie(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);//如果关心子进程的退出状态,
                                //就给status这个参数,就可以得到进程的退出信息
        if(ret == id)//这个id就是要等待的子进程的pid
        {
            printf("wait success, ret: %d\n", ret);
        }
        sleep(5);
    }

    return 0;
}

运行结果:.........

wait success, ret: 26472(子进程的pid), status:256

为什么退出信息是256呢?我们设置的退出码不是1嘛????

0000 0000 0000 0000 <0000 0001> 0000 0000

次低8位是退出码

子进程的退出场景有几种??

三种。代码运行完毕,结果正确

代码运行完毕结果不正确

代码异常终止

父进程等待,期望获得子进程退出的哪些信息呢??

a、子进程代码是否异常?

b、没有异常,结果对吗? 结果对不对,可以有退出码来判断。不对是因为什么?

1 2 3 4 ..不同的退出码表示不同的出错原因!

低8位表示子进程运行的时候是否出异常

检测进程在运行的时候低7位是否为0,如果是0,就代表我们没有收到信号,我们的代码就没有异常。这一步就可以判断我们的代码是否跑完。 再通过次低8位,获得进程的退出状态,即进程的退出码。

还有一个问题,我们不能通过设置一个全局变量,在子进程退出的时候,只需要让子进程将这个变量进行修改,我们父进程就可以拿到子进程的状态,这样做可以吗?

答案是:不能,因为进程具有独立性,子进程将数据进行修改的时候,就会发生写时拷贝,子进程对数据修改,父进程是拿不到修改之后的数据的。因此,只能调用系统调用函数来获得子进程的退出状态。

当子进程执行完毕之后,子进程的退出信息都会被保存在子进程的PCB里。调用pid_t waitpid(pid_t id,int* status,int XXX)  就是读取子进程的task_struct内核数据结构对象。因此,这些行为只能由OS来完成。

pid_ t waitpid(pid_t pid, int *status, int options);
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出) 正常终止那么状态为真。 检验status变量的信号位, 如果为0(0代表没有收到任何信号),那么就是1;如果检测到信号不为0,那么就不为1
WEXITSTATUS(status): WIFEXITED 非零,提取子进程退出码。(查看进程的退出码)
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>

int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork"); //perror是c语言的系统调用,会将错误码转化成错误码描述,
                         //将对应的函数带上再打印出错误信息
        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);  //子进程退出  退出码是1
    }
    else
    {
        //parent
        int cnt = 10;
        whlie(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);//如果关心子进程的退出状态,
                                //就给status这个参数,就可以得到进程的退出信息
        
        if(ret == id)//这个id就是要等待的子进程的pid
        {
            if(WIFEXITED(status))//WIFEXITED判断子进程是否异常,即是否由信号发出,为0代表没有信号发出,WIFEXITED(status)就为真
            {
                printf("进程是正常跑完的,退出码:%d\n",WEXITSTATUS(status));
                                        //WEXITSTATUS提取子进程的退出码
            }
            else
            {
                printf("进程出异常了\n"); //是子进程出异常了
            }

            printf("wait success, ret: %d\n", ret);
        }
        else{
             printf("wait failed!\n");//这是waipid函数调用失败了
        }
        sleep(5);
    }

    return 0;
}

多个进程:

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

#define N 10

void RunChild()
{
    int cnt = 5;
    while(cnt) //子进程执行5秒
    {
        printf("I am Child Process, pid: %d, ppid: %d\n",getpid(),getppid());
        sleep(1);
        cnt--;
    }
}

int main()
{
    for(int i = 0; i < N; i++) //这个for循环因为执行流的原因,父进程会通过for循环创建一批子进程
    {
        pid_t id = fork(); //使用for循环创建出一批进程
        if(id == 0)
        {
            RunChild();
            exit(i); //子进程退出
        }
        printf("create child process: %d success\n",id);//这句话只有父进程会执行
                                                //这个id值是父进程创建出的子进程的pid
    }

    sleep(10);
    //子进程执行5秒,父进程10秒,现在就要让父进程对子进程进行等待
    for(int i = 0; i < N; i++)
    {
        //pid_t id = wait(NULL);  //因为wait每次只等待一个进程退出,所以等待一批进程就要用for循环
        int status = 0;
        pid_t id = waitpid(-1, &status, 0);//-1 代表等待任意一个进程
        if(id > 0)//等待子进程成功
        {
            printf("wait %d success,exit code: %d\n", id, WEXITSTATUS(status));
                                            //WEXITSTATUS(status)获得进程退出码
        }
    }

    sleep(5);
}
pid_ t waitpid(pid_t pid, int *status, int options);
options:
WNOHANG: pid 指定的子进程没有结束,则 waitpid() 函数返回 0 ,不予以等待。若正常结束,则返回该子进程的ID

等待进程的两种方式:当进程不退出时,一直阻塞等待;非阻塞轮询

非阻塞轮询:非阻塞+循环    非阻塞轮询+做自己的事情

ret_pid > 0 等待成功

ret_pid = 0 等待的条件还没有就绪  这种情况会就绪等待,直到大于0或小于0

ret_pid < 0 等待不成功

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

#define TASK_NUM 10
typedef void(*task_t)();
task_t task[TASK_NUM];

//任务
void task1()
{
    printf("这是一个执行打印日志的任务");
}
void task2()
{
    printf("这是一个检测网络健康状态的任务");
}
void task3()
{
    printf("这是一个绘制图形界面的任务");
}

//任务的管理代码
void InitTask()
{
    for(int i = 0;i < TASK_NUM; i++) tasks[i] = NULL;
}

int AddTask(task_t t)
{
    int pos = 0;
    for(;pos < TASK_NUM;pos++)
    {
    if(!task[pos]) break;
    }
    if(pos == TASK_NUM) return -1;
    task[pos] = t;
    return 0;
}
void DelTask()
{}

void CheckTask()
{}

void UpdateTask()
{}
void ExecuteTask()
{
    for(int i = 0; i < TASK_NUM; i++)
    {
        if(!tasks[i] continue;
        tasks[i]();
    }
}



int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork"); //perror是c语言的系统调用,会将错误码转化成错误码描述,
                         //将对应的函数带上再打印出错误信息
        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);  //子进程退出  退出码是1
    }
    else
    {
        //parent
        int cnt = 10;
        whlie(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);//如果关心子进程的退出状态,
                                //就给status这个参数,就可以得到进程的退出信息
    InitTask();
    AddTask(task1);
    AddTask(task2);
    AddTask(task3);   
    

    while(1) //轮询
    {

        pid_t ret = waitpid(id, &status, WNOHANG);  //非阻塞
        if(ret > 0) //说明等待成功了
        {
            if(WIFEXITED(status))//WIFEXITED判断子进程是否异常,即是否由信号发出,为0代表没有信号发出,WIFEXITED(status)就为真
            {
                printf("进程是正常跑完的,退出码:%d\n",WEXITSTATUS(status));
                                        //WEXITSTATUS提取子进程的退出码
            }
            else
            {
                printf("进程出异常了\n"); //是子进程出异常了
            }

           break;
            
        }
        else if(ret < 0)
        {
             printf("wait failed!\n");//这是waipid函数调用失败了
            break;
        }
        else
        {
            //ret == 0
            //printf("子进程还没有退出,我再等等...\n");
            ExecuteTask();

        }
    }
        sleep(5);
        
    }

    return 0;
}

4、程序替换

先看一个现象(单进程版):

#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;
}

我们自己写的代码执行了 ls 只打印了before没有 after,上面这个现象就是程序替换


程序替换的基本原理:

ls这个命令的程序和代码在磁盘上,当在调用程序替换函数的时候,就会用新程序的代码替换旧程序的代码,新程序的数据替换旧程序的数据。替换完之后,只需要将页表的右侧再重新调整一下,就好了,pcb,进程地址空间以及页表的左侧不变。替换完之后,让cpu执行当前新的入口地址,来执行新程序,我们只是将当前进程的代码和数据进行了替换而并没有创建新进程,这就叫程序替换。

多进程版:

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

int main()
{
    pid_t id = fork();
    if(id == 0)
    {    //child
        printf("before: I am a process, pid: %d, ppid: %d\n", getpid(), getppid());
        sleep(5);
        //这类方法的标准写法
        execl("usr/bin/ls", "ls","-a","-l",NULL);//执行一个指定的文件
        printf("after: I am a process, pid: %d, ppid: %d\n", getpid(), getppid());
        exit(0);
    }
    //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;
}

上面代码实验现象:创建子进程,子进程去执行程序替换,执行ls命令,在子进程执行完毕之后,父进程对子进程进行等待回收。

那么,这个子进程发生进程替换为什么不会影响父进程呢?因为写时拷贝和进程具有独立性。那替换数据的时候可以进行写时拷贝,那么替换代码的时候也可以是发生写时拷贝吗?

答案是:是的!代码也可以实现写时拷贝! 当我们要想自己对代码区进行写入时,OS会对我们的行为进行拦截,但是现在我们调用的是系统函数,对代码区进行写时拷贝的工作是由OS来完成的,因此,是可以的。

程序替换有没有创建新进程?

答案是:没有!只进行程序和代码的替换工作。 因此,进程的内核数据结构就没有被释放和重新建立,只需要改某些字段就欧克了。

是父子关系发生写时拷贝,不是父子关系,直接替换。

为什么after没有被打印? 因为after属于老程序的代码,这个代码在execl之后,老代码早已被替换,因此不会执行。程序替换失败了?才可能执行后续的代码。 那么,exec系列的函数只有失败返回值,没有成功返回值!!

替换函数: 有六种exec系列的函数

#include <unistd.h>`
int exec l (const char *path, const char *arg, ...);
前面都是exec, l:list ,后面带l的就是参数一个一个传,并且是可变参数。
第一个参数是我如何找到该程序,并且所有的exec函数都一样。
从第二个参数开始,命令行怎么写,这个参数就怎么传,后面的参数代表找到程序之后如何执行这个程序,主要是要不要涵盖选项,涵盖哪些。
int exec lp (const char *file, const char *arg, ...);
p是PATH,第一个参数不用给路径,只需要给出文件就可以。后面的参数还是一样。
eg:execlp("ls","ls","-a","-l",NULL); 第一个ls是找到执行的程序,后面是选项
int exec le (const char *path, const char *arg, ...,char *const envp[]);
e:env(环境变量)
int exec v (const char *path, char *const argv[]); 字符串指针数组
v:vector 第一个参数是找到可执行程序 第二个是 将我们的选项放到指针数组里
eg:char *const myargv[] = {
                "ls",
                "-l",
                "-a",
                NULL
            };
         execv("user/bin/ls",myargv);
ls是已经编译好的程序,并且它肯定也有main函数,有main函数肯定就有命令函参数,那么它的命令函参数是谁提供的?是execv系统调用由myargv传入的,execv是系统函数,系统获取了命令行参数然后传给ls的main函数就好了
在命令行当中所有的进程都是bash的子进程,因此,所有的进程在启动的时候,都是采用exec系列的函数进行启动执行的。所以,exec系列的函数承担的是一个加载器的效果!! 其实,execv就是代码层面的加载器。可以将可执行程序导到内存里,也可以获得命令行参数,执行ls调用main函数时,就是将myargv参数传进去的。
int exec vp (const char *file, char *const argv[]);
p就是只给文件名 第二个参数和上面相同

环境变量是什么时候给进程的?环境变量也是数据,创建子进程的时候,环境变量就已经被子进程继承下去了! 因此,程序替换中环境变量信息不会被替换!

事实上 , 只有 execve 是真正的系统调用 , 其它五个函数最终都调用 execve,

5、自己写一个shell

shell:是OS外的一层外壳程序,负责帮用户进行指令的执行,负责将指令交给OS,OS执行完之后通过shell将结果交给用户,因此它叫作外壳程序。shell/bash也是一个进程,指令执行的时候,本质就是自己创建子进程去执行的!

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

#define LEFT "["
#define RIGHT "]"
#define LABLE "#"
#define DELIM " "
#define LINE_SIZE 1024
#define ARGC_SIZE 32
#define EXIT_CODE 44
int lastcode = 0;    
int quit = 0;

char commandline[LINE_SIZE];

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

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

const char *getpwd()
{
    return getenv("PWD");
}

void interact(char *cline, int size)
{
    printf(LEFT"%s@%s %s"RIGHT""LABLE" ",getusername(),gethostname(),getpwd());
    //scanf("%s",commandline); //scanf是以空格结束的 输入的时候以空格作为分隔符,只会读入一个,因此不能用scanf
    char *s = fgets(cline, size, stdin);
    assert(s);
    (void)s;//会面不会用s 避免警告提示
    //"abcd\n\0" 输入字符串之后会输入回车  那么我们需要将\n置0 它的下标是strlen(commandline)-1
    cline[strlen(cline)-1] = '\0';

int spliststring(char cline[], char *_argv[])//输出型参数
{
    int i = 0;
    argv[i++] = strtok(cline, DELIM);
    while(_argv[i++] = strtok(NULL,DELIM)); //strtok一次切割一个字符串 所以需要循环调用
    return i - 1; //i 是命令行参数个数  因为最后是NULL,所以需要-1 
}
void NormalExcute(char *_argv[])
{
      pid_t id = fork();
        if(id < 0)
        {
            perror("fork");
            return;
        }
        else if(id == 0)
        {
            //让子进程执行命令
            execvpe(_argv[0],_argv,environ);
            exit(EXIT_CODE);//程序替换有返回值必定出错 如果出错了就以这样的方式退出
        }
        else
        {

            int status = 0;
            pid_t rid = waitpid(id, &status, 0);
            if(rid == id) //等待成功
            {
                lastcode = WEXITSTATUS(status);
            }
        }
}
int bulidCommand(argv,argc)
{
      if(argc == 2 && strcmp(argv[0],"cd") == 0)
      {
      chdir(argv[1]); //更改当前程序的路径
      getpwd();
      sprintf(getenv("PWD"),"PWD=%s",argv[1]);//格式化到环境变量里
      return 1;
      }
      return 0;
}   
int main()
{
    extern char **environ;
    char commandline[LINE_SIZE];
    char *argv[ARGC_SIZE];
    while(!quit) //shell本质上是一个死循环
    {
        //2、交互问题,获取命令行
        interact(commandline ,sizeof(commandline));

        //printf("echo : %s\n", commandline);
        //commandline -> "ls -a -l -n\0" -> "ls" "-a" "-l" "-n"将一个大字符串切割成四个小字符串,那么小字符串的地址保存在哪里???

        //3、子串分割问题,解析命令行
        int argc = spliststring(commandline, argv);
        if(argc == 0) countinue; //切割之后发现是空串 继续输入


        //4、指令的判断 如果是内键命令就让程序自己执行,不需要子进程执行
        
            //内键命令本质就是shell内部的一个函数
       int n = bulidCommand(argv,argc);
        //debug
        //for(int i = 0;argv[i]; i++)printf("[%d]: %s\n", i, argv[i]);
        //5、普通命令的执行
        if(!n) NormalExcute(argv);//传入刚刚解析的argv(也就是需要执行的命令)
    }
    return 0;
}
1. 获取命令行
2. 解析命令行
3. 建立一个子进程( fork
4. 替换子进程( execvp
5. 父进程等待子进程退出( wait
上一篇:【数据结构】二叉树


下一篇:java方法重载