【Linux】进程概念 —— 冯诺依曼体系结构 | 操作系统 | 进程

简介: 冯诺依曼体系结构 | 操作系统 | 进程

@TOC

1. 冯诺依曼体系结构

<img src=" title="">

说明:

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

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

2. 操作系统 operator system

2.1 是什么what?

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

2.2 为什么why?

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

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

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

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

对你做出各种决策,决策依据是你的属性数据

  1. 你的数据如何被校长知道?校长的决策又是如何执行?

通过辅导员。

2.3 怎么管理how?

站在校长角度 ——

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

:heart: 管理的理念 —— 先描述,再组织

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

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

<img src=" title="">

3. 进程

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

3.1 描述进程 - PCB

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

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

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

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

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

:heart: task_struct中有什么属性字段

  1. 标示符:描述本进程的唯一标示符,用来区别其他进程。
  2. 状态:任务状态,退出代码,退出信号等。(进程控制、信号详谈)
  3. 优先级:相对于其他进程的优先级,先后问题。
  4. 程序计数器:程序中正在被执行的下一条指令的地址
  5. 内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
  6. 上下文数据:进程执行时处理器的寄存器中与进程强相关的的临时数据。
  7. I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表
  8. 记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。

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

  9. 其他信息

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

3.2 组织进程

:yellow_heart: 进程 vs 程序

<img src=" title="">

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

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

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

<img src=" title="">

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

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

4. 查看进程

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

:yellow_heart: 查看进程

ps axj | grep "proc" 

关闭进程 ——

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

<img src=" title="">

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

ls /proc

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

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

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

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

:yellow_heart: 查看进程PID

<img src=" title="">

:yellow_heart: 创建子进程

<img src=" 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

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

5.1 如何理解fork创建子进程

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

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

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

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

<img src=" title="">

  1. 如何理解一个函数有两个返回值?return时子进程已被创建,return也是语句,父子都会执行。
  2. 我们创建的子进程和父进程干一样的事情吗?这是没有意义的。

一般是通过if-else分流,让父子进程各自执行不同的代码段,而这就是通过fork的返回值来完成的。
:star:创建失败:<0
:star:创建成功:给父进程返回子进程的PID;给子进程返回0,表示成功创建。

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

实现了分流 ——

<img src=" 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调度。

<img src=" title="">

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

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

<img src=" 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渠道,监视
  1. 运行状态R:写一个死循环,空语句
#include<iostream>      
#include<sys/types.h>      
#include<unistd.h>      
      
int main()      
{      
  while(true);    
  return 0;    
}  

<img src=" title="">

  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资源即可)

<img src=" title="">

  1. 暂停状态T

发送信号。
<img src=" title="">
暂停进程 ——

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

<img src=" 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状态 ——

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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(你自己知道能设置就得了,不建议自己设置)

$$ PRI(new) = PRI(old) + nice $$

:yellow_heart: 调整优先级:用top命令更改已存在进程的nice值(频繁操作可能需要sudo

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

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

<img src=" title="">
nice其取值范围是-20至19,一共40个级别 ——
<img src=" title="">
为什么nice值处在一个相对较小的范围内呢?

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

其他概念 ——

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

持续更新@边通书

相关文章
|
17天前
|
算法 调度 UED
深入理解操作系统:进程调度与优先级队列
【10月更文挑战第31天】在计算机科学的广阔天地中,操作系统扮演着枢纽的角色,它不仅管理着硬件资源,还为应用程序提供了运行的环境。本文将深入浅出地探讨操作系统的核心概念之一——进程调度,以及如何通过优先级队列来优化资源分配。我们将从基础理论出发,逐步过渡到实际应用,最终以代码示例巩固知识点,旨在为读者揭开操作系统高效管理的神秘面纱。
|
18天前
|
安全 Linux 数据安全/隐私保护
Vanilla OS:下一代安全 Linux 发行版
【10月更文挑战第30天】
40 0
Vanilla OS:下一代安全 Linux 发行版
|
10天前
|
消息中间件 安全 算法
深入理解操作系统:进程管理的艺术
【10月更文挑战第38天】在数字世界的心脏,操作系统扮演着至关重要的角色。它不仅是硬件与软件的桥梁,更是维持计算机运行秩序的守夜人。本文将带你走进操作系统的核心——进程管理,探索它是如何协调和优化资源的使用,确保系统的稳定与高效。我们将从进程的基本概念出发,逐步深入到进程调度、同步与通信,最后探讨进程安全的重要性。通过这篇文章,你将获得对操作系统进程管理的全新认识,为你的计算机科学之旅增添一份深刻的理解。
|
14天前
|
算法 调度 UED
深入理解操作系统:进程管理与调度策略
【10月更文挑战第34天】本文旨在探讨操作系统中至关重要的一环——进程管理及其调度策略。我们将从基础概念入手,逐步揭示进程的生命周期、状态转换以及调度算法的核心原理。文章将通过浅显易懂的语言和具体实例,引导读者理解操作系统如何高效地管理和调度进程,保证系统资源的合理分配和利用。无论你是初学者还是有一定经验的开发者,这篇文章都能为你提供新的视角和深入的理解。
35 3
|
16天前
|
Linux 调度 C语言
深入理解操作系统:进程和线程的管理
【10月更文挑战第32天】本文旨在通过浅显易懂的语言和实际代码示例,带领读者探索操作系统中进程与线程的奥秘。我们将从基础知识出发,逐步深入到它们在操作系统中的实现和管理机制,最终通过实践加深对这一核心概念的理解。无论你是编程新手还是希望复习相关知识的资深开发者,这篇文章都将为你提供有价值的见解。
|
17天前
|
算法 调度 UED
深入理解操作系统的进程调度机制
本文旨在探讨操作系统中至关重要的组成部分之一——进程调度机制。通过详细解析进程调度的概念、目的、类型以及实现方式,本文为读者提供了一个全面了解操作系统如何高效管理进程资源的视角。此外,文章还简要介绍了几种常见的进程调度算法,并分析了它们的优缺点,旨在帮助读者更好地理解操作系统内部的复杂性及其对系统性能的影响。
|
18天前
深入理解操作系统:进程与线程的管理
【10月更文挑战第30天】操作系统是计算机系统的核心,它负责管理计算机硬件资源,为应用程序提供基础服务。本文将深入探讨操作系统中进程和线程的概念、区别以及它们在资源管理中的作用。通过本文的学习,读者将能够更好地理解操作系统的工作原理,并掌握进程和线程的管理技巧。
36 2
|
18天前
|
消息中间件 算法 Linux
深入理解操作系统之进程管理
【10月更文挑战第30天】在数字时代的浪潮中,操作系统作为计算机系统的核心,扮演着至关重要的角色。本文将深入浅出地探讨操作系统中的进程管理机制,从进程的概念入手,逐步解析进程的创建、调度、同步与通信等关键过程,并通过实际代码示例,揭示这些理论在Linux系统中的应用。文章旨在为读者提供一扇窥探操作系统深层工作机制的窗口,同时激发对计算科学深层次理解的兴趣和思考。
|
19天前
|
消息中间件 算法 调度
深入理解操作系统:进程管理与调度策略
【10月更文挑战第29天】本文将带领读者深入探讨操作系统中的核心组件之一——进程,并分析进程管理的重要性。我们将从进程的生命周期入手,逐步揭示进程状态转换、进程调度算法以及优先级调度等关键概念。通过理论讲解与代码演示相结合的方式,本文旨在为读者提供对进程调度机制的全面理解,从而帮助读者更好地掌握操作系统的精髓。
31 1
|
15天前
|
消息中间件 算法 调度
深入理解操作系统:进程管理的艺术
【10月更文挑战第33天】本文旨在揭示操作系统中进程管理的神秘面纱,带领读者从理论到实践,探索进程调度、同步以及通信的精妙之处。通过深入浅出的解释和直观的代码示例,我们将一起踏上这场技术之旅,解锁进程管理的秘密。
21 0