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

在这里插入图片描述
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:地址空间(续)与进程控制》的学习就先到这里,如果有什么问题,还请指教指教,希望本篇文章能够对你有所帮助,我们下一篇见!!!

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

请添加图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

RabbitMQ(三)

RabbitMQ中的各模式及其用法 工作队列模式一、生产者代码1、封装工具类2、编写代码3、发送消息效果 二、消费者代码1、编写代码2、运行效果 发布订阅模式一、生产者代码二、消费者代码1、消费者1号2、消费者2号 三、运行效果四、小结 路由模式一、生产者代码二、消费者代码1、消…

ssh,samba,tftp,nfs服务安装和配置

前提准备 sudo ufw disable sudo ufw status sudo apt update ssh服务 sudo apt-get install openssh-server sudo apt-get install openssh-client sudo apt-get install ssh echo "PasswordAuthentication yes" >> /etc/ssh/ssh_config //配置ssh客户…

.NetCore 使用 NPOI 读取带有图片的excel数据

在.NetCore使用NPOI插件进行批量导入时&#xff0c;获取Excel中的所有的图片数据&#xff0c;存到集合中。 1.定义类PictureData 代码如下&#xff1a; public class PictureData { public byte[] Data { get; set; } } 2.数据集引用 using NPOI.XSSF.UserModel; usin…

MAC上安装Octave

1. 当前最新版Octave是9.3版本&#xff0c;需要把mac os系统升级到14版本&#xff08;本人之前的版本是10版本&#xff09; https://wiki.octave.org/Octave_for_macOS octave的历史版本参考此文档&#xff1a;Octave for macOS (outdated) - Octavehttps://wiki.octave.org/Oc…

文档智能:OCR+Rocketqa+layoutxlm <Rocketqa>

此次梳理Rocketqa&#xff0c;个人认为该篇文件讲述的是段落搜索的改进点&#xff0c;关于其框架&#xff1a;粗检索 重排序----&#xff08;dual-encoder architecture&#xff09;&#xff0c;讲诉不多&#xff0c;那是另外的文章&#xff1b; 之前根据文档智能功能&#x…

算法每日双题精讲 —— 二分查找(二分查找,在排序数组中查找元素的第一个和最后一个位置)

&#x1f31f;快来参与讨论&#x1f4ac;&#xff0c;点赞&#x1f44d;、收藏⭐、分享&#x1f4e4;&#xff0c;共创活力社区。 &#x1f31f; 别再犹豫了&#xff01;快来订阅我们的算法每日双题精讲专栏&#xff0c;一起踏上算法学习的精彩之旅吧&#xff01;&#x1f4aa…

【RDMA学习笔记】1:RDMA(Remote Direct Memory Access)介绍

从帝国理工的PPT学习。 什么是RDMA Remote Direct Memory Access&#xff0c;也就是Remote的DMA&#xff0c;是一种硬件机制&#xff0c;能直接访问远端结点的内存&#xff0c;而不需要处理器介入。 其中&#xff1a; Remote&#xff1a;跨node进行数据传输Direct&#xff…

「实战应用」如何为DHTMLX JavaScript 甘特图添加进度线

DHTMLX Gantt是用于跨浏览器和跨平台应用程序的功能齐全的Gantt图表。可满足项目管理应用程序的所有需求&#xff0c;是最完善的甘特图图表库。 今天&#xff0c;您将学习如何使用进度线补充JavaScript 甘特图&#xff0c;以便于监控项目进度。 DHTMLX Gantt 最新试用版下载 …

爬虫后的数据处理与使用(使用篇--实现分类预测)

&#xff08;&#xff09;紧接上文&#xff0c;在完成基本的数据处理后&#xff0c;接下来就是正常的使用了。当然怎么用&#xff0c;确实需要好好思考一下~ 上文&#xff1a;爬虫后的数据处理与使用&#xff08;处理篇&#xff09; 前言&#xff1a; 一般来说&#xff0c;我…

难绷,一种重命名+符号链接禁用EDR(Crowdstrike)的方法

最近看到的一个项目&#xff1a;https://github.com/rad9800/FileRenameJunctionsEDRDisable #include <windows.h>#include <winioctl.h>#include <stdio.h> typedef struct _REPARSE_DATA_BUFFER{ ULONG ReparseTag; USHORT ReparseDataLength; …

数仓建模(三)建模三步走:需求分析、模型设计与数据加载

本文包含&#xff1a; 数据仓库的背景与重要性数据仓库建模的核心目标本文结构概览&#xff1a;需求分析、模型设计与数据加载 目录 第一部分&#xff1a;需求分析 1.1 需求分析的定义与目标 1.2 需求分析的步骤 1.2.1 业务需求收集 1.2.2 技术需求分析 1.2.3 成果输出…

微信小程序-Docker+Nginx环境配置业务域名验证文件

在实际开发或运维工作中&#xff0c;我们时常需要在 Nginx 部署的服务器上提供一个特定的静态文件&#xff0c;用于域名验证或第三方平台验证。若此时使用 Docker 容器部署了 Nginx&#xff0c;就需要将该验证文件正确地映射&#xff08;挂载&#xff09;到容器中&#xff0c;并…

Python Wi-Fi密码测试工具

Python Wi-Fi测试工具 相关资源文件已经打包成EXE文件&#xff0c;可双击直接运行程序&#xff0c;且文章末尾已附上相关源码&#xff0c;以供大家学习交流&#xff0c;博主主页还有更多Python相关程序案例&#xff0c;秉着开源精神的想法&#xff0c;望大家喜欢&#xff0c;点…

usb通过hdc连接鸿蒙next的常用指令

参考官方 注册报名https://www.hiascend.com/developer/activities/details/44de441ef599450596131c8cb52f7f8c/signup?channelCodeS1&recommended496144 hdc-调试命令-调测调优-系统 - 华为HarmonyOS开发者https://developer.huawei.com/consumer/cn/doc/harmonyos-guid…

前端性能-HTTP缓存

前言 开启 HTTP 缓存是提升前端性能的常见手段之一。通过缓存&#xff0c;浏览器可以临时存储资源&#xff0c;在后续请求中直接使用本地副本&#xff0c;从而有效减少 HTTP 请求次数&#xff0c;显著缩短网页加载时间。以下是 HTTP 缓存的几个关键点&#xff1a; 1、减少重复…

【Unity-Game4Automation PRO 插件】

Game4Automation PRO 插件 是一个用于 Unity 引擎 的工业自动化仿真工具&#xff0c;它提供了对工业自动化领域的仿真和虚拟调试支持&#xff0c;特别是在与工业机器人、生产线、PLC 系统的集成方面。该插件旨在将工业自动化的实时仿真与游戏开发的高质量 3D 可视化能力结合起来…

【Linux】--- 进程的等待与替换

进程的等待与替换 一、进程等待1、进程等待的必要性2、获取子进程status3、进程等待的方法&#xff08;1&#xff09;wait&#xff08;&#xff09;函数&#xff08;2&#xff09;waitpid函数 4、多进程创建以及等待的代码模型5、非阻塞接口 轮询 二、进程替换1、替换原理2、替…

一个超快低延迟.Net网络通信库:支持TCP, SSL, UDP, HTTP,HTTPS, WebSocket多协议

今天给大家推荐一个性能好、低延迟.Net网络通信库&#xff0c;基本支持所有协议。 01 项目简介 NetCoreServer是一个基于.NET Core的开源项目&#xff0c;一个高性能、跨平台的异步套接字服务器与客户端库。该项目支持多种传输协议&#xff0c;包括TCP、SSL、UDP、HTTP、HTTP…

苍穹外卖08——(涉及接收日期格式数据、ApachePOI导出报表、sql获取top10菜品数据)

营业额统计 service层 在需要处理空值、与数据库交互或使用集合时&#xff0c;Integer 、Double是更好的选择。 // 导入string工具类 import org.apache.commons.lang.StringUtils; Service // 标记该类为Spring的服务组件 Slf4j // 引入日志功能 public class Repor…

数据结构9——二叉搜索树

&#x1f947;1.二叉搜索树的概念 二叉搜索树(Binary Search Tree,BST)又称二叉排序树或二叉查找树&#xff0c;其要么是一棵空树&#xff0c;要么具有以下性质&#xff1a; ①&#xff1a;左子树上所有节点的值都小于根节点&#xff1b; ②&#xff1a;右子树上所有节点的值都…