【linux】--- 进程概念
进程概念
- 1.认识冯诺依曼结构
- 2. 操作系统(Operator system)
- 2.1 概念
- 2.2 设计OS的目的
- 2.3 理解操作系统
- 2.4 如何理解管理
- 2.5 理解系统调用和库函数
- 3. 进程
- 3.1 基本概念和基本操作
- 3.1.1 描述进程 - PCB
- 3.1.2 task_struct
- 3.1.3 查看进程
- 3.2 进程状态
- 3.2.1 运行&&阻塞&&挂起
- 3.2.2 课本上的说法
- 3.2.3 理解内核链表
- 3.2.4 Linux中的进程状态
- 3.2.5 孤儿进程
- 3.3 进程优先级
- 3.3.1 基本概念
- 3.3.2 查看系统进程
- 3.3.3 补充部分概念
- 3.4进程切换与调度
- 3.4.1死循环进程如何运行
- 3.4.2 CPU和寄存器
- 3.4.3 进程如何切换
- 3.4.4 Linux2.6内核进程O(1)调度队列
- 4. 环境变量
- 4.1 基本概念
- 4.2 命令行参数
- 4.2 认识一个环境变量
- 4.3 认识更多的环境变量
- 4.4 获取环境变量的方法
- 4.5 理解环境变量
- 5. 程序地址空间
- 5.1 程序地址空间回顾
- 5.2 虚拟地址
- 5.3 虚拟地址空间
- 5.5 为什么要有虚拟地址空间呢?
1.认识冯诺依曼结构
这就是现代计算机的体系结构,优化了以运算器为中心的冯诺依曼体系,以存储器为中心。
下面给出一些概念:
输入设备:键盘、鼠标、话筒、磁盘、网卡等。
输出设备:显示器、打印机、磁盘、网卡。
中央处理器(CPU):包含了运算器和控制器,其实现代的CPU还集成了内存中的MAR、MDR,Cache、一些通用寄存器等。
运算器:完成算术运算和逻辑运算。
控制器:计算机的指挥中心,指挥计算机完成取指令、分析指令、执行指令。
存储器:指的是内存或者是主存,所有设备都只能直接和内存打交道。
主机:运算器、控制器、内存。
外设:输入设备、输出设备、外存。
I/O:站在内存的角度,输入设备向内存输入数据就是I(Input),内存向输入设备输出数据就是O(Output)。
摩尔定律:当价格不变时,集成电路上可容纳的晶体管数目大约每 18 到 24 个月翻一倍,计算机的性能也将随之提升
下面抛出一些问题并给出答案:
- 我们知道程序运行之前需要加载到内存,为什么需要先加载到内存后运行呢,那程序运行之前在哪呢?
1.从上面的现代计算器体系结构中可以看出,CPU获取数据只能通过内存,在数据层面之和内存打交道,所谓程序运行就是CPU运行代码、访问数据的过程。程序运行前就是磁盘上的二进制文件,这里的加载就是I(Input).
2. 数据从输入设备-> 内存 -> cpu -> 内存 -> 输出设备,这其实是一个数据拷贝的过程,所以体系结构的效率由设备的"拷贝”效率决定。
- 为什么体系结构一定要存储器的存在呢?输入设备 --> CPU – > 输出设备这样不可以嘛,这里涉及到存储分级。
计算机内有各级存储原件,距离CPU越近,存储容量越小、速度越快、成本更高,距离CPU越远,存储容量越大、速度越快、成本更低。
若没有存储器缓冲,输入输出设备直接与CPU相接,系统的效率就由较慢的外部设备决定了(木桶原理),CPU的大部分时间都是空闲的。
存储器(缓存机制、局部性原理和层次化存储策略)的设置是主要意义在于在成本、容量和速度之间取得最佳平衡,当代计算机是性价比的产物。
- 从硬件的角度来理解数据流动
一个北京的网友通过电脑给南京的网友发信息的图示,请按照现代计算机的体系结构分析数据流动。
北京网友键盘输入(输入设备)的信息被在内存中运行的聊天软件拿到,然后送给运算器加密封包等操作后返回给内存、然后通过网卡(输出设备)、网络传输到南京网友的网卡(输入设备),后加载到内存,进入运算器解密等,回到内存,最后刷新到设备屏幕上(输出设备)。
2. 操作系统(Operator system)
2.1 概念
操作系统是⼀个基本的程序集合,是一款管理软硬件的软件,操作系统包括内核和其他程序。
- 内核(进程管理,内存管理,文件管理,驱动管理),这是狭义上的操作系统,是最核心的部分。
- 其他程序(例如函数库,shell程序等等)
安卓系统的内核程序就是基于Linux的。
2.2 设计OS的目的
- 对下,与硬件交互,管理所有的软硬件资源
- 对上,为用户程序(应用程序)提供⼀个良好的执行环境
- 软硬件体系结构为层状结构,设计的思想为高内聚低耦合。
- 访问操作系统,必须通过系统调用(系统提供的函数),例如C语言的printf函数,本质就是封装了系统调用,通过操作系统对驱动程序进行访问,最后把数据交给硬件。
- 我们的程序,只要判断出其访问了硬件,那么就必须贯穿整个软硬件体系。
2.3 理解操作系统
在整个计算机软硬件架构中,操作系统的定位是:⼀款纯正的“搞管理”的软件。
2.4 如何理解管理
管理的例子:校长、辅导员、学生。
校长:管理者(决策) 类比操作系统
辅导员:(执行) 类比驱动程序
学生:被管理者 类比底层硬件
- 实际上管理者和被管理者不需要直接接触,管理的必要条件不是直接接触,而是管理者可以拿到被管理者的相关数据,例如:校长可以根据一个学生的绩点靠前而发放奖学金,也可以把一个开除一个多门挂科的学生,学生拿到奖学金或者从学校被开除滚回家甚至可以不和校长接触。重要的是如何拿到数据呢?在校长和学生之间有一个辅导员,管理员收集你的信息到教务系统,这样校长就可以通过教务系统拿到数据并对数据做管理了。同理:系统和硬件不需要接触,系统需要的信息从驱动那里获取,系统据对硬件做管理也是通过驱动进行的,系统对硬件的管理本质上是对有关硬件的数据进行管理。
- 假设校长开始是通过execl表格对一个学校的学生进行管理的,例如给绩点最高的学生发放校长奖学金,校长就需要在execl表格中对遍历所有的学生信息,这样效率是低下的。然后校长学会了C语言,把学生描述为了一个结构体 struct_student,结构体中定义了姓名、性别、身高、电话等基本信息。一个学生对应一个结构体变量。后来校长学习了数据结构又在每个struct_student结构体中添加了一个struct_student* 类型的指针,把全体学生组织成了一个链表并实现了了排序等方法,这样校长很方便的找到绩点最高的学生了。校长把对学生的管理转变成了对链表的增删查改。
- 上面这个过程就是一种建模的过程,可以精简为先描述再组织,这6个字适用于对任何管理场景的建模。例如:操作系统是如何管理所有的硬件的,操作系统可以在内部把所有硬件描述为一个结构体或者类,成员包括了硬件的各种信息,这样每个硬件对应了一个结构体变量或者类对象,操作系统对硬件的管理就变成了对各种数据结构的管理。同理操作系统对进程如何管理呢?先描述为task_struct,然后组织成合适的数据结构,这样就把对进程的管理转变为对数据结构的管理。C提供的结构体/C++提供的类就是解决先描述的问题,C++提供的STL(各种数据结构和算法)解决的是组织的问题。各个高级语言中的类特性和数据结构与算法的存在是历史的必然,因为其解决了在计算机中对现实世界建模和各种高效操作的需求。。
操作系统需要对各类场景被描述为的数据进行管理,所以操作系统一定会充满大量的数据结构和该数据结构匹配的算法。
2.5 理解系统调用和库函数
操作系统不相信任何用户,不允许用户访问其的任何细节,但是操作系统还需要给用户提供服务,所以向上给出了封装好的系统调用。一般的系统如:windows、Linux、macos都是C语言来写的,所以系统提供的系统调用都是C风格的函数。函数的参数是用户给操作系统的,返回值是操作系统给用户的,所以系统调用的本质就是用户和操作系统之间的数据交互。
小白不了解系统,进而就不理解系统调用的参数和返回值,使用系统调用的成本很高,并且系统调用的功能比较基础,所以开发者对系统调用进行了各种的封装进而形成了各种的库,并提供了各种shell外壳程序(例如:图形化界面)和指令,这样就降低了用户使用系统的成本。
下面举个例子说明一下:
银行不相信用户,不允许用户进入银行内部,但是还要给用户提供存钱和取钱等服务,所以银行提供了窗口和工作人员,我们可以给工作人员说我要取100块,工作人员按照流程后就会取出100块给你。银行的业务处理的流程一般比较繁琐,所以银行一般会配备一个大堂经理,帮助一些年长者完成业务。
如何判断一个库函数是否封装了系统调用?库函数如果最终访问了硬件,那么就一定封装了系统调用。
3. 进程
操作系统的进程的管理也是表现为先描述,再组织。
3.1 基本概念和基本操作
课本概念:程序的一个执行实例或者正在执行的程序等。
内核概念:担当分配系统资源(CPU时间,内存)的实体。
3.1.1 描述进程 - PCB
当一个计算机开机时,操作系统会被加载到内存中,在我们使用的时候,很多的程序在一段时间内也在内存中运行,这些程序需要被操作系统管理和调度,所以系统需要对这些程序进行描述和组织,使用C语言的结构体对进程的所有属性进行描述,例如:代码地址、数据地址、程序状态、优先级等,然后选取合适的数据结构对程序的对应的结构体进行组织,这样系统可以通过对该结构体的管理实现了对进程的管理。图示如下:
上面提到的结构体就是进程控制块(Process Control Block)
,其包含了对应程序的所有属性,Linux下的PCB叫做task_struct
,这里给出进程的通俗的概念:进程 = 内核数据结构+自己的代码和数据 ,在Linux下可以叫做PCB(task_struct)+自己的代码和数据,这样对进程的管理就变成了对进程链表(数据结构)的增删查改。
举个例子:
找工作的时候,需要提交个人简历,这个个人简历就是对自己的描述,找工作本质不是自己在找工作,而是简历在找工作,自己的个人简历被组织在一打简历里面,这里就是一个简历队列,面试官筛选的不是人,而是简历。这里的简历就是PCB,人就是程序的数据,面试官就是CPU。
3.1.2 task_struct
进程的属性有好几百条,我们后面会学习一些重要的属性。
3.1.3 查看进程
首先要理解,我们历史上所有的指令、工具、自己写的程序运行起来,全部都是进程,用户是以进程的方式访问操作系统的。
此时我们写的程序正在死循环运行,这就是一个进程,那么如何查看这个进程呢?
getpid 获取当前进程的标识符,返回值pid_t
是一个整数类型,下面修改一下程序,查看一下进程的pid。
如何查找系统中的进程呢?ps axj
或者top
,关于ps的指令的具体选项后面会详细的介绍。
ps axj | grep 程序名
可以过滤找到指定的进程。
ps axj | head -1 && ps axj | grep myprocess
,或者改&&
为;
都可以把进程的表头属性打印出来。
这样grep会把自己也查出来,grep -v grep
可以过滤掉自己。
ps axj | head -1 && ps axj | grep myprocess | grep -v grep
我们可以看到同一个程序运行后会获得不同的pid,pid的分配是线性递增的。
如何杀死一个进程呢?ctrl c
或者kill -9 pid
查看进程还可以通过文件的方式查看,ls /proc
,/proc是内存级的文件系统,Linux下一切皆文件,进程甚至可以转换为若干个文件。
数字目录对应的都是各种进程的pid,目录中的内存就是这个进程的动态属性,当前我们程序的pid为32272,那么一定在 /proc下可以查找到名为32272的目录。
同理,当我们杀死这个进程的时候,/proc 下就找不到这个进程了
那么进程目录中有什么东西呢?这次对应的进程号为4185
今天我们要学习的是目录下的cwd和exe。
exe记录下的就是进程对应的可执行文件的绝对路径。
当我们把这个可执行文件删除后,进程还在运行,说明在磁盘中的程序已经被充分的拷贝的内存了。
当我们再次查询的时候,就发现发现进程的可执行文件被删除了。
那cwd是什么呢?current work dir ,即为当前程序所在的工作路径。这也是C语言中fopen("d.txt","w")
不用指定绝对路径的原因。
那我们如果修改了这个进程当前路径呢?
这次的进程号为:10499
查看一下进程对应的文件,cwd果然被修改了
那么fopen创建的文件是在被修改后的工作目录下么?
前面我们提到了getpid可以获取自己的进程号,那getppid呢,其实这个系统调用可以获取当前进程的父进程的进程号。Linux系统中的的所有进程都是被其父进程创建的。下面修改一下代码,查找自己的父进程。
当我们多次启动程序后,发现每次程序对应的进程的父进程的进程号时不变的。
这里的bash是什么,bash其实是命令行解释器,其本身就是一个进程,操作系统会给每个登录用户分配一个bash。
我们之前运行的命令ls pwd cd...
的父进程都是bash。
这就是bash打印的字符串,等待用户输入命令。
下面用代码创建子进程。
可以看出确实创建了一个子进程,下面描述一下原理。
父进程有自己的PCB以及代码和数据,创建子进程的时候,子进程也要有自己的PCB以及代码和数据,子进程的PCB由父进程PCB拷贝而来,并对部分属性做出修改,例如pid、ppid等,但是大部分的属性也是一样的,子进程拥有和父进程一样的地址指针,可以指向父进程的数据和代码,所以子进程被调度的时候就会执行父进程创建出子进程之后的代码。
子进程没有自己独立的代码和数据,因为目前,程序没有被新加载。
下面看一下fork的返回值说明
子进程创建成功,子进程的pid返回给父进程,0返回给子进程。
子进程创建失败 ,-1返回给父进程
先看下面的代码。
fork之后父子代码是共享的,所以程序结果如上。
这里给出问题
- 为什么fork给父子不同的返回值?
Linux系统中,父进程个数:子进程个数 = 1:n,父进程要根据不同的pid来区分不同的子进程,所以会把子进程的pid返回给父进程。
子进程可以通过getppid即可获取父进程的进程号,所以仅表示成功创建即可,返回0。
- 为什么一个函数会返回两次呢?
fork函数如果执行到return 0 ,那么它的核心功能已经执行完毕了。也就是子进程已经创建完成了,所以父子进程都会执行
return id;
,自然会返回两次了。
3. 为什么一个变量即大于0,又等于0,导致if else同时成立?
进程具有独立性,一个进程挂了不会影响另外一个进程,同时父子进程共享父进程的代码和数据,代码是只读的,也不会有影响。
父子进程在数据层面默认是共享的,但是一旦父子进程任何一方要修改数据,OS会在底层把数据拷贝一份,让目标进程修改这个拷贝(写时拷贝)。
下面写代码对写时拷贝做验证。
写时拷贝示意图如下。
3.2 进程状态
3.2.1 运行&&阻塞&&挂起
- 运行状态
1.上面是一个CPU的调度队列,简单的调度算法是FIFO。
2.只要进程在调度队列中,进程的状态就是运行态。
-
阻塞状态
当C程序执行到scanf函数的时候,等待用户输入数据的时候,C程序对应的进程就是一种阻塞状态,阻塞状态就是进程等待某种设备或者资源的状态。
下面通过硬件视角来解读阻塞状态。
操作系统要对硬件做管理,就要先描述在组织,可以组织为一个结构体struct device
,所以操作系统对硬件的管理,就变成了对这些结构体的管理。当运行态一个进程需要等待某种硬件资源,例如键盘的时候,cpu就会把该进程pcb链入到键盘对应的struct device
的等待队列中,当前pcb中属性值也需要被修改,例如状态。
同理,当我们在键盘上输入后,OS会修改键盘对应结构的状态为活跃并检查等待队列,若队列不为空,就会修改队头PCB状态并把该PCB重新链入到运行队列中。
struct device
的属性结构体大致如下
-
挂起状态
当计算机资源严重不足的时候,OS就会把不会被立即访问的数据换出到磁盘(swap分区中):例如某些设备等待队列上阻塞状态的进程的代码和数据,这个就是阻塞挂起,当设备输入的时候,OS就会把代码数据换入到内存中并重新构建队首PCB的指针映射,然后把队首进程链入到运行队列中。更加严重的,OS甚至会把运行队列的末端进程对应的代码和数据换出到磁盘中,这种进程的状态就是就绪挂起。
3.2.2 课本上的说法
上面的是课本上给出的各种进程状态。进程状态转换的本质就是PCB在不同队列里流动。
3.2.3 理解内核链表
Linux中的一个PCB结点可能会在多个数据结构中,在双链表中的同时也可能在一个队列中。
上面是我们在数据结构部分学习的双链表的表示方法,那Linux系统中是如何做到各种数据结构交错呢?task_struct
中有多个类型为struct list_head
的属性。
那么task_struct之间的关系图如下:
遍历无法拿到对应task_struct
的起始地址,那就无法访问各种属性。
如何解决这个问题呢?C语言中的offset
宏给出了答案。
&((struct task_struct *)0->links)
这样就拿到了一个task_struct中 links相对于其起始地址的偏移量。
(struct task_struct*)(next/list - &((struct task_struct *)0->links))
这样就可以访问每个task_struct的属性了。
刚刚说了一个task_struct中有多个list_head
属性,这样一个结点就可以属于多种数据结构了。这也就意味着Linux中的数据结构是一种交错的网状结构。
3.2.4 Linux中的进程状态
先给出进程状态的查看命令ps
a:显示一个终端的所用的进程,包括其他用户的进程
x:显示没有控制终端的进程,例如后台运行的守护进程。
j:显示进程归属的进程组ID、会话ID、父进程ID,以及作用控制相关信息。
u:以用户为中心的格式显示进程信息,提供
进程状态就是一个task_struct内的一个宏定义的整数。
上面就是Linux内核中的进程状态,下面我们一一介绍。
- R(运行态)
写出一下的程序并编译运行
查看一下进程状态
while :; do ps axj | head -1 && ps axj | grep process | grep -v grep; sleep 1; done
process进程不是在循环么,为什么会有S(阻塞状态)呢?,因为代码中有printf,在进程等待IO的时候,进程就变成阻塞态了。
当去除代码中的printf的时候,process进程的状态就总是R了
这里的R+
的+
的意思是程序是在前台启动的,./process &
就可以保证程序在后台运行了,对应的状态也就是R
而非R+
。
后台运行的程序不会影响前台的命令行输入,即使后台程序在向前台打印信息。
kill -9 进程号
可以杀掉对应的后台进程。
- S(sleep) 睡眠状态
Linux下的S状态对应的是系统理论中的阻塞状态。
编译运行一下。
进程运行到scanf函数时,需要等待IO,进程阻塞。
- T(stopped)/t(tracing stop)
gcc code.c -o process -g 编译(gdb调试)
gdb process 启动gdb调试process
l 打印源程序
b 8 在第8行打断点
r 运行程序
process进程被debug,程序暂停了,对应的状态为**t(追踪状态),**进程被调试的时候就是这个状态。
修改一下代码,然后编译运行。
ctrl+z
此时进程的状态为T(暂停状态)。
T和S状态不同,S状态表现为进程在等待资源,T状态表现为进程的某种条件不具备或者进程做了非法操作,T状态为Linux特有的一种状态。
- D disk sleep(磁盘休眠)
S状态称为可中断休眠/潜休眠状态,这个状态的进程可以被杀掉。
D状态称为不可中断休眠/深度休眠状态。
下面讲解一下D状态的场景:
当内存资源严重不足的时候,OS可能会直接杀掉部分进程,当进程在阻塞态(S)等待数据写入磁盘的时候被杀掉并且磁盘空间不足写入失败的时候,这部分数据就会被丢弃且用户端不会察觉(因为相关进程已经被杀死)。这是不合理的,所以在OS内,进程在对磁盘等关键数据存储设备进行高IO访问的时候,进程的状态为D而不是S,同时OS不可杀掉D状态。D状态也是阻塞的一种。
可以使用dd命令模拟高IO场景来看到dd进程的出现D状态。
dd if=/dev/zero of=~/test.txt bs=4096 count=10000000
- X(dead)/Z (zombie)
创建子进程的目的就是为了让子进程完成一项功能,所以在子进程退出之前需要让父进程读取有关数据,这个时候的子进程的状态就是Z状态(僵尸状态),改状态下仅保留了进程的PCB,其他数据已经被释放。
下面尝试模拟Z状态
程序运行5秒后,子进程就处于Z(僵尸状态) 了,如果父进程一直不获取处于Z状态子进程的信息,那么Z进程PCB将会一直维护,内存将会一直被占用,产生内存泄露的问题。
X状态就是Z状态的下一个状态,即对应的PCB被父进程获取并释放,该进程在内存上就不存在了,对应的状态为结束状态。
至于挂起状态,是OS应对内存资源不足的一种策略,在Linux中并没有体现给用户。
3.2.5 孤儿进程
父子进程中,如果父进程先退出,子进程要被1号进程领养,这个被领养的进程就是孤儿进程。
这里可以简单认为1号进程就是OS。
部分系统的一号进程叫做**‘init’**
为什么1号进程要领养孤儿进程呢?
因为如果不领养,子进程可能会变成僵尸进程造成内存泄露的问题。
这里的父进程会被其父进程bash回收,不会有任何问题。
孤儿进程一旦被领养就会变成后台进程,后台进程可以向前台打印消息,但是ctrl c
无法杀掉后台进程,可以使用kill -9 进程号
来杀死后台进程。
3.3 进程优先级
3.3.1 基本概念
进程得到CPU资源的先后顺序就是进程优先级。为什么需要优先级呢?本质在于CPU资源稀缺,导致需要优先级来确定哪个进程先被调度。
3.3.2 查看系统进程
进程优先级在Linux系统中体现为为task_struct中的一种整形性质的变量,这个变量的值越低优先级越高,反之优先级越低。一个进程的优先级可能会变化,但是变化的幅度会不太大。
UID:每个用户都对应一个id,这就是UID
上面的UID记录的是谁启动的相关进程。
系统怎么知道我访问文件的时候,是拥有者或者所属组还是other呢?
我们用指令访问文件的时候,本质就是进程在访问文件,进程记录下启动进程的UID和文件属性中各种角色的UID来对比,判断用户属于什么角色。
PRI:进程优先级,默认值为80。
NI: 进程优先级的修正数据,称为nice值。
进程的真实优先级 = PRI(修正值)= PRI(默认)+ NI ;
修改进程优先级方法1:
- top
- r
- 输入进程号
- 输入nice值
把优先级的NI值设置为10,优先级降低。
修改进行优先级的其他方法:
nice rinice 命令
get/setpriority 系统调用
…
优先级的极值为多少呢?
经测试nice值的修改范围为[-20,19],进程的优先级为[60,99]共四十个数值。
Linux系统的优先级范围设置较小并且不允许频繁修改进程优先级的原因是:这样会导致优先级低的进程长时间无法获得CPU资源,这样就会造成进程饥饿。
3.3.3 补充部分概念
1.竞争性:系统进程数⽬众多,⽽CPU资源只有少量,甚⾄1个,所以进程之间是具有竞争属性的。为了⾼效完成任务,更合理竞争相关资源,便具有了优先级
2.独⽴性:多进程运⾏,需要独享各种资源,多进程运⾏期间互不⼲扰
3**.并⾏**:多个进程在多个CPU下分别,同时进⾏运⾏,这称之为并⾏
4.并发:多个进程在⼀个CPU下采⽤进程切换的⽅式,在⼀段时间之内,让多个进程都得以推进,称之为并发
3.4进程切换与调度
3.4.1死循环进程如何运行
一个进程占领CPU会直接把代码跑完么?
不会,每个进程只会在一个在CPU运行一个时间片,然后进程切换,这其实就是并发。所以死循环进程不会打死系统,因为不会一直占领CPU。
3.4.2 CPU和寄存器
当一个进程被调度的时候,CPU会根据进程PCB中的内存指针来找到进程的代码和数据,然后代码和数据会分批拷贝至CPU内的寄存器中供CPU运算。图示如下。
- 寄存器就是CPU内的保存临时数据的器件。
- 寄存器 不等于 寄存器中的数据
3.4.3 进程如何切换
上下文数据:某时刻存储在CPU寄存器中有关进程的各种数据。
进程切换最核心的就是保存和恢复进程的硬件上下文数据,即CPU中寄存器的内容。
- 进程切换时,进程的上下文数据被保存在哪里了呢?
可以理解保存到进程的PCB中的TSS中,当代Linux已经把TSS从PCB中移除了。
我们找出第一代的Linux代码看一下。
- 如何区分全新的进程和已经被调度过的进程呢?
当代Linux内核中给出了一个 isrunning的属性做为,调度过为1,没有调度过为0。
一个CPU一秒钟会调度很多次,可以把一个实体CPU分为几个逻辑CPU,逻辑CPU效率总和为实体CPU的效率。
3.4.4 Linux2.6内核进程O(1)调度队列
一个CPU一个运行队列,Linux2.6的运行队列名称为runqueue。
1.成员变量有queue[140],类型为struct task_struct *,是一个PCB指针数组
2.140指的是Linux有140个优先级,[0,99] 是实时优先级,对应的是实时操作系统(抢占式的进程切换)。
3.剩下的40个属于分时优先级,也是3.3讲到的优先级,可以通过x-60+(140-40)映射到对应的下标处并链接入队,queue本质就是hash表。
4.宏观上可以根据优先级遍历,局部(即优先级相同的PCB指针队列)使用FIFO遍历。
unsigned int bitmap[5],这是一个位图,可以表示160位bit,和1-140位bit位quque[140]对应。n位为1对应quque[n-1]为空,n为0对应queue[n-1]队列非空,位图的设置简化了对quque[]的遍历。
nr_active 标识了整个调度队列中有多少个进程,调度器挑选进程时候,先查nr_active ,nr_active大于0,再查nitmap[5],确认下标,直接索引queue,找到目标队列,移除队头pcb,执行调度和切换算法。
4. 环境变量
4.1 基本概念
环境变量(environment variables)⼀般是指在操作系统用来指定操作系统运⾏环境的⼀些参数。环境变量通常具有某些特殊⽤途,还有在系统当中通常具有全局特性。
如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪⾥,但是照样可以链接成功,⽣成可执⾏程序,原因就是有相关环境变量帮助编译器进⾏查找。
4.2 命令行参数
main有参数嘛,之前在学习C语言的时候,我们写的C语言程序都是不带参数的。起始main函数是有参数的,main函数也需要被其他函数调用。
int main(int argc, char* argv[])
argv 字符指针数组
argv 数组中的元素个数
命令行命令被以空格分隔成命令行参数,并分布取地址存储到字符指针数组中。
通过上面程序模拟了指令+选项对应不同功能,所以命令行参数的用途是可以让一个程序通过不同的选项来实现不同的子功能。这也是指令可以带选项的原因,指令的本质也是一个可运行二进制程序。
4.2 认识一个环境变量
指令和我们写的程序都是二进制程序,为什么运行系统指令不需要路径呢?因为系统中存在环境变量来帮助找到目标二进制文件 。
这个环境变量为PATH
,PATH
中记录的是搜索指令的默认搜索路径。
env
可以查看Linux系统中的所有的环境变量。
查看一个环境变量可以使用echo $环境变量名
。
环境变量 = 名称+内容。
冒号为路径的分隔符,程序没有路径的时候,系统就会到PATH中依次匹配。
若把当前程序的路径拷贝到PATH中,那么不用指定路径就可以运行程序了。
如何理解环境变量呢?(存储的角度)
环境变量存储在bash中,叫做环境变量表,当我们输入命令’ls -a’的时候,bash就会在环境变量表中匹配,匹配失败就会打印出报错信息,匹配成功就会结合同样存储在bash中的命令行参数表创建子进程。
环境变量最开始是从哪里来的?
环境变量最开始是在系统的配置文件中,当bash进程创建的时候就会在配置文件中读取所有环境变量的值并在自己的内部创建出环境变量表。
配置文件存放在家目录下,.bashrc
,bash_profile
.bash_profile
会加载.bashrc
,.bashrc
会加载etc/bash
.
可以在.bashrc
文件中添加自己需要的PATH路径,这样该PATH路径就常驻系统了。
4.3 认识更多的环境变量
这也是cd ~
能切换到家目录的原因。
SHELL记录的是用户登录的时候使用的是哪一个版本的shell。
USER记录当前用户是谁。
LOGNAME记录登录用户是谁。
一般USER == LOGNAME ,且su 切换用户的时候不会修改这两个环境变量,su - 的表示重新登录才会。
记录历史指令的上限数目,对应命令history(查看历史命令)
记录终端类型
记录主机名
记录当前的工作路径
…
4.4 获取环境变量的方法
1.env
2. echo $XXX
3. export xxx=xxx 导入一个环境变量
4.unset xxx 取消一个环境变量
5.通过代码的方式获取环境变量
<1> main函数参数获取环境变量
main函数的参数可以有三个
argv和env是由父进程传递给我们的,环境变量可以被子进程继承,所以环境变量在系统中有全局特性。
<2> getenv系统调用
获取指定变量的内容
如果我想写一个只能自己用的程序,该如何设计呢?
su - 切换登录用户后。
<3>environ:环境变量的全局数组
4.5 理解环境变量
环境变量具有全局特性,这个上面已经证明,bash的子进程可以获取bash进程的环境变量。
补充两个概念
上面这个是本地变量,本地变量不是环境变量。本地变量不会被子进程继承,只会被bash内部使用。
unset可以取消本地变量。
也可以把本地变量导入到环境变量中。
为什么export命令作为子进程可以把数据传给父进程bash呢,进程之间不是相互独立的么?
export 是内建命令,不需要创建子进程,而是让bash之间亲自执行,一般为bash调用系统调用完成的。
5. 程序地址空间
5.1 程序地址空间回顾
下面是程序地址空间的示意图
写个代码验证一下:
上面的程序地址空间是内存么?不是内存,程序地址空间现在可以称为进程地址空间或者虚拟地址空间了。这是一个系统层面的概念而不是语言层面的概念。
5.2 虚拟地址
如何证明程序地址空间不是实际内存呢?下面写段代码证明:
同一个变量(唯一地址)竟然可以输出两个不同的值,这里的地址不是内存中真实的地址,是虚拟地址。C/C++程序指针用到的地址都是虚拟地址。
一个进程拥有一个虚拟地址空间,在32位机器下,有2^32位地址共4G的空间,1-3G为用户空间,第4G为内核空间。一个变量对应一个虚拟的地址同时在内存中对应一个真实的地址,一个进程也拥有一套页表,页表用来建立不同变量的虚拟地址和实际地址的映射关系.
子进程的PCB、地址空间、页表都是浅拷贝自父进程,父子进程的代码和数据都是共享的,这也是为什么父子进程中打印变量的虚拟地址是相同的。
当子进程要修改变量的时候,系统就会在内存中重新找一块内存空间存储修改变量,并重新建立子进程页面关于该变量的映射关系,这叫做写时拷贝。这就是父子进程打印同一变量的时候打印出了不同的值。
我们是否可以查看父子进程关于g_val变量的物理地址呢?不行,OS把物理地址隐藏只暴露虚拟地址给我们使用。
5.3 虚拟地址空间
上面我们提到了虚拟地址空间,这里我们谈一下其的具体细节。虚拟地址空间是OS给进程画的饼,这让每一个进程认为自己在独占物理内存,每个进程都有一个虚拟地址空间,这个虚拟地址空间在Linux为mm_struct的结构体变量,每个进程的task_struct中都有一个指向该进程的mm_struct的指针,OS也会把所有的虚拟地址空间通过数据结构管理起来。
struct task_struct
{//....struct mm_struct *mm;//.....
}
那么mm_struct是如何描述一个虚拟地址空间的呢?我们知道虚拟地址空间有很多区域划分,例如栈、堆等,那么mm_struct是如何实现区域划分的呢?需要定义变量来记录开始地址和结束地址即可。
struct mm_struct //区域划分形式大致如下
{//...long code_start;long code_end;long init_start;long init_end;//...
修改变量的数据就可以实现区域调整。
来看一下Linux内核中的mm_struct
结构图示如下:
下面我们在深入一点
在mm_struct
中还存在一个vm_area_struct
的指针,vm_are_struct
用来描述进程地址空间的一个连续的区域,vm_area_struct
结构也会被维护成一个链表。
物理地址转变成虚拟地址的过程
- 在虚拟地址空间中调整区域划分(mm_struct 会在程序被加载的时候初始化)。
- 加载程序,申请物理空间
- 进行页表映射
虚拟地址供上层用户使用,物理地址被屏蔽。
5.5 为什么要有虚拟地址空间呢?
这个问题可以转换为:如果程序直接操作物理内存会造成什么问题?
- 安全风险:每个进程都可以访问任意的内存空间,这也就意味着任意一个进程都能够读写系统相关的内存区域,如果是一个木马病毒,那么它就能随意的修改内存空间,让设备瘫痪。
2.地址不确定:如果直接使用物理地址的话,我们无法确定内存现在使用到哪里了,也就是说拷贝的实际内存地址每⼀次运行都是不确定的。
3.效率低下:如果直接使用物理内存的话,一个进程就是作为一个整体的(内存块)来操作的,如果出现内存不够用的情况,需要把整个进程从内存拷贝到磁盘,这样拷贝时间长,效率太低。
虚拟地址空间和分页机制就可以解决上面的问题
- 因为页表映射的存在,程序在物理内存中理论上就可以任意位置加载。但是在进程视角中所有的内存分布都可以是有序的。
2.地址空间和页表是OS创建并维护的,这意味着地址转换的过程中,OS会对地址和操作进行合法判定,进而保护物理内存。 (野指针和字符常量写入)- 因为虚拟地址空间的存在和页表的映射的存在,我们的物理内存中可以对数据进行任意位置的加载,物理内存的分配和进程的管理可以做到没有关系,进程管理模块和内存管理模块就完成了解耦合。
下面基于上面的学习说明一些问题
- 创建进程的时候,先有内核数据结构,然后再加载代码和数据。
- 我们可以先创建内核数据结构和加载少量的代码和数据,进程通过页表找不到虚拟地址对应的物理地址的时候,这个时候进程阻塞,OS自动的会从磁盘中加载数据并建立映射,上面的过程叫做缺页中断。
- 阻塞挂起就是清空页表,并把进程对应的代码和数据换出到磁盘的swap分区中。