【Linux】——进程概念(万字解读)

HackerElite 2024-08-19 12:07:03 阅读 81

一  冯诺依曼体系结构

在此之前,我们先要理解我们计算机的冯诺依曼体系结构,因为是进程的基础

我们所有的操作其实都是基于这样一个模型,比如你在qq上,和别人发送消息,这个消息肯定是先通过输入设备进行输入,输入到存储器(这里只是单指内存),然后通过控制器和运算器的控制和计算,把消息发送到输出设备(这里可以是网卡和显示器)

那我们这些操作肯定需要有人控制吧,没有人控制,单凭一个cpu能去接受这么多信息?别完了,cpu的运行速度是很快的,如果这些事情都要他去处理,那就没有那么高效了。

所有我们引出了操作系统这个概念

二  操作系统

概念

任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。笼统的理解,操作系统包括:

内核(进程管理,内存管理,文件管理,驱动管理)

其他程序(例如函数库,shell程序等等)

设计OS的目的

与硬件交互,管理所有的软硬件资源

为用户程序(应用程序)提供一个良好的执行环境

定位

在整个计算机软硬件架构中,操作系统的定位是:一款纯正的“搞管理”的软件

由于我们的文件和进程错综复杂,都会占据内存,同时也有谁先进行谁后进行的顺序,所以这里我们需要一个管理者去管理好这些资源,不然我们的电脑用几下就会导致死机,所以我们开机第一个启动的软件就是操作系统这个系统软件

管理

那我们操作系统是用什么方式去管理这些资源的呢?? 

这张图表示了我们在进行操作的轮廓图,操作系统在管理底层的硬件的时候,并不能直接去访问,而是应该经过驱动程序间接访问,所以操作系统在管理硬件资源的时候,只是拿到了硬件的数据,并没有直接见到了底层的硬件,既然我们拿到了数据,我们就可以通过数据去判断这个硬件程序是否需要关闭,当然这些数据太多了,而且有些属于一个类型,有些不是,那既然这样,我们就可以通过描述这个硬件的基本属性,同时加上他的数据,这样归纳起来就可以统一管理了,但是这么多硬件,怎么去找呢??创建一个双链表,一个红黑树?把他们管理起来?,可行!!

所以经过以上步骤就形成了一个先描述,再组织的操作

之前说了,只能从上到下访问,那我们电脑上的软件有各种功能,这些功能直接或者间接的操作了我们操作系统,那是不是这些软件可以肆意妄为??并不是!操作系统会防止用户去乱动操作系统里面的东西,但是它又得让用户去访问,于是就有了系统调用这个概念,由于系统调用的存在,使得我们可以在电脑上开发各种应用,使得我们的电脑功能性更全,这种选择性提供也是操作系统管理的一种手段

总结

计算机管理硬件,先用struct结构体描述,再用多种数据结构进行组织,这里为什么是struct呢?因为linux是用c语言写的。

系统调用和库函数概念

在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。

系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。

三  进程

有了上面的认识,我们就可以再来认识进程的概念,一个进程也就是我们所运行的程序,一个qq,一个微信,或者一个英雄联盟,他们都属于进程

所以一句话概括就是进程就是用来吃系统资源的——担当分配系统资源(CPU时间,内存)的实体。

进程也是受操作系统所管理的,那操作系统怎么管理?,和上面一样——先描述再组织

描述进程

对于进程我们得先描述。进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。

课本上称之为PCB(process control block),Linux操作系统下的PCB是: task_struct

这个PCB里面就包含了进程的诸多信息,比如:优先级,上下文数据,指针之类的

在linux下的PCB——task_struct

✨标示符: 描述本进程的唯一标示符,用来区别其他进程。

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

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

✨程序计数器: 程序中即将被执行的下一条指令的地址。

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

✨上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。

✨I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。

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

✨其他信息

组织进程

所有运行在系统里的进程都以task_struct链表的形式存在内核里。

查看进程

查看一个进程,查看的是进程的信息

我们可以通过 ls /proc这个指令查看所有进程的信息

如果我们现在运行一个程序

 如果我们想看他的信息,那么我们就使用一个看起来比较直观的命令(ps/top),因为/proc这个指令可能看起来并不直观,因为/proc 目录下的文件和目录提供的是系统的底层信息,它们的内容通常是以文本形式展示的,但格式和结构可能相对复杂。

这里我们使用ps查看,我们先运行这个程序

这里可能查看到三个进程,但是只有第一个是我们要查的,其他两个是因为我们输入的指令也是一个进程,所以也把他们的进程信息输出了

 这就是进程的pid也就是进程的标识符,linux也是通过这个去区分进程的不同的

除了这种方法可以获取进程的pid,我们还可以通用系统调用去查看;

通过系统调用获取进程标示符

<code>#include <stdio.h>

#include <unistd.h>

#include<sys/types.h>

int main()

{

printf("pid:%d\n",getpid());

return 0;

}

通过这段代码就可以获取到该进程的pid

运行这段程序就可以获得该进程的pid,但是我们注意到其实他还有一个表示符 

这一段是他们的父进程的pid,也就是ppid,每个子进程都有一个父进程,当然,最大的父亲就是我们的bash

我们也可以通过系统调用把他的ppid也打印出来

<code>#include <stdio.h>

#include <unistd.h>

#include<sys/types.h>

int main()

{

printf("pid:%d ppid:%d\n",getpid(),getppid());

return 0;

}

通过系统调用创建进程-fork初识

对于fork也是一个系统调用,他的作用就是创建一个子进程

#include <stdio.h>

#include <sys/types.h>

#include <unistd.h>

int main()

{

int ret = fork();

if(ret < 0)

{

perror("fork");

return 1;

}

else if(ret == 0)

{ //child

printf("I am child : %d!, ret: %d\n", getpid(), ret);

}

else

{ //father

printf("I am father : %d!, ret: %d\n", getpid(), ret);

}

sleep(1);

return 0;

}

对于上面的代码来说,因为fork创建子进程会有一个返回值,如果创建成功对于子进程会返回0,对于父进程会返回子进程的pid,如果失败则是小于0的值

所以根据返回值的不同我们就可以去执行两块程序

fork之后的代码子进程和父进程是共享的

那现在可能会困惑为什么一个变量会有两个返回值,就算是两个进程,但是也就一个变量,一个变量的地址是固定,那么就不可能存在一个变量有两个值的情况

所以我们得先了解虚拟地址空间这个概念

文字说明:我们创建进程的时候会有操作系统先给进程描述一个PCB在linux里也叫tast_struct

这里面包含了进程的信息,既然是信息那么就会包括数据的存放地址,所以这里这就有一个虚拟地址的指针,指向这块内存,这块内存就是我们之前认识到的什么栈和堆,静态区,数据段...这类的空间

这个空间并不是实际数据存放的空间,它的出现只是为了方便管理,形成统一性,那我们怎么去找到物理内存呢(也就是实际的地址),这里采取的方式就算通过页表去映射,把虚拟内存和物理内存的地址一 一 对应起来,如果我们创建子进程,操作系统会创建子进程的PCB,然后会把父进程的虚拟内存的表继承下来,这样,子进程和父进程虚拟内存相同,但是有两份,因为子进程和父进程的代码相同(因为是继承下来的),又因为进程之间是相互独立互不干扰的,所以当子进程中的变量改变的时候,会产生临时拷贝,也就是对于发生改变的数据,会在物理内存中产生一个新的位置,然后再通过页表的映射,改变对应的虚拟内存的映射,这样就完成了两个变量有不同的值,虽然他们的虚拟地址是相同的,但是物理地址却是不同的

图片说明:

这里的进程空间其实是内存的一种数据结构 

注意:这里不要理解为是一个存在的空间,只是一种数据结构而已

进程地址空间就类似于一把尺子,尺子的刻度由0x00000000到0xffffffff,尺子按照刻度被划分为各个区域,例如代码区、堆区、栈区等。而在结构体mm_struct当中,便记录了各个边界刻度,例如代码区的开始刻度与结束刻度,如下图所示:

 

 

进程状态

运行状态

进程 PCB 被调度到 CPU 运行队列中且已被分配 CPU 资源,就叫做 ------ 运行状态

每个进程都有自己的PCB+自己的数据构成,当我们存在多个进程的时候,我们就需要用特定的数据结构把他们组织起来,比如链表,平衡树之类的,这样我们就可以很轻松的通过一个进程找到其他的进程

所以需要运行的进程,会把他们的PCB放在CPU的运行队列中,然后通过相应的数据结构组织起来,为了更加形象下面给出一张图片说明

这张图就是用的双向链表把他们组织起来的,可能还会用到其他的数据结构,因为操作系统的组织比这个可复杂太多了 

如果这个进程需要执行了,就把代码和数据放进CPU中就行了

那如果这个进程运行了很久呢??那不是一直等他完成?

不是,每一个进程都有一个叫做:时间片的概念! 其时间大概是在10 ms左右。所以并不是一个进程一直在执行,而是这多个进程在一个时间段内所有代码都会被执行 —— 这就叫做【并发执行】 所以呢这就一定会存在大量的进程 被CPU放上去、拿下来的动作 —— 这就叫做【进程切换】 

这个速度很快,我们感受不到是很正常的

阻塞状态

阻塞 就是 进程 因等待某种条件就绪,而导致的一种不推进状态(比如等待 键盘输入)。通俗的来说 阻塞 就是 进程卡住了原因就是缺少资源

注意:这里要和上面运行状态的进程切换区分开,上面的是在运行,不缺条件,这里是因为条件的缺失而去等待资源

那么<code>进程需要什么资源呢?

比如 磁盘网卡显卡 等各种外设假设你现在想在 steam 上下载游戏,当你点击下载按钮后提示磁盘空间不足,此时是无法运行 steam下载 这个进程的,因为此 进程 需要等待足够大的 磁盘资源此时我们就称此 进程 为 阻塞 状态

和上面的注意关联起来

总结:进程阻塞就是不被调度

此时 PCB(task_struct) 就会被设置为 阻塞状态,并链入等待的资源提供的等待队列。没错,这里的等待队列 类似于 CPU 运行队列 

 挂起状态

当 <code>CPU 资源紧张时,将 进程的数据和代码 交换至 磁盘 中挂起,此时内存中只有 PCB

 挂起 可以看作一种特殊的 -- 阻塞状态

可能你买苹果电脑的时候就会考虑内存够不够用的问题,也许你从网上看了很多,有说够用,有说不够用,所以这里很多人提到的词就swap,这里的swap就是当我们的内存不够用的时候,会把代码和数据放进磁盘里面,等到需要用的时候,再拿出来,这个操作就算swap,这个是有成本的,所以多次的swap可能会导致电脑性能下降,发热等等

当计算机资源比较吃紧时,操作系统一定要确保自身不会因为资源的紧张而崩溃,所以就会将一些等待资源(阻塞)的进程的代码和数据交换到磁盘的 swap分区 中,这个过程称为唤出。当需要调度此进程时,就会将磁盘的 swap分区 中保存的内容换回到内存中,这个过程称为唤入

注意:交换的是进程的代码和数据,不是PCB!!如果PCB被交换出内存了,那操作系统如何管理呢?

所以当某个进程的代码和数据不在内存中,而被换出到磁盘上时,该进程就为挂起状态。

linux系统下的7种进程状态

运行状态

我们先看一段代码

#include <stdio.h>

#include <unistd.h>

int main(void)

{

while(1);

{

printf("Hello process, pid: %d\n",getpid());

sleep(1);

}

return 0;

}

从图中可以看到状态是S+,实际上却是R+才表示是运行状态,哪为什么 会这样呢?

再看一段代码就知道了

<code> #include <stdio.h>

#include <unistd.h>

int main(void)

{

while(1);

{

//printf("Hello process, pid: %d\n",getpid());

//sleep(1);

}

return 0;

}

 

这样就变成了运行状态

原因就在于 <code>printf 打印语句它是属于 IO流 的一种,第一次因为是循环的缘故,它一直在等IO设备就绪,所以其进程状态就一直为 S+,对应的即是在操作系统中的【阻塞状态】;但是当我们去掉 printf 这种IO流之后呢,它就是在纯纯运行,没有IO,那也就变成了 R 状态

这里的 R+ 代表的就是这个进程是在前台运行的,所以我们在输入任何指令后不会对其造成 任何的影响

那若是我们不以正常的方式去启动这个进程的话,其进程的状态就会不一样了,可以看到我在 <code>./mytest 的后面加上了一个 &;那么其状态变成了 R,此代表的意思就是这个进程它是运行在了【后台】的

 不过呢,R状态并不代表这个进程就在运行,而代表其在运行队列中排队而已.

所以总的一句话来说就是

 "+"代表是前台运行,无"+"代表后台运行,后台运行时可在命令行继续输入指令并执行,但无法用ctrl+c结束,需要用kill -9 pid杀死。想要后台运行某个程序就在后面加"&",如:./test & 

浅度睡眠状态

这个状态上面也提到过就上S+这个状态,等待io设备的输入

<code> #include <stdio.h>

#include <unistd.h>

int main()

{

int a = 0;

printf("Enter# ");

scanf("%d", &a);

printf("echo : %d\n", a);

return 0;

}

将该进程运行起来我们可以看到其是出于 <code>S+ 的状态,因为【shell】此时正在等待用户的输入,这个就对应到了我们上面所讲到的 阻塞状态

深度睡眠状态

除了【浅度睡眠】之外呢,还有一种叫做【深度睡眠】,它们俩呢,都是 阻塞状态

对于浅度睡眠来说,之所以称为 “浅度”,是有原因的:也就是处于这种状态的进程容易被唤醒。例如说我们在上面所讲到的这个处于阻塞状态的进程,我们使用 kill -9 PID 向这个进程发送【9号信号】,那么这个进程就被杀死了,你也可以认为被唤醒了

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

例如,某一进程要求对磁盘进行写入操作,那么在磁盘进行写入期间,该进程就处于深度睡眠状态,是不会被杀掉的,因为该进程需要等待磁盘的回复(是否写入成功)以做出相应的应答。(磁盘休眠状态)

如果在这个过程中,操作系统能够杀死该进程,那么就有可能丢失数据。

所以也就是因为这样,操作系统不敢去打扰这个进程

暂停状态

首先我们要通过下面这句命令来查看一下对应的进程信号

kill - l

我们使用的就是18,19信号

暂停进程

kill -19 PID

启动进程

kill -18 PID

那可能就有疑问了,这个暂停状态和上面的睡眠状态的区别是什么呢??

stopped状态 进程 完全暂停了, 其不会再接收任何信号了一个进程通过 stopped 状态可以控制另一个S 和 D 一定是在等待某种资源,而 T状态 可能在等待某种资源,也可能被其他进程控制

死亡状态

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

第一种方法就是向这个进程发送9号信号,就可以杀掉这个进程

第二种方法就是通过这个进程的名称来杀掉它

<code>kill -9 PID

killall 进程

僵尸状态

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

之所以有这个状态就是因为,我门需要对退出的进程进行资源的查看,不能直接就退出了,可能该进程里有我们需要的信息或者资源

例如,对于以下代码,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(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;

}

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

while :; do ps axj | head -1 && ps axj | grep proc | grep -v grep;echo "######################";sleep 1;done

僵尸进程的危害

1.僵尸进程的退出状态必须一直维持下去,因为它要告诉其父进程相应的退出信息。可是父进程一直不读取,那么子进程也就一直处于僵尸状态。

2.僵尸进程的退出信息被保存在task_struct(PCB)中,僵尸状态一直不退出,那么PCB就一直需要进行维护。

3.若是一个父进程创建了很多子进程,但都不进行回收,那么就会造成资源浪费,因为数据结构对象本身就要占用内存。

4.僵尸进程申请的资源无法进行回收,那么僵尸进程越多,实际可用的资源就越少,也就是说,僵尸进程会导致内存泄漏。

进程一般退出的时候,一般其不会立即彻底退出。如果父进程没有主动回收子进程信息,子进程会一直让自己处于Z状态,这也是为了方便后续父进程读取子进程的相关退出结果。

孤儿状态

在Linux当中的进程关系大多数是父子关系,若子进程先退出而父进程没有对子进程的退出信息进行读取,那么我们称该进程为僵尸进程但若是父进程先退出,那么将来子进程进入僵尸状态时就没有父进程对其进行处理,此时该子进程就称之为孤儿进程。

若是一直不处理孤儿进程的退出信息,那么孤儿进程就会一直占用资源,此时就会造成内存泄漏。因此,当出现孤儿进程的时候,孤儿进程会被1号init进程领养,此后当孤儿进程进入僵尸状态时就由int进程进行处理回收。

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

同时我们一直打印子进程的PID与PPID,这样便于观察现象

<code>#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;

}

四  进程优先级

基本概念

什么是优先级?

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

优先级存在的原因?

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

查看进程优先级信息

<code> ps -al

列出的信息当中有几个重要的信息,如下:

UID:代表执行者的身份。PID:代表这个进程的代号。PPID:代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号。PRI:代表这个进程可被执行的优先级,其值越小越早被执行。NI:代表这个进程的nice值。

PRI与NI

1.PRI代表进程的优先级(priority),通俗点说就是进程被CPU执行的先后顺序,该值越小进程的优先级别越高。

2.NI代表的是nice值,其表示进程可被执行的优先级的修正数值。

3.PRI值越小越快被执行,当加入nice值后,将会使得PRI变为:PRI(new) = PRI(old) + NI。

4.若NI值为负值,那么该进程的PRI将变小,即其优先级会变高。

5.调整进程优先级,在Linux下,就是调整进程的nice值。

6.NI的取值范围是-20至19,一共40个级别。

<code>注意: 在Linux操作系统当中,PRI(old)默认为80,即PRI = 80 + NI。

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

top命令就相当于Windows操作系统中的任务管理器,它能够动态实时的显示系统当中进程的资源占用情况。

使用top命令后按“r”键,会要求你输入待调整nice值的进程的PID。

 输入进程PID并回车后,会要求你输入调整后的nice值。

输入nice值后按“q”即可退出,如果我们这里输入的nice值为10,那么此时我们再用ps命令查看进程的优先级信息,即可发现进程的NI变成了10,PRI变成了90(80+NI)。

注意: 若是想将NI值调为负值,也就是将进程的优先级调高,需要使用sudo命令提升权限。 

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

使用renice命令,后面跟上更改后的nice值和进程的PID即可。

四个重要概念

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

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

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

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

五  环境变量

基本概念

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

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

常见环境变量

PATH: 指定命令的搜索路径。HOME: 指定用户的主工作目录(即用户登录到Linux系统中的默认所处目录)。SHELL: 当前Shell,它的值通常是/bin/bash。

查看环境变量的方法

我们可以通过echo命令来查看环境变量,方式如下:

echo $环境变量名称 

测试PATH

大家有没有想过这样一个问题:为什么执行ls命令的时候不用带./就可以执行,而我们自己生成的可执行程序必须要在前面带上./才可以执行?

要执行一个可执行程序必须要先找到它在哪里,既然不带./就可以执行ls命令,说明系统能够通过ls名称找到ls的位置,而系统是无法找到我们自己的可执行程序的,所以我们必须带上./,以此告诉系统该可执行程序位于当前目录下。

 这和linux一切皆文件就是一个道理了,指令也是一个可执行程序,我们需要找到它才能执行它

而系统就是通过环境变量PATH来找到ls命令的,查看环境变量PATH我们可以看到如下内容:

 上图就是默认的搜索路径

可以看到环境变量PATH当中有多条路径,这些路径由冒号隔开,当你使用ls命令时,系统就会查看环境变量PATH,然后默认从左到右依次在各个路径当中进行查找。

而ls命令实际就位于PATH当中的某一个路径下,所以就算ls命令不带路径执行,系统也是能够找到的。

那既然是这样,我们就可以把我们写的程序的路径加上去,这样,默认搜索也可以搜索到我们的程序了

方式一:将可执行程序拷贝到环境变量PATH的某一路径下。

 

<code> sudo cp proc /usr/bin

因为涉及到权限,所以需要sudo命令

方式二:将可执行程序所在的目录导入到环境变量PATH当中。

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

export PATH=$PATH:/home/cl/dirforproc/ENV

测试HOME

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

 

测试SHELL

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

而该命令行解释器实际上是系统当中的一条命令,当这个命令运行起来变成进程后就可以为我们进行命令行解释。

 和环境变量相关的命令

1、echo:显示某个环境变量的值。

2、export:设置一个新的环境变量。

3、env:显示所有的环境变量。

4、set:显示本地定义的shell变量和环境变量。

 注意:more 命令类似 cat ,不过会以一页一页的形式显示,更方便使用者逐页阅读,而最基本的指令就是按空白键(space)就往下一页显示,按 b 键就会往回(back)一页显示,而且还有搜寻字串的功能(与 vi 相似)

5、unset:清除环境变量。 

环境变量的组织方式

每个程序都会收到一张环境变量表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串,最后一个字符指针为空。

通过代码获取环境变量

main函数其实有三个参数,只是我们平时基本不用它们,所以一般情况下都没有写出来。

我们可以在Windows下的编译器进行验证,当我们调试代码的时候,若是一直使用逐步调试,那么最终会来到调用main函数的地方。

我们可以在linux环境下编写以下代码

现在我们来说说main函数的前两个参数,main函数的第二个参数是一个字符指针数组,数组当中的第一个字符指针存储的是可执行程序的位置,其余字符指针存储的是所给的若干选项,最后一个字符指针为空,而main函数的第一个参数代表的就是字符指针数组当中的有效元素个数

 

 main函数的第三个参数接收的实际上就是环境变量表,我们可以通过main函数的第三个参数来获取系统的环境变量。

 除了使用main函数的第三个参数来获取环境变量以外,我们还可以通过第三方变量environ来获取。

这里的extern修饰的变量是指这个变量的的声明在其他文件中,并不在当前文件和头文件中

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

除了通过main函数的第三个参数和第三方变量environ来获取环境变量外,我们还可以通过系统调用getenv函数来获取环境变量。

getenv函数可以根据所给环境变量名,在环境变量表当中进行搜索,并返回一个指向相应值的字符串指针。

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

因为是字符串指针返回,所以打印的时候用%s打印就行了

 

六  Linux2.6内核进程调度队列

一个CPU拥有一个runqueue

如果有多个CPU就要考虑进程个数的父子均衡问题。

 

优先级

queue下标说明:

普通优先级:100~139。实时优先级:0~99。

我们进程的都是普通的优先级,前面说到nice值的取值范围是-20~19,共40个级别,依次对应queue当中普通优先级的下标100~139。

注意: 实时优先级对应实时进程,实时进程是指先将一个进程执行完毕再执行下一个进程,现在基本不存在这种机器了,所以对于queue当中下标为0~99的元素我们不关心。

也就是说,这种方式的机器太慢了,因为需要等一个进程完毕以后才弄下一个进程,计算机的任务可是很重的,这种时间的损耗对于计算机来说是很大的

活动队列

时间片还没有结束的所有进程都按照优先级放在活动队列当中,其中nr_active代表总共有多少个运行状态的进程,而queue[140]数组当中的一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进程排队调度。

调度过程如下:

  1.从0下标开始遍历queue[140]。

  2.找到第一个非空队列,该队列必定为优先级最高的队列。

  3.拿到选中队列的第一个进程,开始运行,调度完成。

  4.接着拿到选中队列的第二个进程进行调度,直到选中进程队列当中的所有进程都被调度。

  5.继续向后遍历queue[140],寻找下一个非空队列。

注意:bitmap[5]:queue数组当中一共有140个元素,即140个优先级,一共140个进程队列,为了提高查找非空队列的效率,就可以用5 × 32个比特位表示队列是否为空,这样一来便可以大大提高查找效率。这里相当于位图的结构,1代表有,0代表没有

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

过期队列

  过期队列和活动队列的结构相同。

  过期队列上放置的进程都是时间片耗尽的进程。

  当活动队列上的进程被处理完毕之后,对过期队列的进程进行时间片重新计算。

active指针和expired指针

  active指针永远指向活动队列。

  expired指针永远指向过期队列。

由于活动队列上时间片未到期的进程会越来越少,而过期队列上的进程数量会越来越多(新创建的进程都会被放到过期队列上),那么总会出现活动队列上的全部进程的时间片都到期的情况,这时将active指针和expired指针的内容交换,就相当于让过期队列变成活动队列,活动队列变成过期队列,就相当于又具有了一批新的活动进程,如此循环进行即可。



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。