文章目录
- 一.冯·诺依曼体系结构
- 冯诺依曼结构能干什么?
- 二.操作系统
- 概念
- 结构图(不完整)
- 为什么要有操作系统?
- 尝试理解操作系统
- 管理
- 结构图(完整)
- 总结:
- 三.进程
- 进程是什么?
- PCB
- 为什么要有PCB?
- Linux中的PCB
- 进程的task_struct本身内部的属性有哪些?
- 终止进程
- 进程创建的代码方式(fork函数)---重操作,轻原理
- 我们为什么要创建子进程?
- 多进程
- task_struct的内容
- 查看进程(通过/proc查找)
- 四.进程状态
- R状态 S状态
- 状态+中的+是什么意思?
- T状态 t状态
- D 状态
- Z 状态(僵尸进程) 与X 状态
- 孤儿进程
- 纯理论:进程的阻塞和挂起,运行
- 运行状态
- 阻塞状态
- 挂起状态
- 进程切换
- 进程优先级
- 什么是优先级?
- 为什么要有优先级?
- 如何查看优先级呢?
- top更改进程优先级
- nice与renice
一.冯·诺依曼体系结构
那么问题就来了,为什么要加一个储存器
直接输入设备–>CPU–>输出设备不好吗?
短板在输入设备和输出设备,所以加了个存储器
在计算机里面,离CPU越近,效率越高,造价越贵
存储金字塔
总结:硬件流动角度,数据层面中:(蓝色箭头)
1.CPU不和外设直接打交道,CPU之和内存打交道
2.外设(输入和输出)的数据,不是直接给CPU的,而是先要放入内存中的
冯诺依曼结构能干什么?
引例:“你给你的朋友用QQ发送你好!!”的过程
二.操作系统
概念
开机的操作就是启动操作系统
操作系统是一款软硬件资源管理的软件(比如插个U盘,电脑会显示,可以往U盘里写东西)
操作系统广义的认识:操作系统内核+操作系统的外壳周边程序
狭义:只是操作系统的内核
windows也可以没有图形化界面的
安卓的操作系统属于两层
结构图(不完整)
体系结构的层状划分结构如下
因为操作系统需要硬件的数据,那么每次更新硬件,操作系统获得数据的方式可能跟上一个配对的硬件的方式不一样,那么操作系统就得跟着更新,才能与硬件配对,获取数据
所以操作系统不直接找硬件要相关数据,而是通过驱动层,这样只需要更新驱动就行。(驱动层是独立的,操作系统是核心,更新驱动层肯定比直接更新操作系统方便、安全)
就比如鼠标,在插入电脑的时候,不能直接使用,是因为驱动层还没有下好。右下角会弹窗显示插入或者鼠标亮了(现在插进去响一声),才能使用。
为什么要有操作系统?
举个例子:
学校有椅子、桌子、粉笔、图书馆里的书、体育器材
角色有老师、保安、校长、教导主任、医生
图书馆里的书需要图书馆管理员管理,椅子桌子坏了找谁报修,学生生病找谁
有了角色的担任,管理起来就会变得方便
操作系统就是对软硬件进行管理
为用户提供提供一个良好(稳定的、安全的、高效的)运行环境
良好的反例是指:用电脑玩原神,10分钟一黑屏,5分钟一重启,就是因为某个硬件造成的。
尝试理解操作系统
管理
管理在生活中无非在做两件事:1.决策 2.执行
比如说不想上今天的早8,这就决策。今天去早8了,这叫执行
在学校层面:校长为管理者,学生是被管理者
随着被管理的学生的增多,同学的信息也变得非常多,校长很难管理
但是校长会C语言,写了一个关于学生属性的表
张三的属性就可以用结构体表达出来
学生有很多人,校长要在5W个学生中找到张三同学,为了节省时间,校长就加了给指针,形成了链表
校长对学生的管理工作(日常),变成了对链表的增删查改(计算机语言)
于是校长完成了对计算机的建模工作,也就是对学生的管理工作
校长想开除张三,就变成了删除张三的节点
综上所述,管理学生:先把学生的信息描述起来,然后再组织
结论:任何管理:先描述,再组织
引例:C++先提供描述的能力,STL标准库,有各种容器,提供组织的能力
那么如何管理硬件呢?
比如插入鼠标的一瞬间,进程管理会创建一个关于鼠标的结构体,把鼠标的驱动程序读取到的数据写入该进程(先描述),并以算法(链表等)的形式插入前面的节点后面。(再组织)
不仅仅是进程管理是这样,只要是与管理相关的都大差不差
结构图(完整)
谈谈在操作系统上层的部分
作为用户能否直接访问底层硬件吗?
故事:
隔壁的校长是接触不到张三的,万一张三被拐走了,辅导员找不到人了就是管理人员的失职。要找也是找张三的校长
所以任何访问硬件的操作,都得通过操作系统来做
我们(用户)用C语言中的fopen fclose就经过文件管理,再写进磁盘,绕不过操作系统
同理printf是从显示器上读取数据,scanf是从键盘上读取数据,也是绕不开操作系统的
那用户能否直接访问操作系统呢?
故事:
又比如:现在的你改一改操作系统,然后打起了原神,就因为改了操作系统直接蹦掉了。所以用户是不能直接接触操作系统的
上面的故事映射到计算机里面就是:银行中的柜台就是系统调用接口
这样就可以受管控的使用操作系统
故事进入第二阶段:
作为新时代的青年,准备材料去存钱开户什么的都知道。
但是有一天,来了一个老奶奶要存1000万,但是不会操作
银行经理就是外壳程序,帮助用户开发等操作
开发的时候,直接使用系统调用接口,是不具有跨平台性的,在Linux上能跑,windows可不一定能跑
例子:
总结:
三.进程
操作系统中,进程可以同时存在非常多
如何管理这么多进程呢?
先描述,再组织
进程是什么?
假设我们要运行一个名为a.out的可执行文件(程序 = 代码 + 数据)
根据冯诺依曼结构,任何可执行程序都需要进入内存,交给CPU处理
问题来了,下面图中的内存是进程吗?
可以这样想:如果文件很多,哪个先运行,哪个后运行,什么时候到我运行,堆和栈在哪?
所以:
操作系统想要管理进程:就得先描述,再组织。
PCB
PCB全称process control block
PCB在哪里?
在操作系统里,我们电脑或手机开机就是加载操作系统(操作系统也是软件)
注意的是:操作系统是对PCB的管理,而非对可执行程序管理
为什么要有PCB?
OS要进行管理,就得先描述,在组织
Linux中的PCB
通常进程排队指的是PCB(struct task_struct)排队,而不是数据
因为队列的首部的节点是a.out
所以CPU会把a.out的数据弄到CPU中去运行
总结:调度运行进程,本质就是让进程控制task_struct进行排队
举个通俗点的例子:
同学投简历,大厂会筛选简历,就是对简历(进程)的增删查改,就是对同学(数据)的管理
所以在Linux中进程的概念:进程 = 内核task_struct 结构体 + 程序的代码和数据
进程的task_struct本身内部的属性有哪些?
例子:
Makefile文件
myprocess:myprocess.c
gcc -o $@ $^
.PHONY:clean
clean:
rm -f myprocess
myprocess.c文件
#include <stdio.h>
#include <unistd.h>
int main()
{
while(1)
{
printf("I am a process!\n");
sleep(1);
}
return 0;
}
如何查看当前的进程呢?
ps axj
ps:查看当前的进程
a:是all的意思
xj:是显示进程的详细信息
axj的顺序可以乱
一个可执行文件和其进程的名字默认是一样的
查看myprocess文件的进程的命令:
ps axj | grep myprocess
会发现有两个关于myprocess的进程,是因为grep本身的进程也被查到了
显示的每一个信息是什么呢?
显示进程的第一行
ps axj | head -1
结合起来:
一个进程,如何知道自己的pid?
前面讲了,用户无法直接访问操作系统
就需要系统调用接口getpid
myprocess.c的代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t id = getpid();
while(1)
{
printf("I am a process!,pid:%d\n",id);
sleep(1);
}
return 0;
}
发现进程中的pid与运行起来的pid一样
终止进程
ctrl + C 就是在用户层面终止进程
kill -9 pid
可以直接杀掉进程
进程创建的代码方式(fork函数)—重操作,轻原理
发现还有一个getppid,第一个p是parent,第二个p是process,意思是获得当前进程的父进程的id
看一下父进程id是什么
myprocess.c的代码:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t id = getpid();
pid_t parent = getppid();
while(1)
{
printf("I am a process!,pid:%d,ppid:%d\n",id,parent);
sleep(1);
}
return 0;
}
跑起来会发现
其中18839是什么?
是bash
bash是父进程,是命令行解释器,在Linux权限篇中讲了bash是媒婆,派实习生(子进程)的的故事
创造子进程,因为用户是不能直接去触碰操作系统的,所以用系统调用接口fork。
myprocess.c的代码如下
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
printf("process is running,only me!\n");
sleep(3);
fork();
printf("hello world\n");
sleep(5);
return 0;
}
运行上面的可执行程序时,运行下面的指令,方便观察进程。-v 是去掉grep的进程
while :; do ps ajx | head -1 && ps ajx | grep myprocess | grep -v grep;sleep 1;done
运行结果:
可以发现
我们为什么要创建子进程?
想让子进程和父进程执行不一样的代码
fork会返回两个值
成功后,子进程的 PID 将在父进程中返回,在子进程中返回 0。 失败时,父进程中返回 -1,不创建子进程。
验证一下fork
process的代码如下:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
printf("process is running,only me!\n");
sleep(3);
pid_t id = fork();
if(id == -1) //失败
{
return 0;
}
else if(id == 0) //子进程
{
while(1)
{
printf("id:%d,I am child process,pid:%d,ppid:%d\n",id,getpid(),getppid());
sleep(1);
}
}
else //父进程
{
while(1)
{
printf("id:%d,I am parent process,pid:%d,ppid:%d\n",id,getpid(),getppid());
sleep(2);
}
}
return 0;
}
会发现确实有两个返回值,并且子进程的pid将在父进程中返回
于是就出现了两个问题:
1.同一个id,怎么可能即是 == 0,又是 > 0?
2.fork为什么会有两个返回值,并且返回两次
父进程的代码从磁盘中带出来的,子进程只能copy父进程给的代码。
数据上有些独特
解答一下第二个问题
多进程
代码如下:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
void RunChild()
{
while(1)
{
sleep(1);
printf("I am child process,pid:%d,ppid:%d\n",getpid(),getppid());
}
}
int main()
{
int i = 0;
const int num = 5; //创建5个子进程
for(i = 0;i<num;i++)
{
pid_t id = fork();
if(id == 0) //子进程进入
{
RunChild();
}
sleep(1); //父进程继续往下走
}
while(1)
{
sleep(1);
printf("I am parent process,pid:%d,ppid:%d\n",getpid(),getppid());
}
return 0;
}
可以看到进程逐渐变多
task_struct的内容
标识符:ppid pid 用来和别的进程做区分
状态:任务状态、退出代码、退出信号等
优先级:想对于其它进程的优先级
程序计数器: 程序中即将被执行的下一条指令的地址
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]
I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等
其他信息
查看进程(通过/proc查找)
进程的信息也可以通过 /proc
系统文件夹查看
假设要获取进程的PID==1的程序
直接查看/proc/1
文件就行
举个例子:
进入proc文件,会发现文件里面的目录都是进程,目录名称就是PID
其中,/proc/PID
目录中值得注意的是这两个
第一个表示的是:进程的当前工作路径
第二个表示的是:进程的PCB中会记录自己对应的可执行程序的路径
把这个进程干掉,发现程序还在跑。(这个图片显示不出来,得自己实验)
因为把磁盘中的文件删掉了,但是进程的代码与数据已经在内存里了
然后发现cwd和exe没了
每个进程在启动的时候,会记录自己当前在哪个路径下启动
验证:下面是个简单的文件函数
#include <stdio.h>
int main()
{
FILE* fp = fopen("log.txt","w");
(void)fp;//ignore warning
return 0;
}
因为是在该目录下运行的进程(a.out),所以默认在该目录下创建log.txt文件
更改进程的所在路径
用到的函数时chdir
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
chdir("/home/sjl");//更改的路径
FILE* fp = fopen("log.txt","w");
(void)fp;//ignore waring
fclose(fp);
while(1)//显示出进程在运行
{
printf("I am a process,pid:%d\n",getpid());
sleep(1);
}
return 0;
}
进程的路径被更改
会发现在更改的路径下创建log.txt,而运行可执行程序的路径下时没有的
所以一旦把当前进程的工作目录改了,那么新建对应的一个文件,他就在改过进程的当前目录下
四.进程状态
大概就是下面这种形式
下面的状态在kernel源代码里定义:
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状态 S状态
引例:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
while(1)
{
printf("I am a process,pid:%d\n",getpid());
}
return 0;
}
运行的时候老样子,用以下指令查看进程(可执行程序为a.out)
while :; do ps ajx | head -1 && ps ajx | grep a.out | grep -v grep;sleep 1;done
会发现是S+的状态,是sleep,休眠状态
然后把printf注释掉
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
while(1)
{
//printf("I am a process,pid:%d\n",getpid());
}
return 0;
}
再次运行代码,会发现是R+状态,R为进程运行状态
原因:打印的时候,是往屏幕上打印的,屏幕是外设,CPU运行比外设快很多,CPU需要等显示屏的资源就绪。所以CPU大部分处于S状态,运气好的时候遇到CPU正在执行printf就能查到在R状态
S(休眠状态)本质分为两种:
第一种:进程在等待“资源”就绪(就是上面的例子)
第二种:可中断睡眠(进程处于睡眠的状态(比如sleep函数),但依旧可以被ctrl + c中断)
状态+中的+是什么意思?
代表的是在前台运行还是后台运行
在进程后面加个&代表在后台运行
运行后你就会发现ctrl + c中止不了
用kill -9 PID终止进程
T状态 t状态
显示出Linux的信号的指令:-l是list
kill -l
其中9号信号是不是很眼熟,杀掉进程
上面的信号底层就是宏,跟C语言中的INT_MAX意义差不多
其中19号信号SIGSTOP就是暂停进程
验证:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
while(1)
{
sleep(1);
printf("I am a process,pid:%d\n",getpid());
}
return 0;
}
S状态直接变为T状态
T状态:让进程暂停,等待被进一步唤醒(进程被暂停了,自动变为后台)
18号信号SIGCONT,continue的意思,让进程继续
调试就是进程暂停
遇到断点处,进程就暂停了
D 状态
D:Linux系统比较特有的一种状态(disk是磁盘)
因为配置的原因很难演示出来,所以拿图来讲解:
进程要往磁盘中存1个GB的数据
内存严重不足,操作系统马上被毙掉了,然后看见这个进程悠然自得的SLEEP,一怒之下,把他删了。磁盘找不到了,造成了数据的丢失
这不行啊,好比说内存是银行的系统,被删掉的进程是银行的转账记录,直接被干没了,造成了上百万的损失。
为了防止这类事情的发生,规定凡是进程要进行数据IO等待外设的时候,进程要将自己的状态设成D状态
D状态:不可被杀,被称为深度睡眠,也被称为不可中断睡眠(S就是浅度睡眠,可被杀和中断)
要唤醒D状态有两种方法:
1.进程自己醒来
2.重启,重启不行就断电
Z 状态(僵尸进程) 与X 状态
一个进程退出的时候,他会把自己退出的信息保留在PCB当中,如果没有人读取PCB中退出进程的消息,这个进程就一直不释放,一般会把代码和数据释放掉,但PCB内容与数据结构会一直存在,直到将来对该进程进行等待,如果不等待就一直处于僵尸状态。如果等待了就会转化为X状态,进而将进程所有的资源全部释放掉
引例:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
int cnt = 5;
while(cnt)//子进程运行5次后退出
{
printf("I am child,cnt:%d,pid:%d\n",cnt,getpid());
sleep(1);
cnt--;
}
}
else
{
//parent
while(1)//父进程一直不退出
{
printf("I am a parent,running always!pid:%d\n",getpid());
sleep(1);
}
}
return 0;
}
子进程运行结束后就是Z状态(僵尸状态)
子进程运行完毕,但需要维持自己的退出信息,在自己的进程task_struct会记录自己的退出信息,未来让父进程来读取
因为X状态释放是一瞬间的,所以查不出来
平时子进程创建出来是要完成任务的,得知道完成任务怎么样(获取子进程退出的信息)
kill不掉僵尸进程的,已经死进程了,杀不了了。
孤儿进程
如果父进程先退出了,那么子进程就是孤儿进程
就是与僵尸进程相反,所以验证的代码也相反:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
while(1)
{
printf("I am child,pid:%d\n",getpid());
sleep(1);
}
}
else
{
int cnt = 5;
//parent
while(cnt)
{
printf("I am a parent,cnt:%d,pid:%d\n",cnt,getpid());
sleep(1);
cnt--;
}
}
return 0;
}
发现两个S的进程就剩1个S的进程,剩下的那个S进程就是孤儿进程
问题就来了:父进程都没了,谁去回收孤儿进程?
答:孤儿进程一般都会被1号进程(OS本身)进行领养的
孤儿进程为什么要被OS领养?
答:依旧要保证子进程正常被回收
孤儿进程是可以被kill的
以前我们启动的所有进程(运行C语言代码)。怎么从来没有关心国】过僵尸进程呢?内存泄漏?
答:直接在命令行中启动的进程,他的父进程是bash,bash会自动回收新进程的Z
父进程的一般核心工作都是回收子进程
纯理论:进程的阻塞和挂起,运行
大多数书上的进程状态
运行状态
进程在运行队列中,该进程的状态就是R状态。
R状态不是指直接在CPU上去运行,而是值我已经准备好了,可以随时被调度了
一个进程一旦持有CPU,会一直运行到这个进程结束吗?
不会,就比如里面写个死循环,CPU调度就不会走了,其他进程被卡死了。
现实就是打着原神,微信还能收到信息。
是因为当代的内核基于时间片轮转调度的。
假设时间片设置的2毫秒,2毫秒一到,当前进程就会在CPU上剥离下来,插到队列的尾部,然后下一个进程上前,进入CPU
这样就可以让多个进程以切换的方式进行调度,在一个时间段内同时得以推进代码。就叫做并发
Linux不是这样调度的,上面方法太简单了。只是调度算法的一种。
如果存在两个CPU,就有两个调度队列
根据不同的进程,放到不同队列,也可以放进两个队列
任何时刻,都同时有多个进程在同时运行,我们叫做并行
阻塞状态
问题:我们在写C语言的时候,用过一个接口scanf,如果我们不输入的话,进程处于什么状态?另外scanf在等待什么资源呢?
测试代码如下:
#include <stdio.h>
int main()
{
int a = 0;
scanf("%d",&a);
printf("a = %d\n",a);
return 0;
}
会发现是S状态,其实D也算阻塞状态,不过无法演示
他在等什么呢?在等键盘资源是否就绪,键盘上面有没有被用户按下的按键,按键数据交给进程
键盘上面没数据,进程是不OK,该进程并没有被运行的(R状态),所以一定不在运行队列中
操作系统要对所有的硬件做管理,那么操作系统如何对硬件做管理?
管理肯定离不开先描述,在组织
一个进程如何等待一个设备呢?
不只是CPU才有等待队列,设备中也有等待队列wait_queue
演示一下scanf等待键盘资源的总过程:
总结:阻塞和运行状态的变化,往往伴随着PCB被连入到不同的队列中
入队列的不是进程的代码和数据,而是进程的task_struct
挂起状态
我们在装系统的时候,会有一个swap分区,大多数情况swap分区是内存的1.5倍2倍,可以自己定义。
当OS中内存吃紧,且有进程处于阻塞状态(暂时不会被调度),那么就把该进程的代码和数据唤出到swap分区中。这样内存就会腾出空间
这种状态叫做阻塞挂起态
如果该设备又等待就绪了,就会把进程的代码和数据唤入到内存
这样可以更合理的使用内存资源
还有很多挂起,就绪挂起等。本质都是一样的。
频繁的唤入唤出会导致效率问题。牺牲效率换取空间
不建议把swap分区放大太,因为操作系统会过渡依赖swap分区,一压力大就唤出。要逼着操作系统合理的分配内存
swap分区的位置
ls /proc/swaps
进程切换
CPU内部的所有寄存器中的临时数据,叫做进程的上下文
寄存器本身是硬件,具有数据的存储能力,CPU的寄存器硬件只有一套
但CPU内部的数据可以有多套,有几个进程,就有几套和该进程对应的上下文数据
进程优先级
什么是优先级?
指定进程获取某种资源(比如CPU)的先后顺序
进程控制模块task_struct中的内部字段有一个或多个整型(比如int prio =)的优先级数字
Linux中优先级数字越小,优先级越高
为什么要有优先级?
进程访问的资源(CPU)是有限的
进程比硬件资源多,显示屏就一个,网页有很多
操作系统关于调度和优先级的原则:分时操作系统,保证了基本公平
如果进程因长时间不被调度,就造成了饥饿问题(比如学生打饭,教导主任差一下队,校长插一下队)
如何查看优先级呢?
引例:
#include <stdio.h>
#include <unistd.h>
int main()
{
while(1)
{
printf("I am a process,pid:%d\n",getpid());
sleep(1);
}
return 0;
}
查看当前终端的进程
ps -l
想查看全部终端的进程
ps -al
PRI:进程优先级
NI:进程优先级的修正数据(nice值),新的优先级 = 优先级 + nice
top更改进程优先级
top
先输入top
然后输入r
输入需要调整优先级的PID
输入100,然后按回车
会发现优先级是99,不是80+100
这是因为nice并不能任意调整,是有范围的(-20~19)
如果能随便调,把优先级调很高,就可能会一直运行一个进程,影响了调度的平衡
第二次改的时候不让改了,因为一个进程的优先级是不能频繁改的
这里切root用户更改,nice的值为6
发现不是99+6,而是80+6
因为每次调整优先级,都是从80开始的
nice与renice
这两也可以更改优先级
nice命令格式:nice -n [NI值] [命令]
加不加n都行
renice命令格式:renice [NI值] [PID]