@TOC
1. 冯诺依曼体系结构
" title="">
说明:
- 输入设备:键盘、磁盘、网卡、显卡、话筒、摄像头...
- 输出设备:显示器、磁盘、网卡、显卡、音响...
- 存储器:注意指的是内存,不是磁盘。
- 中央处理器(CPU):其中运算器进行算术运算和逻辑运算
:heart: 注意,CPU不直接和外设打交道,因为CPU很快,外设很慢。因此有存储器在二者间起缓冲作用。在数据层面,任何外设,基本优先对内存读写;CPU也是直接对内存读写,内存是体系结构的核心设备,IO = input + output
。
2. 操作系统 operator system
2.1 是什么what?
操作系统,是一款专门针对软硬件进行管理的软件。
2.2 为什么why?
在整个计算机软硬件架构中,操作系统的定位是:一款纯正的“搞管理”的软件。
- [ ] 对上:管理好软硬件资源 —— 方式
- [ ] 对下:给用户提供稳定、高效、安全的运行环境 —— 目的
:yellow_heart: 以学校中的管理类比,操作系统中——
- 管理者和被管理者并不会直接打交道(就像我从来没见过校长一样,真没见过,就在百米开外见过书记哈哈)
决策 —— 管理者 eg.校长
执行 —— 执行者 eg.执行者 - 如何管理你?
对你做出各种决策,决策依据是你的属性数据。
- 你的数据如何被校长知道?校长的决策又是如何执行?
通过辅导员。
2.3 怎么管理how?
站在校长角度 ——
- [ ] 如何聚合一个学生的数据?用类/结构体描述
- [ ] 如何将多个学生的聚合数据产生关联?用特定的数据结构来组织,于是对学生的管理工作,变成了对数据结构的增删查改。
:heart: 管理的理念 —— 先描述,再组织
- [ ] 先描述:被管理的对象
- [ ] 再组织:将被管理的对象用特定的数据结构组织起来
对应到操作系统,它承担着承上启下的角色 ——
" title="">
3. 进程
系统中存在大量的进程,操作系统是如何进行管理的?先描述,再组织!
3.1 描述进程 - PCB
为什么要有PCB?因为要管理进程,就要先描述进程。
任何进程在形成之时,操作系统要为进程创建PCB(process control block),进程控制块 —— 就是描述进程的结构体
struct PCB
{
//进程的所有属性!
}
在Linux系统中,PCB 是 task_struct
,相当于媒婆和王婆的关系 ——
struct task_struct
{
//进程的所有属性!
}
:heart: task_struct
中有什么属性字段?
- 标示符:描述本进程的唯一标示符,用来区别其他进程。
- 状态:任务状态,退出代码,退出信号等。(进程控制、信号详谈)
- 优先级:相对于其他进程的优先级,先后问题。
- 程序计数器:程序中正在被执行的下一条指令的地址。
- 内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据:进程执行时处理器的寄存器中与进程强相关的的临时数据。
- I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
进程创建出来,CPU要执行它对应的代码,然而CPU很少,进程很多。因此OS内有一个调度模块,负责较为均衡的调度每一个进程,较为公平的获得CPU资源。
- 其他信息
上下文数据,后文马上详谈。
3.2 组织进程
:yellow_heart: 进程 vs 程序
" title="">
:heart: 结论:曾经所有的程序启动,本质上都是在系统上面创建进程。
有了进程控制快,所有的进程管理任务与进程对应的代码和数据毫无关系,与内核创建的该进程的PCB
强相关。
把进程控制块PCB
用双向链表组织在一起,于是操作系统对进程的管理,变为对数据的管理,本质上就是对双链表的增删查改。
" title="">
下面详谈一下进程控制块中的上下文数据 ——
进程的代码是不可能在很短时间运行完的,规定每个进程的时间片(单次运行的最长时间),用户感受到的多个进程同时运行,本质上是CPU的快速切换。CPU只有一套寄存器,为了保护上下文,进程的这些临时数据被写入在PCB中,再来执行时,恢复上下文。
4. 查看进程
我写了一段程序Myproc.c
就是隔1s打印"Always",./
运行,同时复制SSH渠道再打开一个窗口,便于监视进程。
:yellow_heart: 查看进程
ps axj | grep "proc"
关闭进程 ——
[Ctrl + C]
kill -9 [pid] 向目标进程发送9号信号 -- 同时也证明pid能标识系统上的唯一进程
" title="">
:yellow_heart: 以文件形式查看进程 ——/proc
是Linux系统下查看进程的目录
ls /proc
进程启动后,会在/proc
下形成目录,以自身PID
的编号作为目录文件名 ——
" title="">
:yellow_heart: 查看该进程属性信息,注意其中的 ——
" title="">
cwd:这就是为什么文件操作时,不指定路径,会默认在当前目录下创建文件。
5. 通过系统调用创建进程 - fork
:yellow_heart: 查看进程PID
" title="">
:yellow_heart: 创建子进程
" title="">
执行如下代码 ——
#include<iostream>
#include<sys/types.h>
#include<unistd.h>
int main()
{
fork();
std::cout << "hello proc:" << getpid() << " hello parent:" << getppid() << std::endl;
sleep(1);
return 0;
}
发现调用了fork后,打印了两次,并且这两个进程是有父子关系的,且普通进程的父进程基本是bash
。
" title="">这令人感到奇怪,怎么会打印两次呢?但其实就是有两个进程在执行代码段,我们来详谈。
5.1 如何理解fork创建子进程
目前创建进程主要有两种方式,./cmd
或run command
,fork在操作系统角度,和它们没有差别。
:heart: fork本质是创建进程,系统中多了一个进程,就多了一份与进程相关的内核数据结构 + 进程的代码和数据。 我们fork只是创建了子进程,但是子进程对应的代码和数据呢?
- 默认情况下,子进程会“继承”父进程的代码和数据
:star:代码:fork之后,产生的子进程和父进程代码是共享的。代码是不可被修改的,这意味着父子代码只有一份完全共享。
:star:数据:默认情况下,数据也是“共享的”,不过修改时会发生写时拷贝来维护数据的独立性。 - 子进程内核的数据结构
task_struct
,也会以父进程的为模板初始化自身
5.2 返回值
我们把代码稍作修改,打印一下返回值 ——
#include<iostream>
#include<sys/types.h>
#include<unistd.h>
int main()
{
pid_t id = fork();
std::cout << "hello proc:" << getpid() << " hello parent:" << getppid() << "ret:"<< id << std::endl;
sleep(1);
return 0;
}
发现一个函数居然有两个返回值 ——
" title="">
- 如何理解一个函数有两个返回值?return时子进程已被创建,return也是语句,父子都会执行。
- 我们创建的子进程和父进程干一样的事情吗?这是没有意义的。
一般是通过if-else
分流,让父子进程各自执行不同的代码段,而这就是通过fork的返回值来完成的。
:star:创建失败:<0
:star:创建成功:给父进程返回子进程的PID
;给子进程返回0
,表示成功创建。
- 返回值是数据,return时需要写入。谁先返回,就会发生写时拷贝,可以看到两个返回值的确不同。
注:fork之后,父子谁会先运行?这是不确定的,是由调度器来确定的。
多进程代码,让父子执行不同的事情 ——
#include<iostream>
#include<sys/types.h>
#include<unistd.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
std::cout << "I am child,pid: " << getpid() << ", ppid:" << getppid() << std::endl;
sleep(1);
}
else if (id > 0)
{
//parent
std::cout << "I am parent,pid: " << getpid() << ", ppid:" << getppid() << std::endl;
sleep(1);
}
else
{
//TODU
}
return 0;
}
实现了分流 ——
" title="">
6. 进程状态
进程的状态信息也是在task_struct(PCB)
中。进程状态的意义在于,方便OS快速判断进程,并完成特定的功能,比如调度。本质上是一种分类。
6.0 进程状态
下面的状态在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 */
};
:heart: R运行状态(running)
运行状态不一定在占用CPU哦,只是表示当前进程在运行队列中,随时可以被CPU调度。
" title="">
:heart: S浅度睡眠状态(sleeping) ,也叫做可中断睡眠(interruptible sleep)
当完成某种任务是,任务条件不具备,需要进程进行某种等待(S/D)。可以随时接收信号[Ctrl + c]掉
" title="">
:star: 我们把运行状态的task_struct
从运行队列(run_queue)放到等待队列(wait_queue)中,叫做挂起等待(阻塞)
:star: 把从等待队列放到运行队列中,被CPU调度,叫做唤醒进程。
注:千万不要认为,进程只会等待CPU资源。进程可能会因为运行需要,在不同的队列里,所处状态就不同,本质上进程状态就是一种分类。
:heart: D深度睡眠状态(Disk sleep),也叫不可中断睡眠状态(uninterruptible sleep),
进程处于D状态,不可以被杀掉!(很难演示) ,在这个状态的进程通常会等待IO的结束。
:heart: T暂停状态(stopped)
可以通过发送 SIGSTOP 信号给进程来停止进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
:heart: X死亡状态(dead)
回收进程资源。进程相关的内核数据结构&代码和数据。
:heart: Z僵尸状态(Zombie)
为什么要有僵尸状态?因为需要辨别退出/死亡原因,把进程退出的信息(数据)写入到task_struct
中,供系统/父进程读取。
演示R/S/T状态:同样的复制SSH渠道,监视
- 运行状态R:写一个死循环,空语句
#include<iostream>
#include<sys/types.h>
#include<unistd.h>
int main()
{
while(true);
return 0;
}
" title="">
- 睡眠状态S:循环打印
#include<iostream>
#include<sys/types.h>
#include<unistd.h>
int main()
{
while(true)
{
std::cout << "Always" << std::endl;
}
return 0;
}
可以看到大多数处于睡眠状态,还有少部分在运行状态 ——
这是因为,打印到显示器上,显示器是外设,很慢,IO等待外设就绪是要花时间的。而CPU太快了,挂起运行挂起运行特别快,虽然给人感受一直在运行,实际上相当长的时间都在休眠。
(这也是为什么刚才要看到R状态时,只写了一个空语句,因为这样没有IO,不用等待,排队CPU资源即可)
" title="">
- 暂停状态T:
发送信号。
" title="">
暂停进程 ——
" title="">
此时,发送信号恢复状态,会发现S
后面没有+号,[ctrl + C] 也没法终止程序,这是因为你的暂停和继续让进程变成了后台运行。
那怎么干掉呢?$ kill -9 3061
即可
" title="">
:yellow_heart: 前台进程和后台进程的区别 ——
- 前台进程:
./myproc
,输入指令无效,[ctrl + c] 可终止进程 - 后台进程:
./myproc &
,可以执行指令,[ctrl + c] 不能终止进程,退出进程要用kill
6.1 僵尸进程
写一个监控命令行脚本,语法类似C语言 ——
while :; do ps axj | head -1 && ps axj | grep myproc | grep -v grep; sleep 1; echo "########################"; done
子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程就进入Z状态。
下面一段代码,在50秒内,我把子进程杀掉,父进程不退出休眠啥也不干,此时子进程成为僵尸进程。
#include<iostream>
#include<unistd.h>
using namespace std;
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
while(true)
{
cout << "I am a child, running!" << endl;
sleep(2);
}
}
else
{
//parent
cout << "father do nothing!" << endl;
sleep(50);
}
return 0;
}
如果没有人检测和回收(由父进程来做),该进程退出就进入Z状态 ——
僵尸进程会以终止状态保持在进程表中,等待父进程读取退出状态代码。会造成内存泄漏,如何避免,下下下篇文章详谈。
6.2 孤儿进程
父进程先退出,子进程就称之为“孤儿进程”。把代码做一点点改动 ——
#include<iostream>
#include<unistd.h>
#include<stdlib.h>
using namespace std;
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
while(true)
{
cout << "I am a child, running!" << endl;
sleep(2);
}
}
else
{
//parent
cout << "father do nothing!" << endl;
sleep(10);
exit(1); //终止程序
}
return 0;
}
孤儿进程被1号进程init领养,资源由init进程回收。
7. 进程优先级
CPU资源分配的先后顺序,就是指进程的优先权(priority)。为什么会有优先级?因为资源太少,本质上是分配资源的一种方式。类似食堂排队抢饭。
7.1 查看优先级
ps -l
注 ——
- PRI:进程的优先级,值越小优先级越高
- NI:进程的nice值,优先级的修正数据
- UID:用户ID。相当于你身份证号一样的东西
7.2 调整优先级
调整优先级,并不不直接改PRI(你自己知道能设置就得了,不建议自己设置)
$$ PRI(new) = PRI(old) + nice $$
:yellow_heart: 调整优先级:用top命令更改已存在进程的nice值(频繁操作可能需要sudo
)
top
进入top后按"r" → 输入进程PID → 输入nice值
可以看到,PRI通常都是80 ——
" title="">
nice其取值范围是-20至19,一共40个级别 ——
" title="">
为什么nice值处在一个相对较小的范围内呢?
因为优先级再怎么设置,也只能是一种相对的优先级,不能出现绝对的优先级,否则会出现严重的进程饥饿的问题。
其他概念 ——
- 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级,调度器通过优先级确定谁先谁后。
- 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰。
- 并行: 多个进程在多个CPU下,分别同时进行运行,这称之为并行
- 并发: 多个进程在一个CPU下,采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。
持续更新@边通书