前言
上文介绍了进程的基本概念,进程=内核数据结构+可执行程序,查看进程的方式ps 和/proc指令。
又熟悉常见的进程状态,状态修改的本质就是将PCB标志位更改,然后放到指定的队列中。
本文继续介绍进程的概念,将介绍进程的优先级、O(1)调度算法、环境变量、进程地址空间等等。
进程的优先级
为什么要有优先级?
因为CPU资源是少数的,系统内的进程是多数的,所以进程之间会存在竞争CPU资源,就要对进程进行排队,这个排队的过程就是确定进程的优先级。
查看系统的优先级
ps -al 查看系统默认的优先级
PRI和NICE
- linux下利用PRI和NICE同时确认系统的优先级
- PRI是默认优先级,每一次都会被设置为80,而NICE是调整值。
- 如果NICE是-10, 优先级=80-10=70。如果NICE=15,优先级=80+15=95。
- 优先级的范围是60-99,不在这个范围的优先级会被调整为边界。
为什么要限定优先级的范围?
本质就是让CPU调度的时候,能够较为均衡的把每一个进程都调度,防止进程饥饿问题。
修改系统的优先级
TOP+R指令
进程的上下文切换
CPU只有一套寄存器,进程是独立的,也就是说每一个进程都能使用这一套寄存器。
进程的上下文数据会被保存在寄存器中,上下文数据就是产生的临时数据等等。
进程的时间片结束时候,会将寄存器中的上下文数据保存到进程PCB当中,当该进程的时间片再次到来,会将保存在PCB中的上下文数据恢复到寄存器当中。这个过程就叫做上下文切换。
Linux内核2.6下的O(1)调度算法
处于运行队列的PCB都会等待CPU调度。
运行队列会维护俩份队列和位图,活跃进程指针和过期队列进程指针。
调度进程时候,会先到通过活跃进程的bitmap找到哪一位上不为0,然后到活跃队列的对应下标去取PCB来调度。而调度完的PCB和后来的PCB都应该被放到过期队列中。
当活跃队列为空时,就交换活跃队列和过期队列的指针。
所谓的插队就是将后来的进程放到活跃队列中,而不是放到过期队列中。
- 提高效率的操作是bitmap:5*32=160位,就可以32位一起判断,而不是遍历整个队列。
- 保证均衡调度的是:维护双队列,后来的放到过期队列,不影响先来正在调度的进程。
命令行参数
命令行参数是main函数中的参数,可以在执行前通过命令行可执行程序+选项的方式,被进程获取。
测试简单的命令行参数
注意:
- 命令行参数是以字符串的形式组织起来。
- 在最后一个参数结束后,会以nullptr结尾。
命令行参数的意义?
支持各种指令级别选项设置。
实际上,我们在linux下输入的各种指令就是可执行进程,如果输入选项字段,就是命令行参数。
环境变量
linux各种指令的本质就是可执行程序,这一点是可以被验证的。
模拟touch创建一个文件
1 #include<stdio.h>2 #include<stdlib.h>3 int main(int argc,char * argv[]){4 if(argc!=2){5 printf("command error!\n");6 exit(0);7 }8 FILE * fp;9 fp=fopen(argv[1],"w+");10 11 fclose(fp);12 return 0;13 }
与linux下不同的是,我们需要指明路径,而OS的程序就不用。
主要是因为OS会通过环境变量的方式内保留可执行程序的搜索路径
环境变量的概念
环境变量是指操作系统用来指定操作系统运行环境的一些参数。
比如在链接动静态库的时候,不用指明具体路径,照样可以链接成功。
环境变量通常有特定的功能,具有全局属性。
常见的环境变量
通过env查看全部的环境变量
常见的环境变量:
- PATH:默认的搜索路径
- PWD:当前目录
- LOGNAME:日志名称
- HOME:家目录,用户登录到linux下的目录。
查看环境变量
通过echo $(环境变量)的方式查看莫一个环境变量
echo &PATH
测试环境变量
将之前撰写的mytouch添加进行PATH搜索路径环境变量
- 1.将mytouch添加进行指定的目录
- 2.为PATH添加路径
验证一:
验证二:
通过指令为PATH添加mytouch的路径
PATH=$PATH:/home/lyh/proctest
获取环境变量
通过miai函数传参
其实main函数的第三个参数是一张环境变量表
通过main函数的第三个参数就能获取到环境变量表,这个环境变量表和env指令看到的是一样的。
当环境变量作为参数传递给main函数,实际上是以字符串数组的形式,末尾是NULL结尾
通过getenv
系统调用getenv获取指定的环境变量
环境变量是从父进程继承过来的,但是bash进程的环境变量是从哪里来的?
从配置文件中读取而来的。
在home目录中存在一个/profile 文件,配置着环境变量
我们也可以自主添加环境变量,比如:变量名=值
但是这样添加的,只能称为本地变量,如果想成为环境变量,就必须通过export导入
删除导入的环境变量:unset
注意:
即使通过export导入的环境变量,在shell重新登录时候,都会被清空,因为shell会读取配置文件的环境变量,除非你把自定义的环境变量导入到配置文件中。
获取环境变量的第三种方式
通过environ全局参数,用法和main函数的参数一致。
int main(){//这里只是声明一下extern char ** environ;for(int i=0;environ[i];i++){ printf("environ[%d]:%s\n",i,environ[i]);} return 0;}
环境变量具有全局属性,会随着子进程一直继承下去。
本地变量vs环境变量
本地变量只在当前会话的bash中有效,并且不会随着进程继承下去。环境变量会在所有的子进程中继承。
linux下指令分类:常规的进程(fork子进程程序替换实现)。2.内建命令:由bash提供的函数。
进程地址空间
实验:在父进程中创建全局变量值为100,fork创建子进程。五秒后,子进程将值修改为200。观察父子进程的值是否一致。
最后发现父进程的值为100,子进程的值为200。val的值是不一致的,但是同一个值的地址确不同。
说明我们这个地址肯定不是内存上的地址。到达是什么?是虚拟地址。本文就来探讨一下进程地址空间的相关内容。
每一个进程都有一个虚拟地址空间的存在
当进程被启动的时候,会创建PCB建立地址空间。操作系统必然会对这些地址空间管理,所以地址空间就是内核的数据结构。会被记录在进程的PCB中。
进程地址空间就是(struct mm_struct)
进程间不会共享同一个地址空间,因为要保证进程的独立性。让进程知道自己有机会获取到所有的物理空间。
页表:(空间映射)
每个进程都存在自己的地址空间,当磁盘上的数据被加载到内存上时,会先建立虚拟地址和物理地址的映射。并且将这种映射关系保存到页表上,用于从虚拟地址,找到实际内存上的地址。
什么是地址空间
回答这个问题前,先了解一下区域划分。在一根直尺上,如果最小刻度是cm。
那么假设区域1的起始地址是0cm,结束地址是10cm。区域2的起始地址是 11cm,结束是30cm。
那对于区域1中,我们也可以继续划分,我们可以按照mm划分,划分成为更小的刻度。
所以区域划分的本质就是用start和end标志你的空间。
进程地址空间的划分也是如此
struct destop_area{_size,字符常量区_start,_end,代码区 _start,_end,栈区 _start,_end......
}
看一看内核中的定义
asmlinkage int
osf_set_program_attributes(unsigned long text_start, unsigned long text_len,unsigned long bss_start, unsigned long bss_len)
{struct mm_struct *mm;lock_kernel();mm = current->mm;mm->end_code = bss_start + bss_len;mm->brk = bss_start + bss_len;
#if 0printk("set_program_attributes(%lx %lx %lx %lx)\n",text_start, text_len, bss_start, bss_len);
#endifunlock_kernel();return 0;
}
内核中就是由start和end组成
所以进程地址空间就是struct结构体,维护了start和end指针对物理地址做了虚拟的空间划分。
页表是被保存在寄存器上的,也可以通过指针找到页表。
页表的地址是虚拟还是物理地址?物理地址。
回答实验的问题
为什么一个变量,有俩个值。
当一个进程被启动时,就会创建pcb结构体,创建mm_struct地址空间。同时保存页表,对必要的数据做物理内存到虚拟地址上的映射,也就是说假设变量的地址为0x00003344那么物理地址映射的可能就是0x22334455。这时创建子进程,子进程就会以父进程的PCB为模板创建子进程的PCB,同时也会复制一份父进程的地址空间,以及页表映射。
上图有点小错误,应该指向的是已经初始化的全局区。
当子进程对全局变量修改时,由于进程间的独立性,就会发生写时拷贝。
子进程会重写开辟物理内存,将父进程的值拷贝进去,然后进行修改,重新建立页表的映射。
所以我们看到的地址就是虚拟地址,同一个虚拟地址在不同的进程下,就有了俩个值。
为什么要有进程地址空间
1.让进程可以以统一的视角看待内存。任意一个进程 通过地址空间+页表将乱序的内存变得井然有序。
2.可以对访问内存进行安全检查。比如我们不能修改代码区的字段。
实际上,页表还存在第三列,就是权限位。比如字符常量区的权限就是r,栈区的就是rw
当我们要修改字符常量区时,就会被权限位拦住,不让你访问。
缺页中断:
页表还存在一列表示内存是否已经分配。
当程序被加载到内存时候,不需要把整个代码+数据全部加载,而是加载一部分。但是会先建立页表的左边部分,然后确定第4列数据是否分配。如果访问到尚未分配的地址时候,就会暂停进程,加载资源,修改页表,再去执行进程。
这一个过程就叫做缺页中断。
3.实现进程管理和内存管理的解耦:进程只需要负责去访问地址读取资源,加载资源由OS自动完成。
写时拷贝
什么时候进行写时拷贝?
当父进程fork创建子进程时候,就会将父进程的页表权限位全部修改位只读。然后为子进程创建PCB,地址空间,页表等等。
当用户进行读写的时候,就会被页表的权限位拦住出错:1.真的出错了,访问到真的只读区,直接不访问。2.同一份资源,就会重新申请物理内存,拷贝旧内容到新地址处。这就是写时拷贝。
关于进程概念暂时到此为止了,后续也会进行补充,进程的概念是庞大的,涉及到的知识点也比较复杂。