【Linux】第四篇:进程控制

本文深入探讨了Linux中的进程管理,包括程序地址空间、父子进程的地址空间共享、虚拟地址空间、写时拷贝技术、fork函数、进程创建与退出、进程等待、进程替换以及简易shell的实现。内容涵盖了进程的生命周期、内存管理机制以及如何在程序中安全地执行其他程序。
摘要由CSDN通过智能技术生成


在这里插入图片描述


1.程序地址空间

程序是由若干个逻辑分段组成的,如可由代码分段、数据分段、栈段、堆段组成。 程序中内容的存放位置的布局大致如下图所示:

在32位环境下,物理内存=内核+用户空间=4GB

代码验证:

#include <stdio.h>
#include <stdlib.h>
int uinit_g_val;
int init_g_val=1;

int main(int argc,char* argv[],char* env[])
{

    int i=0;
    for(;argv[i];++i)
    {
        printf("argv address :%p\n",argv[i]);
    }

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


    int a=10;//栈空间
    int b=20;//栈向下
    printf("stack &a:%p\n",&a);
    printf("stack &b:%p\n",&b);


    int* c=(int*)malloc(4);
    int* d=(int*)malloc(4);//堆空间向上生长
    printf("heap &c:%p\n",&c); 
    printf("heap &d:%p\n",&d);

  
    printf("uninitialized_global_value &uinit_g_val:%p\n",&uinit_g_val); 
    printf("initialized_global_value &init_g_val:%p\n",&init_g_val);

    const char* s="hello";


    printf("string_readonly s:%p\n",s); 

    printf("code address %p\n",main);

    return ;
}

测试父子进程的地址空间

在上篇中提到,子进程会与父进程共用代码,并且子进程会写时拷贝以往的进程数据。

数据在发生修改时会进行写时拷贝,于是拷贝的数据与原数据一定是存储在不同的物理内存中才对。

是否真的如此呢?我们通过下方代码来一睹究竟:

//address.c
#include <stdio.h>
#include <unistd.h>

int g_val=10;
int main()
{
    pid_t childpid=fork();

    if(childpid==0)//子进程
    {
  
        printf("i'm a child process ,the original g_val:%d ,the original address of g_val : %p\n",g_val,&g_val);
        g_val=20;
        printf("change g_val\n");
        printf("i'm a child process ,the changed g_val:%d ,the changed address of g_val : %p\n",g_val,&g_val);
        sleep(1);
    }
    else//父进程
    {
        printf("i'm a parent process , g_val:%d , address of g_val : %p\n",g_val,&g_val);
        sleep(1);
    }
}

这很奇怪,子进程在修改g_val前,这个全局变量应该是和父进程共用的,地址一样可以理解,但是后来子进程改变了g_val,应该触发写时拷贝,这样才能保证进程间独立性。可是为何子进程的g_val仍然在原来的地址呢?

可见系统给我们看到的地址并非真正的物理内存地址

所以,这里使用的地址是虚拟的进程地址空间

虚拟进程地址空间

虚拟地址的由来

在早期,程序指令访问的内存地址就是物理内存地址

但是随着进程数量的增多,物理内存的有限空间就逐步被瓜分,这会造成:

  1. 进程由于可直接访问物理内存,那么进程间无法有效做到隔离,会给恶意进程随便访问修改其他进程数据留有可乘之机,或是非恶意进程的bug程序也可能不小心修改了其他程序的内存数据,导致其他程序出现异常。用户是无法容忍这种情况发生的。
  2. 如果内存不足就要将先前程序的数据拷贝到硬盘中,然后将新进程载入内存,大量数据的导入导出是的内存的使用效率低下
  3. 程序运行的地址不确定。由于进程的物理内存空间是随机分配的,但是我们的某些硬件是需要在固定的地址上去开始运行的,但是如果这个地址后边被我们的程序占有,那么我们对这块内存的修改,就可能导致某些硬件不可用了。

于是我们必须保证进程间的独立性,在其中一个进程异常时,不能影响到其他任务。

关键就在于进程使用了绝对物理地址,而这正是需要极力避免的!

进程资源互不干扰
资源用时再给
统一视角看待进程,cpu固定访问

于是操作系统为每一个进程都分配了 虚拟地址空间来保护物理内存和进程数据的安全,进程是不理会虚拟地址如何连接物理地址的,进程只会在自己专属虚拟地址空间中安排自己的代码和数据的位置,而且在进程看来虚拟空间的大小与物理内存大小是一样的。

虚拟地址空间有了,意味着我们给进程的“大饼”画好了,可是真正的资源还在物理内存那里,尚未分配过来。

虚拟系统又是如何与物理内存连接的呢?——MMU(Memory Management Unit),内存管理单元,是CPU架构中的一部分。

MMU主要包含以下功能:

  • 虚实地址翻译

在用户访问内存时,将用户访问的虚拟地址翻译为实际的物理地址,以便CPU对实际的物理地址进行访问。

  • 访问权限控制

可以对一些虚拟地址进行访问权限控制,以便于对用户程序的访问权限和范围进行管理,如代码段一般设置为只读,如果有用户程序对代码段进行写操作,系统会触发异常。

  • 引申的物理内存管理

对系统的物理内存资源进行管理,为用户程序提供物理内存的申请、释放等操作接口。

CPU执行单元发出的内存地址将被MMU截获,从CPU到MMU的地址称为虚拟地址,而MMU将这个地址翻译成另一个地址发到CPU芯片的外部地址引脚上,也就是将虚拟地址映射成物理地址。

经过MMU转换之后的外地址总线则不一定是32位的。也就是说,虚拟地址空间和物理地址空间是独立的,32位处理器的虚拟地址空间是4GB,而物理地址空间既可以大于也可以小于4GB

关于虚拟内存的两种实现方法:内存分段与内存分页。可以参考博主小林coding的博客:20 张图揭开「内存管理」的迷雾,瞬间豁然开朗,写的很详细。

mm_struct

虚拟地址空间的书写实际上是进程中的一个数据结构体来负责——mm_struct

Linux中的mm_struct(内存描写符)来实现进程的内存管理。

mm_struct 描写一个进程的全部虚拟空间,而在进程的task_struct(PCB)结构体中包括1个指向mm_struct结构的指针。

1个进程的虚拟空间中可能有多个虚拟区间,而每个虚拟区间是由一个 vm_area_struct结构来描述的。

一个简化了的mm_struct:

vm_area_struct 结构含有指向 vm_operations_struct结构的1个指针,vm_operations_struct描写了在这个区间的操作。

对这些虚拟空间的组织方式有两种

  1. 当虚拟区较少时采取单链表,由mmap指针指向这个链表
  2. 当虚拟区间多时采取红黑树进行管理,由mm_rb指向这棵树。

由于程序中用到的地址常常具有局部性,因此,最近1次用到的虚拟区间极可能下次还要用到,因此把最近用到的虚拟区间结构放到高速缓存,这个虚拟区间就由 mmap_cache指向。

虽然每一个进程只有1个虚拟空间,但是这个虚拟空间可以被别的进程来同享。如:子进程同享父进程的地址空间.

页表

当操作系统创建了一个内存,这其中便包括了进程控制块(task_struct),内存描写符(mm_struct)页表。

虚拟地址与物理地址之间通过页表来映射:

一个进程不同的数据段由页表分别映射至物理内存中(未必是连续的),
这样就可以避免外部内存碎片。

如果内存空间不足,操作系统会将长时间未被使用的内存页面暂时写在硬盘上。

更进一步,当程序向内存申请空间时,操作系统不需要立刻申请空间,而是现在页表上的虚拟地址处做下记录,一旦真正需要使用申请的空间时,操作系统发现还未建立映射,此时触发缺页中断,再去申请空间,随后建立虚拟与物理的地址映射。这个思想和“写时拷贝”类似,等到需要修改数据时再去开辟空间。

CPU在运行程序时原本面对的是错综复杂的物理地址环境,通过页表+虚拟地址空间的帮助,cpu便无需费心去寻找程序入口,因为地址映射的过程都交给操作系统来完成。

再回到上述的父子进程为何可以“共用数据地址”的问题便得到了解答。

子进程创建时会拷贝父进程的进程数据(页表,进程控制块)。

所以相同的只是虚拟地址,如果子进程没有改变数据,他将会与父进程共用数据,即相同的物理地址。一旦子进程对数据进行了修改,那么操作系统另行开辟空间进程写时拷贝,虚拟地址将会映射至这块新的内存空间。


2.进程创建

fork函数再探

之前谈过fork()是linux中非常重要的函数,他为已存在的进程创建一个子进程。

#include <unistd.h>
pid_t fork(void);

返回值类型是pid_t,子进程返回0,父进程返回子进程PID,出错返回-1。

子进程的是以父进程为模板创建的。

进程调用fork后,内核执行如下操作:

  1. 为子进程创建进程(为其分配内存块和内核数据结构)
  2. 【将父进程的部分数据结构内容拷贝至子进程
  3. 添加子进程进入就绪态
  4. fork返回值,调度器调度子进程。

我们将程序自fork()开始分为上下两部分

before
fork();
after

当主进程执行至fork()时,系统创建子进程并拿到了父进程的数据,其中的 程序计数器便指引了子进程开始执行程序的位置。那么随后父子进程会从after位置处开始执行程序。

fork的返回值

fork()函数的返回值:

  • 父进程返回子进程的PID
  • 子进程返回0

原因:父进程可以有多个子进程,而一个子进程只能有一个父进程。于是子进程仅需知道PPID便可知道父进程是谁;但是对于父进程来说,子进程是需要被标识的,只有知道了子进程的PID,才能对子进程指派任务。

fork为什么会有两个返回值呢?

fork()在return之前子进程就已创建完毕,fork()的return语句父子进程都要执行。

写时拷贝

父子进程会共享代码和数据(节省系统资源),由于进程间应当具有独立性,当子进程需要对数据进行修改时,会按需拷贝出来再行修改,是为写时拷贝。

fork 的用处

  • 父进程希望子进程复制自己,从而通过判断fork的返回值来使父子进程分别执行同一程序的不同代码段,例如父进程等待客户端请求,生成子进程处理请求。
  • 父进程中通过子进程执行另一个程序,例如子进程从fork返回后调用exec*函数。

fork调用失败

  • 系统中进程过多,超过了进程数的上限。

3.进程终止

进程的退出场景

  • 代码运行完成,结果正确
  • 代码运行完成,结果不正确
  • 代码异常终止

进程常见退出方法

正常终止

  1. 从main函数返回
  2. 调用exit
  3. 调用_exit

异常退出

  • ctrl+c 信号终止

退出码:代表了一个程序的退出状态,使用退出码来判定程序运行正确与否。

0具有唯一辨识性,通常为正常退出的退出码

非0代表退出异常,不同的退出码可以为我们标识出异常退出的各种原因。

strerror函数可以对非零的退出码获取相应的错误信息(包含头文件string.h)。

#include <stdio.h>
#include <string.h>
int main()
{

    int i;
    for(i=1;i<140;++i)
    {
        printf("%d : %s\n",i,strerror(i));
    }

    return 0;
}

  • echo $? 输出最近一次进程退出时的退出码

如果程序异常退出,退出码就没有意义了。

exit 函数

  • man 3 exit

程序执行时,使进程终止,status定义了进程的退出状态,父进程通过wait来获取该状态。

exit一共干了三件事:

  1. 执行用户在 atexiton_exit 上定义的清理函数(按出栈顺序)。
  2. 刷新所有打开的标准io(stdio)流,并关闭
  3. 执行_exit 函数
[sjl@VM-16-6-centos return_value]$ cat mycode.c 
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int func()
{
    printf("hello\n");
    return 1; 
}

int main(int argc,char* argv[],char* env[])
{
    func();
    exit(10);
    int i;
    for(i=1;i<140;++i)
    {
        printf("%d : %s\n",i,strerror(i));
    }

    return 0;
}
[sjl@VM-16-6-centos return_value]$ ./mycode 
hello
[sjl@VM-16-6-centos return_value]$ echo $?
10

_exit 函数

程序运行至该函数亦会终止进程,与exit()的不同之处在于,_exit()不会刷新stdio流。

测试代码:

[sjl@VM-16-6-centos return_value]$ cat mycode.c 
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main(int argc,char* argv[],char* env[])
{
    printf("hello");
    sleep(3);
    _exit(10);
    return 0;
}
[sjl@VM-16-6-centos return_value]$ ./mycode 
[sjl@VM-16-6-centos return_value]$ 

输出流中的"hello"没有得到刷新。

4.进程等待

为何需要等待进程

  • 子进程若已退出,父进程如若不等待而自顾自进行,则会造成子进程成为僵尸进程,从而产生内存泄漏问题。
  • 进程一旦成为僵尸进程,使用 kill -9 也无法结束僵尸进程,因为无法杀死一个已经死亡的进程。
  • 保证时序问题,不让子进程成为孤儿进程:父进程交给子进程的任务需要知道运行的结果如何,父进程通过进程等待的方式,回收子进程的资源并且获取子进程退出信息。

如何进程等待

wait

  • man 2 wait

使用方法

#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
返回值:
成功返回被等待进程pid,失败返回-1。
参数status:
输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

测试代码:

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

int main(int argc,char* argv[])
{

    if(fork()==0)
    {
        //子进程
        int count=5;
        while(count)
        {
            printf("child,PID:%d,PPID:%d\n",getpid(),getppid());
            sleep(1);
            count--;
        }
        //防止子进程继续执行之后的代码,这里直接终止子进程
        exit(0);
    }
    sleep(10);
    //父进程需在此等待子进程的终止
    pid_t ret=wait(NULL);
    if(ret>0)
    {
        printf("child process complete ,PID:%d\n",ret);
    }
    else{
        printf("parent wait failed\n");
    }

    return 0;
}

wait便可以让父进程等待子进程的结束并回收资源。

waitpid

  • man 2 waitpid

返回值:

使用方法

pid_ t waitpid(pid_t pid, int *status, int options);

返回值:

  • 当正常返回的时候waitpid返回收集到的子进程的进程ID;
  • 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
  • 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;

参数:

  • pid:

Pid=-1,等待任一个子进程。与wait等效。
waitpid(-1, &status, 0);
等价于:
wait(&status);
Pid>0.等待其进程ID与pid相等的子进程。

  • status:

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

  • options:

WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进
程的ID。

总结:

如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。

如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。

如果不存在该子进程,则立即出错返回-1。

获取 status

status为输入性参数,由操作系统填充。

  • 如果传递NULL,表示不关心子进程的退出状态信息。
  • 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。

测试代码:

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

int main(int argc,char* argv[],char* env[])
{

    int id=fork();
    if(id==0)
    {
        //子进程
        int count=5;
        while(count)
        {
            printf("child,PID:%d,PPID:%d\n",getpid(),getppid());
            sleep(1);
            count--;
        }
        //防止子进程继续执行之后的代码,这里直接终止子进程
        exit(0);
    }
  
    //父进程需在此等待子进程的终止
    int status=0;
    pid_t ret=waitpid(id,&status,0);//传入status地址
    if(ret>0)
    {
        printf("parent wait success ,PID:%d,status:%d\n",ret,status);
    }
    else{
        printf("parent wait failed\n");
    }
}

显然这里status收到了来自子进程的退出码,

我们让子进程给出退出码:

//子进程程序
{
    ...
    exit(10)
}

结果:

  • status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(现在只研究status低16比特位):

  • (status>>8) & 0xFF 便可得到正常退出的进程的退出码
  • status & 0x7F 可得到导致异常退出的终止信号

我们修改下上面输出status的代码

printf("parent wait success ,PID:%d,status exit code:%d,status exit signal:%d\n",ret,(status>>8)&0xFF,status&0x7F);

我们再测试下信号中断下进程返回的status:

至此也得到了终止信号。

bash是所有命令行启动程序的父进程,bash是以wait的方式得到子进程的退出码,所以可以通过 echo $? 查到子进程的退出码。

但是移位操作始终不太方便,于是系统又分别定义了两个不用进行位操作的宏

  • WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
  • WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
int main(int argc,char* argv[],char* env[])
{

    int id=fork();
    if(id==0)
    {
        //子进程
        int count=5;
        while(count)
        {
            printf("child,PID:%d,PPID:%d\n",getpid(),getppid());
            sleep(1);
            count--;
        }
        //防止子进程继续执行之后的代码,这里直接终止子进程
        exit(10);
    }
  
    printf("wait begin\n");
    //父进程需在此等待子进程的终止
    int status=0;
    pid_t ret=waitpid(id,&status,0);//ret得到子进程PID
    if(ret>0)
    {
        if(WIFEXITED(status))//正常exit
        {
            printf("exit code:%d\n",WEXITSTATUS(status));//拿到exit的退出码
        }
        else//异常退出
        {
            printf("parent wait failed\n");
        }
    }

    return 0;
}

正常退出:

异常退出

option:WNOHANG

waitpid(PID , &status,0)

选项为0的情况下,父进程挂起(转为S态),等待子进程的结束(阻塞等待期间父进程不再被调度),子进程结束后,父进程被调度为就绪态,等待cpu执行。

  • WNOHANG:非阻塞等待选项(wait no hang)

waitpid(PID , &status,WNOHANG)

若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待,基于非阻塞的轮询方案。若正常结束,则返回该子进程的PID。

我们需要的是父进程不阻塞的同时还能保证等到子进程结束并返回退出码——轮询检测

测试代码:

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

int main(int argc,char* argv[],char* env[])
{

    int id=fork();
    if(id==0)
    {
        //子进程
        int count=5;
        while(count)
        {
            printf("child,PID:%d,PPID:%d\n",getpid(),getppid());
            sleep(1);
            count--;
        }
        //防止子进程继续执行之后的代码,这里直接终止子进程
        exit(0);
    }
   

    while(true)
    {
  
         int status=0;
         pid_t ret=waitpid(id,&status,WNOHANG);
  
        if(ret==0)
        {
            //子进程尚未结束,等待是成功的,所以父进程需要重复等待
            printf("do father own stuff\n");
        }
        else if(ret>0)
        {
            //子进程结束,等待成功,ret获得子进程pid,status获取对应的退出状态
            if(WIFEXITED(status))
            {
                printf("exit code:%d\n",WEXITSTATUS(status));
            }
            else
            {
                printf("parent wait failed\n");
            }
            break;
        }
        else
        {
            //ret<0 等待失败
            perror("waitpid error");
            break;
        }
        sleep(1);
    }
    return 0;
}

总结

总结:

wait是什么?

wait是父进程通过wait等系统调用,用来等待子进程状态的一种现象,这就叫做进程等待,

父进程为什么需要以wait的方式去等待子进程

如果父进程不用wait去等待子进程,那么就会导致子进程发生僵尸进程,从僵尸进程进而引发的内存泄漏问题,无法读取子进程的退出信息

为了防止子进程发生僵尸问题我们需要怎么办?

让父进程使用wait/ waitpid等系统调用,以阻塞/非阻塞等待的方式等待子进程

5.进程替换

fork创建的子进程一般会使它具备:

  • 子进程执行父进程的一部分代码(子承父业)
  • 子进程执行和父进程完全不同的代码,也就是程序替换(儿子创业)

之前谈到,创建子进程的目的是使用if else来使其执行父进程代码中的一部分,那么我们如何使子进程执行一个“全新”的代码呢?

接下来的进程替换便是讨论实现此功能。

替换原理

用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种 exec*函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换(写时拷贝),从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变

程序要运行,就必须把磁盘中的代码和数据使用加载器加载到进程的上下文中,其底层便是运用了替换函数。

替换函数

  • exec*:exec 系列函数

执行 man 3 exec 可查看所有的进程替换函数

罗列如下:

#include <unistd.h>

extern char **environ;

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

头文件:#include <unistd.h>

系统环境变量: extern char** environ;

函数返回值:

  • exec* 调用成功,便可直接执行新函数,不再返回值
  • exec* 仅在调用失败的时候返回值-1。

所以exec函数只有出错时会有返回值,而没有成功的返回值

参数:

  • path 要执行程序的绝对路径
  • arg,... 列表的形式形参(执行的指令的选项)如 ls 指令的 “-a” “-l”
  • arg[] 数组的形式传参
  • envp[] 自己维护的环境变量

exec系列函数 函数尾缀解释

  • l(list):参数采用列表
  • v(vector):参数采用数组
  • p(path):自动搜索环境变量PATH
  • e(env):自己维护的环境变量

使用方法

函数名参数格式是否带路径是否使用当前环境变量
execl列表
execlp列表
execle列表否,自己组装环境变量
execv数组
execvp数组
execve数组否,自己组装环境变量

p —自带当前默认环境变量路径

e —使用自己的环境变量

exec*函数应用演示

execl函数

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

int main(int argc,char* argv[],char* env[])
{
  
    printf("i am a process,PID:%d,PPID:%d\n",getpid(),getppid());

    execl("/usr/bin/ls"/*需要执行的文件路径*/,"ls","-a","-l"/*执行选项*/,NULL);//execl执行程序替换
    exit(1);
}

进程在执行到execl函数后,便转去执行新代码,自己的原代码不会再执行,除非exec* 调用失败。

execv 函数

int main()
{
    char* argv[]={"ls" ,"-a","-l","-n",NULL};
    if(fork()==0)
    {
        printf("command begin\n");
        execv("/usr/bin/ls",argv);
        printf("command complete\n");
        exit(1);
    }
    pid_t ret=wait(NULL);
    if(ret>0)
    {
        printf("wait success\n");

    }
    return 0;
}

execlp 函数

只需给文件名即可,系统会在环境变量中寻找

int main()
{
    if(fork()==0)
    {
        printf("command begin\n");
        execlp("ls","ls","-a","-l","-n",NULL);
        printf("command complete\n");
        exit(1);
    }
    pid_t ret=wait(NULL);
    if(ret>0)
    {
        printf("wait success\n");

    }
    return 0;
}

execvp 函数

int main()
{
    char* argv[]={"ls" ,"-a","-l","-n",NULL};
    if(fork()==0)
    {
        printf("command begin\n");
        execvp("ls",argv);
        printf("command complete\n");
        exit(1);
    }
    pid_t ret=wait(NULL);
    if(ret>0)
    {
        printf("wait success\n");
    }
    return 0;
}

execle 函数

e 需要自己组装环境变量

我们在mycode.c文件中执行程序替换,转去执行myexe.c程序,且mycode.c将往myexe.c中导入我们自己写的环境变量,替换掉原有的系统环境变量。

//myexe.c
#include <stdio.h>
int main()
{
    extern char** environ;
    printf("This is myexe!\n");
    int i;
    for(i=0;environ[i];i++)
    {
        printf("%s\n",environ[i]);
    }
    return 0;
}

我们使用 myexe.c程序来打印环境变量:此时将打印系统的环境变量

如果我们在mycode.c中执行程序替换

//mycode.c
int main()
{
    //自定义的环境变量
    char* env[]={"MYENV1=111","MYENV2=222","MYENV3=333",NULL};
    if(fork()==0)
    {
        printf("command begin\n");
        execle("./myexe","myexe",NULL,env);
        printf("execle failed\n");
        exit(1);
    }

    pid_t ret=wait(NULL);
    if(ret>0)
    {
        printf("wait success\n");

    }

    return 0;
}

打印了我们通过execle传输进的环境变量:

execve 函数

以字符数组形式输入选项,改动如下,其他代码和上述execle一致:

char* argv[]={"myexe",NULL};
execve("./myexe",argv,env);

execvpe 函数

此函数第一个参数不用提供路径只提供文件名即可,其余功能与execve相同。

有了exec*程序,也可以调用其他语言的程序,例如python

//mycode.c
execl("/usr/bin/python","python","test.py",NULL);

注意:所有的exec*函数实际上是对系统调用execve的封装所成为的库函数!

6.自制简易 shell

有了程序替换的知识,我们便可以在自己的程序中实现简易版shell。

在我们执行mini_shell时,若要删除字符时需使用:ctrl+backspace

例外:cd指令只能改变子进程的路径,我们在fork的子进程中改变了子进程的路径(这相当于第三方命令),父进程依旧在原来的路径,等到再使用pwd指令,新的子进程还是继承了父进程的原本路径,cd指令就显得“无效”了。

需要以内建命令的方式进行运行,让父进程shell自己执行。cd指令的内建命令为-chdir

步骤如下:

  1. 给出提示符
  2. 获取命令字符串
  3. 拆解命令字符串
  4. 判断是否为内建指令,若是执行内建指令
  5. 若为第三方指令,使用fork开辟子进程执行
  6. 获取退出码
int  main()
{
    char command[NUM];
    command[0]=0;
    char* argv[CMD_NUM]={NULL};
    while(1)
    {
        //1.提示符
        printf("[user@hostname mydir]# ");
        fflush(stdout);

        //2.获取指令字符串
        fgets(command,NUM,stdin);
        command[strlen(command)-1]=0;
        //printf("echo %s\n",command);

        //"ls -a -l -n"
        //3.拆解字符串 得到 char* argv[]
        //strtok函数
        char token[]=" ";
        argv[0]=strtok(command,token);
        int i=1;
        while(argv[i]=strtok(NULL,token))
        {
            ++i;
        }
          
        i=0;
        for(;argv[i];++i)
        {
             printf("argv[%d]:%s\n",i,argv[i]);
        }


        //4.检测是否需要内建指令
        if(strcmp("cd",argv[0])==0)
        {
            if(argv[1]!=NULL)
            {
                chdir(argv[1]);
            }
            continue;
        }

        //5.执行第三方指令
        if(fork()==0)
        {
            execvp(argv[0],argv);
            exit(0);
        }

        //6.获取退出码
        int status;
        pid_t ret=waitpid(-1,&status,0);
        if(ret>0)
        {
            if(WIFEXITED(status))
            {
                printf("exit code:%d\n",WEXITSTATUS(status));
            }
        }
        else 
        {
            printf("wait failed\n");
        }
      
    }
    return 0;
}

实验:

进程的调用和函数调用相似,传参->函数操作->返回值。这种通过参数和返回值在拥有私有数据的函数间通信的模式是结构化程序设计的基础。Linux鼓励将这种应用于程序之内的模式扩展到程序之间,就如同我们在bash上执行各种系统指令一般。


青山不改 绿水长流

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值