【Linux练习生】两万字带你看透进程(相关概念)


呼~ 拿捏了!
在这里插入图片描述

1.进程基本概念

  • 课本概念:程序的一个执行实例,正在执行的程序等
  • 内核观点:担当分配系统资源(CPU时间,内存)的实体

理解:
程序员写完代码后,当你的代码进行编译链接后便会生成一个可执行程序,这个可执行程序本质上是一个文件,是放在磁盘上的。当我们双击这个可执行程序将其运行起来时,本质上是将这个程序的代码和数据加载到内存当中了,因为只有加载到内存后,CPU才能对其进行逐行的语句执行,而一旦将这个程序加载到内存后,我们就不应该将这个程序再叫做程序了,严格意义上将应该将其称之为进程

在这里插入图片描述

注意:系统当中可以同时存在大量进程
eg: 在这里插入图片描述

操作系统是做管理工作的,而其中就包括了进程管理。而系统内是存在大量进程的,那么操作系统是如何对进程进行管理的呢?

先描述,再组织
操作系统作为管理者是不需要直接和被管理者(进程)直接进行沟通的,当一个进程出现时,操作系统就立马对其进行描述,之后对该进程的管理实际上就是对其描述信息的管理。
进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合,课本上称之为PCB(process control block)。

操作系统将每一个进程都进行描述,形成了一个个的进程控制块(PCB),并将这些PCB以双链表的形式组织起来。
在这里插入图片描述
因此,操作系统只要拿到这个双链表的头指针,便可以访问到所有的PCB。此后,操作系统对各个进程的管理就变成了对这条双链表的一系列操作。

例如创建一个进程实际上就是先将该进程的代码和数据加载到内存,紧接着操作系统对该进程进行描述形成对应的PCB,并将这个PCB插入到该双链表当中。而退出一个进程实际上就是先将该进程的PCB从该双链表当中删除,然后操作系统再将内存当中属于该进程的代码和数据进行释放或是置为无效。

总的来说,操作系统对进程的管理实际上就变成了对该双链表的增、删、查、改等操作。

2.描述进程-PCB

进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
课本上称之为PCB(process control block), Linux操作系统下的PCB是: task_struct

task_struct(PCB的一种)

进程控制块(PCB)是描述进程的,在C++当中我们称之为面向对象,而在C语言当中我们称之为结构体,Linux操作系统是用C语言进行编写的,所以Linux当中的进程控制块是用结构体来实现的。

  • 在Linux中描述进程的结构体叫做task_struct。
  • task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息
task_ struct内容分类

task_struct就是Linux当中的进程控制块,task_struct当中主要包含以下信息:

  • 标示符: 描述本进程的唯一标示符,用来区别其他进程。
  • 状态: 任务状态,退出代码,退出信号等。
  • 优先级: 相对于其他进程的优先级。(本质实在资源【CPU,网卡,显卡,磁盘】有限的前提下,确立谁先访问资源,谁后访问资源的问题)
  • 程序计数器(pc)–寄存器: 程序中即将被执行的下一条指令的地址。
  • 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
  • 上下文数据: 进程执行时处理器的寄存器中的数据。
  • I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
  • 记账信息: 可能包括处理器时间总和,使用的时钟总和,时间限制,记账号等。
  • 其他信息。

3.查看进程

1.通过系统目录查看
在根目录下有一个名为proc的系统文件夹。
在这里插入图片描述
文件夹当中包含大量进程信息,其中有些子目录的目录名为数字。
在这里插入图片描述
这些数字其实是某一进程的PID,对应文件夹当中记录着对应进程的各种信息。我们若想查看PID为10的进程的进程信息,则查看名字为1的文件夹即可。
在这里插入图片描述
2.通过ps命令查看

在这里插入图片描述

ps命令与grep命令搭配使用,即可只显示某一进程的信息。
在这里插入图片描述

通过系统调用获取进程的PID和PPID
通过使用系统调用函数,getpid和getppid即可分别获取进程的PID和PPID。
测试:
在这里插入图片描述

当运行该代码生成的可执行程序后,便可循环打印该进程的PID和PPID:
在这里插入图片描述

4.进程上下文

当一个进程在执行时,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容被称为该进程的上下文。当内核需要切换到另一个进程时,它需要保存当前进程的所有状态,即保存当前进程的上下文,以便在再次执行该进程时,能够得到切换时的状态执行下去。在LINUX中,当前进程上下文均保存在进程的任务数据结构中。在发生中断时,内核就在被中断进程的上下文中,在内核态下执行中断服务例程。但同时会保留所有需要用到的资源,以便中继服务结束时能恢复被中断进程的执行。
在这里插入图片描述

时间片的概念是什么?
时间片即CPU分配给各个程序的时间,每个线程被分配一个时间段,称作它的时间片,即该进程允许运行的时间,使各个程序从表面上看是同时进行的。如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。而不会造成CPU资源浪费。在宏观上:我们可以同时打开多个应用程序,每个程序并行不悖,同时运行。但在微观上:由于只有一个CPU,一次只能处理程序要求的一部分,如何处理公平,一种方法就是引入时间片,每个程序轮流执行。

5.(通过系统调用创建进程)- fork

fork函数创建子进程

fork是一个系统调用级别的函数,其功能就是创建一个子进程

例如
在这里插入图片描述
若是代码当中没有fork函数,我们都知道代码的运行结果就是循环打印该进程的PID和PPID。而加入了fork函数后,代码运行结果如下:
在这里插入图片描述

运行结果是循环打印两行数据,第一行数据是该进程的PID和PPID,第二行数据是代码中fork函数创建的子进程的PID和PPID。我们可以发现fork函数创建的进程的PPID就是proc进程的PID,也就是说proc进程与fork函数创建的进程之间是父子关系。

理解fock
  • 1.我们知道加载到内存当中的代码和数据是属于父进程的,那么fork函数创建的子进程的代码和数据又从何而来呢?
    在这里插入图片描述

  • 2.实际上,使用fork函数创建子进程,在fork函数被调用之前的代码被父进程执行,而fork函数之后的代码,则默认情况下父子进程都可以执行。

  • 3.需要注意的是,父子进程代码是共享的,但是父子进程的数据各自开辟空间(采用写时拷贝)—即父子共享用户的代码,但用户的数据各自私有一份{这是因为用户代码是只读不可以修改和写入的;在操作系统中,进程具有独立性,为了进程不互相干扰,故用户数据各自私有一份}

  • 4.使用fork函数创建子进程后就有了两个进程,这两个进程被操作系统调度的顺序是不确定的,这取决于操作系统调度算法的具体实现。

6.使用if进行分流

在fork之后我们通常使用if语句进行分流,即让父进程和子进程做不同的事。

fork函数的返回值:
1、如果子进程创建成功,在父进程中返回子进程的PID,而在子进程中返回0(这里对于fork有两个返回值的原因不做赘述)。
2、如果子进程创建失败,则在父进程中返回-1。

既然父进程和子进程获取到fork函数的返回值不同,那么我们就可以据此来让父子进程执行不同的代码,从而做不同的事。
例如:
在这里插入图片描述
fork创建出子进程后,子进程会进入到 if 语句的循环打印当中,而父进程会进入到 else if 语句的循环打印当中
在这里插入图片描述

7.Linux进程状态

Linux是一个多用户,多任务的系统,可以同时运行多个用户的多个程序,就必然会产生很多的进程,而每个进程会有不同的状态, 进程的当前状态是保存到自己的进程控制块(PCB)当中的,在Linux操作系统当中也就是保存在task_struct当中的。在这里插入图片描述
为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在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 *task_state_array[] = {
	"R (running)",       /*  0*/
    "S (sleeping)",      /*  1*/
    "D (disk sleep)",    /*  2*/
    "T (stopped)",       /*  4*/
    "T (tracing stop)",  /*  8*/
    "Z (zombie)",        /* 16*/
    "X (dead)"           /* 32*/
};
进程状态查看

Linux操作系统当中我们可以通过 ps aux 或 ps axj 命令查看进程的状态:

[zjl@ubuntu18 1.13]$ ps aux

在这里插入图片描述

[zjl@ubuntu18 1.13]$ ps axj

在这里插入图片描述

进程状态解析
运行状态-R(可执行状态)

只有在该状态的进程才可能在CPU上运行。而同一时刻可能有多个进程处于可执行状态,这些进程的task_struct结构(进程控制块)被放入对应CPU的可执行队列中(一个进程最多只能出现在一个CPU的可执行队列中)。进程调度器的任务就是从各个CPU的可执行队列中分别选择一个进程在该CPU上运行
注意:一个进程处于运行状态(running),并不意味着进程一定在CPU上运行,运行状态表明一个进程要么在运行中,要么在运行队列里,表明它可以被时刻调度。

浅度睡眠状态-S(可中断的睡眠状态)

一个进程处于浅度睡眠状态(sleeping),意味着该进程正在等待某件事情的完成,处于浅度睡眠状态的进程随时可以被唤醒,也可以被杀掉。

通过ps命令我们会看到,一般情况下,进程列表中的绝大多数进程都处于TASK_INTERRUPTIBLE状态(除非机器的负载很高)。毕竟CPU就这么一两个,进程动辄几十上百个,如果不是绝大多数进程都在睡眠,CPU又怎么响应得过来

深度睡眠状态-D(不可中断的睡眠状态)

我们来形象举个例子:假设当前内存某一进程需要往磁盘写入数据,磁盘处理完数据再返回给此进程,中间这个过程是需要时间的,此时这个进程处于休眠状态,当前如果系统内存资源严重不足,操作系统就会闲置出来内存资源,此等待进程就可能被杀掉,但是当磁盘处理数据完成后需要返回数据给该进程,但是此时该进程已经被杀掉了,那么这些数据就可能丢失,所以就体现了深度睡眠状态的作用:

一个进程处于深度睡眠状态(disk sleep),表示该进程不会被杀掉,即便是操作系统也不行,只有该进程自动唤醒才可以恢复。该状态有时候也叫不可中断睡眠状态(uninterruptible sleep),处于这个状态的进程通常会等待IO的结束。

暂停状态-T(或跟踪状态)
  • 向进程发送一个SIGSTOP信号,它就会因响应该信号而进入暂停状态(stopped)(除非该进程本身处于TASK_UNINTERRUPTIBLE状态而不响应信号)-
  • 进程发送一个SIGCONT信号,可以让其从TASK_STOPPED状态恢复到TASK_RUNNING状态

下面写一个小程序来观察一下:
在这里插入图片描述
我们可以看到每隔一秒打印一次 hello world!

使用kill命令可以列出当前系统所支持的信号集:

c [zjl@ubuntu18 1.13]$ kill -l
在这里插入图片描述
我们可以看到暂停状态恢复暂停状态到TASK_RUNNING状态的序号分别为1918

此时我们对这个进程发送SIGSTOP信号(序号为19),该进程就进入到了暂停状态:
在这里插入图片描述
我们再对该进程发送SIGCONT信号(序号为18),该进程就会继续运行了:

[zjl@ubuntu18 1.13]$ kill -18 22774
僵尸状态-Z

当一个进程将要退出的时候,在系统层面,该进程曾经申请的资源并不是立即被释放,而是要暂时存储一段时间,以供操作系统或是其父进程进行读取,如果退出信息一直未被读取,则相关数据是不会被释放掉的,一个进程若是正在等待其退出信息被读取,那么我们称该进程处于僵尸状态(zombie)。

  • 僵尸状态的存在是必要的,因为进程被创建的目的就是完成某项任务,那么当任务完成的时候,调用方(操作系统或是其父进程)是应该知道任务的完成情况的,所以必须存在僵尸状态,使得调用方得知任务的完成情况,以便进行相应的后续操作。

进程退出的信息(例如退出码),是暂时被保存在其进程控制块当中的,在Linux操作系统中也就是保存在该进程的task_struct当中。

死亡状态-X

死亡状态只是一个返回状态,当一个进程的退出信息被读取后,该进程所申请的资源就会立即被释放,该进程也就不存在了,所以你不会在任务列表当中看到死亡状态(dead)。

8.僵尸进程

对于一般进程来说:正常情况下–
子进程由父进程创建,子进程再创建新的进程。父子进程是一个异步过程,父进程永远无法预测子进程的结束,所以,当子进程结束后,它的父进程会调用wait()或waitpid()取得子进程的终止状态,回收掉子进程的资源

子进程退出了,但是父进程没有用wait或waitpid去获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中,这种进程称为僵死进程

结合前面说到的,一个进程若是正在等待其退出信息被读取,那么我们称该进程处于僵尸状态。而处于僵尸状态的进程,我们就称之为僵尸进程


举个例子:
对于以下代码,fork函数创建的子进程在打印5次信息后会退出,而父进程会一直打印信息。也就是说,子进程退出了,父进程还在运行,但父进程没有读取子进程的退出信息,那么此时子进程就进入了僵尸状态

补充:任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程的数据结构,等待父进程去处理。如果父进程在子进程exit()之后,没有及时处理,出现僵尸进程,并可以用ps命令去查看,它的状态是“Z”。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
	printf("I am running...\n");
	pid_t id = fork();
	if(id == 0){ //child
		int count = 5;
		while(count){
			printf("I am child...PID:%d, PPID:%d, count:%d\n", getpid(), getppid(), count);
			sleep(1);
			count--;
		}
		printf("child quit...\n");
		exit(1);
	}
	else if(id > 0){ //father
		while(1){
			printf("I am father...PID:%d, PPID:%d\n", getpid(), getppid());
			sleep(1);
		}
	}
	else{ //fork error
	}
	return 0;
} 

运行该代码后,我们可以通过监控脚本,每隔一秒对该进程的信息进行检测:

[zjl@ubuntu18 1.13]$ while :; do ps axj | head -1 && ps axj | grep jincheng | grep -v grep;echo "######################";sleep 1;done

在这里插入图片描述


僵尸进程危害
  • 僵尸进程的退出状态必须一直维持下去,因为它要告诉其父进程相应的退出信息。可是父进程一直不读取,那么子进程也就一直处于僵尸状态。
  • 僵尸进程的退出信息被保存在task_struct(PCB)中,僵尸状态一直不退出,那么PCB就一直需要进行维护。
  • 若是一个父进程创建了很多子进程,但都不进行回收,那么就会造成资源浪费,因为数据结构对象本身就要占用内存。
  • 僵尸进程申请的资源无法进行回收,那么僵尸进程越多,实际可用的资源就越少,也就是说,僵尸进程会导致内存泄漏。

9.孤儿进程

父进程结束了,而它的一个或多个子进程还在运行,那么这些子进程就成为孤儿进程(father died)。子进程的资源由init进程(进程号PID = 1)回收。

通俗来讲:孤儿进程是没有父进程的进程,为避免孤儿进程退出时无法释放所占用的资源而变为僵尸进程),进程号为 1 的 init 进程将会接受这些孤儿进程,这一过程也被称为“收养”。init 进程就好像是一个孤儿院,专门负责处理孤儿进程的善后工作。每当出现一个孤儿进程的时候,内核就把孤 儿进程的父进程设置为 init ,而 init 进程会循环地 wait() 它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init 进程就会代表党和政府出面处理它的一切善后工作。因此孤儿进程并不会有什么危害。


举例说明:
对于以下代码,fork函数创建的子进程会一直打印信息,而父进程在打印5次信息后会退出,此时该子进程就变成了孤儿进程

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
	printf("I am running...\n");
	pid_t id = fork();
	if(id == 0){ //child
		int count = 5;
		while(1){
			printf("I am child...PID:%d, PPID:%d\n", getpid(), getppid(), count);
			sleep(1);
		}
	}
	else if(id > 0){ //father
		int count = 5;
		while(count){
			printf("I am father...PID:%d, PPID:%d, count:%d\n", getpid(), getppid(), count);
			sleep(1);
			count--;
		}
		printf("father quit...\n");
		exit(0);
	}
	else{ //fork error
	}
	return 0;
} 

在这里插入图片描述

通过运行结果看:在父进程未退出时,子进程的PPID就是父进程的PID,而当父进程退出后,子进程的PPID就变成了1,即子进程被1号进程领养了。

10.进程优先级

什么是(进程)优先级?

优先级实际上就是获取某种资源的先后顺序,而进程优先级实际上就是进程获取CPU资源分配的先后顺序,就是指进程的优先权(priority),优先权高的进程有优先执行的权力。

进程优先级存在的原因?

优先级存在的主要原因就是资源是有限的,而存在进程优先级的主要原因就是CPU资源是有限的,一个CPU一次只能跑一个进程,而进程是可以有多个的,所以需要存在进程优先级,来确定进程获取CPU资源的先后顺序。

查看系统进程

在Linux或者Unix操作系统中,用ps -l命令会类似输出以下几个内容:

[zjl@ubuntu18 1.14]$ ps -l

在这里插入图片描述
列出的信息当中有几个重要的信息:

UID:代表执行者的身份。(Linux中,标识一个用户,并不是通过用户名标识,而是UID,因为计算机比较擅长处理数据)
PID:代表这个进程的代号。
PPID:代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号。
PRI:代表这个进程可被执行的优先级,其值越小越早被执行。
NI:代表这个进程的nice值。
PRI与NI
  • PRI代表进程的优先级(priority),通俗点说就是进程被CPU执行的先后顺序,该值越小进程的优先级别越高。
  • NI代表的是nice值,其表示进程可被执行的优先级的修正数值。

PRI值越小越快被执行,当加入nice值后,将会使得PRI变为:PRI(new) = PRI(old) + NI。
若NI值为负值,那么该进程的PRI将变小,即其优先级会变高。 调整进程优先级,在Linux下,就是调整进程的nice值。
NI的取值范围是-20至19,一共40个级别

在Linux操作系统当中,PRI(old)默认为80,即PRI = 80 + NI

查看进程优先级信息

讲之前首先我们先创建一个名字为proc的进程,并运行

在这里插入图片描述

当我们创建一个进程后,我们可以使用ps -al命令查看该进程优先级的信息。

  • 在Linux操作系统中,初始进程一般优先级PRI默认为80,NI默认为0

在这里插入图片描述

通过top命令更改进程的nice值

top命令能够动态实时的显示系统当中进程的资源占用情况以及总信息。
在这里插入图片描述
特别注意:更改nice值,最好要在top命令前加上sudo提升权限

使用sudo top命令后按“r”键,会要求你输入待调整nice值的进程的PID
在这里插入图片描述
输入进程PID并回车后,会要求你输入调整后的nice值:
在这里插入图片描述
输入13:

输入nice值后按“q”即可退出,如果我们这里输入的nice值为13,那么此时我们再用ps-al命令查看进程的优先级信息,即可发现进程的NI变成了13,PRI变成了93(80+NI) 在这里插入图片描述

PRI(old)默认为80(为什么)

接下来,同样的方法,在此基础上我们再修改一次:

1.sudo top
2.输入 "r"3.输入进程PID
4.输入nice值

这次我们把nice值设为10,进程上次修改完的PRI为93,根据:
PRI(new) = PRI(old) + NI
这次的PRI按理应该变为了103,但是从下图我们得知了此时的PRI是90,这也就是说每次修改前,PRI都会默认变为80,即不会继承上次修改完的PRI值。

在这里插入图片描述
PRI(old)默认为80是系统规定的,有什么好处呢?
1.有一个基准值,方便调整
若没有这个基准值,我们每次修改进程优先级时都需要查看当前PRI值。
2.设计上,实现比较合理
我们知道系统规定:NI(nice值)的取值范围是-20至19,同时限定PRI值上下限分别为99和60(是否如此下面还会验证),如果不默认PRI每次为80,那么基于NI(nice值)的取值范围在数次修改nice值后可能会超出PRI值的上下限。这本身体现了优先级的可控性,一定程度上保证了"公平性"。

验证PRI上下限

我们再修改两次PRI值,nice值分别设为 100和 -100

在这里插入图片描述
在这里插入图片描述
我们可以看到,当nice 值设定超出它的取值范围时,会自动默认nice值为它的上下限值(即-20或19),相应的,PRI值会根据nice值显示为60或99,即PRI值永远在[60,99]这个区间内。

通过renice命令更改进程的nice值

使用renice命令,后面跟上更改后的nice值和进程的PID即可
在这里插入图片描述
注意: 若是想使用renice命令将NI值调为负值,也需要使用sudo命令提升权限

简单理解其他重要概念
  • 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便有了优先级。

  • 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰。

  • 并行: 多个进程在多个CPU下分别同时进行运行,这称之为并行。

  • 并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。

11.环境变量

基本概念

环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数。环境变量通常具有某些特殊用途,并且在系统当中通常具有全局特性。

例如,我们编写的C/C++代码,在各个目标文件进行链接的时候,从来不知道我们所链接的动静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查,或者平时我们学习时装一些虚拟机时也需要配一些环境变量。

Windows系统也有相应的环境变量:
在这里插入图片描述

查看环境变量的方法

我们可以通过echo命令(显示某个环境变量的值)来查看环境变量:

echo$NAME //NAME为待查看的环境变量名称

例如,查看环境变量PATH:

[cl@VM-0-15-centos ENV]$ echo$PATH

常见环境变量
1.PATH( 指定命令的搜索路径)

客观来说,其实平时我们理解的程序,命令,指令,可执行程序等都是一个概念
那么有一个奇怪的现象,为什么我们自己写的可执行程序程序(以下面proc为例)执行时要在前面加./,而我们平时学习到的ls ll top 类似这些指令不需要呢,自己写的可执行程序程序前面不加./则会显示找不到(command not found)
在这里插入图片描述
其实,要执行一个可执行程序必须要先找到它在哪里,既然不带./就可以执行ls ll top等命令,说明系统能够通过ls ll top名称找到它的位置,而系统是无法找到我们自己的可执行程序的,所以我们必须带上./,以此告诉系统该可执行程序位于当前目录下

而系统就是通过环境变量PATH来找到ls命令的,查看环境变量PATH我们可以看到如下内容:
在这里插入图片描述
环境变量PATH当中有多条路径(绝对路径),这些路径由冒号隔开,当你使用ls命令时,系统就会查看环境变量PATH,然后默认从左到右依次在各个路径当中进行查找。而ls命令实际就位于PATH当中的某一个路径下,所以就算ls命令不带路径执行,系统也是能够找到的。
在这里插入图片描述
那可不可以让我们自己的可执行程序也不用带路径就可以执行呢?下面给出两种方式:

方式一:将可执行程序拷贝到环境变量PATH的某一路径下
既然在未指定路径的情况下系统会根据环境变量PATH当中的路径进行查找,那我们就可以将我们的可执行程序拷贝到PATH的某一路径下,此后我们的可执行程序不带路径系统也可以找到了。

[zjl@ubuntu18 1.14]$ sudo cp proc /usr/bin

在这里插入图片描述

方式二:将可执行程序所在的目录导入到环境变量PATH当中 将可执行程序所在的目录导入到环境变量PATH当中,这样一来,没有指定路径时系统就会来到该目录下进行查找了。
export:设置一个新的环境变量

[zjl@ubuntu18 1.14]$ export PATH=$PATH:/home/zjl/1.14

在这里插入图片描述
特别提示:导入时必须写成 PATH=$PATH:/home/zjl/1.14,如果直接写成PATH=:/home/zjl/1.14,那么PATH环境变量原有路径就会消失,只剩下/home/zjl/1.14,这样会导致很多命令直接不能使用。不过好在只要你重启系统重新登录,PATH环境变量就会恢复原有路径!
在这里插入图片描述

2.HOME【指定用户的主工作目录(即用户登录到Linux系统中的默认所处目录)】

不同用户在运行系统登录时都有自己的主工作目录(家目录),环境变量HOME当中即保存的该用户的主工作目录。

root用户:
在这里插入图片描述
普通用户:
在这里插入图片描述

3.SHELL( 当前Shell,它的值通常是/bin/bash)

我们在Linux操作系统当中所敲的各种命令,实际上需要由命令行解释器(例如bash、sh)进行解释,我们可以通过查看环境变量SHELL来知道自己当前所用的命令行解释器的种类。
在这里插入图片描述

和环境变量相关的命令
  1. echo: 显示某个环境变量值
  2. export: 设置一个新的环境变量
  3. env: 显示所有环境变量
  4. unset: 清除环境变量
  5. set: 显示本地定义的变量和环境变量

在这里插入图片描述
理解:
env: 显示所有环境变量,
set: 显示本地定义的变量和环境变量

在这里插入图片描述

我们定义本地变量bitt=1234,通过set可以查看,通过env却不可以,把bitt通过export设置成环境变量后,再次通过env则可以看到bitt=1234
在这里插入图片描述

获取环境变量
1.通过代码获取环境变量

1.1使用main函数的第三个参数来获取环境变量

我们平时使用main函数时,在main函数参数这一栏常常省略不写,或者填上(void)。实际上main函数是具有参数的,并且它的参数有很大作用。
具体关于main函数的三个参数作用可以参考某博主的【浅谈】main函数的三个参数,或者自行了解。

现在我们来说说main函数的第三个参数。

main函数的第三个参数接收的实际上就是环境变量表,我们可以通过main函数的第三个参数来获取系统的环境变量。
举个例子:
编写以下代码,生成可执行程序并运行:

#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来获取环境变量

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

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

2.通过系统调用获取环境变量

除了通过main函数的第三个参数和第三方变量environ来获取环境变量外,我们还可以通过系统调用getenv函数来获取环境变量。getenv函数可以根据所给环境变量名,在环境变量表当中进行搜索,并返回一个指向相应值的字符串指针。常用getenv和putenv函数来访问特定的环境变量

例如,使用getenv函数获取环境变量PATH的值。

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

12.进程地址空间

我们学习C/C++的时候一定了解过下面这张图,也就是程序地址空间分布图

在这里插入图片描述
按照之前的说法,在C/C++程序地址空间当中,地址的增长方向是自底向上。分别是代码段、已初始化全局数据区、未初始化全局数据区、堆区、栈区、命令行参数、环境变量。

其中,堆栈相对而生。栈向地址减小方向生长,堆向地址增大方向生长。堆栈之间的区域称为共享区。再往上,叫做内核空间。

1、代码段不是从0号地址开始的,它有一个确定的地址。
2、共享区里一般放的是动态库,共享内存等。

这是程序地址空间。但是呢程序是一个个死的代码,所以叫做程序地址空间不合适。程序运行起来之后,才会占用内存。所以我们称为进程地址空间,进程地址空间会在进程整个生命周期存在,直到进程退出。

下面,我们在Linux操作系统中,通过一些代码对该布局图进行一下验证:

在这里插入图片描述

下面将运行结果与图一一对应:
在这里插入图片描述
我们再对上图结果做个两个小小的测试:
1.分别定义四个堆区和栈区成员,通过地址打印看出两者 “对向而生”
在这里插入图片描述
2.static改变了变量的生命周期
在这里插入图片描述
了解这些以后,我们来看一段代码:

#include<stdio.h>
#include<stdlib.h>

int g_val=0;
在这里插入图片描述

代码当中用fork函数创建了一个子进程,然后让父子进程分别每隔一秒打印全局变量g_val的值和地址,其中让子进程在打印五次后将全局变量g_val的值从0改为100后打印。代码运行结果如下:
在这里插入图片描述
fork函数创建了一个子进程之后,代码共享,数据是各自私有一份的,打印五次之后,子进程值变为0,但是父子进程的地址却还是相同的,也就是说父子进程在同一个地址处读出的值不同

如果说我们是在同一个物理地址处获取的值,那必定是相同的,而现在在同一个地址处获取到的值却不同,这只能说明我们打印出来的地址绝对不是物理地址!!!

实际上,我们在语言层面上打印出来的地址都不是物理地址,而是虚拟地址。根据冯诺依曼体系要求,数据和代码一定是在物理内存上的,所以这就需要有由虚拟地址向物理地址转化这一过程,这一工作由操作系统完成。
物理地址用户一概是看不到的,是由操作系统统一进行管理的。

所以就算父子进程当中打印出来的全局变量的地址(虚拟地址)相同,但是两个进程当中全局变量的值却是不同的。

进程地址空间
下面我们就进程虚拟进程空间做更近一步理解:

概念:进程虚拟地址空间是操作系统为进程描述的一个“假”的地址空间。目的是为了让进程认为自己拥有一块连续的线性的完整的地址空间。但是实际上一个进程使用的内存并非连续存储的,而是通过页表映射了虚拟地址与物理地址之间的关系。为了让进程通过页表获取物理地址,进而实现数据的离散式存储。

进程地址空间本质上是内存中的一种内核数据结构,在Linux当中进程地址空间具体由结构体mm_struct实现。

进程地址空间就类似于一把尺子,尺子的刻度由0x00000000到0xffffffff,尺子按照刻度被划分为各个区域,而在结构体mm_struct当中,便记录了各个边界刻度,即各个区域的开始刻度与结束刻度,堆向上增长以及栈向下增长实际就是改变mm_struct当中堆和栈的边界刻度。
在这里插入图片描述
各个边界刻度之间的刻度都代表一个虚拟地址,这些虚拟地址通过页表映射与物理内存建立联系。虚拟地址是由0x00000000到0xffffffff线性增长的。

每个进程被创建时,其对应的进程控制块(task_struct)和进程地址空间(mm_struct)也会随之被创建。而操作系统可以通过进程的task_struct当中的一个结构体指针(存储的是mm_struct的地址)找到其mm_struct。

我们再来解释一下上面的例子:为什么同样的地址,呈现的却是不一样的内容?

父进程有自己的task_struct和mm_struct,该父进程创建的子进程也有属于其自己的task_struct和mm_struct,父子进程的进程地址空间当中的各个虚拟地址分别通过页表映射到物理内存的某个位置。
当子进程刚刚被创建时,子进程和父进程的数据和代码是共享的,即父子进程的代码和数据通过页表映射到物理内存的同一块空间
如下图:
在这里插入图片描述

当子进程通过页表映射要修改val值的时候,页表知道子进程要修改数据,所以会先在物理内存上重新开一块空间,并且将val值拷贝到新空间上,此时更新子进程的页表映射,使子进程上的val值映射到物理内存上新的位置。在此过程中,进程1和进程2的虚拟地址空间上val的空间是不会改变的。所以进程间就实现了数据独有。
在这里插入图片描述
这种等到需要更改数据时,再去申请内存,重新拷贝一份,并且更改页表的对应关系的过程称为写时拷贝技术

1、为什么数据要进行写时拷贝?

进程具有独立性。多进程运行,需要独享各种资源,多进程运行期间互不干扰,不能让子进程的修改影响到父进程。

2、为什么不在创建子进程的时候就进行数据的拷贝?

子进程不一定会使用父进程的所有数据,并且在子进程不对数据进行写入的情况下,没有必要对数据进行拷贝,我们应该按需分配,在需要修改数据的时候再分配(延时分配),这样可以高效的使用内存空间。

3、代码会不会进行写时拷贝?

90%的情况下是不会的,但这并不代表代码不能进行写时拷贝,例如在进行进程替换的时候,则需要进行代码的写时拷贝。

为什么要有进程地址空间?

  • 1、有了进程地址空间后,就不会有任何系统级别的越界问题存在了。例如进程1不会错误的访问到进程2的物理地址空间,因为你对某一地址空间进行操作之前需要先通过页表映射到物理内存,而页表只会映射属于你的物理内存
  • 2、有了进程地址空间后,每个进程都认为看得到都是相同的空间范围,包括进程地址空间的构成和内部区域的划分顺序等都是相同的,这样一来我们在编写程序的时候就只需关注虚拟地址,而无需关注数据在物理内存当中实际的存储位置。
  • 3、有了进程地址空间后,每个进程都认为自己在独占内存(一个富豪有十个儿子,彼此不认识,每个儿子都认为自己拥有富豪的全部资产),这样能更好的完成进程的独立性以及合理使用内存空间(当实际需要使用内存空间的时候再在内存进行开辟),并能将进程调度与内存管理进行解耦或分离。

学完这些,当再提到进程的时候,我们就应该有更深理解了,即进程的创建实际上伴随着其进程控制块(task_struct)进程地址空间(mm_struct)以及页表的创建。


– the End –

以上就是我分享的进程(相关概念),感谢阅读!

本文收录于专栏Linux
关注作者,持续阅读作者的文章,学习更多知识!
https://blog.csdn.net/weixin_53306029?spm=1001.2014.3001.5343

2022/1/19
————————————————

  • 22
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 22
    评论
评论 22
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

…狂奔的蜗牛~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值