1. 冯诺依曼体系结构

说明:

  1. 输入设备:键盘、磁盘、网卡、显卡、话筒、摄像头…
  2. 输出设备:显示器、磁盘、网卡、显卡、音响…
  3. 存储器:注意指的是内存,不是磁盘。
  4. 中央处理器(CPU):其中运算器进行算术运算逻辑运算

❤️ 注意,CPU不直接和外设打交道,因为CPU很快,外设很慢。因此有存储器在二者间起缓冲作用。在数据层面,任何外设,基本优先对内存读写;CPU也是直接对内存读写,内存是体系结构的核心设备IO = input + output

2. 操作系统 operator system

2.1 是什么what?

操作系统,是一款专门针对软硬件进行管理软件

2.2 为什么why?

在整个计算机软硬件架构中,操作系统的定位是:一款纯正的“搞管理”的软件。

  • 对上:管理好软硬件资源 —— 方式
  • 对下:给用户提供稳定、高效、安全的运行环境 —— 目的

💛 以学校中的管理类比,操作系统中——

  1. 管理者和被管理者并不会直接打交道(就像我从来没见过校长一样,真没见过,就在百米开外见过书记哈哈)
    决策 —— 管理者 eg.校长
    执行 —— 执行者 eg.执行者

  2. 如何管理你?
    对你做出各种决策,决策依据是你的属性数据

  3. 你的数据如何被校长知道?校长的决策又是如何执行?
    通过辅导员。

2.3 怎么管理how?

站在校长角度 ——

  • 如何聚合一个学生的数据?用类/结构体描述
  • 如何将多个学生的聚合数据产生关联?用特定的数据结构来组织,于是对学生的管理工作,变成了对数据结构的增删查改。

❤️ 管理的理念 —— 先描述,再组织

  • 先描述:被管理的对象
  • 再组织:将被管理的对象用特定的数据结构组织起来

对应到操作系统,它承担着承上启下的角色 ——

3. 进程

系统中存在大量的进程,操作系统是如何进行管理的?先描述再组织

3.1 描述进程 - PCB

为什么要有PCB?因为要管理进程,就要先描述进程。

任何进程在形成之时,操作系统要为进程创建PCB(process control block),进程控制块 —— 就是描述进程的结构体

struct PCB
{
    //进程的所有属性!
}

在Linux系统中,PCB 是 task_struct,相当于媒婆和王婆的关系 ——

struct task_struct
{
    //进程的所有属性!
}

❤️ task_struct中有什么属性字段

  1. 标示符:描述本进程的唯一标示符,用来区别其他进程。

  2. 状态:任务状态,退出代码,退出信号等。(进程控制、信号详谈)

  3. 优先级:相对于其他进程的优先级,先后问题。

  4. 程序计数器:程序中正在被执行的下一条指令的地址

  5. 内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针

  6. 上下文数据:进程执行时处理器的寄存器中与进程强相关的的临时数据。

  7. I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表

  8. 记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。

    进程创建出来,CPU要执行它对应的代码,然而CPU很少,进程很多。因此OS内有一个调度模块,负责较为均衡的调度每一个进程,较为公平的获得CPU资源。

  9. 其他信息

上下文数据,后文马上详谈。

3.2 组织进程

💛 进程 vs 程序

❤️ 结论:曾经所有的程序启动,本质上都是在系统上面创建进程

有了进程控制快,所有的进程管理任务与进程对应的代码和数据毫无关系,与内核创建的该进程的PCB强相关。

把进程控制块PCB用双向链表组织在一起,于是操作系统对进程的管理,变为对数据的管理,本质上就是对双链表的增删查改。

下面详谈一下进程控制块中的上下文数据 ——

进程的代码是不可能在很短时间运行完的,规定每个进程的时间片(单次运行的最长时间),用户感受到的多个进程同时运行,本质上是CPU的快速切换。CPU只有一套寄存器,为了保护上下文,进程的这些临时数据被写入在PCB中,再来执行时,恢复上下文

4. 查看进程

我写了一段程序Myproc.c就是隔1s打印"Always",./运行,同时复制SSH渠道再打开一个窗口,便于监视进程。

💛 查看进程

ps axj | grep "proc" 

关闭进程 ——

[Ctrl + C]
kill -9 [pid] 向目标进程发送9号信号 -- 同时也证明pid能标识系统上的唯一进程

💛 以文件形式查看进程 ——/proc是Linux系统下查看进程的目录

ls /proc

进程启动后,会在/proc下形成目录,以自身PID的编号作为目录文件名 ——

💛 查看该进程属性信息,注意其中的 ——


cwd:这就是为什么文件操作时,不指定路径,会默认在当前目录下创建文件。

5. 通过系统调用创建进程 - fork

💛 查看进程PID

💛 创建子进程

执行如下代码 ——

#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

这令人感到奇怪,怎么会打印两次呢?但其实就是有两个进程在执行代码段,我们来详谈。

5.1 如何理解fork创建子进程

目前创建进程主要有两种方式,./cmdrun commandfork在操作系统角度,和它们没有差别。

❤️ fork本质是创建进程,系统中多了一个进程,就多了一份与进程相关的内核数据结构 + 进程的代码和数据。 我们fork只是创建了子进程,但是子进程对应的代码和数据呢?

  1. 默认情况下,子进程会“继承”父进程的代码和数据
    ⭐️代码fork之后,产生的子进程和父进程代码是共享的。代码是不可被修改的,这意味着父子代码只有一份完全共享
    ⭐️数据:默认情况下,数据也是“共享的”,不过修改时会发生写时拷贝来维护数据的独立性
  2. 子进程内核的数据结构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;                                
}    

发现一个函数居然有两个返回值 ——

  1. 如何理解一个函数有两个返回值?return时子进程已被创建,return也是语句,父子都会执行。

  2. 我们创建的子进程和父进程干一样的事情吗?这是没有意义的。
    一般是通过if-else分流,让父子进程各自执行不同的代码段,而这就是通过fork的返回值来完成的。
    ⭐️创建失败:<0
    ⭐️创建成功:给父进程返回子进程的PID;给子进程返回0,表示成功创建。

  3. 返回值是数据,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;    
} 

实现了分流 ——

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 */
};

❤️ R运行状态(running)

运行状态不一定在占用CPU哦,只是表示当前进程在运行队列中,随时可以被CPU调度。

❤️ S浅度睡眠状态(sleeping) ,也叫做可中断睡眠(interruptible sleep)

当完成某种任务是,任务条件不具备,需要进程进行某种等待(S/D)。可以随时接收信号[Ctrl + c]掉

⭐️ 我们把运行状态的task_struct从运行队列(run_queue)放到等待队列(wait_queue)中,叫做挂起等待阻塞

⭐️ 把从等待队列放到运行队列中,被CPU调度,叫做唤醒进程

注:千万不要认为,进程只会等待CPU资源。进程可能会因为运行需要,在不同的队列里,所处状态就不同,本质上进程状态就是一种分类。

❤️ D深度睡眠状态(Disk sleep),也叫不可中断睡眠状态(uninterruptible sleep),

进程处于D状态,不可以被杀掉!(很难演示) ,在这个状态的进程通常会等待IO的结束。

❤️ T暂停状态(stopped)

可以通过发送 SIGSTOP 信号给进程来停止进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。

❤️ X死亡状态(dead)

回收进程资源。进程相关的内核数据结构&代码和数据。

❤️ Z僵尸状态(Zombie)

为什么要有僵尸状态?因为需要辨别退出/死亡原因,把进程退出的信息(数据)写入到task_struct中,供系统/父进程读取。

演示R/S/T状态:同样的复制SSH渠道,监视

  1. 运行状态R:写一个死循环,空语句
#include<iostream>      
#include<sys/types.h>      
#include<unistd.h>      
      
int main()      
{      
  while(true);    
  return 0;    
}  

  1. 睡眠状态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资源即可)

  1. 暂停状态T

发送信号。

暂停进程 ——


此时,发送信号恢复状态,会发现S后面没有+号,[ctrl + C] 也没法终止程序,这是因为你的暂停和继续让进程变成了后台运行。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-brxKkfEG-1647243295338)(C:\Users\13136\AppData\Roaming\Typora\typora-user-images\image-20220311204814783.png)]
那怎么干掉呢?$ kill -9 3061即可

💛 前台进程和后台进程的区别 ——

  • 前台进程:./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状态 ——

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z9XAQVOU-1647243295345)(C:\Users\13136\AppData\Roaming\Typora\typora-user-images\image-20220311213037066.png)]

僵尸进程会以终止状态保持在进程表中,等待父进程读取退出状态代码。会造成内存泄漏,如何避免,下下下篇文章详谈。

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进程回收。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iZWdInY6-1647243295347)(C:\Users\13136\AppData\Roaming\Typora\typora-user-images\image-20220311214706614.png)]

7. 进程优先级

CPU资源分配的先后顺序,就是指进程的优先权(priority)。为什么会有优先级?因为资源太少,本质上是分配资源的一种方式。类似食堂排队抢饭。

7.1 查看优先级

ps -l

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NVG1c09y-1647243295349)(C:\Users\13136\AppData\Roaming\Typora\typora-user-images\image-20220314084521025.png)]

注 ——

  • PRI:进程的优先级,值越小优先级越高

  • NI:进程的nice值,优先级的修正数据

  • UID:用户ID。相当于你身份证号一样的东西 在这里插入图片描述

7.2 调整优先级

调整优先级,并不不直接改PRI(你自己知道能设置就得了,不建议自己设置)
P R I ( n e w ) = P R I ( o l d ) + n i c e PRI(new) = PRI(old) + nice PRI(new)=PRI(old)+nice
💛 调整优先级:用top命令更改已存在进程的nice值(频繁操作可能需要sudo

top
进入top后按"r" → 输入进程PID → 输入nice值

可以看到,PRI通常都是80 ——


nice其取值范围是-20至19,一共40个级别 ——

为什么nice值处在一个相对较小的范围内呢?

因为优先级再怎么设置,也只能是一种相对的优先级,不能出现绝对的优先级,否则会出现严重的进程饥饿的问题。

其他概念 ——

  • 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级,调度器通过优先级确定谁先谁后。
  • 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
  • 并行: 多个进程在多个CPU下,分别同时进行运行,这称之为并行
  • 并发: 多个进程在一个CPU下,采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。

持续更新@边通书

GitHub 加速计划 / li / linux-dash
10.39 K
1.2 K
下载
A beautiful web dashboard for Linux
最近提交(Master分支:2 个月前 )
186a802e added ecosystem file for PM2 4 年前
5def40a3 Add host customization support for the NodeJS version 4 年前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐