一 冯诺依曼体系结构
在此之前,我们先要理解我们计算机的冯诺依曼体系结构,因为是进程的基础
我们所有的操作其实都是基于这样一个模型,比如你在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,我们还可以通用系统调用去查看;
通过系统调用获取进程标示符
#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也打印出来
#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){ //childprintf("I am child : %d!, ret: %d\n", getpid(), ret);}else{ //fatherprintf("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放上去、拿下来的动作 —— 这就叫做【进程切换】
这个速度很快,我们感受不到是很正常的
阻塞状态
- 阻塞 就是 进程 因等待某种条件就绪,而导致的一种不推进状态(比如等待 键盘输入)。
- 通俗的来说 阻塞 就是 进程卡住了,原因就是缺少资源
注意:这里要和上面运行状态的进程切换区分开,上面的是在运行,不缺条件,这里是因为条件的缺失而去等待资源
那么进程
需要什么资源呢?
- 比如
磁盘
、网卡
、显卡
等各种外设 - 假设你现在想在
steam
上下载游戏,当你点击下载按钮后提示磁盘空间不足,此时是无法运行steam下载
这个进程的,因为此进程
需要等待足够大的磁盘资源
- 此时我们就称此
进程
为阻塞
状态
和上面的注意关联起来
总结:进程阻塞就是不被调度
- 此时 PCB(task_struct) 就会被设置为 阻塞状态,并链入等待的资源提供的等待队列。
- 没错,这里的等待队列 类似于 CPU 运行队列
挂起状态
- 当
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+才表示是运行状态,哪为什么 会这样呢?
再看一段代码就知道了
#include <stdio.h>#include <unistd.h>int main(void){while(1); {//printf("Hello process, pid: %d\n",getpid());//sleep(1);}return 0;}
这样就变成了运行状态
- 原因就在于
printf
打印语句它是属于 IO流 的一种,第一次因为是循环的缘故,它一直在等IO设备就绪,所以其进程状态就一直为S+
,对应的即是在操作系统中的【阻塞状态】;但是当我们去掉printf
这种IO流之后呢,它就是在纯纯运行,没有IO,那也就变成了R
状态
- 这里的
R+
代表的就是这个进程是在前台运行的,所以我们在输入任何指令后不会对其造成 任何的影响
- 那若是我们不以正常的方式去启动这个进程的话,其进程的状态就会不一样了,可以看到我在
./mytest
的后面加上了一个&
;那么其状态变成了R
,此代表的意思就是这个进程它是运行在了【后台】的
- 不过呢,R状态并不代表这个进程就在运行,而代表其在运行队列中排队而已.
所以总的一句话来说就是
"+"代表是前台运行,无"+"代表后台运行,后台运行时可在命令行继续输入指令并执行,但无法用ctrl+c结束,需要用kill -9 pid杀死。想要后台运行某个程序就在后面加"&",如:./test &
浅度睡眠状态
这个状态上面也提到过就上S+这个状态,等待io设备的输入
#include <stdio.h>#include <unistd.h>int main(){int a = 0;printf("Enter# ");scanf("%d", &a);printf("echo : %d\n", a);return 0; }
- 将该进程运行起来我们可以看到其是出于
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号信号,就可以杀掉这个进程
- 第二种方法就是通过这个进程的名称来杀掉它
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){ //childint 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){ //fatherwhile(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,这样便于观察现象
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{printf("I am running...\n");pid_t id = fork();if(id == 0){ //childint count = 5;while(1){printf("I am child...PID:%d, PPID:%d\n", getpid(), getppid(), count);sleep(1);}}else if(id > 0){ //fatherint 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资源的先后顺序。
查看进程优先级信息
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个级别。
注意: 在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的某一路径下。
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指针的内容交换,就相当于让过期队列变成活动队列,活动队列变成过期队列,就相当于又具有了一批新的活动进程,如此循环进行即可。