【Linux】操作系统的理解/进程/环境变量/虚拟地址空间

发布于:2025-02-13 ⋅ 阅读:(15) ⋅ 点赞:(0)

目录

1. 整体学习思维导图

2. 了解计算机的构架

2.1 冯洛伊曼体系结构

2.1.1 为什么我们需要存储器当做中间角色进行数据的处理?

2.1.2 理解数据流动:

3. 操作系统(OS)

3.1 操作系统的结构

3.2 为什么设计OS

3.3 理解OS

3.3.1 理解管理

3.3.2 理解系统调用

3.3.3 理解库函数

4. 进程

4.1 什么是进程

 4.1.1 进程文件夹

4.2 理解进程

4.2.1 创建一个进程

4.2.2 用代码创建一个子进程 

4.3 进程状态

4.3.1 运行/阻塞/挂起

4.3.2 理解同一个task_struct存在于多种数据结构

4.3.3 Linux的进程状态

4.4 进程的优先级

4.4.1 什么是进程的优先级

4.4.2 为什么要有优先级

4.4.3 了解优先级

4.5 进程的切换

4.5.1 理解竞争/独立/并行/并发

4.5.2 切换概念

4.6 Linux2.6内核进程O(1)调度队列

4.6.1 实时进程和普通进程

调度策略:

4.6.2 运行队列的哈希映射

5. 环境变量

5.1 基本概念

5.2 main函数参数和命令行

5.3 PATH环境变量

5.4 获取环境变量的方法

5.5 本地变量/环境变量 

6. 程序地址空间

6.1 虚拟地址空间与进程地址空间的关系

6.2 为什么要有虚拟地址空间


1. 整体学习思维导图

2. 了解计算机的构架

2.1 冯洛伊曼体系结构

 

  • 输入设备:键盘/鼠标/麦克风/网卡/磁盘......

  • 输出设备:显示器/音响/网卡/磁盘......

  • CPU = 运算器+控制器

  • 存储器:内存 磁盘:外存

2.1.1 为什么我们需要存储器当做中间角色进行数据的处理?

CPU的速率太快,和输入设备/输出设备不是一个量级,如果没有存储器作为中间角色,那么CPU处理数据时还需要等待输入设备的数据Input,这无疑让CPU的速率取决于输入设备的速率,这会导致CPU处理的效率大大降低,这是我们不希望的,有了存储器,我们可以实现事先加载一部分数据到存储器给CPU计算,CPU计算好后传到存储器给输出设备输出,有了这种预加载,CPU的速率得到了极大提升,提高了计算机的运行效率。

由此我们可以得知处理一个数据时,软件运行时,需要先加载!从输出设备到存储器是I过程,也可以称为拷贝过程,数据是从一个设备"拷贝"到另一个设备,体系结构的效率就区间于拷贝的效率了!

2.1.2 理解数据流动:

案例:北京小明给远在上海的小刚通过QQ发了一句"你好!",这么理解这个过程?

我们可以将小明和小刚的计算机看作为两个冯诺依曼体系结构:

 

3. 操作系统(OS)

3.1 操作系统的结构

 

操作系统是一款进行硬件管理的软件!

3.2 为什么设计OS

  • 对于上层,为用户的应用程序提供一个良好的执行程序的环境(目的)

  • 对于下层:为软硬件之间搭建一个桥梁,管理所有的软硬件资源(手段)

简单说OS就是一款纯搞管理的软件!

3.3 理解OS

  • 软硬件管理的体系结构是层状结构

  • 一款软件如果涉及了硬件使用,那么它一定贯彻这个操作系统

  • 我们访问使用操作系统,本质就是系统函数的调用,只不过这个函数是操作系统提供给我们的

3.3.1 理解管理

从以上的知识我们了解到OS是一款管理硬件的软件,那么什么是管理呢,如何理解管理?

案例:我们在学校中,有校长,辅导员,学生。校长就是管理层,辅导员是执行层,学生是被管理层,我们知道辅导员是可以经常见到的,但是校长我们可能四年就见一两次,那么校长在不了解每一位同学的情况下怎么进行管理这么多学生的呢?----->学生管理系统,这个系统记录了同学们的各种状态,比如说学号,性别,名字,成绩,绩点.....,校长就是根据这些数据来进行管理,哪个学院哪个班级的成绩不好了,那么校长就找到这个学院负责人或者辅导员让他们执行校长的命令,所以总结以上内容:管理就是,先描述,在组织!

描述出每个同学的特征,使用一个数据结构进行组织起来,这样校长就可以实现不见同学面的情况下进行管理了!

3.3.2 理解系统调用

相信我们平时有过去银行的经历,我们会发现银行会给我们准备一个大厅包含有各个办理业务的窗口,我们存取钱的时候需要挂号到窗口进行办理业务,那么为什么银行不直接让我们去金库取钱呢,为什么要搞一套复杂的窗口办理体系?原因是对我们不信任,怕我们乱来,操作系统也是如此怕我们对硬件乱来,所以设置特定的系统调用窗口供我们使用!

3.3.3 理解库函数

我们理解了系统调用是为了防止我们误操作或者对硬件乱来,那么库函数又是什么呢?简单的例子还是银行,我们都知道不是所有人都懂银行的存取规则的,那么银行会在大厅有着一些服务人员,他们会帮助我们顺利办取我们需要的业务。库函数也是这个职责,不是所有人都懂系统的调用,所以一些编程大佬们编写了库函数方便我们理解和使用!

库函数就是对系统调用的上层封装!

4. 进程

4.1 什么是进程

 

  • Linux系统中,OS必定需要对内存中的可执行程序进行一个管理:先描述,在组织

  • OS对一个程序的描述使用的是一个名为task_struct(Linux下PCB的名称)结构体,我们将其称作为一个进程控制模块PCB(process control block)

  • 进程就是:PCB+程序的数据代码

  • 对进程的管理也变化为对程序列表的管理,增删查改这个列表即可!

  • 进程的所有属性都可以通过task_struct(PCB)找到

    • 内容分类

          • 标示符:描述本进程的唯⼀标示符,⽤来区别其他进程。

          • 状态:任务状态,退出代码,退出信号等。

          • 优先级:相对于其他进程的优先级。

          • 程序计数器:程序中即将被执⾏的下⼀条指令的地址。

          • 内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针

          • 上下文数据:进程执⾏时处理器的寄存器中的数据[休学例⼦,要加图CPU,寄存器。

          • I∕O状态信息:包括显⽰的I/O请求,分配给进程的I∕O设备和被进程使⽤的⽂件列表。

          • 记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。

          • 其他信息等等

  • 我们历史上所有的指令,程序,工具,运行起来都是一个个进程!

Linux源码中的task_struct:

 4.1.1 进程文件夹

[ouyang@iZ2ze0j6dd76e0o9qypo2rZ proc]$ pwd
/proc
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ proc]$ ls
1      12     19     25902  28547  302  39   52    612   859        consoles     filesystems  key-users   modules       self           timer_stats
10     12504  19456  26     28550  308  397  5437  619   9          cpuinfo      fs           kmsg        mounts        slabinfo       tty
10400  12601  2      260    28551  310  419  5500  620   922        crypto       interrupts   kpagecount  mtrr          softirqs       uptime
10408  13     20     26615  28622  327  47   5511  65    924        devices      iomem        kpageflags  net           stat           version
10419  14     21     26894  289    328  471  568   6515  acpi       diskstats    ioports      loadavg     pagetypeinfo  swaps          vmallocinfo
10685  14641  22     27     29     334  49   587   6522  buddyinfo  dma          irq          locks       partitions    sys            vmstat
11     14657  23     27932  290    36   5    591   6533  bus        driver       kallsyms     mdstat      sched_debug   sysrq-trigger  zoneinfo
118    16     24     28     298    37   50   595   7     cgroups    execdomains  kcore        meminfo     schedstat     sysvipc
1184   18     25     28377  3      38   51   605   8     cmdline    fb           keys         misc        scsi          timer_list

每个数字文件都表示一个进程,这些数字表示进程的PID

4.2 理解进程

4.2.1 创建一个进程

 

  • 当前进程id(PID),获取当前进程id的函数getpid();

  • 父进程id(PPID),获取当前进程的父进程id的函数getppid();

  • 子进程由父进程创建

 

[ouyang@iZ2ze0j6dd76e0o9qypo2rZ ~]$ ps ajx | head -1 && ps ajx | grep 26091 | grep -v grep
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
26090 26091 26091 26091 pts/0    26926 Ss    1001   0:00 -bash     父进程是bash
26091 26926 26926 26091 pts/0    26926 S+    1001   0:00 ./process
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ ~]$ ps ajx | grep process | grep -v grep 忽视grep
12565 13387 13387 12565 pts/0    13656 T     1001   0:00 ./process
12565 13656 13656 12565 pts/0    13656 S+    1001   0:00 ./process

红色框的分别是该进程当前工作的路径和程序可执行文件的路径。

  • 更改当前路径cwd

我们看以下代码:

 

[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_1]$ ll
total 20
-rw-rw-r-- 1 ouyang ouyang    0 Feb  1 10:50 hello.txt
-rw-rw-r-- 1 ouyang ouyang   80 Feb  1 10:39 makefile
-rwxrwxr-x 1 ouyang ouyang 8600 Feb  1 10:50 process
-rw-rw-r-- 1 ouyang ouyang  227 Feb  1 10:50 process.c

 我们会发现创建的文件会和cwd拼接创建到该路径下,我们需要更改创建的路径就需要使用系统调用函数chdir

CHDIR(2)                 Linux Programmer's Manual                 CHDIR(2)
NAME
       chdir, fchdir - change working directory
chdir("/*路径*/");

4.2.2 用代码创建一个子进程 

int main()
{                                                                                                             
      printf("我是父进程,我的pid:%d\n", getpid());
      pid_t id = fork();                           
      if(id == 0)       
      {          
          // 子进程
          printf("我是子进程,我的pid:%d, 我的父进程是:%d\n", getpid(), getppid());
      }                                                                            
      else if(id > 0)
      {              
          // 父进程
          printf("我是父进程,我的pid:%d\n", getpid());
      }                                                
      else{
          perror("fork error!\n");
          return 1;               
      }            
      return 0;
 }            

 

 fork()会返回两个值:

 父进程创建子进程,OS会分别分配给父/子进程task_struct,但是子进程指向的数据和代码是父进程的!

 

问题:

  • 为什么子进程返回0,父进程返回子进程的pid

    • 因为父进程 : 子进程 --->> n : 1

  • 为什么一个函数可以返回两次

    • 我们知道进程之间具有独立性,比如我们QQ崩溃了不影响我们听音乐,父/子进程也是如此,两个进程同时运行调用函数自然返回值有两个。

  • 为什么变量id可以即是0,又是别的数据

    • 写时拷贝 --> 当父/子进程任意一方想要修改代码/数据时,OS会拷贝一份出来以供修改!操作系统的理解

[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_1]$ ./process 
100
我是父进程,我的pid:30458
110
我是子进程,我的pid:30459, 我的父进程是:30458

4.3 进程状态

4.3.1 运行/阻塞/挂起

运行状态:

我们知道每个进程都是由task_struct进行描述管理起来的,在了解进程状态之前,我们需要知道一个名词:调度队列!

每一个CPU有一个调度队列(FIFO),该队列的程序由不同将要执行,已经就绪的进程task_struct组成。

  • 我们将在调度队列的进程状态称作为运行状态!

阻塞状态:

我们也知道OS需要管理底层硬件也是用task_struct进行描述管理起来的,同样也有相应的管理数据结构,而这个task_struct中包含有一个等待队列的指针

struct task_struct
{
   // ....
   task_struct* wait;
};

比如我们一个进程调用scanf/cin函数时需要等待键盘设备就绪,此时OS就会将该进程的task_struct从调度队列转向到等待队列.

  • 而在链接在等待队列的进程我们称之为阻塞状态!

进程的状态变化本质之一就是PCB在不同数据结构的增删查改!

挂起状态:

最后一个挂起状态拥有两种形式

  • 运行挂起

  • 阻塞挂起

挂机状态的出现一般在极端情况下,内存实在没有空间分配,会将task_struct指向内存的数据代码转移到磁盘上的swap区域存放!

  • 而这种状态就称作为挂起!如果是阻塞队列的数据代码被交换就称为阻塞挂起,相同的运行挂起也是如此。

4.3.2 理解同一个task_struct存在于多种数据结构

  • 我们之前习惯创建的双向链表,我们的指向是直接指向节点

struct List
{
    struct List* next;
    struct List* prev;
};

  • task_struct中的设计确是将List封装成一个成员变量放入结构体中,有多少个数据结构就创建多少个List成员变量,使用偏移量计算出节点的具体地址(首成员的地址就是节点的地址),简单说原本的指针指向的是对象整体,现在指向的是对象内部的一个指针对象。

struct list_head
{
    struct list_head* next, *prev;
};

以上可以做到一个PCB,但是所有数据结构都可以拥有并且使用!

4.3.3 Linux的进程状态

  • 查询进程命令

    ps aux / ps ajx 命令
    • a:显示⼀个终端所有的进程,包括其他用户的进程。

    • x:显示没有控制终端的进程,例如后台运行的守护进程。

    • j:显示进程归属的进程组ID、会话ID、父进程ID,以及与作业控制相关的信息

    • u:以用户为中心的格式用户进程信息,提供进程的详细信息,如用户、CPU内存使用情况等

[ouyang@iZ2ze0j6dd76e0o9qypo2rZ ~]$ ps -al | head -1 && ps ajx | grep process | grep -v grep
F S   UID   PID  PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
  443  4519  4519   443 pts/0     4519 S+    1001   0:00 ./process
 4519  4520  4519   443 pts/0     4519 S+    1001   0:00 ./process
  • Linux的进程分类

/* *The task state array is a strange "bitmap" of *reasons to sleep. Thus "running" is zero, and *you can test for combinations of others with *simple bit tests. */
static const char *const task_state_array[] = { 
    "R (running)", /*0 */         运行
    "S (sleeping)", /*1 */        阻塞(可以被终止)
    "D (disk sleep)", /*2 */      阻塞(不可以被终止)
    "T (stopped)", /*4 */         OS自动暂停的程序
    "t (tracing stop)", /*8 */    cgdb/gdb调试(断点)暂停的程序
    "X (dead)", /*16 */           死亡状态
    "Z (zombie)", /*32 */         僵尸状态 ---> 为了获取进程退出的信息诞生的
};  
  • R运⾏状态(running):并不意味着进程⼀定在运⾏中,它表明进程要么是在运⾏中要么在运⾏队列⾥。

  • S睡眠状态(sleeping) :意味着进程在等待事件完成(这⾥的睡眠有时候也叫做可中断睡眠(interruptiblesleep))。

  • D磁盘休眠状态(Disksleep)有时候也叫不可中断睡眠状态(uninterruptiblesleep),在这个状态的进程通常会等待IO的结束。(一个进程需要在磁盘中写入数据,但是在磁盘还未将数据写完返回给进程时,此进程就被杀掉就会触发数据丢失的后果,为了防止这种情况发生有了D状态)

  • T停⽌状态(stopped):可以通过发送SIGSTOP信号给进程来停⽌(T)进程。这个被暂停的进程可以通过发送SIGCONT信号让进程继续运⾏。

  • X死亡状态(dead):这个状态只是⼀个返回状态,你不会在任务列表⾥看到这个状态。

[ouyang@iZ2ze0j6dd76e0o9qypo2rZ ~]$ ps ajx |grep head -1 26120 9013 9013 26120 pts/1 9013 R+ 1001 0:00 ps ajx 26120 9014 9013 26120 pts/1 9013 S+ 1001 0:00 grep --color=auto head -1

+号代表是前台运行,要想后台运行需要在可执行程序后加上&

[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_1]$ ./process &
  • 僵尸进程

僵尸状态的目的是为了收集程序的信息,就好比一个人意外死亡在路上后需要经过警察和法医的排查他杀后才运走,而排查的这个过程状态就是僵尸状态,这个状态多出现于子程序执行完相关代码父程序还未终止需要收集子程序的信息。并且此时的子进程已经是死亡状态,无非使用kill指令杀掉,如果父进程一直不收集子进程的信息,僵尸进程将会带来内存泄漏的问题,解决问题:进程等待

[ouyang@iZ2ze0j6dd76e0o9qypo2rZ ~]$ ps ajx | grep 10746 | grep -v grep
10093 10094 10094 10094 pts/0    10746 Ss    1001   0:00 -bash
10094 10746 10746 10094 pts/0    10746 S+    1001   0:00 ./ZP
10746 10747 10746 10094 pts/0    10746 Z+    1001   0:00 [ZP] <defunct>
  • 孤儿进程

孤儿进程指的是父进程已经结束返回,但是子进程还在运行,此时子进程就会被1号进程(也叫做系统进程接管),如果没有接管该子进程可能会造成内存泄漏的问题!

并且子进程在成为孤儿进程之后,会调用到后台运行,但是还可以给前台输出信息!

需要调用kill -9 指令进行杀除。

 

4.4 进程的优先级

4.4.1 什么是进程的优先级

进程的优先级指的是调用CPU资源的先后顺序

  • 优先级 Vs 权限

    • 优先级:已经确定可以获得资源,只不过是顺序先后的问题

    • 权限:能否获得资源的权力

4.4.2 为什么要有优先级

在高峰访问期间,某种资源稀缺需要优先级来进行管理获得资源的先后顺序

4.4.3 了解优先级

  • 查看系统进程

    [ouyang@iZ2ze0j6dd76e0o9qypo2rZ ~]$ ps -l
    F S   UID   PID  PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
    0 S  1001   959   958  0  80   0 - 28920 do_wai pts/1    00:00:00 bash
    0 R  1001  2841   959  0  80   0 - 38336 -      pts/1    00:00:00 ps
    • UID: 代表执⾏者的⾝份

    • PID: 代表这个进程的代号

    • PPID:代表这个进程是由哪个进程发展衍生而来的,亦即⽗进程的代号

    • PRI:代表这个进程可被执行的优先级,其值越小越早被执⾏

    • NI:代表这个进程的nice值

1. 先解释UID,Linux系统确定一个用户的身份是通过UID进行确认的,包括对于访问一个文件的权限也是如此,系统会对比用户访问进程的UID和文件的拥有者/所属组进行对比

[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_2]$ ll
total 8
-rw-rw-r-- 1 ouyang ouyang  73 Feb  2 10:47 makefile
-rw-rw-r-- 1 ouyang ouyang 520 Feb  2 10:38 myprocess.c

/* 查看相对应用户的UID */
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_2]$ ls -ln
total 8
-rw-rw-r-- 1 1001 1001  73 Feb  2 10:47 makefile
-rw-rw-r-- 1 1001 1001 520 Feb  2 10:38 myprocess.c
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ ~]$ ps -l
F S   UID   PID  PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
0 S  1001   959   958  0  80   0 - 28920 do_wai pts/1    00:00:00 bash
0 R  1001  2841   959  0  80   0 - 38336 -      pts/1    00:00:00 ps

2. 进程的优先级由PRI + IN 决定:

PRI的默认值是80,IN的取值范围[-20, 19],也就是说优先级的范围是[60, 99],优先级共有40个级别,数字越小,进程的优先级越高!

为什么要设置PRI默认为80,通过IN修改?为什么优先级要有一定的范围?

  • 优先级有一定范围是防止资源平均分配,不会出现一个进程由于优先级过低而长时间得不到CPU资源导致进程饥饿!

3. 调整优先级

  • top 命令

top
进入top --> r --> 输入进程PID --> nice值
  • nice 命令:启动新进程时指定优先级。

  • renice 命令:修改已运行进程的优先级。

[ouyang@iZ2ze0j6dd76e0o9qypo2rZ ~]$ ps -al | head -1 && ps -al | grep process | grep -v grep
F S   UID   PID  PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
0 S  1001  4519   443  0  80   0 -  1054 hrtime pts/0    00:00:00 process
1 S  1001  4520  4519  0  80   0 -  1054 hrtime pts/0    00:00:00 process
renice:
sudo renice -n IN值 -p PID
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ ~]$ sudo renice -n -20 -p 4519
4519 (process ID) old priority 0, new priority -20
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ ~]$ ps -al | head -1 && ps -al | grep process | grep -v grep
F S   UID   PID  PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
0 S  1001  4519   443  0  60 -20 -  1054 hrtime pts/0    00:00:00 process
1 S  1001  4520  4519  0  80   0 -  1054 hrtime pts/0    00:00:00 process

4.5 进程的切换

4.5.1 理解竞争/独立/并行/并发

  • 竞争:对于同一个CPU的资源调度,资源是少数的,通过优先级来实现谁先调度资源,这一过程称作为竞争

  • 独立:多个进程运行时不会相互干预,相互影响

  • 并行:多个进程在多个CPU上进行运行操作

  • 并发:多个进程在同一个CPU上采用切换的方式同时运行

4.5.2 切换概念

我们在大学当兵的过程就是一个切换的过程,我们当兵时需要向导员提交学籍保留申请,导员告知学校,我们拿到学籍保留的申请批改后去当兵了,过了两年回到学校,通过学籍保留书上的内容学校让导员给我安排上学信息!

这个过程中学校就是CPU,导员就是调度器,我就是进程,我在学校上学就是进程运行,学籍申请书审批保留学籍(学籍就是进程在寄存器上运行的临时数据)就是把上次运行的数据从寄存器中拷贝回给进程的task_struct,恢复学籍就是将保存的上下文数据恢复到寄存器中继续执行,当兵的过程就是将进程从CPU上剥离下来重新放回调度队列的队尾。

计算机中是按照时间片调度的规则进行,每次分配给一个进程一段调度时间,一旦时间到了就要进行一次切换。

 

  • 通过以上我们了解到一个进程的执行不是一直占有CPU的,时间片会觉得该进程在CPU执行多少时间,时间一到就会将进程剥离放到队列后面去,而在这个时间过程中所运行的上下文数据会通过寄存器(空间)中的临时对象存放,最后在进程剥离后将数据拷贝保存到进程的task_struct的tss中!然后下一个进程将tss中的数据覆盖到寄存器进行计算。这个一次过程称作为一次进程切换!

4.6 Linux2.6内核进程O(1)调度队列

4.6.1 实时进程和普通进程

调度策略
  • SCHED_FIFO(先进先出):

    • 进程一旦占用CPU,会一直运行直到主动释放(如阻塞或完成)。

    • 更高优先级的实时进程可以抢占当前进程。

  • SCHED_RR(时间片轮转):

    • 类似SCHED_FIFO,但每个进程分配一个时间片,时间片用完会让出CPU给同优先级的其他进程。

    • 优先级高的进程仍可抢占低优先级进程。

  • SCHED_OTHER(默认策略):

    • 基于完全公平调度算法CFS,公平分配CPU时间。

    • 优先级通过nice值调整,但最终调度由内核根据进程的虚拟运行时间(vruntime)动态平衡。

 

4.6.2 运行队列的哈希映射

  • 前面我们说过一个CPU有一个运行队列

 

 

随着CPU的运行,活跃队列的进程会越来越少,过期队列的进程会越来越多,如果更改一个进程的优先级,是到过期队列再更改,也就是只是将当前次运行完后才会更改。最后活跃队列完毕,Swap()两个队列的指针就可以了!使过期队列直接变为活跃队列!

  • bitmap[5]:⼀共140个优先级,⼀共140个进程队列,为了提⾼查找⾮空队列的效率,就可以⽤5*32个比特位表示队列是否为空,这样,便可以⼤⼤提⾼查找效率!

 

  • 在系统当中查找⼀个最合适调度的进程的时间复杂度是⼀个常数,不随着进程增多⽽导致时间成本增加,我们称之为进程调度O(1)算法!

5. 环境变量

5.1 基本概念

  • 环境变量(environmentvariables)⼀般是指在操作系统中用来指定操作系统运⾏环境的⼀些参数,有点类似于全局变量提供使用。

  • 我们写程序代码时,生成可执行程序编译器链接动静态库就是去环境变量中查找链接(即使我们不提供库的地址参数也可以链接成功!)

5.2 main函数参数和命令行

  • main函数是程序员的入口函数,不是系统的第一个调用函数,那么main函数有参数吗?

 

[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_9]$ ./main -1 -2 -3
argv[0]: ./main
argv[1]: -1
argv[2]: -2
argv[3]: -3

我们可以发现,main函数将我们输入的字符当做参数传入了!这不和命令行很相似吗? 

ls -a -l

进程拥有一张argv的表,bash帮我们切分-a -l 选项,这样我们可以根据参数实现选项功能!

 

[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_9]$ ./main ls -a
argv[0]: ./main
argv[1]: ls
argv[2]: -a
简单列出可见+隐藏文件
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_9]$ ./main ls -l
argv[0]: ./main
argv[1]: ls
argv[2]: -l
详细列出可见文件

5.3 PATH环境变量

我们知道命令也是程序,如ls,echo等等,那么他们为什么直接就可以使用,而不是需指定路径呢?

// 我们的程序
./code
// 命令
ls

这是因为系统中默认已经记录了他们的PATH路径,也就是环境变量,执行指令时系统会自动去寻找记录的路径,找到指定程序就会执行,找不到就会报错! 

[ouyang@iZ2ze0j6dd76e0o9qypo2rZ ~]$ which ls
alias ls='ls --color=auto'
        /usr/bin/ls

查看环境变量PATH 

echo $PATH
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ ~]$ echo $PATH 
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/ouyang/.local/bin:/home/ouyang/bin

我们可以看见默认的ls所在的路径,因此命令可以直接执行

  • 环境变量最开始是从哪边来的?

由系统的配置文件而来

  • 如何解释ls -a -l的操作

从存储角度,bash控制着环境变量,bash中有两张表,一张环境变量表,一张argv[]表:

 

argv[0]会去寻找匹配的环境变量,后续调用ls进程,传入参数-a,-l执行相应的逻辑!

  • 查看所有环境变量

env
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ ~]$ env
XDG_SESSION_ID=40492
HOSTNAME=iZ2ze0j6dd76e0o9qypo2rZ
TERM=xterm
SHELL=/bin/bash
HISTSIZE=1000
SSH_CLIENT=183.209.212.170 1468 22
SSH_TTY=/dev/pts/0
USER=ouyang
LD_LIBRARY_PATH=:/home/ouyang/.VimForCpp/vim/bundle/YCM.so/el7.x86_64
LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=01;05;37;41:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.Z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.jpg=01;35:*.jpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.axv=01;35:*.anx=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=01;36:*.au=01;36:*.flac=01;36:*.mid=01;36:*.midi=01;36:*.mka=01;36:*.mp3=01;36:*.mpc=01;36:*.ogg=01;36:*.ra=01;36:*.wav=01;36:*.axa=01;36:*.oga=01;36:*.spx=01;36:*.xspf=01;36:
MAIL=/var/spool/mail/ouyang
PATH=/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/ouyang/.local/bin:/home/ouyang/bin  // 环境变量
PWD=/home/ouyang     // 当前路径
LANG=en_US.UTF-8
HISTCONTROL=ignoredups
SHLVL=1
HOME=/home/ouyang   // 家目录
LOGNAME=ouyang
SSH_CONNECTION=183.209.212.170 1468 172.24.44.204 22
LESSOPEN=||/usr/bin/lesspipe.sh %s
XDG_RUNTIME_DIR=/run/user/1001
_=/usr/bin/env
  • 环境变量相关指令

echo:显⽰某个环境变量值
export:设置⼀个新的环境变量
env:显⽰所有环境变量
unset:清除环境变量
set:显示本地定义的shell变量和环境变量  
  • export 将我们的路径设置为环境变量即可直接执行了

[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_9]$ export PATH=$PATH:/home/ouyang/Linux_Git/linux_-git_-warehouse/dir_2025_2_9
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_9]$ echo $PATH
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/ouyang/.local/bin:/home/ouyang/bin:/home/ouyang/Linux_Git/linux_-git_-warehouse/dir_2025_2_9
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_9]$ main
argv[0]: main
Segmentation fault

5.4 获取环境变量的方法

  • 通过for循环打印环境变量

#include <stdio.h>
int main(int argc, char* argv[], char* env[]) {
    int i = 0;
    for (; env[i]; i++) {
        printf("%s\n", env[i]);
    }
    return 0;
}
  • 通过第三⽅变量environ获取 
#include <stdio.h>
int main(int argc, char* argv[]) {
    extern char** environ;
    int i = 0;
    for (; environ[i]; i++) {
        printf("%s\n", environ[i]);
    }
    return 0;
}

libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头⽂件中,所以在使⽤时要⽤extern声明。

  • 系统调用getenv()

 

[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_9]$ ./main 
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/ouyang/.local/bin:/home/ouyang/bin

5.5 本地变量/环境变量 

// 本地变量
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_9]$ i=10
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_9]$ echo $i
10

环境变量是可以被子进程继承的,就比如我们前面打印环境变量,是属于Bash的,而main进程是子进程却能打印说明是由父进程传下来的。但是本地变量只能是Bash使用!

  • 问题来了exprt为什么可以将环境变量导入Bash,export不是一个子进程吗?

export是内建命令!

6. 程序地址空间

我们之前肯定见过这样一张图:

程序地址空间-->进程地址空间-->虚拟地址空间

 

我们需要先了解,我们平时打印的地址和所写的程序返回的地址都是虚拟地址!不是真实的硬件数据所在的地址!

关联虚拟地址和真实地址的机制叫做页表机制!页表是通过一种映射的方式来管理的!

  • 一个进程有一份虚拟地址空间

  • 一个进程有一份页表

前面我们说过父进程和子进程之间存在写时拷贝,写时拷贝的核心实现在于页表,我们看下面图:

 我们可以发现很多点:

 

  • 即使发生了写时拷贝,我们去取变量的地址父子进程地址仍然一样,但是由于页表映射机制所指的内存的数据早已分离

  • 页表机制可以将内存中杂乱的空间资源有序的"管理"起来

  • 写时拷贝的优点:

    • 减少拷贝时间,提高效率。子进程复用父进程的数据代码(浅拷贝)。

    • 减少空间资源浪费,只对需要修改的数据代码额外开辟空间,其余的依旧使用父进程数据代码。

6.1 虚拟地址空间与进程地址空间的关系

我们知道一个程序运行可以分配到的空间资源有4G,这是虚拟地址空间的定义,事实上真的一个程序完全占有这4G空间吗,答案是否定的,这4G空间只是一个大饼说是都属于你这一个进程,但是我们要知道CPU可以并发执行,存在多个进程所以,只要当该进程去申请空间资源,才会将空间分配给你,你实际是没有4G的空间,是动态分配的!

有了这个管理想法势必需要相应的结构来进行管理-->mm_struct(管理进程的空间信息)

struct mm_struct *mm; //对于普通的⽤⼾进程来说该字段指向他的虚拟地址空间的⽤⼾空间部分,对于内核线程来说这部分为NULL。

struct mm_struct {
    /*...*/
    struct vm_area_struct* mmap; /* 指向虚拟区间(VMA)链表 */
    struct rb_root mm_rb;        /* red_black树 */
    unsigned long task_size;     /*具有该结构体的进程的虚拟地址空间的⼤⼩*/
    /*...*/
    // 代码段、数据段、堆栈段、参数段及环境段的起始和结束地址。
    unsigned long start_code, end_code, start_data, end_data;
    unsigned long start_brk, brk, start_stack;
    unsigned long arg_start, arg_end, env_start, env_end;
    /*...*/
}

 

6.2 为什么要有虚拟地址空间

  • 可以通过虚拟地址空间来解决越界访问,解引用nullptr等等。

  • 可以将内存中碎片化的空间资源整合管理起来,通过页表的映射机制使其顺序化!

  • 页表映射机制也可以加上权限访问的概念,看下面代码:

    char* str = "Hello"
    *str = "H";
    // *str是常量字符串,不可以更改,在页表后面的权限部分就没有w权限,如果更改就会程序崩溃!
  • 安全上避免了进程之间访问内存,减少了耦合性!


网站公告

今日签到

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