Linux 入门:操作系统进程详解(上)
目录
一.冯诺依曼体系结构
一). 软件运行前为什么要先加载?程序运行之前在哪里?
二).理解数据流动
二.操作系统OS(Operator System)
一).概念
二).设计OS的目的
三).如何理解操作系统OS的“管理”
四).系统调用和库函数概念
三.进程
一).进程基本概念与基本操作
1.描述进程-PCB
2.task_ struct
3.查看进程
1). 进程的信息可以通过 /proc 系统文件夹查看
2). 大多数进程信息同样可以使用top和ps这些用户级工具来获取
4.通过系统调用获取进程标示符
5.通过系统调用创建进程-fork初识
1).认识fork
2).父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)
3).fork有两个返回值
4).fork 之后通常要用 if 进行分流
二).进程状态
1.Linux内核源代码怎么说
2.运行 && 阻塞 && 挂起
3.进程状态查看
4.Z(zombie)-僵尸进程
5.孤儿进程
三).进程优先级
1.基本概念
2.查看系统进程
3.查看修改进程优先级
4.补充概念---竞争、独立、并行、并发
四).进程切换
1.死循环进程如何运行
2. CPU和寄存器
3.如何切换
五).Linux真实的调度算法
1.优先级
2.活动队列
3.过期队列
4.active指针和expired指针
一.冯诺依曼体系结构
这里我们讨论学习的是红色的数据信号。
我们所认识的计算机,都是由一个个的硬件组件组成
- 输入单元:包括键盘, 鼠标,扫描仪, 写板,磁盘等
- 中央处理器(CPU):含有运算器和控制器等
- 输出单元:显示器,打印机,磁盘等
- 存储器:内存
关于冯诺依曼,必须强调几点:
1.这里的存储器指的是内存。
2.不考虑缓存情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备)。
3.外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取。
4.一句话,所有设备都只能直接和内存打交道。
那为什么有这些设定呢?速度差距大。
我们设计出不同存储速度的内存进行存储。当代计算机是性价比的产物。
我们从两个问题开始详细认识这个体系。
一). 软件运行前为什么要先加载?程序运行之前在哪里?
二).理解数据流动
请解释,从你登录上qq开始和某位朋友聊天开始,数据的流动过程。从你打开窗口,开始给他发消息,到他的到消息之后的数据流动过程。如果是在qq上发送文件呢?
二.操作系统OS(Operator System)
一).概念
任何计算机系统都包含一个基本的程序集合,称为操作系统(OS),操作系统是一款进行软硬件管理的软件。笼统的理解,操作系统包括:
• 内核(进程管理,内存管理,文件管理,驱动管理)
• 其他程序(例如函数库,shell程序等等)
二).设计OS的目的
对下,与硬件交互,管理所有的软硬件资源(目的)
对上,为用户程序(应用程序)提供一个良好的执行环境(手段)
对于操作系统我们要认识以下几点:
1.软硬件体系结构------>层状结构
2.访问操作系统,必须使用系统调用------其实就是函数,不过是系统提供的
3.我们的程序,只要你判断出它访问了硬件,那么它必须贯穿整个软硬件体系结构。比如:printf的本质:是你把你的数据写到了硬件里。
4.库可能在底层封装了系统调用
在整个计算机软硬件架构中,操作系统的定位是:一款纯正的“搞管理”的软件
三).如何理解操作系统OS的“管理”
对于OS的管理,我们使用生活中的例子来进行描述。对于一件事情我们分为两个步骤1.决策2.执行
管理的例子 ----- 学生,辅导员,校长。
加速理解:
1.对于校长管理学生,我们可以知道,我们一学期甚至大学四年都见不到校长几次 ,而对于校长的决策,比如:什么时候举办运动会,将全校最高的几个男生组成篮球队打比赛,我们都是要去执行的。
2.对于选全校身高最高的学生这个决策如何进行呢?校长一个一个的找同学去问吗?并不是,而是通过学生的身高数据去筛选。
3.那么,身高数据从哪来?那当然是辅导员统计上交的啦。
那么,我们可以得到几个结论:
1.要管理,管理者和被管理者,可以不需要见面
2.管理者怎么管理被管理者,根据“数据”进行管理
3.不需要见面,如何得到数据?由中间层获取。
那么校长对于学生数据就可以进行数据建模,使用结构体来描述一个学生的信息
校长进行数据建模的行为,我们可以认为是先描述,再组织。
操作系统对于计算机管理硬件
1. 描述起来,用struct结构体。
2. 组织起来,用链表或其他高效的数据结构。
四).系统调用和库函数概念
在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。
系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。
三.进程
操作系统是怎么管理进行进程管理的呢?很简单,先把进程描述起来,再把进程组织起来!
一).进程基本概念与基本操作
课本概念:程序的一个执行实例,正在执行的程序等
内核观点:担当分配系统资源(CPU时间,内存)的实体
加速理解:
我们可以简单的形容为我们找工作的过程:内存中的代码和数据就是“我们”,进程控制块就是“我们的简历”,CPU就是“面试官”,进程列表就是“我们的简历在排队”,我们在等待面试官筛选的过程本质上是我们的简历在等待被筛选,也就是进程控制块在被读取而不是我们的代码和数据,而进程控制块上的代码和数据地址就是“我们的电话号码和居住地址”。这样应该就能稍微理解进程了。
1.描述进程-PCB
task_struct ---- PCB的一种
在Linux中描述进程的结构体叫做task_struct。
task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息
总结:
进程 = 内核数据结构对象 + 自己的代码和数据
在这里就是:进程 = PCB(task_struct) + 自己的代码和数据
2.task_ struct
内容分类
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
- 上下文数据: 进程执行时处理器的寄存器中的数据。
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息。
组织进程
可以在内核源代码里找到它。所有运行在系统里的进程都以task_struct链表的形式存在内核里
3.查看进程
1). 进程的信息可以通过 /proc 系统文件夹查看
如:要获取PID为1的进程信息,你需要查看 /proc/1 这个文件夹。
2). 大多数进程信息同样可以使用top和ps这些用户级工具来获取
我们历史上执行的所有的指令,工具,自己的程序,运行起来,全部都是进程。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>int main()
{while(1){sleep(1);printf("我是一个进程pid: %d\n",getpid());}return 0;
}
getpid() 的作用
查看运行程序的进程 ps axj | grep code
添加属性列:ps axj | head -1;ps axj | grep code
杀掉进程 ctrl + c 或 kill -9 pid
4.通过系统调用获取进程标示符
- 进程id(PID)
- 父进程id(PPID)
1 #include <stdio.h>2 #include <unistd.h>3 #include <sys/types.h>4 5 int main()6 {7 while(1)8 {9 sleep(1);10 printf("我是一个子进程pid: %d,我的父进程:%d\n",getpid(),getppid());11 } 12 }
那么这个135370进程是什么呢?
操作系统会给每一个用户分配一个bash
查看bash:while :; do ps axj |head -1;ps axj | grep 'bash' | grep -v grep ; sleep 1 ; done
5.通过系统调用创建进程-fork初识
1).认识fork
运行 man fork
1 #include <stdio.h>2 #include <unistd.h>3 #include <sys/types.h>4 5 int main()6 {7 8 printf("父进程开始运行,pid: %d\n", getpid());9 pid_t id = fork();10 printf("进程开始运行,pid: %d\n", getpid());11 }
2).父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)
3).fork有两个返回值
返回值中子进程为0,父进程为子进程的pid
4).fork 之后通常要用 if 进行分流
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{printf("父进程开始运行,pid: %d\n", getpid());pid_t id = fork();if(id < 0){perror("fork");return 1;}else if(id == 0){// childwhile(1){sleep(1);printf("我是一个子进程 !, 我的pid: %d, 我的父进程id: %d\n", getpid(), getppid());}} else{ //fatherwhile(1){ sleep(1);printf("我是一个父进程 !, 我的pid: %d, 我的父进程id: %d\n", getpid(), getppid());} }printf("进程开始运行,pid: %d\n", getpid());
}
那么我们会产生下面几个问题
1.fork为什么会有两个返回值?
因为 父进程 :子进程 == 1 :n 父进程可能有多个子进程,父进程需要管理子进程,所以需要得到子进程的pid,所以父进程的返回值为子进程的pid,而子进程只需要管理好自己,子进程pid可以通过getpid得到,所以返回0。
2.为什么一个函数会被返回两次?
fork语句后创建了子进程且被调度了,父进程执行一次return,子进程也执行一次return
3.一个变量怎么能让 if 和 else if 同时成立?这个问题需要在后面才能解释清楚。
首先我们要知道一个前提:进程具有独立性。也就是父进程挂掉了子进程也无影响。
代码测试:
#include <stdio.h> #include <unistd.h> #include <sys/types.h>int gval=100; int main() {printf("父进程开始运行,pid: %d\n", getpid());pid_t id = fork();if(id < 0){perror("fork");return 1;}else if(id == 0){printf("我是一个子进程 !, 我的pid: %d, 我的父进程id: %d, gval: %d\n", getpid(), getppid(), gval);sleep(5);// childwhile(1){sleep(1);printf("子进程修改变量: %d->%d", gval, gval+10);gval+=10; // 修改printf("我是一个子进程 !, 我的pid: %d, 我的父进程id: %d\n", getpid(), getppid());}}else{//fatherwhile(1){sleep(1);printf("我是一个父进程 !, 我的pid: %d, 我的父进程id: %d, gval: %d\n", getpid(), getppid(), gval); }}printf("进程开始运行,pid: %d\n", getpid()); }
二).进程状态
1.Linux内核源代码怎么说
为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)。
下面的状态在kernel源代码里定义:
*
*The task state array is a strange "bitmap" of
*reasons to sleep. Thus "running" is zero, and
*you can test for combinations of others with
*simple bit tests.
*/
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 */
}
进程状态就是一个整数
2.运行 && 阻塞 && 挂起
挂起:将进程交换到磁盘的swap分区
运行:进程在调度队列中,进程的状态都是running
阻塞:等待某种设备或资源就位(键盘,显示器,网卡....)
进程状态的变化表现之一:就是要在不同的队列中进行流动,本质都是数据结构的增删查改
3.进程状态查看
循环查看进程状态:while :; do ps axj |head -1; ps axj | grep code ;sleep 1; done
a:显示一个终端所有的进程,包括其他用户的进程。
x:显示没有控制终端的进程,例如后台运行的守护进程。
j:显示进程归属的进程组ID、会话ID、父进程ID,以及与作业控制相关的信息
u:以用户为中心的格式显示进程信息,提供进程的详细信息,如用户、CPU和内存使用情况等
我们通过代码来具体查看进程状态
R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。
D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
这个状态很少见,当磁盘老化,内存严重不足时才有可能出现
T/t停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
小 t 状态,程序被debug,断点,程序被暂停了
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
4.Z(zombie)-僵尸进程
僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲)没有读取到子进程退出的返回代码时就会产生僵死(僵尸)进程
僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
我们使用下面这段代码查看僵尸状态
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>int main()
{pid_t id = fork();if(id == 0){//childint count = 5;while(count) //5次后退出{printf("我是子进程,我正在运行: %d\n", count);sleep(1);count--;}}else {while(1){printf("我是父进程,我正在运行...\n");sleep(1);}}return 0;
}
僵尸进程危害
- 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!那么就会造成内存泄漏。
- 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护?是的!
- 那⼀个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存。
5.孤儿进程
父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?
父进程先退出,子进程就称之为“孤儿进程”
孤儿进程被1号systemd进程领养,当然要有systemd进程回收喽。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>int main()
{pid_t id = fork();if(id == 0){// childwhile(1){printf("我是一个子进程, pid: %d, ppid: %d\n", getpid(), getppid());sleep(1);}}else{// fatherint cnt = 5;while(cnt){printf("我是一个父进程, pid: %d, ppid: %d\n", getpid(), getppid());cnt--;sleep(1);}} return 0;
}
那么这个1号进程是什么呢? 为什么要领养呢?
1号进程就是操作系统。
如果不领养,子进程退出,进入僵尸状态后就无法回收,造成内存泄漏。
三).进程优先级
1.基本概念
- cpu资源分配的先后顺序,就是指进程的优先权(priority)。
- 优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
- 还可以把进程运行到指定的CPU上,这样⼀来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能
当然,这么看还是很陌生,我们从三个方向认识。
是什么?
是进程得到cpu资源的先后顺序。
为什么?
目标资源稀缺,导致要通过优先级确认谁先谁后的问题。
怎么办?
也是一种数字。值越低,优先级越高,反之,优先级越低。基于时间片的分时操作系统,考虑公平性,优先级可能变化,但是变化幅度不能太大。
2.查看系统进程
ps -al | head -1 && ps -al |grep code
- UID : 代表执行者的身份
- PID : 代表这个进程的代号
- PPID :代表这个进程是由哪个进程发展衍⽣而来的,亦即父进程的代号
- PRI :代表这个进程可被执行的优先级,其值越小越早被执行
- NI :代表这个进程的nice值
注意:
1.nice其取值范围是-20⾄19,一共40个级别 。
2.当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行。
3.程真实的优先级=PRI(默认优先级80)+NI。
3.查看修改进程优先级
用top命令更改已存在进程的nice:
- top
- 进入top后按“r”‒>输⼊进程PID‒>输入nice值。注意是输入nice值。
命令 | 用途 |
---|---|
nice | 启动时设置Nice值 |
renice | 修改运行中进程的Nice值 |
chrt | 查看/设置实时优先级 |
ps -o pid,ni,pri,rtprio,comm | 查看各种优先级 |
top | 交互式查看和修改优先级 |
注意:优先级设立不合理,会导致优先级低的进程长时间得不到CPU资源,进而导致:进程饥饿
4.补充概念---竞争、独立、并行、并发
竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级。
独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰。
并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行。
并发: 多个进程在⼀个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。
四).进程切换
CPU上下文切换:其实际含义是任务切换, 或者CPU寄存器切换。当多任务内核决定运行另外的任务时, 它保存正在运行任务的当前状态, 也就是CPU寄存器中的全部内容。这些内容被保存在任务自己的堆栈中, 入栈工作完成后就把下一个将要运行的任务的当前状况从该任务的栈中重新装入CPU寄存器, 并开始下一个任务的运行, 这一过程就是context switch。
当然这么看还是不理解,我们先认识下几个问题 。
1.死循环进程如何运行
- 一旦一个进程占有CPU,会把自己的代码执行完吗?不会,只执行一段时间片的东西。
- 死循环进程不会一直占有CPU。
2. CPU和寄存器
3.如何切换
加速理解:
可以理解为我们进入大学后去服两年兵役,此时我们需要向导员报告,学校需要保存我们的学籍信息。两年后,我们回来继续上学,那么我们要提前通知导员,让学校恢复我们的学籍信息。我们可以粗略的认为:学校-----CPU;导员-----调度器;我们-----进程;学籍-----进程运行的临时数据,CPU内寄存器里面的内容(当前进程的上下文数据);保留学籍-----保存进程上下文数据,CPU内寄存器里面的内容保存起来;恢复数据-----恢复进程上下文数据,恢复到CPU内寄存器里;去服兵役-----进程从CPU上剥离下来。
时间片:当代计算机都是分时操作系统,没有进程都有它合适的时间片(其实就是⼀个计数器)。时间片到达,进程就被操作系统从CPU中剥离下来。
五).Linux真实的调度算法
图是Linux2.6内核中进程队列的数据结构
1.优先级
- 普通优先级:100〜139(我们都是普通的优先级,想想nice值的取值范围,可与之对应!)
- 实时优先级:0〜99(不关心)
2.活动队列
- 时间片还没有结束的所有进程都按照优先级放在该队列
- nr_active: 总共有多少个运行状态的进程
- queue[140]: 一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度,所以,数组下标就是优先级!
- 从该结构中,选择一个最合适的进程,过程是怎么的呢?
1. 从0下表开始遍历queue[140]
2. 找到第一个非空队列,该队列必定为优先级最高的队列
3. 拿到选中队列的第一个进程,开始运行,调度完成!
4. 遍历queue[140]时间复杂度是常数!但还是太低效了 - bitmap[5]:一共140个优先级,一共140个进程队列,为了提高查找非空队列的效率,就可以用5*32个比特位表示队列是否为空,这样,便可以大大提高查找效率!
3.过期队列
- 过期队列和活动队列结构一模一样
- 过期队列上放置的进程,都是时间片耗尽的进程
- 当活动队列上的进程都被处理完毕之后,对过期队列的进程进行时间片重新计算
4.active指针和expired指针
- active指针永远指向活动队列
- expired指针永远指向过期队列
- 可是活动队列上的进程会越来越少,过期队列上的进程会越来越多,因为进程时间片到期时⼀直都存在的。
- 没关系,在合适的时候,只要能够交换active指针和expired指针的内容,就相当于有具有了一批新的活动进程!
那么我们理解了这算法,我们也就知道了为什么要有 PRI和NI两个值了,如果没有NI,那当我们修改进程的优先级时,是在活动队列直接将进程的优先级修改然后再排序吗?不,那会大大增加时间成本;而是将修改的结果计算好,当进程在活动队列运行结束后进入过期队列后直接链接,不用排序。
- PRI :代表这个进程可被执行的优先级,其值越小越早被执行
- NI :代表这个进程的nice值
总结: 在系统当中查找一个最合适调度的进程的时间复杂度是一个常数,不随着进程增多而导致时间成本增加,我们称之为进程调度O(1)算法!