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

news2024/12/23 5:35:58

目录

  • 前言
  • 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结构体的部分代码
在这里插入图片描述

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

  2. PCB中有一个指针,用于指向进程对应的(可执行文件中的)代码和数据。
    在这里插入图片描述

  3. task_ struct内容分类

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

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

1.3 查看进程的两种方法

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

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

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

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

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


1.4 getpid、getppid和fork函数

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

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

  • 我们需要注意fork进程的返回值,在父子进程中有所不同。
    在这里插入图片描述

🎈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.

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


2 进程状态

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

2.1 普遍概念下的进程状态

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

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

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

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

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

  • 挂起状态
    挂起是操作系统节省内存空间一种手段。给一个场景:若在某一时刻内存中有多个进程处于阻塞状态,正在等待某种资源的就绪,这些进程的代码数据都在内存中保存着,短期内不会被使用。随着阻塞状态的进程数量增多,内存可能会空间不足。既然这些进程的代码数据暂时不会被使用,何不将它们移出内存呢?
    操作系统是这样做的:将处于阻塞状态的进程的代码数据暂时移出内存,重新挪动到磁盘上,而该进程的PCB依然留存在内存中的资源等待队列,待到资源就绪之时,再通过PCB找到相应的代码数据,从磁盘中再加载入内存。
    在这里插入图片描述
    以上操作我们称其为进程代码数据的换入换出操作,节省了内存空间,供正在运行的进程使用而挂起状态就是进程代码数据被暂时换出到磁盘上的状态。
    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 <stdio.h>                                                 

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

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

在这里插入图片描述

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

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

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

#include <stdio.h>                                                 

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

进程状态为 S 睡眠状态

在这里插入图片描述

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

在这里插入图片描述

💡原因很简单,进程执行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

在这里插入图片描述

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

在这里插入图片描述

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

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

在这里插入图片描述

查看进程状态,观察到当前进程处于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;
}

观察到僵尸状态

在这里插入图片描述

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

  • 僵尸进程的危害

💀父进程如果一直不回收已退出的子进程,读取其退出状态代码,那么子进程的退出状态就要一直被维护下去,一直是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下分别同时运行,称为并行。
    在这里插入图片描述
  • 并发
    多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称为并发。
    在这里插入图片描述

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

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


3.2 进程切换

  • 什么是进程切换?

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

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

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

在这里插入图片描述

  • 进程切换如何进行?

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

在这里插入图片描述


3.3 进程的优先级

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

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

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

在这里插入图片描述

🔎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’结尾)的指针。

在这里插入图片描述

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


4.4 环境变量的获取方式

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

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

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

写出以下程序

// test2.c
#include <stdio.h>
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;
}

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

在这里插入图片描述


4.4.1 通过代码获取

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

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

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

// test2.c
#include <stdio.h>

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数组就是环境变量表

在这里插入图片描述

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

2️⃣ 第三方变量environ

系统中有一个第三方变量environ,指向环境变量表的首元素,因此我们也可以直接用environ来获取环境变量。

#include <stdio.h>

int main(int argc,char* argv[],char* env[])
{
    extern char** environ;
    int i = 0;
    while(environ[i])
    {
        printf("%s\n",environ[i]);
        ++i;
    }
    return 0;
}

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

在这里插入图片描述


4.4.2 通过系统调用函数获取

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

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

SYNOPSIS
       #include <stdlib.h> // 头文件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 <stdio.h>
#include <stdlib.h>

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 引入

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

在这里插入图片描述

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

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

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,看看会发生什么。

运行程序

在这里插入图片描述

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

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

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

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


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

🔎每个程序都有两套地址:

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

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

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

写出以下C程序。

#include <stdio.h>

int g_val = 100;

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

	g_val = 200;
}

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

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

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

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

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

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

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

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

在这里插入图片描述

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

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

在这里插入图片描述

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

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

📝总流程图:

在这里插入图片描述

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

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

在这里插入图片描述

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

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

在这里插入图片描述

完。

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

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

相关文章

kafka和sparkStreaming

1、Kafka 1、kafka集群架构 producer 消息生产者&#xff0c;发布消息到Kafka集群的终端或服务 broker Kafka集群中包含的服务器&#xff0c;一个borker就表示kafka集群中的一个节点 topic 每条发布到Kafka集群的消息属于的类别&#xff0c;即Kafka是面向 topic 的。 更通俗…

HDFS 常用命令

一、HDFS常用命令 1、查看版本 hdfs version 2、创建 HDFS 文件系统目录。 格式&#xff1a; hdfs dfs -mkdir /user/dir1 3、列出目录下的所有文件 类似 Linux Shell 的 ls 命令。用它可以列出指定目录下的所有文件 hdfs dfs -ls /user/ 4、把本地文件系统文件和目录拷贝…

整合Tkinter GUI界面的古诗词词云生成

Python语言提供的wordcloud词云功能&#xff0c;使文本数据的可视化&#xff0c;简单而美丽。但网上的大多数词云生成功能&#xff0c;多半没有可交互的GUI界面&#xff0c;使用起来稍觉不便。笔者结合网上的中文词云功能&#xff0c;以唐诗三百首&#xff0c;宋词三百首&#…

拟合算法(模型+代码)

拟合的结果是得到一个确定的曲线 最小二乘法的几何解释&#xff1a; argmin 存在参数k&#xff0c;b使括号里的值最小 第一种有绝对值&#xff0c;不易求导&#xff08;求导在求最小值&#xff09;&#xff0c;计算较为复杂&#xff1b;所以我们往往使用第二种定义&#xff0…

什么软件可以录屏?这3款宝藏录屏软件,码住收藏

当我们处理剪辑视频时&#xff0c;我们需要使用到很多素材。有些素材我们可以直接从电脑网上进行下载。但有些素材我们在网上无法进行下载&#xff0c;这个时候就需要使用录屏软件进行录屏。什么软件可以录屏&#xff1f;今天小编向您分享3个宝藏录屏软件&#xff0c;赶紧码住收…

jmeter基础使用方法

文章目录一 配置环境变量二 Jmeter默认语言设置三 启动线程组的创建发送http请求数据报告一 配置环境变量 设置JMETER_HOME,及jemeter解压目录。 设置CLASSPATH,此处分别配置ApacheJMeter_core.jar和jorphan.jar所在位置。 关于环境变量配置多个值&#xff0c;在多个参数中间…

动态规划——状态压缩dp

文章目录概述状态压缩使用条件状压dp位运算棋盘&#xff08;基于连通性&#xff09;类问题概述例题蒙德里安的梦想小国王玉米田炮兵阵地集合类问题概述例题最短Hamilton路径愤怒的小鸟总结概述 状态压缩 状态压缩就是使用某种方法&#xff0c;简明扼要地以最小代价来表示某种…

MySQL 进阶篇2.0 存储过程 触发器 锁 InnoDB引擎

45.存储过程-介绍 46.存储过程-基本语法 -- 查看 select * from information_sc

Python中import语句用法详解

一. 什么是模块&#xff08;module&#xff09;&#xff1f; 在实际应用中&#xff0c;有时程序所要实现功能比较复杂&#xff0c;代码量也很大。若把所有的代码都存储在一个文件中&#xff0c;则不利于代码的复用和维护。一种更好的方式是将实现不同功能的代码分拆到多个文件…

案例丨多元业态管理服务厂商如何走通数字化转型之路

对于多元业态管理服务厂商来说&#xff0c;不同业态客户的使用习惯不一样&#xff0c;从而导致服务过程中的服务有所区别&#xff0c;是这类服务厂商数字化转型的核心需求。下面就以全国领先的阳光智博为例&#xff0c;看下他们是怎样数字化转型的。 一、企业介绍 阳光智博服务…

ASEMI整流二极管A7二极管和M7二极管能代换吗

编辑-Z A7二极管和M7二极管不仅外观封装很像&#xff0c;各项参数也是非常接近的&#xff0c;那么A7二极管和M7二极管能代换吗&#xff1f;我们先来看看他们的详细参数对比&#xff1a; A7二极管参数&#xff1a; 型号&#xff1a;A7二极管 封装&#xff1a;SOD-123 最大重…

Docker- 7.1、跨主机网络-macvlan

一、macvlan介绍 macvlan 本身是 linxu kernel 模块&#xff0c;其功能是允许在同一个物理网卡上配置多个 MAC 地址而实现虚拟多块网卡&#xff0c;即多个 interface&#xff0c;每个 interface 可以配置自己的IP。macvlan 本质上是一种网卡虚拟化技术。macvlan 的最大优点是性…

教你这样找到Mac“其他”文件并删除它

当我们通过「关于本机」>「存储空间」查看硬盘的空间占用情况时。系统会将存储空间根据不同文件类别所占的空间大小显示在条状图上&#xff0c;大部分类型看文字都比较好理解&#xff0c;但对于“其他”这一类很多小伙伴都感觉很困惑&#xff0c;会产生一些问题如&#xff1…

如何在PPT中嵌入交互式图表?LightningChart助力炫酷展示

我们在PPT演示文稿中嵌入图表很容易&#xff0c;但嵌入交互式图表似乎就没听说过了&#xff0c;接下来我们就一起来看看通过交互式图表在PPT中展示病人心跳的效果&#xff1a; PPT中展示病人心跳下方是一个实时地图在PPT中的展现实例 LightningChart2以上在PPT中展示实时交互的…

Nacos服务注册发现、配置管理

Nacos服务注册发现 引入依赖 <dependencyManagement><dependencies><!-- nacos管理依赖 --><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-alibaba-dependencies</artifactId><version>…

T-SQL程序练习03

目录 一、利用 &#x1d446;&#x1d452;&#x1d44e;&#x1d460;&#x1d45c;&#x1d45b; 表&#xff0c;参照结果&#xff0c;写一个存储过程&#x1d446;&#x1d452;&#x1d44e;&#x1d460;&#x1d45c;&#x1d45b;&#x1d43f;&#x1d44e;&#x1d4…

【机器学习】ID3_C4.5_CART算法总结与对比

问题 ID3、C4.5、CART算法总结与对比 前言 ID3、C4.5、CART算法是三种不同的决策树算法&#xff0c;区别主要在最优划分属性的选择上&#xff0c;下面把之前在随机森林中汇总过的复制过来&#xff0c;然后再总结下三者的不同。 三种算法所用的最优属性选择方法详述 信息增…

知识图谱系统课程笔记(二)——知识抽取与挖掘

知识图谱系统课程笔记&#xff08;二&#xff09;——知识抽取与挖掘 文章目录知识图谱系统课程笔记&#xff08;二&#xff09;——知识抽取与挖掘OWL、RDF与RDFS关系知识抽取任务定义和相关比赛知识抽取技术知识获取关键技术与难点知识抽取的子任务实体抽取命名实体识别非结构…

【微服务架构实战】第2篇之Spring Cloud Gateway介绍

我们的项目采用 Spring Cloud Gateway gateway 的版本为&#xff1a;2.2.8.RELEASE 简介&#xff1a; Spring Cloud Gateway&#xff08;2.2.8.RELEASE&#xff09; 是 Spring 公司基于 Spring 5.0&#xff0c; Spring Boot 2.0 和 Project Reactor 等技术开发的网关&#xf…

Linux系统运行时参数命令--网络IO性能监控

目录 5 网络IO性能监控 5.1 性能指标 5.2 网络信息 5.2.1 套接字信息 5.2.2 协议栈统计信息-netstat命令 5.2.3 网络吞吐-sar命令 5.2.4 连通性和延时 5.3 其他常用的网络相关命令 telnet nc mtr连通性测试 nslookup traceroute iptraf强大的网络监控 tcpdump- …