目录
一、硬件方面介绍
1.冯诺依曼体系结构
2.存储分级
二、软件 方面
1.操作系统是一款进行管理的软件,它可以管理硬件也可以管理软件
2.操作系统如何管理?
三、进程
1.概念
总结
四、linux中对进程的管理
1.task_ struct内容分类
2.查看进程
3.通过系统调用获取进程标示符
4.知识补充
5.一些信号
6.通过系统调用创建进程-fork
1.对于fork返回值有两个的解释
2.如何做到一个函数返回两次?
3.一个变量如何储存两个值?
4.父子进程谁先运行呢?
bash
7.进程状态
1.运行
2.阻塞
3.挂起
8.linux下状态
R运行状态(running):
S睡眠状态(sleeping):
知识补充
D磁盘休眠状态(Disk sleep)
T停止状态(stopped):
X死亡状态(dead):
Z僵死状态(Zombies)
孤儿进程
9.进程内的访问
10.进程优先级
1.优先级是什么?
2.为什么要有优先级
3.linux中的进程优先级
4.用top命令更改已存在进程的nice:
5.优先级的工作原理
如何判断当前运行的那个队列是否为空
一、硬件方面介绍
1.冯诺依曼体系结构
abcde都是独立的个体,所以各个单元必须要用“线”连接起来,称为总线,为图中红色
1.系统总线 连接运算器和控制器
2.io总线 连接存储器和输入输出设备
一个程序要运行必须先加载到内存,是因为冯诺依曼体系就是这样子规定的
2.存储分级
二、软件 方面
1.操作系统是一款进行管理的软件,它可以管理硬件也可以管理软件
笼统的理解,操作系统包括:
内核(进程管理,内存管理,文件管理,驱动管理)
其他程序(例如函数库,shell程序等等)
总体纵览图
操作系统存在的意义是通过管理好底层的软硬件资源,为用户提供一个良好的执行环境
操作系统里面会有各种数据,但是操作系统不相信任何用户
因此为了保护自身数据的安全,也为了能够给用户提供服务,操作系统以接口的方式给用户提供调用的入口,来获取操作系统内部的数据。
这些接口是操作系统提供的,用c语言实现的,自己内部的函数调用---------系统调用(即上图的系统调用接口)
库函数(lib)vs系统调用
库函数和系统调用是上下层,调用与被调用的关系
所有访问操作系统的行为都只能通过系统调用完成,任何库函数只要试图访问操作系统或者硬件(软硬件资源),都需要经过系统调用
2.操作系统如何管理?
先描述后组织
管理者决策者 操作系统 类比校长
执行者 驱动程序 辅导员
被管理者 软硬件资源 学生
我们要管理学生,先得对学生的信息进行描述,例如学院 姓名 班级 专业等等
每个学生都可以转换成一个结构体
我们再将这些结构体进行组织,例如使用双向链表将每一个结构体进行连接
那么我们对学生的管理就转换成对链表的增删查改了
在操作系统中任何管理对象,最终都可以转化成为对某种数据结构的增删查改
三、进程
1.概念
一个已经加载到内存中的程序,被称为进程,也被称为任务
可以理解成,正在运行的程序,叫做进程
而一个操作系统不仅仅只能运行一个进程,可以同时运行多个进程,因此我们也必须将进程管理起来,如何管理呢?先描述再组织。
进程 = 内核PCB数据结构对象+ 我们自己的代码和数据
这个数据结构对象
描述这个进程的所有属性值
任何一个程序在加载到内存的时候,形成真正的进程时,操作系统要先创建描述进程的结构体对象----PCB (process ctrl block 进程控制块),PCB就是一个进程属性的集合
这个集合就是一个struct结构体,里面包含例如:进程编号(PID) 进程的状态 优先级等等,根据进程的PCB类型,为该进程创建对应的PCB对象
我们只需要对PCB进行管理就可以管理这个进程,PCB中含有进程的各个属性,因此也顺理成章地会去记录代码和数据的位置,记录下指针。
(操作系统也是软件,所以开机时也会加载到内存中)
在操作系统之中,对进程进行管理,就变成了对单链表进行增删查改
总结
计算机管理硬件
1. 描述起来,用struct结构体
2. 组织起来,用链表或其他高效的数据结构
四、linux中对进程的管理
Linux操作系统下的PCB是: task_struct
所以
在Linux中描述进程的结构体叫做task_struct。
task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息
1.task_ struct内容分类
1.标示符: 描述本进程的唯一标示符,用来区别其他进程。
2.状态: 任务状态,退出代码,退出信号等。
3.优先级: 相对于其他进程的优先级。
4.程序计数器: 程序中即将被执行的下一条指令的地址。
5.内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
6.上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
7.I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
8.记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等
9.其它信息
2.查看进程
1.ps ajx(我们通常会搭配grep来使用)
COMMAND系统运行这个进程时是调用什么指令
2. ls /proc
这个文件夹内的信息实际上是操作系统将内存中的进程进行可视化(因此这个文件夹里面的信息是动态变化的)
这些蓝色的数字就是相应进程的PID
(同一个程序进行多次运行时,所产生的PID也会不同)
进入目录,里面存储的是该进程的一些属性,我们这里简单看一下cwd和exe
cwd (current work dir)后面的内容指的是:当前的工作目录
例如我们touch 文件名 来创建一个文件,当touch载入内存变成进程的时候会记录它启动时的工作目录,所以创建的文件在没有指定路径的时候,默认就会在当前路径下创建
exe 后的内容指的是:这个进程对应的文件是这个路径下的这个文件,即指针信息可视化
3.通过系统调用获取进程标示符
进程id(PID) 使用getpid()
父进程id(PPID)使用getppid()
进程的pid也是它的属性,所以pid也在task_struct中存放
ppid在同一个终端下启动一般不会改变,所有子进程的父进程都是bash
4.知识补充
1.在底行模式下,使用!man也可以直接查询手册
2.多条不同指令可以在同一行进行输入,可以使用&&进行分隔,从左到右一次执行
5.一些信号
kill -9 PID 杀死对应pid的进程
6.通过系统调用创建进程-fork
fork可以创建子进程,即系统里多了一个进程,那么这个进程也需要有自己的task_struct和数据以及代码
因为子进程没有自己的代码所以一般而言,fork之后的代码,父子共享
1.对于fork返回值有两个的解释
我们为什么要创建子进程呢?是因为我们想让父子进程做不同的事情,所以需要让父和子执行不同的代码块,为了实现这个功能,fork就具有了不同的返回值,对父进程返回子进程的pid(因为字进程查询父进程pid成本低,但是父进程无法准确查询子进程),对子进程返回0,如果失败就返回-1,所以我们可以使用if else来对代码进行分流
2.如何做到一个函数返回两次?
我们可以猜测到fork函数内的大概功能
pid_t fork(void)
{
1.创建子进程pcb
2.填充子进程pcb相应内容
3.让父子进程共享同样的代码
......
此时由于父子进程都有独立的pcb了,他们都可以被cpu调度运行了
return ;
}
我们可以看到,return位于函数的最后,而此时函数的功能已经被实现,因此return的代码也被共享了
3.一个变量如何储存两个值?
首先我们需要知道进程之间是相互独立的
因为数据可能被修改所以我们不能让父子进程共享同一份数据(可以共享代码是因为运行时代码已经不会被修改了,不会影响进程间的独立性)
所以为了解决这个问题,字进程会进行写时拷贝,当子进程修改父进程的数据时,会重新开辟一块空间,因此当return 返回的值被写入父进程的数据时候,父进程的数据直接被修改,而系统会给子进程开辟一块空间用于储存另一个返回值
4.父子进程谁先运行呢?
由调度器决定,是不确定的
调度器会相对公平地调度
bash
bash,即命令行解释器,它通过fork创建子进程,执行相应的指令,而它本身继续接收我们的指令,这就是fork创建的父子进程的实际应用
7.进程状态
操作系统学科上的进程状态一般分为,运行,阻塞,挂起
1.运行
操作系统会将已经准备好运行的进程放入运行队列中,操作系统根据顺序去调度这些进程,处于这个状态的进程就为运行状态:R
一个进程并不是放上去一直到执行结束的,每一个进程都有一个叫时间片的概念,在一个时间段内所有的进程代码都会被执行,即并发执行
大量地把进程放到cpu上和拿下来的动作被称为进程切换
2.阻塞
操作系统会像管理进程一样管理硬件,每一个硬件属性的结构体中会存在一个等待队列, 如果进程想要读取某个硬件的数据,那么它就会被排在该硬件的等待队列中,这时候进程就处于阻塞状态,一直等待到硬件已经准备好了,然后这个进程就会被排到运行队列中
等待特定设备的进程,我们称该进程处于阻塞态
3.挂起
当操作系统内部的内存不够了,那么在阻塞状态的进程,它的代码和数据就会被换出到磁盘中(swap盘),等到程序准备好运行了,它的代码就又会被换入到内存中,代码和数据被换出状态下的程序就处于挂起状态
8.linux下状态
static const char * const task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};
R运行状态(running):
并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
S睡眠状态(sleeping):
意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠
(interruptible sleep))
由于cpu的速度很快,所以一个进程有很大一部分时间是在等待i/o设备就绪,我们查询的时候基本上是处于S状态,但是当我们执行一个空语句,即例如
while(1)
此时程序不进行输入输出,它就一直处于运行状态了
所以S状态可以对应阻塞状态
知识补充
如果程序处于 例如S+ R+的状态那么说明该程序在前台运行,这时候我们就不能继续在命令行解释器输入指令了,我们可以
./test.exe &
这样程序就会转为到后台运行,状态后的加号也会消失,这种进程只能使用kill来杀死
D磁盘休眠状态(Disk sleep)
有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
在等待期间这个进程不能被杀死,对应的也是阻塞状态
T停止状态(stopped):
可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可
以通过发送 SIGCONT 信号让进程继续运行。
kill -19 暂停进程,暂停之后进程状态就会变成T
kill -18继续进程 进程继续后会转为后台运行
T状态有自己的应用场景,可能是要等待资源也可能是单纯被其它进程控制了
例如我们使用gdb调试进程时,运行某个程序并且在某个位置打上断点,进程在该代码处停止的状态就处于t状态,(t和T区别不是很大,暂时可以理解为同一种状态)
X死亡状态(dead):
这个状态只是一个返回状态,你不会在任务列表里看到这个状态
Z僵死状态(Zombies)
是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲)
没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
如果处于Z状态的程序没有被读取,僵尸进程会一直占用内存,但不会继续运行,会造成内存泄露
孤儿进程
当一个子进程的父进程被终止(在代码中使用exit(0)),但是子进程继续运行,那么子进程的父进程会被更改为1进程,即操作系统 ,因为如果子进程不被托管,在子进程运行结束后就不会被回收了
(如果将父进程直接ctrl+c终止,那么在父进程被回收的一瞬间,子进程在将父进程更改为操作系统后,子进程也会被回收)
9.进程内的访问
某个进程,它的pcb信息可以储存多种用于访问不同数据结构的指针,也就是说一个进程可以被存放在链表中也可以同时被存放在多叉树等等中。
下面我们简单讲一下进程如何相互访问,下是三点前提信息
1.这是一个双链表的结构体,我们现在有一个struct node* 类型的start变量,储存了第一个进程的一个地址
2.这个进程指向的是link
3.link节点内是指向 前后进程的指针
首先我们假设地址0是一个task_struct*类型的一个指针
(task_struct*)0
我们让它指向它的结构体内的link
(task_struct*)0->link
我们再将其取地址,得到的就是link与结构体的地址(即结构体最开始的那个变量的地址)之间的地址差,因为结构体地址为0。
&(task_struct*)0->link
因为我们现在已经有了一个结构体的link的地址,因此我们只需要将它的地址减去上面所算出的地址差,就找到了结构体的地址。(要使用int强转,转换成一个单纯数值的运算,而不是地址间的运算)
(int)start-(int)&(task_struct*)0->link
我们就得到了结构体的地址的数值,将其强转成task_struct*类型,即可访问该进程的其它内容了
(task_struct*)(int)start-(int)&(task_struct*)0->link
10.进程优先级
1.优先级是什么?
优先级是决定某个进程被访问顺序的一项属性。是一个[60,99]之间的一个数值
2.为什么要有优先级
因为资源是有限的,进程是多个的,因此进程之间具有竞争性,操作系统必须保证大家良性竞争,确认优先级
如果进程长时间得不到cpu资源,该代码得不到推进,进程就会面临饥饿问题。
3.linux中的进程优先级
我们使用ps-l指令可以看到
其中
PRI :代表这个进程可被执行的优先级,其值越小越早被执行
NI :代表这个进程的nice值
PRI即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小,进程的优先级别越高
NI就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值,进程nice值会影响到进
程的优先级变化
程序一般的PRI值为80
当我们对程序进行修正,加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice
这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行
nice其取值范围是-20至19,一共40个级别
我们每次修改进程的优先级,PRI(old)都为80,当nice的值太大或者太小时,nice值会被限制在-20和19。
4.用top命令更改已存在进程的nice:
top
进入top后按“r”–>输入进程PID–>输入nice值
(只有root才能更改优先级)
nice和renice也可以更改
5.优先级的工作原理
在运行队列中会存在两个指针数组,他们指向对应的对应的结构体,其中下标[100,139]对应[60.99] 的PRI的值,根据PRI值,我们将相应的task_struct链接上去,相同PRI值遵循先来后到的原则,类似于哈希表的处理办法
运行队列中有两个相同的数组,这里为了区分我们将其命名为,waiting和running,即当前run和当前wait,当running中的进程依次开始运行时,新进入运行队列的进程会被插入到waiting中,当running中的进程都运行完毕,他们两个的职能就互换,这样能够避免先进入但是PRI较大的进程一直处在队列的末尾得不到运行。
为了实现上述功能,两个数组的指针会不断被run和wait交换,这两个二级指针用于寻找当前run和当前wait的数组
如何判断当前运行的那个队列是否为空
我们可以创建数组
char bits[5],里面有40个比特位,对应四十级优先级,每个比特位上的1与0表示该优先级所对应的进程队列里面是否有进程,当bits为0时,说明当前run的数组里面指向的进程都已经被运行完毕。