Linux——进程控制

发布于:2022-12-06 ⋅ 阅读:(239) ⋅ 点赞:(0)

进程创建

makefile

myproc:myproc.c
     gcc -o $@ $^
   .PHONY:clean
   clean:                                                                           
     rm -f myproc

myproc.c

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

int main()
{
    printf("我是父进程\n");
    
    pid_t id = fork();
    if(id < 0)
    {
        printf("创建子进程失败\n");
        return 1;    
    }
    else if(id == 0)
    {
        //子进程
        while(1)
        {
            printf("我是子进程: pid: %d, ppid: %d\n",getpid(),gitppid());
            sleep(1);                    
        }    
    }
    else{
        //父进程
        while(1)
        {
            printf("我是父进程:pid: %d, ppid: %d\n",getpid(),getppid());
            sleep();        
        }    
    }
    return 0;
}

在这里插入图片描述

可以看到父子关系成立

 

fork

面试题:请你描述一下,fork创建子进程,OS都做了什么?
fork创建子进程,就是系统里多了一个进程
进程 = 内核数据结构(OS) + 进程代码和数据(一般从磁盘中来也就是c、c++程序加载之后的结果)

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

  • 分配新的内存块和内核数据结构给子进程
  • 将父进程部分数据结构内容拷贝至子进程
  • 添加子进程到系统进程列表当中
  • fork返回(return),开始调度器调度

创建子进程,给子进程分配对应的内核结构必须是子进程独有的。
因为进程具有独立性,理论上子进程也要有自己的代码和数据
可是一般而言,我们没有加载的过程,也就是说,子进程没有自己的代码和数据。
所以子进程只能使用父进程的代码和数据

代码:只读不可写,所以父子共享没有问题
数据:可能被修改,必须分离
对于数据而言:
创建进程的时候,直接拷贝分离。
问题:可能会拷贝,子进程根本不会用到的数据空间,即使以后用到了,也只是读取。

为了节省空间,创建子进程时不需要将不会被访问的或者只会被读取的数据拷贝一份过来
必须拷贝的数据:将来会被父或子进程写入的数据

 

写时拷贝

父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本
本质是延迟申请

一般而言OS无法提前知道哪些空间可能会被写入
即使提前拷贝了,也不一定会立马使用(也会造成浪费)
OS选择写时拷贝技术,对父子进程进行数据分离
用的时候再分配,高效使用内存
这就是为什么OS选择写时拷贝技术的原因

因为写时拷贝这种技术的存在,所以父子进程得以彻底分开,就完成了进程独立性的技术保证(优点)

fork之后,父子进程代码共享(是after共享还是所有共享?)
所有共享

我们的代码汇编之后会有很多行代码,而且每行代码加载到内存之后都有对应的地址
因为进程随时肯被中断(可能并没有执行完),下次回来还必须从之前的位置继续运行(不是最开始),就必须要求CPU随时记录当前进程的执行位置(CPU内有对应寄存器数据用来记录当前进程的执行位置)

pc指针:程序计数器
记录当前正在执行代码的下一行代码的地址
进程的上下文数据
寄存器在CPU内只有一份,但是寄存器内有很多数据

fork常规用法

  • 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
  • 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。

fork调用失败的原因

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

 
 

进程终止

1. 进程终止时,操作系统做了什么?
释放进程申请的相关内核数据结构和对应的数据及代码
本质就是释放系统资源

2. 进程终止的常见方式?

  • 代码跑完,结果正确
  • 代码跑完结果不正确
  • 代码没有跑完,程序奔溃了(涉及一点信号部分
    程序奔溃的时候,退出码无意义。
    一般而言退出码对应的return语句没有被执行

前两个涉及了main函数的返回值
myproc.c

  #include<stdio.h>  
    
  int main()  
  {  
    printf("hello world\n");
  
    return 0;                                                                      
  }   

main函数返回的意义是什么?
返回值叫进程的退出码,表示结果是否正确
意义:返回给上一级进程,用来评判该进程执行结果的,可以忽略
使用命令echo $? 可以获取最近一次进程执行完毕的退出码
如果程序运行结束之后结果不正确,方便根据返回值定位错误的原因细节

return 0 的含义是什么?
返回0表示正确,返回非0表示非正确
非0值有无数个,不同的非0值就可以标识不同的错误原因

为什么总是0?
并不是总是0,可以是其他值

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

int main()
{
    for(int number = 0;number < 150;number++)
    {
        printf("%d: %s\n",number,strerror(number));    
    }
}

在这里插入图片描述

还有很多种错误:0~133
我们可以使用这些退出码的含义,但是如果想直接定义,也可以自己设计一套退出方案

最好不要不要随意设置,需要根据具体情况设置

 

3. 用代码如何终止一个进程?

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

int sum(int top)
{
int s = 0;
for(int i = 1; i < top; i++)
{
s += i;
}
return s;

}
int main()
{
printf("hello world1\n");
printf("hello world1\n");
printf("hello world1\n");
int res = sum(10);
return 11;

printf("hello world2\n");
printf("hello world2\n");
printf("hello world2\n");
printf("hello world2\n");

}

退出码是11

正确的终止:
main函数内return语句就是终止进程的,return后面的数字就是进程对应的退出码
(其他函数内的return是函数返回,只有main函数内的return才是终止进程的)

exit

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

int sum(int top)
{
int s = 0;
for(int i = 1; i < top; i++)
{
s += i;
}
return s;

}
int main()
{
printf("hello world1\n");
printf("hello world1\n");
printf("hello world1\n");
int res = sum(10);
exit(111);

printf("hello world2\n");
printf("hello world2\n");
printf("hello world2\n");
printf("hello world2\n");

}

退出码是111
exit达到了和return一样的效果
 

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

int sum(int top)
{
int s = 0;
for(int i = 1; i < top; i++)
{
s += i;
}
exit(222);

}
int main()
{
printf("hello world1\n");
printf("hello world1\n");
printf("hello world1\n");
int res = sum(10);
exit(111);

printf("hello world2\n");
printf("hello world2\n");
printf("hello world2\n");
printf("hello world2\n");

}

退出码是222
总结:exit在任何地方调用,都表示直接终止进程
 
 

_exit

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

int sum(int top)
{
int s = 0;
for(int i = 1; i < top; i++)
{
s += i;
}
_exit(222);

}
int main()
{
printf("hello world1\n");
printf("hello world1\n");
printf("hello world1\n");
int res = sum(10);
_exit(111);

printf("hello world2\n");
printf("hello world2\n");
printf("hello world2\n");
printf("hello world2\n");

}

退出码依旧是222

 

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

int main()
{
    printf("你可以看见我吗?\n")
    sleep(3);
    exit(13);
}
先执行printf还是sleep?
先出现文字,3s之后退出

int main()
{
    printf("你可以看见我吗?")
    sleep(3);
    exit(13);
}
去掉\n
空白,3s之后出现文字+退出

int main()
{
    printf("你可以看见我吗?")
    sleep(3);
    _exit(13);
}

改成_exit
不会打印出文字

说明:_exit是一个系统调用

推荐使用exit

层状结果
库函数 vs 系统接口
库函数在上(语言,应用)exit
系统接口在下(操作系统)_exit

printf不写\n时,数据是保留在缓冲区的。

  1. 那么缓冲区在哪里?
    一定不在OS内部(如果在,那么刚刚实验的_exit也能刷新出数据)
  2. 谁维护?
    C标准库来维护的

 
 

进程等待

进程等待必要性

  1. 子进程退出,父进程不管子进程,子进程就要处于僵尸状态——导致内存泄漏
  2. 父进程创建子进程是要让子进程工作的,那么子进程把任务完成如何,父进程需要关心吗?如果需要如何得知?如果不需要该怎么处理?
  • 代码跑完,结果正确
  • 代码跑完结果不正确
  • 代码没有跑完,程序奔溃了(涉及一点信号部分

这些就需要进程等待来完成回收僵尸进程
父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

 

wait

基本验证回收僵尸进程的问题

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


int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork");
        exit(1); //标识进程运行完毕,结果不正确
    }
    else if(id == 0)
    {
        //子进程
        int cnt = 5;
        while(cnt)
        {
            printf("cnt: %d, 我是子进程, pid: %d, ppid : %d\n", cnt, getpid(), getppid());
            sleep(1);
            cnt--;
            
        }
    
        exit(0);
    }
    else
    {
        //父进程

        while(1)
        {
            printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid());
            sleep(1);
        }
    }
}
#include<sys/types.h>
#include<sys/wait.h>



int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork");
        exit(1); //标识进程运行完毕,结果不正确
    }
    else if(id == 0)
    {
        //子进程
        int cnt = 5;
        while(cnt)
        {
            printf("cnt: %d, 我是子进程, pid: %d, ppid : %d\n", cnt, getpid(), getppid());
            sleep(1);
            cnt--;
            
        }
    
        exit(0);
    }
    else
    {
        //父进程


printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid());

pid_t ret = wait(NULL); //阻塞式的等待!

if (ret > 0)
{
// 0x7F -> 0000.000 111 1111
printf("等待子进程成功, ret: %d\n",ret);
}
//基本没有意义的,只是为了对比演示
//while(1)
//{
//    printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid());
//    sleep(1);
//}
    }
}

 
 

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相等的子进程。
pid_t ret  = waitpid(id,NULL,0);//阻塞等待

 

获取子进程的status

status:输出型参数
status并不是按照整数来整体使用的,是按照比特位,将32个比特位进行划分(只学习16位)

次低8位表示子进程退出的退出码
(status >> 8)&0xFF

程序异常退出或者奔溃,本质是OS杀掉了进程
OS如何杀掉呢

本质是通过信号发送的方式
每个信号都有编号(31)
kill -l 查看

在这里插入图片描述

最低7个比特位表示进程收到的信号
status & 0x7F

野指针,溢出
程序异常,不光光是代码有问题,也可能是外力杀掉的kill -9 id

int code = 0;

int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork");
        exit(1); //标识进程运行完毕,结果不正确
    }
    else if(id == 0)
    {
        //子进程
        int cnt = 5;
        while(cnt)
        {
            printf("cnt: %d, 我是子进程, pid: %d, ppid : %d\n", cnt, getpid(), getppid());
            sleep(1);
            //cnt--;
            //int * p = NULL;
            //*p = 100;
           // int a = 10;
           // a /= 0;
        }
        code = 15;
        exit(15);
    }
    else
    {
        //父进程
        printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid());
        //sleep(7);
        //pid_t ret = wait(NULL); //阻塞式的等待!
        int status = 0; 
        pid_t ret = waitpid(id, &status, 0); //阻塞式的等待!
        if(ret > 0)
        {
            // 0x7F -> 0000.000 111 1111
            printf("等待子进程成功, ret: %d, 子进程收到的信号编号: %d,子进程退出码: %d\n",\
                    ret, status & 0x7F ,(status >> 8)&0xFF); //0xff --> 0000...000 1111 1111
            printf("code: %d\n", code);
        }
       
    }
}

 
 

  1. 父进程通过wait/waitpid 可以拿到子进程的退出结果,为什么要用这两个函数呢?直接code(15)全局变量不行吗?
    不可以,进程具有独立性,数据会发生写时拷贝,父进程无法拿到,信号也无法接受到
  1. 既然进程具有独立性,进程退出码不也是子进程的数据吗?父进程又凭什么拿到呢?wait/waitpid究竟干了什么呢?
    僵尸进程:至少会保留该进程的PCB的信息。task_struct里面保留了任何进程退出时的退出结果信息
    wait/waitpid本质就是读取了子进程的task_struct信息
    int exit_code,exit_signal;
  1. wait/waitpid有这个权利吗?
    task_struct是内核数据结构对象,这两个函数是系统调用,就是操作系统,是有权利的
    父进程调用接口让OS去拿退出结果

 

  1 #include <stdio.h>                                                               
  2 #include <unistd.h>
  3 #include <stdlib.h>
  4 #include <sys/wait.h>
  5 
  6 int main()
  7 {
  8   pid_t id = fork();
  9   if(id == 0)
 10   {
 11     //子进程
 12     int cnt = 5;
 13     while(cnt)
 14     {
 15       printf("我是子进程:%d\n",cnt--);
 16       sleep(1);
 17     }
 18     exit(11);//测试
 19   }
 20   else{
 21     //父进程
 22     int status = 0;
 23     //只有子进程退出的时候,父进程才会waitpid函数进行返回。【父进程依旧还活着】
 24     //waitpid/wait 可以在目前的情况下,让进程退出具有一定的顺序性
 25     //将来可以让父进程进行更多的收尾工作
 26     pid_t result = waitpid(id,&status,0);//默认是在阻塞状态去等待子进程状态变化(    退出)
 27     if(result >0)
    {
 29       printf("父进程等待成功,退出码:%d, 退出信号:%d\n",(status>>8)&0xFF,status    &0x7F);
 30     }
 31                                                                                  
 32   }
 33 }

在这里插入图片描述

id > 0 等待知道进程
id == 0 TODO(
id == -1 等待任意一个子进程退出==wait();

上面的检测太麻烦了,有另外一种方法:

WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

if(result >0)
    {
    //  printf("父进程等待成功,退出码:%d, 退出信号:%d\n",(status>>8)&0xFF,status    &0x7F);
     if(WIFEXITED(status))
     {
         //子进程是正常退出的
         printf("子进程执行完毕,子进程的退出码:%d\n",WEXITSTATUS(status));
         else{
             printf("子进程异常退出:%d\n",WIFEXITED(status));         
         }     
     }
     }

 
 

options:

默认为0,表示阻塞等待

WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID
WNOHANG选项代表父进程非阻塞等待
系统提供的大写标记位其实就是

阻塞和非阻塞

阻塞等待:
一般都是在内核中阻塞,等待被唤醒
阻塞伴随着被切换

非阻塞等待:
如果父进程检测了子进程的退出状态发现子进程没有退出。父进程通过调用waitpid进程等待,用waitpid这个系统调用立马返回
还有基于非阻塞调用的轮询检查

waitpid 是父进程调用的

waitpid(chlid_id, status, flag)
{
    //下面是内核中waitpid的实现,属于操作系统的
    //检测子进程的退出状态,就是去检查task_struct中子进程的运行信息
    if(status == 退出){
        return child_pid;   
        status |= chlid->sig_number;
        status |= ((chlid->exit_code)<8); 
    }
    else if(status == 没退出){
        if(flag = 0)//挂起父进程
        //father_pcb->等待队列中
        //进程阻塞本质是进程阻塞在系统函数的内部中
        //后面的代码不会再执行
        //当条件满足时,父进程被唤醒,是从哪里唤醒?
        //waitpid重新调用,还是从if的后面
       
        else if(flag == WNOHANG)
        //不阻塞进程
         
          
            return 0;    
    }
    else{
        //出错了等其他原因
        return -1;    
    }
}

 

将阻塞代码修改为非阻塞:
makefile

myproc:myproc.cc
    gcc++ -o $@ $^
.PHONY:clean
clean:
    rm -rf myproc    
mv myproc.c myproc.cc

 

myproc.cc

#include <iostream>
#include <vector>

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

std::vector<handler_t> handlers; //函数指针数组

void fun_one()
{
    printf("这是一个临时任务1\n");
}
void fun_two()
{
    printf("这是一个临时任务2\n");
}



// 设置对应的方法回调
// 以后想让父进程闲了执行任何方法的时候,只要向Load里面注册,就可以让父进程执行对应的方法喽!
void Load()
{
    handlers.push_back(fun_one);
    handlers.push_back(fun_two);
}

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        // 子进程
        int cnt =  5;
        while(cnt)
        {
            printf("我是子进程: %d\n", cnt--);
            sleep(1);
        }

        exit(11); // 11 仅仅用来测试
    }
    else
    {
        int quit = 0;
        while(!quit)
        {
            int status = 0;
            pid_t res = waitpid(-1, &status, WNOHANG); //以非阻塞方式等待
            if(res > 0)
            {
                //等待成功 && 子进程退出
                printf("等待子进程退出成功, 退出码: %d\n", WEXITSTATUS(status));
                quit = 1;
            }
            else if( res == 0 )
            {
                //等待成功 && 但子进程并未退出
                printf("子进程还在运行中,暂时还没有退出,父进程可以在等一等, 处理一下其他事情??\n");
                if(handlers.empty()) Load();
                for(auto iter : handlers)
                {
                    //执行处理其他任务
                    iter();
                }
            }
            else
            {
                //等待失败
                printf("wait失败!\n");
                quit = 1;
            }
            sleep(1);
        }





         父进程
        //int status = 0;
         只有子进程退出的时候,父进程才会waitpid函数,进行返回!![父进程依旧还活着呢!!]
         waitpid/wait 可以在目前的情况下,让进程退出具有一定的顺序性!
         将来可以让父进程进行更多的收尾工作.
         id > 0 : 等待指定进程
         id == -1: 等待任意一个子进程退出, 等价于wait();
         options: 默认为0,代表阻塞等待, WNOHANG: 叫做非阻塞等待
        //pid_t result = waitpid(id, &status, 0); // 默认是在阻塞状态去等待子进程状态变化(就是退出!)
        //if(result > 0)
        //{
        //    // 可以不这么检测
        //    //printf("父进程等待成功, 退出码: %d, 退出信号: %d\n", (status>>8)&0xFF, status & 0x7F);
        //    if(WIFEXITED(status))
        //    {
        //        //子进程是正常退出的
        //        printf("子进程执行完毕,子进程的退出码: %d\n", WEXITSTATUS(status));
        //    }
        //    else{
        //        printf("子进程异常退出: %d\n", WIFEXITED(status));
        //    }
        //}
    }
}

 
 

进程的程序替换

概念:

fork()之后,父子各自执行父进程代码的一部分。但是如果子进程就行执行一个全新的程序
父子代码共享,数据写时拷贝各自一份。但子进程想拥有自己的代码
为了满足以上情况,就需要进程的程序替换来完成

程序替换:
通过特定的接口,加载磁盘上的一个权限的程序(代码和数据),加载到调用进程的地址空间中
让子进程执行其他程序

 

原理

用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。
 
当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。
 
调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
 
将磁盘上新的程序加载到内存并和当前进程的页表重新建立映射
进程替换,没有创建新的子进程

如何理解将程序放入内存中?
加载
所谓的exec*函数,本质就是如何加载程序的函数

 
 

基础演示

execl

可看做list

int execl(const char*path, const char *arg,...);

const char*path找到路径:path:路径+目标文件名
…是可变参数列表->可以传入多个不定个数参数《C深度解刨》
我们在命令行上怎么执行的,就在这里怎么一个一个填参数。
最后一个参数必须是NULL,标志参数传递完毕。

可以先用which查命令的路径
myproc.cc

#include <unistd.h>

int main()
{
    printf("当前进程的开始代码!\n");

    //execl("/usr/bin/ls", "ls", NULL);
    execl("/usr/bin/ls", "ls", "--color=auto","-l", NULL);
    exit(1);
    //execl("/usr/bin/ls", "ls", "-l", "-a", "-i", NULL);
    //execl("/usr/bin/ls", "ls", "-l", "-a", "-i", NULL);
    //execl("/usr/bin/top", "top", NULL);

    printf("当前进程的结束代码!\n");
    return 0;
}

为什么结果不会打印第二个printf中的内容?
因为execl是程序替换,调用该函数成功之后,会将当前进程的所有代码和数据都进行替换。包括已经执行和没有执行的
所以一旦调用成功,后续的代码全都不会执行

execl为什么调用成功,没有返回值呢?
不需要进行函数返回值判定

exec.c

  1#include <stdio.h>
  2 #include <unistd.h>
  3 #include <stdlib.h>
  4 #include <sys/wait.h>
  5 
  6 int main()
  7 {
  8   pid_t id = fork();
  9   if(id == 0)
 10   {
 11     //子进程
 12     //ls -a -l
 13     printf("子进程开始运行,pid:%d\n",getpid());
 14     execl("/usr/bin/ls","ls","-a","-l",NULL);                                    
 15     exit(1);
 16   }
 17   else{
 18     //父进程
  printf("父进程开始运行,pid:%d\n",getpid());
 21     int status = 0;
 22     pid_t id = waitpid(-1,&status,0);//阻塞等待,一定是子进程先运行完毕然后父进程    获取之后才退出!
 23     if(id > 0)
 24     {
 25       printf("wait success,exit code:%d\n",WEXITSTATUS(status));
 26     }
 27   }
 28 
 29   return 0;
 30 }

为什么要创建子进程呢?
为了不影响父进程
要让父进程聚焦在读取数据,解析数据,指派进程执行代码的功能
如果不创建,我们替换的进程只能是父进程,如果创建了,替换的就是子进程,不会影响父进程

加载新程序之前,父子的数据与代码的关系?
代码共享,数据写时拷贝
当子进程加载新程序的时候,不就是一种写入吗
代码要不要写时拷贝(将父子的代码分离)?
必须分离->要写时拷贝
此时父子进程在代码和数据上就彻底分开了,虽然曾经两者并不冲突

 
 

execv

可看做vector

int execv (const char *path, char *const argv[]);
 12 if(id == 0)
 13   {
 14     //子进程
 15     //ls -a -l
 16     printf("子进程开始运行,pid:%d\n",getpid());
 17     //execl("/usr/bin/ls","ls","-a","-l",NULL);
 18     sleep(3);
 19     char *const _argv[NUM] = {
 20       (char*) "ls",
 21       (char*)"-a",
 22       (char*)"-l",
 23       NULL
 24     };
 25     execv("/usr/bin/ls",_argv);                                                  
 26     exit(1);
 27   }

execv与execl没有区别,只是传参方式的不同
在这里插入图片描述

 

execlp

int execlp(const char *file, const char *arg,...);

要执行程序,必须先找到程序:带路径
不带路径可以找到吗?
p:代表会自己在环境变量PATH中查找,不用写环境路径

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

两个ls一样吗?
第一个:你要执行谁 找到程序
第二个:你想怎么执行 程序能找到,你要传递什么选项

 

execvp

int execlp(const char *file, char  *const argv[]);

char* 指针数组

char *const _argv[NUM] = {
       (char*) "ls",
        (char*)"-a",
        (char*)"-l",
        NULL
      };
  execvp("ls", _argv);

以上四个接口没什么问题

如何执行其他我自己写的c、c++二进制程序
如何执行其他语言的程序
例如:
再写一个文件
想用一个可执行程序去调用另一个可执行程序
以下图片中,是两个文件,但只有一个可执行文件

在这里插入图片描述
Makefile

  1.PHONY:all
  2 all:exec mycmd
  3                                                                                  
  4 exec:exec.c   
  5   gcc -o $@ $^
  6 mycmd:mycmd.c 
  7   gcc -o $@ $^
  8 .PHONY:clean
  9 clean:            
 10   rm -f exec mycmd 

形成了两个可执行程序
在这里插入图片描述
mycmd.c

  1#include <stdio.h>
  2 #include <string.h>
  3 #include <stdlib.h>                                                              
  4 
  5 // ./mycmd -a/-b/-c...
  6 int main(int argc, char *argv[])
  7 {
  8     if(argc != 2)
  9     {
 10         printf("can not execute!\n");
 11         exit(1);
 12     }
 13 
 14     //MY_105_VAL: 目前并不存在!
 15     printf("获取环境变量: MY_105_VAL: %s\n", getenv("MY_105_VAL"));
 16 
 17     if(strcmp(argv[1], "-a") == 0)
 18     {
 19         printf("hello a!\n");
   }
 21     else if(strcmp(argv[1], "-b") == 0)
 22     {
 23         printf("hello b!\n");
 24     }
 25     else{
 26         printf("default!\n");
 27     }                                                                            
 28 
 29     return 0;
 30 }

在这里插入图片描述
exec*:其功能就是加载器的底层接口

 
 

execle

int execle(const char *path, const char *arg, ..., char * const envp[]);
char *const _env[NUM] = {
    (char *) "MY_105_VAL=55667788",
       NULL 
      }; 

execle(myfile, "mycmd","-a",NULL,_env);

 
环境变量具有全局属性,可以被子进程继承下去的

int main(int argc, char*argv[], char *env[])
   {
       .....
       //execle(myfile, "mycmd","-a",NULL,_env);
       execle(myfile, "mycmd","-a",NULL,env);
       .....
       }

 
 
系统提供的基本封装:
满足不同的调用场景

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

 
 

简易shell(命令行显示器):

cp …/Makefile . 拷贝上级目录中的Makefile
用所有的名义shell去替换exec
底行模式 :%s/exec/myshell/g
Makefile

myproc:myproc.cc
	gcc++ -o $@ $^
.PHONY:clean
clean:
	rm -rf myproc

 
myshell.c

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

#define NUM 1024
#define SIZE 32
#define SEP " "

//保存完整的命令行字符串
char cmd_line[NUM];
//保存打散之后的命令行字符串
char *g_argv[SIZE];

// shell 运行原理 : 通过让子进程执行命令,父进程等待&&解析命令
int main()
{
    //0. 命令行解释器,一定是一个常驻内存的进程,不退出
    while(1)
    {
        //1. 打印出提示信息 [whb@localhost myshell]# 
        printf("[root@localhost myshell]# ");
        fflush(stdout);
        memset(cmd_line, '\0', sizeof cmd_line);
        //2. 获取用户的键盘输入[输入的是各种指令和选项: "ls -a -l -i"]
        if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL)
        {
            continue;
        }
        cmd_line[strlen(cmd_line)-1] = '\0';
        //"ls -a -l -i\n\0"
        //printf("echo: %s\n", cmd_line);
        //3. 命令行字符串解析:"ls -a -l -i" -> "ls" "-a" "-i"
      //把一个字符串打散成为多个子串
          g_argv[0] = strtok(cmd_line, SEP); //第一次调用,要传入原始字符串
        int index = 1;
        if(strcmp(g_argv[0], "ls") == 0)
        {
            g_argv[index++] = "--color=auto";
        }
        if(strcmp(g_argv[0], "ll") == 0)
        {
            g_argv[0] = "ls";
            g_argv[index++] = "-l";
            g_argv[index++] = "--color=auto";
        }
        //?
        while(g_argv[index++] = strtok(NULL, SEP)); //第二次,如果还要解析原始字符串,传入NULL

        //for debug
        //for(index = 0; g_argv[index]; index++)
        //    printf("g_argv[%d]: %s\n", index, g_argv[index]);
        //4. TODO,内置命令, 让父进程(shell)自己执行的命令,我们叫做内置命令,内建命令
        //内建命令本质其实就是shell中的一个函数调用
        if(strcmp(g_argv[0], "cd") == 0) //not child execute, father execute
        {
            if(g_argv[1] != NULL) chdir(g_argv[1]); //cd path, cd ..

            continue;
        }
        //5. fork()
        pid_t id = fork();
        if(id == 0) //child
        {
            printf("下面功能让子进程进行的\n");
            //cd cmd , current child path
            execvp(g_argv[0], g_argv); // ls -a -l -i
            exit(1);
        }
        //father
        int status = 0;
        pid_t ret = waitpid(id, &status, 0);
        if(ret > 0) printf("exit code: %d\n", WEXITSTATUS(status));
    }
}

 

为什么要替换?
一定和场景应用有关,我们有时候必须让子进程执行新的程序

 

shell执行的命令,通常有两种
1.第三方提供的对应在磁盘中有具体的二进制文件的可执行程序(由子进程执行
 
2.shell内部,有自己实现的方法,由自己(父进程)来进行执行 有些命令就是要影响shell本身的cd,export shell代表的是用户

 

shell的环境变量是从哪里来的呢?
环境变量,是写在卑职文件中的,shell启动时候,通过读取配置文件获得的起始环境变量


网站公告

今日签到

点亮在社区的每一天
去签到