Linux:地址空间(续)与进程控制

news2025/1/16 12:28:22

在这里插入图片描述
hello,各位小伙伴,本篇文章跟大家一起学习《Linux:地址空间与进程控制》,感谢大家对我上一篇的支持,如有什么问题,还请多多指教 !
如果本篇文章对你有帮助,还请各位点点赞!!!

话不多说,开始正题:

文章目录

    • 续上文章[《Linux:进程概念、进程状态、进程切换、进程调度、命令行参数、环境变量,进程地址空间》](https://blog.csdn.net/2301_80153885/article/details/142438914?fromshare=blogdetail&sharetype=blogdetail&sharerId=142438914&sharerefer=PC&sharesource=2301_80153885&sharefrom=from_link)谈页表
      • 虚拟地址结构(mm_struct)的初始化
      • 为什么要有虚拟内存?
      • 怎么维护虚拟内存和页表?
    • 进程控制
      • 进程创建
      • 进程终止
      • 进程等待
      • 进程替换
      • 认识全部接口

续上文章《Linux:进程概念、进程状态、进程切换、进程调度、命令行参数、环境变量,进程地址空间》谈页表

在这里插入图片描述
在页表中有好几个标志位,这里讲两个:其中一个是权限标记位rwx有关,另一个是是否存在isexits

那么比如说我将代码区的代码通过页表映射到物理地址,我可以将这些代码设置为只读r,又比如说将数据区中的gval设置为rw可读可写

当我通过虚拟地址对gval进行修改,OS会通过该虚拟地址查页表,发现存在不会立马直接转换为物理地址,OS知道你这个动作是写入操作,所以会查看是否符合权限,发现有写入权限才会将物理地址给予上层访问,若没有写入权限,则访问失败

就好比代码中:

char* str = "asdfg";
*str = "bbb";

程序会崩掉,其实是进程被杀了,因为常量字符串在常量区,只能读不能写

既然如此,为什么编译不报错呢?那是因为这跟编译器没关系,这个问题只有在程序跑起来的时候系统发现问题,正因为编译器发现不了,所以引入了关键字const来协助我们写程序

isexit分批加载、挂起等的作用

在将磁盘中的数据加载到内存中时,是先加载数据结构,再加载磁盘中的数据的,假设我这个程序有1个G,如果我要将这个程序全部加载到内存里跑完要花10秒,其中有1秒钟的时间后半部分的代码并不执行,但是却还在内存中,这不就浪费空间了吗?又比如说:前半部分的代码已经跑完了,后面大概率也不会再跑一遍,那还占着内存,这不也是浪费空间吗?假设该进程被阻塞,就算已经加载进内存,OS也不会调度,如果此时内存紧张,那么OS就可能会把这段内存与磁盘进行交换。

isexits说白了就是检查目标内容是否在内存中,在就直接返回上层访问,不在就只有两种情况,要么就是没有加载到内存中,要么就是被切换了,OS会帮你从磁盘中把该数据换回物理内存来

为什么会没有加载呢?其实在跑一个进程的时候并不是将所有的数据都加载到内存当中,OS只是简单建立起虚拟地址就不管了,当程序要进行访问的时候,发现isexits标志位为false,那么OS就会帮你加载到内存当中,这就是分批加载,节省空间的同时不影响程序运行

举个例子:就比如说,打游戏的时候,明明这个游戏几十GB,但是在运行的时候只有几GB,因为剩下的并没有用上,这就是分批加载

虚拟地址结构(mm_struct)的初始化

我们知道mm_struct是数据结构,所以要初始化,那么怎么初始化呢?数据是怎么来的?
我们通过readelf可以查看到:
在这里插入图片描述
可执行程序编译的时候,各个区域的大小信息已经有了,所以初始化的数据就是从可执行程序来的!!!说白了就是在磁盘中就已经记录好各个区域的大小信息

可执行程序:

  • 分段:已经帮我们分好段了,数据区、代码区、常量区……
  • 包含属性:哪里到哪里是属于数据区……

也就是说:操作系统(进程管理)、编译原理、编译器、可执行程序也有关系,操作系统要从已经编译好的可执行程序中得到相关的信息(数据区的大小、可执行程序的分段、属性……)用来构建页表……等等

那么栈区、堆区这些是哪里来的?是操作系统自己动态开辟的
在这里插入图片描述
操作系统自己创建的空间:栈、堆
对于栈,只有程序调用函数的时候会使用寄存器来开辟栈空间

对于堆,我们自己malloc、new的空间其实只是扩展了虚拟地址空间,并没有真正的在物理地址申请空间,只有在真正使用的时候系统才会申请空间,毕竟我们申请了空间通常并不会立即使用

为什么要有虚拟内存?

  • 保护物理内存:
    想想要是我们用指针随便指向一个地址直接访问,这不就是野指针了吗?有了虚拟内存和页表,操作系统就会检测到你这指针指向的虚拟地址不存在,就会发生拦截,相比于直接访问物理内存多了一层软件层的保护

所以什么是野指针?为什么野指针会发生崩溃?
其实就是指向的虚拟地址不对导致权限不对,又或者是指向的虚拟地址不存在,操作系统就会杀掉进程

  • 进程管理和内存管理在系统层面上进行解耦合:
    进程管理task_struct只需要在虚拟内存申请空间,并不需要直接向物理内存申请,而且申请空间之后并不一定要立即向物理内存申请空间,只有当需要用到的时候才申请,这就有了延迟性;而物理内存并不需要知道操作系统要干什么,只需要分配内存空间就行了,所以使得进程管理和内存管理在系统层面上进行解耦合

  • 让进程以统一的视角看到内存:
    经过对虚拟内存和对页表的讲解,问问大家对于磁盘中可执行程序的代码和数据加载到内存当中时,是任意加载还是加载到特定地方?答案是:任意加载。因为我们已经有页表来对虚拟内存和物理内存之间的映射,不管物理内存是怎样的乱,在虚拟内存看来就是在这里插入图片描述
    都会使得“无序”变“有序”,只不过是区域大小不一样罢了

怎么维护虚拟内存和页表?

其实只需要维护好进程就可以了,虚拟内存地址空间的数据结构是mm_struct依附于task_struct,所有内容都是OS亲自完成

全局变量和字符常量为什么会有全局性?是因为全局变量和字符常量存在于数据区,一直伴随着进程,所以进程在,全局变量和字符常量就在,不像堆区和栈区随时可以释放,也就是说全局变量一直可以被看到

进程控制

进程创建

我们知道fork创建子进程,子进程会拷贝父进程的代码和数据(浅拷贝),并且在修改内容之前父子进程会指向同一份物理地址,也就是说代码和数据是共享的,但是一旦发生修改,系统就会对其进行写时拷贝

那为什么系统不直接拷贝一份而是搞一个写时拷贝?原因很简单,对于一个父进程10M大小,而他的子进程要发生改变的数据可能只有1M,如果全部重新拷贝一份,不就妥妥的浪费空间吗?

那么问题来了,系统怎么知道要发生写时拷贝?

其实在fork之前,会更新权限位,将代码和数据权限全部改为只读,所以当要发生写入修改,系统就会检测到你正在对只读数据进行写入,触发系统错误!!

对于系统错误有很多种情况,那是什么情况?这个时候就会触发缺页中断!就相当于一种系统检测,来判定要发生写时拷贝,对于这个判定后续文章有关内存会讲解,比较复杂,在这不多说明

确定是写时拷贝后就会申请内存,发生拷贝,修改页表,恢复父子进程的读写权限,然后进行原先的写入操作

进程终止

在主程序中我们有main函数,而main函数的返回值是给谁返回呢?

答案:父进程或者是系统,这里说系统是为了强调父进程有可能是系统某个角色来承担父进程,比如说bash进程

通常在shell使用echo $?来查看最近一个进程的退出码,如例子:

#include<stdio.h>

int main()
{
        printf("hello\n");
        return 10;
}

在这里插入图片描述
为什么第二次echo $?结果是0?别忘了echo也是一个进程,当你使用echo $?之后,最近的进程的退出码当然就是echo $?的退出码。

退出码的意义就是指明错误的原因,举个例子:
当我们安装一个app的时候,如果安装失败,那么系统就会回滚操作,将原先已经下载了的、安装了的全部清除,系统怎么知道安装失败了呢?这个时候就需要退出码这些相关信息来告诉系统。一般来讲都是用0来表示成功,非0来表示错误,也就是用不同的数字来表示不同的原因。所以,都会提前约定相关的数字来表示错误码。

系统提供的错误码,想必大家都见过这三个有关错误信息:

strerror
errno
perror

Linux有134个错误码,可以用以下程序看看:
指令:

g++ -o del del.cc -std=c++11

这里我用的是C++11!后续也是!!

#include <errno.h>
#include <iostream>
#include <string.h>
int main()
{
    for(int i = 0; i < 200; i++)
    {
            std::cout << "code" << i << ": " << strerror(i) << std::endl;
    }
    return 0;
}

0 ~ 133都是系统已经约定好的错误码信息,后面的全是未知错误

当然了,并不是一定要使用系统给出的退出码,比如说在Linux中杀掉一个不存在的进程:

kill -9 888888

在这里插入图片描述
此时的退出码是1,但是我们根据上述查到退出码是1时,应该输出:在这里插入图片描述
而在这里却是:No such process

那是因为退出码自己定也是可以的,如果跟系统强相关确实可以用系统的退出码,但是不相关用来干嘛呢。

进程终止的方式:

  1. main函数返回return

  2. exit在代码的任何地方,表示正常进程结束
    在这里插入图片描述
    里面的参数status其实就是退出码,这是你设定的退出码或者使用系统提供的退出码,是整个进程直接退出,和return不一样的就是return表示的是函数的退出,函数结束后会继续执行后续代码。

  3. _exitexit有点像:在这里插入图片描述
    参数也是一样,只不过头文件不一样,这是系统提供的函数

exit_exit的不同:
要注意这个时候hello后面是带着\n

#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <unistd.h>
int main()
{
        printf("hello\n");
        sleep(2);
        exit(20);
        sleep(2);
        return 0;
}

当你运行这个程序的时候,会发现先输出hello,停顿两秒,然后进程结束。
现在把hello后面是带着\n去掉:

#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <unistd.h>
int main()
{
        printf("hello");
        sleep(2);
        exit(20);
        sleep(2);
        return 0;
}

运行这个程序的时候,会发现先停顿两秒,再输出hello,然后进程结束。

也就是说exit会把缓冲区的数据做刷新!!!

但是这时候把exit换成_exit,你就会发现hello不出现了,也就是缓冲区的数据没了!!

#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <unistd.h>
int main()
{
        printf("hello");
        sleep(2);
        _exit(20);
        sleep(2);
        return 0;
}

所以此时若需要把缓冲区的数据做刷新,就可以在hello后面加上\n来刷新。

exit_exit的区别:刷新缓冲区的问题!!

从系统角度上来理解:
在这里插入图片描述
exit在标准库里面,_exit在系统调用接口里,其实exit就是调用了_exit,只不过进行了封装

我们上述的缓冲区在哪里?其实是语言级缓冲区,也就是和glibc在同一层,若此时用系统调用接口_exit,那么就无法对缓冲区进行刷新,会直接跳转到操作系统,用语言层exit为什么就会刷新呢?要知道exit可是进行过封装的,所以也许会加入fflush这类函数对缓冲区进行刷新。

进程等待

#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <string.h>

int main()
{
    int id = fork();
    if(id < 0)
    {
        printf("error: %d, strerror: %s\n", errno, strerror(errno));
    }
    else if(id == 0)
    {
        int cnt = 5;
        while(cnt--)
        {
            printf("子进程正在运行: pid = %d\n", getpid());
            sleep(1);
        }
        exit(0);
    }
    else
    {
        while(1)
        {
            printf("父进程正在运行: pid = %d\n", getpid());
            sleep(1);
        }
    }
    return 0;
}

在这里插入图片描述

上述代码让子进程运行5秒之后直接exit(0)退出,根据查看进程发现,子进程变成了僵尸状态等待父进程对其进行回收,等待的时候,子进程不退出父进程就会阻塞在wait队列当中

在这里插入图片描述
wait的返回值pid_t是所等待的子进程的pid,等待失败返回值就小于0,对于参数int *wstatus后续讲waitpid再讲

wait作用就是等待任意一个子进程,我们用wait对子进程进行等待:

#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    int id = fork();
    if(id < 0)
    {
        printf("error: %d, strerror: %s\n", errno, strerror(errno));
    }
    else if(id == 0)
    {
        int cnt = 5;
        while(cnt--)
        {
            printf("子进程正在运行: pid = %d\n", getpid());
            sleep(1);
        }
        exit(0);
    }
    else
    {
        pid_t rid = wait(nullptr);
        if(rid > 0)
        {
            printf("等待子进程成功, rid: %d\n", rid);
        }
        while(1)
        {
            printf("父进程正在运行: pid = %d\n", getpid());
            sleep(1);
        }
    }
    return 0;
}

在这里插入图片描述
发现子进程的僵尸状态不见了,那是因为父进程对其进行了回收,返回值就是子进程的pid

对于我们回收了子进程,那么是不是想要知道子进程完成任务完成的怎么样?所以为了支撑,我们使用更多的调用是waitpid

pid_t waitpid(pid_t pid, int *wstatus, int options);

这里的参数pid如果>0,就是等待指定一个子进程;要是=-1,就是等待任意一个子进程

要是想要waitpid来替换掉上述的wait,只需要更换成:

waitpid(-1, nullptr, 0);

也可以等待指定子进程:

waitpid(id, nullptr, 0);

等待失败也会有相应的错误码和错误信息,举个例子:
让子进程退出5秒后,父进程再进行回收waitpid,回收的进程id是id + 1故意出错

#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    int id = fork();
    if(id < 0)
    {
        printf("error: %d, strerror: %s\n", errno, strerror(errno));
    }
    else if(id == 0)
    {
        int cnt = 5;
        while(cnt--)
        {
            printf("子进程正在运行: pid = %d\n", getpid());
            sleep(1);
        }
        exit(0);
    }
    else
    {
        //pid_t rid = wait(nullptr);
        sleep(10);
        pid_t rid = waitpid(id + 1, nullptr, 0);
        if(rid > 0)
        {
            printf("等待子进程成功, rid: %d\n", rid);
        }
        else
        {
            perror("waitpid");
        }
        while(1)
        {
            printf("父进程正在运行: pid = %d\n", getpid());
            sleep(1);
        }
    }
    return 0;
}

在这里插入图片描述
如上说明了waitpid能够回收子进程,那么怎么获取子进程的信息呢?

就是第二个参数int *wstatus,如下述代码:

#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    int id = fork();
    if(id < 0)
    {
        printf("error: %d, strerror: %s\n", errno, strerror(errno));
    }
    else if(id == 0)
    {
        int cnt = 5;
        while(cnt--)
        {
            printf("子进程正在运行: pid = %d\n", getpid());
            sleep(1);
        }
        exit(1);
    }
    else
    {
        //pid_t rid = wait(nullptr);
        int status = 0;
        sleep(10);
        pid_t rid = waitpid(id, &status, 0);
        if(rid > 0)
        {
            printf("等待子进程成功, rid: %d, status: %d\n", rid, status);
        }
        else
        {
            perror("waitpid");
        }
        while(1)
        {
            printf("父进程正在运行: pid = %d\n", getpid());
            sleep(1);
        }
    }
    return 0;
}

从代码上来讲,我将子进程的退出码设置为1,也就是exit(1),那么从我们的角度,status的值应该就是1
在这里插入图片描述
不对,打印出来的结果是256,问题在于我们设置的exit表示的是进程正常退出,但是进程不一定是正常退出的啊,所以status并不仅仅包含了退出码,其实status是一个32比特的位图,我们只关心低16位:
在这里插入图片描述
从上图可知退出码(退出状态)在第8到15比特位,所以要想拿到退出码,只需要status右移8位,然后按位与0xFF

(status>>8)&0xFF

在这里插入图片描述
也许会好奇我们能不能使用全局变量来获取退出码?显然是不能的,进程是具有独立性的,即使是父子关系,也不行,所以我们只能够使用系统调用来获取退出码,让操作系统来将退出码给status让我们获取

我们自己写的代码,自己约定退出码,这没有问题,问题在于如果进程异常退出!

举个例子:

int a = 1 / 0;

这肯定是异常退出:
在这里插入图片描述
此时的退出码是没有意义的,因为程序时异常退出的!!!

出现异常不用担心,操作系统会帮助我们把异常进程给杀掉,会通过发送信号杀掉进程在这里插入图片描述
在这里插入图片描述
被信号所杀,低7位就是终止信号,status&0x7F
在这里插入图片描述
看到终止信号是8,8号信号是:
在这里插入图片描述
符合1/0的报错信息,当然了,获取退出码和退出信息,系统给我们提供了宏:
在这里插入图片描述

if(rid > 0)
        {
            if(WIFEXITED(status))
                printf("等待子进程成功, rid: %d, status code: %d\n", rid, WEXITSTATUS(status));
            else
                printf("等待子进程成功, 子进程异常退出: exit signal: %d\n",status&0x7F);
        }

在这里插入图片描述

option:阻塞等待、非阻塞等待:

一般默认option为0,阻塞等待,是很可靠的等待方法,编码上没难度
WNOHANG:非阻塞等待,其实就是一个宏,一个整数

举个实例:你打电话给小明,让他上号打游戏,问他上号没有,小明说还没有就把电话挂了,过了一会你又打电话过去问,小明还是说没有,又挂电话了,每隔一段时间你就循环往复问,直到小明上号,这就是非阻塞等待,由你自己循环调用非阻塞接口轮询检测,在等待期间你可以先刷会抖音,和朋友聊聊天……,也就是父进程不会因为此进程就不做其他事情,可以让父进程做更多自己的事;但是如果你打电话给小明,让他上号,不挂电话一直等到小明上号,这就是阻塞等待。

进程替换

先来看看接口:
在这里插入图片描述
在这里插入图片描述

就后面的...是可变参数列表,先讲讲excel

#include <iostream>
#include <cstdio>
#include <unistd.h>

int main()
{
    execl("/bin/ls", "ls", "-l", "-a", nullptr);
    return 0;
}

他第一个参数不是要路径嘛,我就给他路径,可以看到后续所需写的其实就是ls -l -a,然后再以nullptr结尾,运行程序可以发现就是实现了系统的命令ls -l -a,不过还是有点不一样,没有带光标

在我们所写的代码中并没有实现ls -l -a的代码,也就是说是“偷取”操作系统的代码实现的,其实就是替换成ls -l -a进程
再来调用top命令也是可以的(一定要以nullptr结尾):

execl("/usr/bin/top", "top", nullptr);

这种特性就是进程的程序替换!

execl的原理:当我们一开始运行程序时,会形成一个进程,会有PCB、页表、物理内存、磁盘的交互,当执行到execl的时候,哪个进程调用execl,哪个进程中的代码和数据就会被磁盘中execl指定路径下的可执行程序的代码和数据覆盖

所以进程替换是创建新的进程吗?
不是,从上述我所讲,只不过将物理内存的代码和数据进行替换,修改页表映射,PCB是没有变化的

那进程替换是什么呢?接着往下看

execl第一个参数带路径的可执行程序,表示的是你要执行谁
后续的可变参数根据上述代码可以得知:你命令行怎么写的,你就怎么写,最重要的是要记得以nullptr结尾 ——> 你想怎么执行
execl不仅仅可以替换成系统的命令,还可以替换成我们自己的可执行程序

证明并没有创建新进程:
myexec.cc:

#include <iostream>
#include <cstdio>
#include <unistd.h>

int main()
{
    printf("我是myexec, pid: %d\n", getpid());
    execl("./other", "other", nullptr);
    return 0;
}

other.c:

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

int main()
{
    printf("我是ohter, pid: %d\n", getpid());
    return 0;
}

other.c转化为可执行程序后,执行myexec,如果真的没有创建新的进程,那么打印出来的pid将会是一样的:
在这里插入图片描述
符合猜想!

下一个问题:execl的返回值?上述所说会将可执行程序覆盖掉原先的代码和数据,那么返回值还有吗?如下:

#include <iostream>
#include <cstdio>
#include <unistd.h>

int main()
{
    printf("我是myexec, pid: %d\n", getpid());
    int val = execl("/bin/ls", "ls", "--color", "-l", "-a", nullptr);

    printf("return val: %d\n", val);
    return 0;
}

在这里插入图片描述
并没有!要是我故意写错呢?

int val = execl("/bin/lssss", "lsssss", "--color", "-l", "-a", nullptr);

在这里插入图片描述
出现了,execl的返回值,是-1

  • 显然execl成功是不会有返回值的,因为代码和数据完全被覆盖了
  • 反之,只要execl有返回值,说明失败了

由于代码和数据完全被覆盖了,所以我们一般创建子进程来进行程序替换:

#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    printf("我是myexec, pid: %d\n", getpid());
    int id = fork();
    if(id < 0)
    {
        perror("fork");
    }
    else if(id == 0)
    {
        execl("/bin/ls", "ls", "--color", "-l", "-a", nullptr);
        exit(1);
    }
    
    int rid = waitpid(id, nullptr, 0);
    if(rid > 0)
    {
        printf("等待子进程成功\n");
    }
    else
    {
        perror("waitpid");
    }

    return 0;
}

没什么难度就不多说了,在Linux当中运行程序其实就是不断的创建子进程来执行可执行程序,创建进程得首先有代码和数据结构,这不就是fork出子进程吗?要是我们不断的从外面读取数据,并循环调用fork创建子进程来执行可执行程序,不就相当于命令行解释器了吗?当然命令行解释器并没有那么简单,但是原理基本上就是这样。

一般fork之后的子进程指向的都是父进程所指向的空间,所以一旦execl就相当于写入数据和代码,就会发生写时拷贝,代码也一样!进程就彻底独立了!

认识全部接口

先讲
在这里插入图片描述

int execv(const char *pathname, char *const argv[]);

第一个参数和execl一样你想执行谁,后面的参数是一个字符数组指针,表示的是你想怎么执行,其实就是把execl的可变参数全部写入一个数组

你可以这么理解execll是链表list,要以nullptr结尾;execvv是数组vector,最后一个元素是nullptr

直接看代码:

#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    printf("我是myexec, pid: %d\n", getpid());
    int id = fork();
    if(id < 0)
    {
        perror("fork");
    }
    else if(id == 0)
    {
        //execl("/bin/ls", "ls", "--color", "-l", "-a", nullptr);
        char *const str[] = {"ls", "--color", "-l", "-a", nullptr};
        execv("/bin/ls", str);
        exit(1);
    }
    
    int rid = waitpid(id, nullptr, 0);
    if(rid > 0)
    {
        printf("等待子进程成功\n");
    }
    else
    {
        perror("waitpid");
    }

    return 0;
}

当然,由于str里面是字符串常量,会报警告,也可以将里面的常量强转为char*,如:

(char*)ls

这个时候难免会想到:

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

所以联想一下,是谁传递的参数?是命令行forkexecv,将ls、-l、-a、nullptr构建成一张表(数组),找到路径下(第一个参数/bin/ls)的main函数,通过execv将那张表传递main函数

exec*中带p的就是:你想运行谁,不要求带路径:
在这里插入图片描述
也就是说直接这样写即可:

execlp("ls", "ls", "-l", "--color", "-a", nullptr);

有人就会好奇两个ls不会重复吗?
当然不会,这两个ls表示的意思都不一样呀,第一个表示的是你想执行谁;第二个表示的是你想要怎么执行。

所以来解释一下为什么不用带路径:
不卖关子:原因就是环境变量PATHexecl这些不带p的就是不会向环境变量中去寻找,而execlp就会带着第一个参数去环境变变量中寻找,环境变量是从左到右寻找的,如果有相同的环境变量,会直接找第一个,因为已经找到了,所以p就是PATH

execvp就不用我多说了吧,跳过

下一个在这里插入图片描述
这种带e的,其实就是环境变量,多了的第三个参数就是让我传环境变量,那要是我不传,发生程序替换会拿到环境变量吗?
当然可以,环境变量可是有全局属性,只不过需要extern char** environ的方式来获取罢了

那么要是我传环境变量呢?
其实很好理解,就会使用你传的环境变量

关于环境变量:

  • 子进程会继承父进程的环境变量
  • 如果要传递全新的环境变量(自己定义,自己传递)
  • 新增环境变量?如下:

可以使用putenv这个函数:
在这里插入图片描述

putenv("新的环境变量名");

谁调用putenv,谁就会新增环境变量

补充:
在这里插入图片描述
所以这么多的接口其实就只有传参方式的差别,为了满足不同的情况而诞生。

你学会了吗?
好啦,本章对于《Linux:地址空间(续)与进程控制》的学习就先到这里,如果有什么问题,还请指教指教,希望本篇文章能够对你有所帮助,我们下一篇见!!!

如你喜欢,点点赞就是对我的支持,感谢感谢!!!

请添加图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2277531.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

链家房价数据爬虫和机器学习数据可视化预测

完整源码项目包获取→点击文章末尾名片&#xff01;

亿道三防丨三防笔记本是什么意思?和普通笔记本的优势在哪里?

三防笔记本是什么意思&#xff1f;和普通笔记本的优势在哪里&#xff1f; 在现代社会中&#xff0c;笔记本电脑已经成为人们工作和生活中不可或缺的一部分。然而&#xff0c;在一些特殊行业或环境中&#xff0c;普通笔记本电脑由于其脆弱性和对环境条件的敏感性&#xff0c;往…

通过proto文件构建 完整的 gRPC 服务端和客户端案例

基础教程-简单案例&#xff08;快入入门java-grpc框架&#xff09; 参考官方入门案例教程&#xff1a;里面我看proto编译&#xff0c;其实直接用maven就能直接将.proto文件编译成java代码。快速入门 | Java | gRPC 框架https://grpc.org.cn/docs/languages/java/quickstart/ …

UML系列之Rational Rose笔记九:组件图

一、新建组件图 二、组件图成品展示 三、工作台介绍 最主要的还是这个component组件&#xff1b; 然后还有这几个&#xff0c;正常是用不到的&#xff1b;基本的使用第四部分介绍一下&#xff1a; 四、基本使用示例 这些&#xff0c;主要是运用package还有package specifica…

K8S 节点选择器

今天我们来实验 pod 调度的 nodeName 与 nodeSelector。官网描述如下&#xff1a; 假设有如下三个节点的 K8S 集群&#xff1a; k8s31master 是控制节点 k8s31node1、k8s31node2 是工作节点 容器运行时是 containerd 一、镜像准备 1.1、镜像拉取 docker pull tomcat:8.5-jre8…

c++领域展开第十二幕——类和对象(STL简介——简单了解STL)超详细!!!!

文章目录 前言STL简介什么是STLSTL的版本STL的六大组件STL的重要性如何学习STL 总结 前言 上篇博客我们了解了初阶的模版函数&#xff0c;以及有关的一些使用方法。 今天我们来了解了解STL库的有关知识 跟我一起上车吧 STL简介 什么是STL STL&#xff1a;是C标准库的重要组成…

Onedrive精神分裂怎么办(有变更却不同步)

Onedrive有时候会分裂&#xff0c;你在本地删除文件&#xff0c;并没有同步到云端&#xff0c;但是本地却显示同步成功。 比如删掉了一个目录&#xff0c;在本地看已经删掉&#xff0c;onedrive显示已同步&#xff0c;但是别的电脑并不会同步到这个删除操作&#xff0c;在网页版…

科研绘图系列:R语言绘制微生物物种系统发育树(phylogenetic tree)

禁止商业或二改转载,仅供自学使用,侵权必究,如需截取部分内容请后台联系作者! 文章目录 介绍构成要素有根树与无根树构建方法应用领域说明的问题教程加载R包数据下载导入数据数据预处理系统发育树可视化准备画图数据1. 构建基础系统发育树 `p1`2. 添加条形图 `p2`3. 添加热图…

1️⃣Java中的集合体系学习汇总(List/Map/Set 详解)

目录 01. Java中的集合体系 02. 单列集合体系​ 1. Collection系列集合的遍历方式 &#xff08;1&#xff09;迭代器遍历&#xff08;2&#xff09;增强for遍历​编辑&#xff08;3&#xff09;Lambda表达式遍历 03.List集合详解 04.Set集合详解 05.总结 Collection系列…

微信小程序:跨页面数据修改全攻略

一、引言 在微信小程序开发中&#xff0c;常常会遇到需要在不同页面之间修改数据的情况。比如在商品详情页添加商品到购物车后&#xff0c;购物车页面需要实时更新商品数量和总价&#xff1b;在用户设置页面修改了个人信息&#xff0c;首页的用户信息展示区域也需要同步更新。…

寒假第一次牛客周赛 Round 76回顾

AC数&#xff1a;2&#xff08;A、C&#xff09; B 思路&#xff1a; 等价于求&#xff1a; 数量最多的字符 #include<stdio.h> int main() {int n,num;int a[26]{0};//用于存储字母 a 到 z 的出现次数。scanf("%d",&n);char s[n];scanf("%s",s)…

【 PID 算法 】PID 算法基础

一、简介 PID即&#xff1a;Proportional&#xff08;比例&#xff09;、Integral&#xff08;积分&#xff09;、Differential&#xff08;微分&#xff09;的缩写。也就是说&#xff0c;PID算法是结合这三种环节在一起的。粘一下百度百科中的东西吧。 顾名思义&#xff0c;…

Ubuntu打开文件夹不显示文件

1.情况介绍 使用ubuntu打开文件夹不显示文件夹里面的内容&#xff0c;而是直接打开了资源查看器。 2.解决办法 命令行安装nautilus sudo apt-get install nautilus

java.text.SimpleDateFormat (日期)

前言&#xff1a; 小编最近让流感折磨的快嘎啦&#xff0c; 呜呜呜&#xff0c;拖更了俩天&#xff0c; 从明天开始我们继续日更&#xff01;&#xff01;&#xff01;&#xff01; 我们一直都是以这样的形式&#xff0c;让新手小白轻松理解复杂晦涩的概念&#xff0c; 把Ja…

游戏市场成果及趋势

2024 年的游戏行业发展情况如何&#xff1f;这是一个既关系到开发商&#xff0c;又关系到玩家的问题&#xff0c;而市场分析师可以为我们揭晓答案。下面&#xff0c;就让我们来看看分析师给出的结论以及他们对未来趋势的预测。 玩家 自 2021 年起&#xff0c;全球平均游戏时间…

Java版-oracle数据库连接测试工具-Maven配置JDBC

一、目标: 1)数据迁移方案,原RAC,新RAC 2)关闭原RAC环境,修改新RAC环境的IP=原RAC环境的IP,优点:所有的应用端不用修改数据库连接字符串。 3)测试工具目标: 3.1 Java程序,运行后cmd窗口, 3.2 链接原RAC数据库IP,每2秒查询并显示数据; 3.3 关闭/断掉原RAC服务器,…

微信小程序实现个人中心页面

文章目录 1. 官方文档教程2. 编写静态页面3. 关于作者其它项目视频教程介绍 1. 官方文档教程 https://developers.weixin.qq.com/miniprogram/dev/framework/ 2. 编写静态页面 mine.wxml布局文件 <!--index.wxml--> <navigation-bar title"个人中心" ba…

数据结构-ArrayLIst-一起探索顺序表的底层实现

各位看官早安午安晚安呀 如果您觉得这篇文章对您有帮助的话 欢迎您一键三连&#xff0c;小编尽全力做到更好 欢迎您分享给更多人哦 大家好&#xff0c;我们今天来学习java数据结构的第一章ArrayList&#xff08;顺序表&#xff09; 1.ArrayList的概念 那小伙伴就要问了线性表到…

Unity2017 控制.abc格式的三维动画播放

首先需要导入插件Alembic&#xff0c;否则导入abc动画&#xff0c;Unity是不会识别的。 Unity2017版本及以下直接从我这儿下载&#xff1a;https://download.csdn.net/download/qq_41603955/90272382 高版本Unity&#xff0c;请移步AssetStore商店搜找。 导入abc之后&#x…

docker虚拟机平台未启用问题

在终端中输入如下代码&#xff0c;重启电脑即可 Enable-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform 对于Docker Desktop - Unexpected WSL error问题 参考链接 解决WSL2与docker冲突问题