【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下,采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。

持续更新@边通书

相关文章
|
6天前
|
存储 运维 监控
深入Linux基础:文件系统与进程管理详解
深入Linux基础:文件系统与进程管理详解
43 8
|
7天前
|
缓存 资源调度 安全
深入探索Linux操作系统的心脏——内核配置与优化####
本文作为一篇技术性深度解析文章,旨在引领读者踏上一场揭秘Linux内核配置与优化的奇妙之旅。不同于传统的摘要概述,本文将以实战为导向,直接跳入核心内容,探讨如何通过精细调整内核参数来提升系统性能、增强安全性及实现资源高效利用。从基础概念到高级技巧,逐步揭示那些隐藏在命令行背后的强大功能,为系统管理员和高级用户打开一扇通往极致性能与定制化体验的大门。 --- ###
27 9
|
7天前
|
缓存 运维 网络协议
深入Linux内核架构:操作系统的核心奥秘
深入Linux内核架构:操作系统的核心奥秘
24 2
|
10天前
|
安全 网络协议 Linux
Linux操作系统的内核升级与优化策略####
【10月更文挑战第29天】 本文深入探讨了Linux操作系统内核升级的重要性,并详细阐述了一系列优化策略,旨在帮助系统管理员和高级用户提升系统的稳定性、安全性和性能。通过实际案例分析,我们展示了如何安全有效地进行内核升级,以及如何利用调优技术充分发挥Linux系统的潜力。 ####
30 1
|
14天前
|
安全 Linux 数据安全/隐私保护
Vanilla OS:下一代安全 Linux 发行版
【10月更文挑战第30天】
37 0
Vanilla OS:下一代安全 Linux 发行版
|
17天前
|
人工智能 安全 Linux
|
4月前
|
安全 Linux 网络安全
部署07--远程连接Linux系统,利用FinalShell可以远程连接到我们的操作系统上
部署07--远程连接Linux系统,利用FinalShell可以远程连接到我们的操作系统上
|
4月前
|
Linux 虚拟化 数据安全/隐私保护
部署05-VMwareWorkstation中安装CentOS7 Linux操作系统, VMware部署CentOS系统第一步,下载Linux系统,/不要忘, CentOS -7-x86_64-DVD
部署05-VMwareWorkstation中安装CentOS7 Linux操作系统, VMware部署CentOS系统第一步,下载Linux系统,/不要忘, CentOS -7-x86_64-DVD
|
1月前
|
Unix 物联网 大数据
操作系统的演化与比较:从Unix到Linux
本文将探讨操作系统的历史发展,重点关注Unix和Linux两个主要的操作系统分支。通过分析它们的起源、设计哲学、技术特点以及在现代计算中的影响,我们可以更好地理解操作系统在计算机科学中的核心地位及其未来发展趋势。
|
3月前
|
编解码 安全 Linux
基于arm64架构国产操作系统|Linux下的RTMP|RTSP低延时直播播放器开发探究
这段内容讲述了国产操作系统背景下,大牛直播SDK针对国产操作系统与Linux平台发布的RTMP/RTSP直播播放SDK。此SDK支持arm64架构,基于X协议输出视频,采用PulseAudio和Alsa Lib处理音频,具备实时静音、快照、缓冲时间设定等功能,并支持H.265编码格式。此外,提供了示例代码展示如何实现多实例播放器的创建与管理,包括窗口布局调整、事件监听、视频分辨率变化和实时快照回调等关键功能。这一技术实现有助于提高直播服务的稳定性和响应速度,适应国产操作系统在各行业中的应用需求。
109 3