进程的学习 —— Linux下的进程

目录

  • 前言
  • 1 认识进程
    • 1.1 进程的概念
    • 1.2 进程的管理
    • 1.3 查看进程的两种方法
    • 1.4 getpid、getppid和fork函数
  • 2 进程状态
    • 2.1 普遍概念下的进程状态
    • 2.2 Linux下的进程状态
      • 2.2.1 测试Linux的各种进程状态
      • 2.2.2 僵尸进程
    • 2.3 孤儿进程
  • 3 进程切换与进程优先级
    • 3.1 并行、并发
    • 3.2 进程切换
    • 3.3 进程的优先级
  • 4 环境变量
    • 4.1 常见的环境变量
    • 4.2 环境变量相关的一些指令
    • 4.3 环境变量的组织方式
    • 4.4 环境变量的获取方式
      • 4.4.1 通过代码获取
      • 4.4.2 通过系统调用函数获取
    • 4.5 环境变量的全局属性
  • 5 进程地址空间
    • 5.1 引入
    • 5.2 进程地址空间的概念分析


前言

我们常说,程序要加载到内存中才能运行,其原因已经在《冯诺依曼体系结构、操作系统的认识》一文中探讨过了。那么,程序加载到内存后,会发生什么呢?程序如何运行呢?这就涉及到本文将要讨论的重点——进程

本文将初步认识进程以及在Linux操作系统下进程的特性。


1 认识进程

1.1 进程的概念

  • 进程是一个程序的执行实例,正在执行中的程序,从内核的角度看,它是分配内存资源(CPU时间,内存空间)的实体。也就是说,当程序加载到内存中并开始运行时,就成为了一个进程。

1.2 进程的管理

由常识我们知道,计算机几乎不可能在一个时刻只运行一个程序,就像我们平时用电脑,会开着各种app,它们是同时运行的。也就是说,内存中的进程不止有一个,而多个进程同时在工作时,操作系统必然要对它们进行管理,使得计算机中的工作有序地进行。怎么管理呢?显然还是操作系统一贯的管理模式——先描述,再组织!

  • 描述
  1. 进程的信息被存储在一个叫做进程控制块的结构体上,可以理解为是进程信息的集合。
    进程控制块简称为PCB(process control block),在Linux操作系统中,这个结构体命名为task_struct

Linux内核PCB结构体的部分代码
进程的学习 —— Linux下的进程_第1张图片

  1. 每当一个程序(一个二进制可执行文件)被加载到内存中,形成进程,操作系统都会生成一个该进程对应的PCB,我们对进程操作基本都是对PCB进行操作,而不是直接对加载入内存的二进制可执行文件进行操作。

  2. PCB中有一个指针,用于指向进程对应的(可执行文件中的)代码和数据。
    进程的学习 —— Linux下的进程_第2张图片

  3. task_ struct内容分类

  • 标示符: 描述本进程的唯一标示符,用来区别其他进程。
  • 状态: 任务状态,退出代码,退出信号等。 优先级: 相对于其他进程的优先级。
  • 程序计数器: 程序中即将被执行的下一条指令的地址。
  • 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
  • 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
  • I/ O状态信息:包括显示的I/O请求,分配给进程的I/ O设备和被进程使用的文件列表。
  • 记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。 其他信息
  • 组织
    很显然,操作系统不会对进程的代码和数据做组织,而是对进程的PCB做组织。在linux下,所有运行进程的PCB以链表的形式存储在内核中。
    进程的学习 —— Linux下的进程_第3张图片

总结:
进程的概念可以简化为:进程 = 内核数据结构+程序的代码和数据

1.3 查看进程的两种方法

我们通常需要查看某个进程的属性、状态等信息,以确定对其下一步的操作。在Linux下如何查看进程?

  1. 用ps命令获取
    ps ajx:查看所有进程
    进程的学习 —— Linux下的进程_第4张图片
    ps ajx | grep ...:ps ajx加上管道和grep筛选,获取我们想要查看的进程
    写一个能够死循环运行的程序
    进程的学习 —— Linux下的进程_第5张图片
    使其运行起来后,查看该进程
    在这里插入图片描述
    想要前面这些数字和字符是什么意思?试着显式ps ajx中的第一行文本(各个数字、字符的意义)
    在这里插入图片描述

PID:描述该进程的唯一标识符,用于区分其它进程。每个进程都有一个独一无二的PID。
PPID:该进程的父进程的PID。
STAT:该进程的状态,后面展开讨论

  1. 通过/proc系统文件夹查看,该文件夹其实是内存级的,但在Linux中它作为根目录下一个文件夹,可以查看进程信息。
    进程的学习 —— Linux下的进程_第6张图片
    该文件夹中,有许多以数字命名的子文件夹,其实这些文件夹每个的名称对应一个进程,里面存放着对应进程的信息。
    在这里插入图片描述
    通过ps命令得到hello进程的PID是1274,我们来看看/proc/1274里面都藏着什么?
    进程的学习 —— Linux下的进程_第7张图片

其中大多数我们尚且不认识,但是其中exe -> /home/ckf/lesson5/hello其实就是当前进程对应的二进制文件。 如果进程运行时,我们将对应的在磁盘上的可执行文件删掉,该进程还能运行吗?答案是可以的,因为在磁盘上的可执行文件已经加载到内存中了,外部无法对内存造成影响,CPU依然能找到当前进程的代码和数据。


1.4 getpid、getppid和fork函数

这三个函数属于系统调用函数

函数 功能
getpid 获取当前进程的PID
getppid 获取当前进程的父进程的PID
fork 以当前进程为父进程,为父进程创建子进程
  • 所谓父子进程,子进程是由父进程创建的完全独立于父进程的一个进程,父子进程共享一段代码和数据,子进程相当于父进程的副本

  • 我们需要注意fork进程的返回值,在父子进程中有所不同。
    进程的学习 —— Linux下的进程_第8张图片

man手册中关于fork返回值的详细介绍

RETURN VALUE
On success, the PID of the child process is returned in the parent, and 0 is returned in the
child. On failure, -1 is returned in the parent, no child process is created, and errno is set
appropriately.

利用这三个函数,我们可以对父子进程进行验证:
进程的学习 —— Linux下的进程_第9张图片
运行程序,发现两个循环同时运行,说明父进程成功创建了子进程。且子进程的ppid是父进程的pid也得到了验证。
进程的学习 —— Linux下的进程_第10张图片
运行过程中,通过ps命令也可以查看父子进程
在这里插入图片描述


2 进程状态

上面我们初步了解了进程是什么,综上所述我们可以得到,进程是程序加载到内存后的一个执行实体,我们通常称进程在内存中运行。而进程运行过程中,总会出现一些特殊情况,就像人有工作状态、休息状态等等,进程也会有不同的状态。下面我们要探究进程有哪些状态,本着普遍到特殊的探究理念,先看普遍概念的进程状态,再看Linux下的进程状态。

2.1 普遍概念下的进程状态

进程状态有很多,运行、就绪、阻塞、挂起、等待、新建等等,这里我们先讨论运行状态、阻塞状态和挂起状态

  • 运行状态
    进程的任务是由CPU执行的,进程需要从内存载入CPU才能运行,因此,进程的运行状态通常会被误认为是当一个进程占有CPU资源并执行其任务时的状态,其实不然。内存中的进程有多个,而CPU往往只有一个,为了保证每个进程都能运行,CPU在内核中有一个对应的等待队列,进程的PCB会进入到这个队列中等待CPU资源准备运行,而进程PCB在CPU等待队列中准备运行的状态就称为运行状态。进程的学习 —— Linux下的进程_第11张图片
    ⭕PS:这里进程PCB和进程的代码数据在内存中的地址空间不会改变,只是建立了指针链接关系。

CPU通过等待队列中的头指针找到当前“队头”的进程PCB,便可找到其对应的代码和数据从而执行任务,当然CPU与进程的“交涉过程”没这么简单,涉及到了进程切换、进程地址空间,后面再作了解,这里我们只先掌握进程运行的状态。

  • 阻塞状态
    进程运行过程中,除了需要占有CPU资源,有时候也需要其他资源(下面拿外设资源来举例),如显示器、磁盘、网卡和键盘等等。因此各种外设资源也需要在内核中有自己的等待队列,供进程排队等待资源的占有使用。我们知道,操作系统通过先描述再组织的方式管理外设,在内核中为每个外设建立了一个结构体存储其属性、信息、操作方法等,类似进程的PCB,而外设的等待队列也就在该结构体当中。
    进程的学习 —— Linux下的进程_第12张图片
    在《冯诺依曼体系结构、操作系统的认识》一文中我们探讨过,CPU与外设的执行速度差距非常大,当CPU在运行进程时,发现进程需要访问外设,但外设资源不能立即使用需要等待,这对CPU来说是个非常漫长的过程,CPU不可能陪着进程一起等待外设资源就绪。
    因此操作系统是这样做的:我们将当前需要访问外设的进程称为进程A,将进程A从CPU运行队列转移到外设资源等待队列,然后CPU继续执行下一个进程,而进程A在外设资源等待队列中等待资源就绪的状态就称为阻塞状态。当进程A获取了外设资源时,操作系统将其调离外设等待队列,状态从阻塞状态改回运行状态,再入CPU的运行队列准备运行。
    综上所述,阻塞状态就是进程在等待某种其它资源(非CPU)就绪时的状态

总的来说,所谓进程状态,本质上就是进程在不同队列中等待某种资源,而进程何时前往哪个队列,依靠的是操作系统的调度。

  • 挂起状态
    挂起是操作系统节省内存空间一种手段。给一个场景:若在某一时刻内存中有多个进程处于阻塞状态,正在等待某种资源的就绪,这些进程的代码数据都在内存中保存着,短期内不会被使用。随着阻塞状态的进程数量增多,内存可能会空间不足。既然这些进程的代码数据暂时不会被使用,何不将它们移出内存呢?
    操作系统是这样做的:将处于阻塞状态的进程的代码数据暂时移出内存,重新挪动到磁盘上,而该进程的PCB依然留存在内存中的资源等待队列,待到资源就绪之时,再通过PCB找到相应的代码数据,从磁盘中再加载入内存。
    进程的学习 —— Linux下的进程_第13张图片
    以上操作我们称其为进程代码数据的换入换出操作,节省了内存空间,供正在运行的进程使用而挂起状态就是进程代码数据被暂时换出到磁盘上的状态。
    PS:挂起不只是在进程处于阻塞状态下会发生,可能是阻塞挂起,也可能是就绪挂起、新建挂起……只要是需要节省空间的场景都可能发生挂起。挂起是操作系统节省空间的一种策略,它会帮我们自动完成,因此通常我们不关心。

2.2 Linux下的进程状态

了解了抽象的三个进程状态概念,接下来我们要具体化地了解Linux下的进程状态

下面是关于进程状态的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 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};

2.2.1 测试Linux的各种进程状态

  1. R (running):运行状态,并不一定意味着正在运行,它要么表示进程正在运行要么表示进程在运行队列中等候。

写一个简单的死循环C程序(程序1)

#include                                                  

int main()
{
	int a = 0;
	while(1)
	{
		++a;
		--a;
	}
	return 0;
}

运行起来,查看进程,可以看到该进程当前状态为R+,R我们理解了,是运行状态,但为什么会有个+号呢?

在这里插入图片描述

这里涉及到前台进程与后台进程的概念,简单了解一下:
前台进程 (带+):和用户交互,需要较高的响应速度。前台进程运行时,命令行解析无效。能用ctrl+c结束前台进程。
后台进程 (不带+):基本上不和用户交互,后台进程运行时,命令行解析依然有效,但不能用ctrl+c结束前台进程。

  1. S(sleeping):睡眠状态,意味着进程在等待事件完成,对应阻塞状态。

写一个访问显示器(printf)的C程序(程序2)

#include                                                  

int main()
{
	while(1)
	{
		printf("hello world\n");
	}                                                 
	return 0;
}

进程状态为 S 睡眠状态

在这里插入图片描述

但我们看到的现象却是进程一直在运行,不断往显示器打印文本,按理来说进程状态应该是R,这里为什么是S呢?

进程的学习 —— Linux下的进程_第14张图片

原因很简单,进程执行printf时需要获取显示器资源,会从运行状态(R)变为阻塞状态(S),进程会到显示器资源等待队列中。因为显示器(外设)的读取速度远远慢于CPU的处理速度,所以进程绝大部分时间都是在等待显示器资源,也就是S状态。因此我们会看到进程在我们查看的时刻处于S状态,也有可能是R状态,不过是小概率事件。

  1. D(disk sleep):磁盘休眠状态,有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束,IO结束前不会被操作系统自动回收。

  2. T(stopped):停止状态,可以通过发送SIGSTOP信号(kill -19)使进程停止运行,也可以发送SIGCONT(kill -18)信号使进程继续运行。

关于kill指令

用法:kill -选项 进程PID

进程的学习 —— Linux下的进程_第15张图片

让程序2运行起来后,进行如下操作,可以观察到状态T,且SIGSTOP信号会让前台进程转为后台

进程的学习 —— Linux下的进程_第16张图片

  1. t(tracing stop):也是一种停止状态,只不过t状态的进程会被追踪,比如gdb调试时在断点处停下时,进程也停下了,此时的进程就处于t状态。

gdb调试test(程序2),然后打一个断点,运行程序,会在该断点处停下。

进程的学习 —— Linux下的进程_第17张图片

查看进程状态,观察到当前进程处于t状态。

在这里插入图片描述

  1. X(dead):死亡状态,这个状态只是一个返回状态,你不会在任务列表里看到这个状态,因此当一个进程变成死亡状态时,操作系统会立即或延时将其回收。

Z(zombie)是僵尸状态,我们需要重点关注一下。


2.2.2 僵尸进程

  • 什么是僵尸进程?

僵尸进程指的是处于僵尸状态的进程,子进程为了保留进程退出状态,在退出之后不会立刻被回收,而是处于僵尸(Z)状态,等待父进程(或OS)读取它的退出状态代码。所以,只要子进程退出,父进程还在运行,但父进程尚未读取子进程状态,子进程就会进入Z状态。

子进程退出的方式有多种:程序崩溃退出、调用exit退出、kill指令退出等等

通俗理解就是子进程退出之后要告诉父进程任务完成得怎么样,所以还需留存一段时间,等待父进程获知它的完成情况,这段时间里子进程就处于僵尸状态。

写出如下代码以测试僵尸进程

int main()
{
    int id = fork();
    if(id > 0) // 父进程
    {
        while(1)
        {
            printf("I am parent,pid:%d,ppid:%d\n",getpid(),getppid());
            sleep(1);
        }
    }
    else if(id == 0) // 子进程
    {   
        int a = 10;
        while(1)
        {
            if(a == 0)
            {
                int* p;
                *p = 10;// 当子进程运行10s后,会因程序崩溃而退出
            }
            --a;
            printf("I am child,pid:%d,ppid:%d\n",getpid(),getppid());
            sleep(1);
        }
    }
    return 0;
}

观察到僵尸状态

进程的学习 —— Linux下的进程_第18张图片

僵尸进程可不是好事,僵尸进程的存在具有危害

  • 僵尸进程的危害

父进程如果一直不回收已退出的子进程,读取其退出状态代码,那么子进程的退出状态就要一直被维护下去,一直是Z状态。维护退出状态本身就是要用数据维护,也属于进程基本信息,保存在子进程的PCB中,PCB一直存在,肯定会消耗内存空间。所以,如果父进程创建了多个子进程,又不回收,就可能会导致内存泄漏

那么如何避免这个问题?在进程控制模块再详谈。


2.3 孤儿进程

僵尸进程是子进程先退出父进程后退出的情况。而孤儿进程则是父进程先退出子进程后退出的情况,这种情况下的子进程称为孤儿进程。 因为子进程要被父进程回收,所有孤儿进程并不是真正的“孤儿”,在其父进程退出后,它会被1 号进程(pid为1,又称init进程,Liinux操作系统启动后自动创建) 领养,并最终由1号进程回收。

孤儿不是一种进程状态,孤儿进程是一种进程

以如下C程序测试孤儿进程

void test()
{
    int id = fork();
    if(id>0)
    {
        int cnt = 5;
        while(1) // 父进程
        {
            if(cnt == 0)
            {
                printf("父进程已退出\n"); // 父进程运行5s后退出
                exit(1);
            }
            printf("I am parent,pid:%d,ppid:%d\n",getpid(),getppid());
            sleep(1);
            --cnt;
        }
    }
    else if(id == 0)
    {
        int cnt = 10;
        while(1) // 子进程
        {
            if(cnt == 0)
            {
                printf("子进程已退出\n"); // 子进程运行10s后退出
                exit(1);
            }
            printf("I am child,pid:%d,ppid:%d\n",getpid(),getppid());
            sleep(1);
            --cnt;
        }
    }
}

分三个时间节点观察进程状态。

  1. 父进程退出之前
    在这里插入图片描述

  2. 父进程退出后,子进程退出前
    在这里插入图片描述
    可以看到此时的子进程PPID变为1,即父进程变为1号进程。并且从前台转到后台。

  3. 子进程退出后
    在这里插入图片描述
    查无相关进程,子进程被1号进程回收。


3 进程切换与进程优先级

上文讨论的是进程是什么,下面要展开谈谈进程加载到内存后,是如何运行的?

进程需要加载到CPU中才能运行,由CPU负责运算工作。而从前面进程状态中运行状态的阐述我们知道,CPU只有一个而进程有多个,因此将要运行的进程需要在CPU的运行队列中等待。可是这样一来,不是每个时刻只能运行一个进程吗?这就与我们的认知相悖了,我们平时使用计算机时,往往会开着多个应用,同时运行,这是为什么呢?

3.1 并行、并发

这里需要了解CPU的两个概念——并行、并发

参考文章:不懂并行和并发?一文彻底搞懂并行和并发的区别

  • 并行
    多个进程在多个CPU下分别同时运行,称为并行。
    进程的学习 —— Linux下的进程_第19张图片
  • 并发
    多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称为并发。
    进程的学习 —— Linux下的进程_第20张图片

真正的“多个进程同时运行”实际上只有多核CPU通过并行的方式才能做到,而单核CPU都是采用并发的方式运行进程的。对于单核CPU来说,每一时刻只能运行一个进程,但是由于进程切换的速度很快,所以用户看起来是“多个进程同时运行”,这是一种OS欺骗用户的现象。

那么进程切换到底如何进行?接下来我们来探讨


3.2 进程切换

  • 什么是进程切换?

进程切换是并发式单核CPU采取时间片轮转的策略,给每个进程分配时间片,快速切换时间片以营造进程同时运行的假象。在每一个时间片内,进程不一定会全部运行完,时间片结束后进程上下文信息会被保存,然后重新参与轮转,CPU运行下一个进程。

举个栗子,若当前CPU运行队列中有五个进程,给每个进程分配时间片10ms,那么,五个进程都分别进行一次需要50ms,若CPU工作1s,则每个进程都会被CPU执行20次。

每个进程的执行时长不同,OS为其分配的时间片数量也不同,CPU运行进程是以时间片为单位而不是以进程为单位。

进程的学习 —— Linux下的进程_第21张图片

  • 进程切换如何进行?

CPU内有一套寄存器,用以存储当前进程的临时数据。
进程切换时,为了保存当前进程上下文信息数据,保证下次轮转到该进程时正常进行,当前CPU寄存器上的数据会被存入该进程的PCB中。
进程恢复运行时,要进行上下文信息的恢复,即从PCB中读取上下文数据到CPU的寄存器中。

进程的学习 —— Linux下的进程_第22张图片


3.3 进程的优先级

进程需要排队等待CPU运行它,要排队必然就有先后顺序,有先后顺序就会有优先级,就像平时我们到车站、医院等场景都会有军人优先的窗口,这表明在排队过程中军人的优先级高于普通人。进程也有优先级,OS会根据进程的优先级调度进程。

  1. 进程优先级就是进程占用CPU资源的先后顺序,优先级高的进程有优先执行的权利。
  2. 为什么会存在优先级?因为资源有限!进程之间具有竞争性,为了高效完成任务,更合理竞争相关资源,OS要根据优先级为进程分配资源,
  3. 优先级的本质就是进程PCB中一个数字,用这个数字表示该进程的优先级

Linux中,可以用ps -l指令查看进程的优先级

进程的学习 —— Linux下的进程_第23张图片

Linux中的进程优先级比较特殊,又PRI和NI两个数值组成。

优先级 = 老优先级+nice值(NI)(老优先级值得是未作修改前进程的PRI值)

  • 可以通过top工具修改nice值,从而修改进程的优先级。步骤:进入top后按“r”–>输入进程PID–>输入nice值

  • nice值的取值范围是-20至19,一共40个级别

  • PRI越小,进程优先级越高。


4 环境变量

概念

环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数。

4.1 常见的环境变量

  • PATH:指定命令的搜索路径

在Linux下,我们平时运行自己写的可执行程序,需要在程序名前面加上 ./如(./test),这是为了通过相对路径找到对应的程序,能找到才能运行。而平时我们用的指令(如:ls、cd、which等等)说到底也都是程序,执行它们的时候也是运行对应的程序,但用这些指令时却不用加上./这样的路径去寻找对应的程序。这是为什么呢?
原因就是操作系统中具有PATH环境变量,它储存了一系列的路径,对于直接调用的程序,系统会到PATH中的路径下查找对应程序。而平时用的指令的路径就在PATH中。

⭕测试

使用which指令查看指令所在路径,发现是 /usr/bin

[ckf@VM-8-3-centos lesson6]$ which pwd cd
/usr/bin/pwd
/usr/bin/cd

使用echo指令可以查看PATH环境变量,发现/usr/bin在其中。(注意:各个路径间以冒号分隔)

[ckf@VM-8-3-centos lesson6]$ echo $PATH
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/ckf/.local/bin:/home/ckf/bin

可以使用export指令对PATH做修改,加上我们自己写的程序的路径,这样就可以直接运行该程序

[ckf@VM-8-3-centos lesson6]$ ls // 我们在lesson6路径下有如下程序
mycmd  process
[ckf@VM-8-3-centos lesson6]$ pwd // 查看lesson6的绝对路径
/home/ckf/lesson6
[ckf@VM-8-3-centos lesson6]$ export PATH=$PATH:/home/ckf/lesson6 // PATH添加lesson6的绝对路径
[ckf@VM-8-3-centos lesson6]$ echo $PATH // 新的PATH值
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/ckf/.local/bin:/home/ckf/bin:/home/ckf/lesson6
[ckf@VM-8-3-centos lesson6]$ mycmd // 程序现在可以直接运行
hello world
hello world
hello world
^C
[ckf@VM-8-3-centos lesson6]$ process
I am parent,pid:24709,ppid:21254
I am child,pid:24710,ppid:24709
I am parent,pid:24709,ppid:21254
I am child,pid:24710,ppid:24709
^C
  • HOME:指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
[ckf@VM-8-3-centos lesson6]$ echo $HOME // 普通用户
/home/ckf
[root@VM-8-3-centos lesson6]# echo $HOME // root
/root

4.2 环境变量相关的一些指令

  1. echo $name :显式名为name的环境变量值
  2. export:设置新的环境变量值
  3. env:查看系统中所有环境变量
  4. unset:清除环境变量
  5. set:显示本地定义的shell变量和环境变量

4.3 环境变量的组织方式

系统中的环境变量以一张表组织起来。这张表是一个字符串指针数组,称之为环境变量表,数组中每个元素都是一个指向环境变量字符串(以’\0’结尾)的指针。

进程的学习 —— Linux下的进程_第24张图片

每个进程都会收到一张环境变量表。那么这个变量表中的变量怎么获取的?变量表又是从哪来的?下面主要探讨这两个问题。


4.4 环境变量的获取方式

了解环境如何获取环境变量,先要认识main函数的三个参数,这是我们平时不太注意的。

main函数其实有三个隐含的参数
int main(int argc,char* argv[],char* env[])

  • argv是命令行参数字符串指针数组,即平时使用指令时的选项,每个指针指向一个命令行参数字符串
  • argc是argv数组的元素个数
  • env便是环境变量表。

写出以下程序

// test2.c
#include 
int main(int argc,char* argv[],char* env[])
{
    printf("%d\n",argc);

    for(int i = 0;i<argc;++i)
    {
        printf("argv[%d]->%s\n",i,argv[i]);
    }
    return 0;
}

加上一些选项运行程序,观察现象

进程的学习 —— Linux下的进程_第25张图片


4.4.1 通过代码获取

1️⃣ main函数的第三个参数:env

  • 证明env数组是进程接收到的环境变量表。

⭕修改test2.c以便于观察env数组,假设env数组就是环境变量表,那么最后一个元素是NULL指针,我们可以以此为结束标志进行遍历数组。

// test2.c
#include 

int main(int argc,char* argv[],char* env[])
{
    for(int i = 0;env[i];++i)
    {
        printf("env[%d]->%s\n",i,env[i]);
    }
    return 0;
}

运行程序,与env指令查看系统中所有环境变量对比,发现env数组中指向的内容就是系统中的环境变量,证明env数组就是环境变量表

进程的学习 —— Linux下的进程_第26张图片

因此,我们可以在程序中通过env数组获取环境变量,这是一种方法。

2️⃣ 第三方变量environ

Linux的unistd.h头文件中包含一个全局变量environ,指向环境变量表的首元素,因此我们也可以直接用environ来获取环境变量。

// man手册中environ的部分摘要
#include 
extern char **environ;
#include 

int main(int argc,char* argv[],char* env[])
{
    extern char** environ; // 或者是直接包含unistd.h,就不用extern引入第三方变量了
    int i = 0;
    while(environ[i])
    {
        printf("%s\n",environ[i]);
        ++i;
    }
    return 0;
}

程序运行结果与第一种方法相同(截取部分)

进程的学习 —— Linux下的进程_第27张图片


4.4.2 通过系统调用函数获取

通过getenv函数也可以在程序中获取环境变量

// man手册中关于getenv函数的介绍
NAME
       getenv, secure_getenv - get an environment variable

SYNOPSIS
       #include  // 头文件stdlib.h
       char *getenv(const char *name);// 参数:表示某环境变量名的字符串
//...
RETURN VALUE // 返回值:成功匹配参数,返回指向参数环境变量值的指针。失败则返回NULL指针
       The getenv() function returns a pointer to the value in the environment, or NULL if there  is  no match.

根据 getenv 的特性,写出以下C程序,功能:若当前用户为root,则成功访问,若为普通用户则禁止访问。

// test.c
int main()
{
    char* name = getenv("HOME");
    
    if(strcmp(name,"/root") == 0)
    {
        printf("success!\n");
    }
    else
    {
        printf("not permitted!\n");
    }
    return 0;
}

测试

[ckf@VM-8-3-centos lesson6]$ ./test // 普通用户下测试
not permitted!
[ckf@VM-8-3-centos lesson6]$ sudo ./test // sudo提权测试(相当于在root下测试)
success!

4.5 环境变量的全局属性

前面提到,每个进程都会收到一张环境变量表。这是为什么呢?

答:因为环境变量通常具有全局属性,会被子进程继承下去!
也就是说,子进程会接收父进程的环境变量表,这样一层层下去,使环境变量具有全局属性。

事实上,我们平时在shell窗口下输入指令以及各种操作,都是基于有一个进程在此运行——bash。bash是一个命令行解释器,而我们在bash上运行的进程都属于bash的子进程。bash从系统登入时就开始运行了,系统会载入一个环境变量表到bash中,而bash的环境变量又会被子进程继承,这样一来,环境变量便具有全局属性。

测试

导出一个MY_ENY环境变量

[ckf@VM-8-3-centos lesson6]$ export MY_ENV=200

写出如下程序,观察是否继承了环境变量MY_ENV

// myenv.c
#include 
#include 

int main()
{
    char* envname = getenv("MY_ENV");

    printf("%s\n",envname);

    return 0;
}

验证成功。

[ckf@VM-8-3-centos lesson6]$ ./myenv
200

与之相反的是,若我们直接在bash上定义变量,这个变量是不能被子进程继承的,只在bash有效。我们称这种变量为本地变量,无全局属性


5 进程地址空间

5.1 引入

之前我们讨论的程序的空间、地址等概念,是以下图的布局为标准的。我们默认程序独占了内存空间。

进程的学习 —— Linux下的进程_第28张图片

但我们了解了进程概念之后,就必须进一步地了解进程地址空间的概念,很多地方才能解释得通,先来看一段代码。

#include 
#include 

int g_val = 10;

int main()
{
    int id = fork();

    if(id > 0) // 父进程
    {
        while(1)
        {
            printf("I am parent %d %p\n",g_val,&g_val);
            sleep(1);
        }
    }

    else if(id == 0) // 子进程
    {
        int time = 3;
        while(1)
        {
            printf("I am child  %d %p\n",g_val,&g_val);
            sleep(1);

            if(time == 0)
            {
            	g_val = 20;
                printf("子进程修改了g_val!!\n");               
            }
            --time;
        }
    }
    return 0;
}

g_val变量是父子进程共享的数据,试着在运行过程中,子进程修改g_val,看看会发生什么。

运行程序

进程的学习 —— Linux下的进程_第29张图片

可以看到,子进程修改g_val前,很好理解,父子进程中的g_val值相同,地址也相同。子进程修改g_val后,子进程中的g_val值变化,但父进程的g_val不变,而g_val的地址依然不变,在父子进程中都相等。一个相同的物理地址,怎么可能存储两个不同的变量值呢?

综上所述我们可以得出结论:

  • 变量内容不一样,所以父子进程输出的变量绝不是同一个变量。
  • 同一个地址存放不同的变量内容,该地址绝不是物理地址

⭕ 事实上,在Linux下,这种地址称之为虚拟地址。我们用C/C++语言写代码时,所用到的内存地址就是这个虚拟地址!物理地址一般是用户看不到的,由OS统一管理,OS负责将虚拟地址转化为物理地址。


5.2 进程地址空间的概念分析

每个程序都有两套地址:

  1. 虚拟地址(又称逻辑地址):程序内部使用的地址
  2. 物理地址:程序加载入内存中,代码数据的地址。

⭕*虚拟地址从编译器生成可执行文件时就已经在使用了,此时我们一般称其为逻辑地址。逻辑地址是指程序内部用于函数跳转、数据寻址等操作的地址,是用户所能看见的地址。

可以通过反汇编来观察逻辑地址的作用。

写出以下C程序。

#include 

int g_val = 100;

void fun()
{
	printf("hello world\n");

	g_val = 200;
}

int main()
{
	fun();
	return 0;
}

运行,并转到反汇编观察。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

可以看到,程序内部做跳转、寻址时,都会用到地址,这个地址就是程序的逻辑地址(虚拟地址)。当程序从磁盘中加载到内存时,这个地址依然存在,进程开始运行时用的也是这个地址,在内存中,我们称之为进程的虚拟地址

当然,以上这些数据都是存储在数据区、代码区上的,所以在编译时就已经完成了逻辑地址的布局,而堆区、栈区上的数据则是运行时才载入的。

每个进程都会有属于自己的虚拟地址,此处抛出两个问题

  1. 进程的虚拟地址如何管理?

答:根据操作系统先描述再组织的管理思路,进程的虚拟地址会以一个数据结构mm_struct来管理。该数据结构中以区间的形式存放进程的虚拟地址。进程一旦被加载到内存中,操作系统会给进程创建一个mm_struct,并与进程的PCB建立链接关系。

进程的学习 —— Linux下的进程_第30张图片

  1. 虚拟地址与物理地址如何转换?

答:页表。
页表是一个建立虚拟地址与物理地址关系的表,OS用其完成将虚拟地址转化为物理地址的工作。每个进程都会有一个页表,页表也是在进程载入时由OS构建的。如下图。

进程的学习 —— Linux下的进程_第31张图片

页表不仅能完成虚拟地址和物理地址的映射,还能起到拦截的作用,若程序访问到非法的地址(如野指针、数组越界等),在页表处就会被直接拦截,不会访问到物理空间。

这样一来,当CPU运行进程时,会通过进程PCB找到进程的虚拟内存块,获取进程代码数据的虚拟地址,操作系统负责转换为物理地址,使得CPU获取到进程代码数据。总的来说,CPU是不会见到物理地址的,只是在虚拟地址上运行,代码中有需要寻址的操作也是到虚拟内存中找。

总流程图:

进程的学习 —— Linux下的进程_第32张图片

至此,我们已经可以回答 5.1引入 中的问题,不过这还涉及到另外一个概念 —— 写时拷贝!

子进程未修改g_val时,父子进程虚拟内存与物理内存关系如下,二者的g_val虚拟地址与物理地址都相同。

进程的学习 —— Linux下的进程_第33张图片

⭕由于进程的独立性,各个进程在运行期间互不干扰,而父子进程又共享数据(这里的g_val就是父子进程的共享数据)。因此,父子进程任一方对共享数据做修改时,就会发生写时拷贝,OS在物理空间上开辟一块新的空间,并将欲修改数据拷贝过去,修改数据方对应的虚拟地址不变,物理地址指向新的物理内存空间,然后再做修改。

子进程修改g_val的值为20的过程如下:

进程的学习 —— Linux下的进程_第34张图片

完。

你可能感兴趣的:(Linux,linux,学习,运维)