彻底理解进程

彻底理解进程

操作系统的"进程"很早就出现了,许多教科书上讲述这个概念总是晦涩难懂。计算机技术发展太快了,简单的概念经过无数次演化,也会变得复杂。我们追溯一下操作系统的发展历史,就能理解进程解决了什么问题、为什么这样设计。

1 操作系统的发展

1.1 手工操作阶段

第一代计算机使用真空管构成集成电路实现计算,采用磁鼓或者磁芯作为储存器,采用打孔卡片作为输入输出的介质,卡片上的孔洞位置代表机器指令或者数据。

程序员直接与硬件打交道,不需要操作系统。先将已穿孔的纸带装入输入机,然后启动输入机把程序和数据输入计算机内存,通过控制台开关启动计算程序。计算完毕,打印机输出计算结果。用户取走结果并卸下纸带后,才让下一个用户上机。这种手工操作方式有两个弊端:1. 用户独占全机,资源利用率低;2. CPU等待手工操作,利用不充分。

第一代计算机
1.2 联机批处理系统

第二代计算机采用联机批处理系统,主机与输入机之间增加速度较快的磁带机,在监督程序的自动控制下,计算机先将用户作业读入磁带,再把磁带上的作业读入主机内存并输出计算结果。完成上一批作业后,监督程序又从输入机上输入另一批作业,按上述步骤重复处理。监督程序实现了作业到作业的自动转接,减少了手工操作时间,提高了计算机的利用率,可以看作操作系统的雏形。

在等待作业输入和结果输出时,主机的CPU仍处于空闲状态,系统利用率依然不高。

第二代计算机
1.3 多道程序系统

第三代计算机采用晶体管逻辑元件及快速磁芯存储器,计算机速度从每秒几千次提高到几十万次,主存储器的存贮量,从几千提高到10万以上。1958年,IBM公司制成了第一台全部使用晶体管的计算机RCA501型。

此时的磁带、硬盘等IO设备,完全跟不上高速CPU的脚步,CPU利用率非常低。人们引入了多道程序设计技术。把多个程序放入内存,并允许它们在CPU中交替运行,共享系统中的各种软硬件资源。当一道程序因IO请求而暂停运行时,CPU便立即转去运行另一道程序。多道程序设计技术使CPU得到充分利用,提高了整个系统的资源利用率和吞吐量。出现了作业调度管理、处理机管理、存储器管理、外部设备管理、文件系统管理等功能,这标志着操作系统日趋成熟。

第三代计算机
1.4 分时系统

第四代计算机允许多个用户同时联机使用计算机,分时系统应运而生,进一步提高系统整体利用率。

第四代计算机

若一个用户的作业在分配的时间片内不能完成计算,则暂时中断该作业,把处理机让给另一作业使用,等待下一轮再继续其运行。计算机速度很快,作业运行轮转得很快,给每个用户独占了一台计算机的错觉。用户可以通过自己的终端向系统发出控制命令,在充分的人机交互情况下完成作业的运行。

1960年,麻省理工学院(MIT)开发了兼容分时系统(Compatible Time Sharing System),支持大型主机通过多个终端机来联机进行运算,并将结果从主机传输到终端机。此时终端机只具有输入输出的功能,本身不具任何运算或软件安装的能力。

为了强化大型主机的功能,让主机的资源让更多人来利用。1965年,由贝尔实验室(Bell)、麻省理工学院(MIT)及通用电器公司(GE)共同发起了MULTICS(多路信息计算系统)的计划,目标是让大型主机连接1000部终端机,支持300个用户同时使用。MULTICS项目是所有现代操作系统的鼻祖,并且首次提出了“进程”的概念。

进程就是用户独占计算机错觉的基础,每个用户的作业被包装成一个或者多个进程。进程是独立功能的程序的一次动态执行过程,也是系统资源分配的独立实体。

2 低速的IO设备

1928年,德国工程师Fritz Pfleumer发明了存储模拟信号的录音磁带。工作原理是将粉碎的磁性颗粒用胶水粘在纸条上,但是纸条比较脆弱,录音磁带无法实用化。

1932年,IBM公司的奥地利裔工程师Gustav Tauschek发明了磁鼓存储器,长度为16英寸,有40个磁道,每分钟可旋转12500转,仅可以存储10KB数据。在磁芯存储器出现之前广泛用于计算机内存,被认为是硬盘驱动器的前身。磁鼓的优点为实用可靠、经济实惠,而缺点是存储容量太小、利用率低。

1948年,美国哈佛大学实验室的王安博士发明了磁芯存储器,次年又实现了磁芯存储器“读后即写”的技术。原理是在铁氧体磁环里穿进一根导线,导线中流过不同方向的电流时,可使磁环按两种不同方向磁化,代表“1”或“0”的信息便以磁场形式储存下来。最初的磁芯存储器只有几百个字节的容量。在20世纪70年代被广泛用作计算机的主存储器,直到Intel的半导体DRAM内存批量生产。

1952年,IBM公司发布了计算机业内的第一款磁带机,需要人工手动操作,比如装载、卸载和归档。

1956年,IBM公司制造了世界上第一块硬盘350RAMAC。盘片直径为24英寸,盘片数为50片,重量则是上百公斤,相当于两个冰箱的体积,储存容量只有5MB。

从早期的磁鼓到固态硬盘,IO设备速度提升了万倍,依然追不上CPU,根本原因是计算和存储的目标不一样。CPU的最重要指标是速度快;而存储设备则要容量大,其次是速度快,最后是造价低廉,这三点不可兼得。就好比两个运动员,一个练短跑,一个练长跑,对身体素质的要求不一样。

3 进程详解

我们以Linux系统为例,了解一下进程的基本构成。

3.1 进程的分类
  • 交互进程:由一个shell启动的进程,优先级较高,交互进程既可以在前台运行,也可以在后台运行。
  • 批处理进程:这种进程和终端没有联系,是一个进程序列,负责按照顺序启动其它进程。
  • 守护进程:在后台运行且不受任何终端控制的特殊进程,用于执行特定的系统任务。它一般在Linux启动时开始执行,系统关闭时才结束。
3.2 进程的组成

Linux内核把进程称为task,task_struct结构体是操作系统的进程控制块,包含着一个进程的所有信息。task_struct结构体非常复杂,感兴趣的朋友可以查阅文件https://github.com/torvalds/linux/blob/master/include/linux/sched.h,以下列举了比较重要的字段:

  • 1)进程的标识

pid_t pid:进程的唯一标识
pid_t tgid:线程组的领头线程的pid成员的值

  • 2)进程调度信息

need_resched:调度标志
Nice:静态优先级
Counter:动态优先级;重新调度进程时会在run_queue中选出Counter值最大的进程。也代表该进程的时间片,运行中不断减少。
Policy:调度策略开始运行时被赋予的值。
rt_priority:实时优先级

  • 3)进程通信有关信息

unsigned long signal:进程接收到的信号。每位表示一种信号,共32种。
unsigned long blocked:进程所能接受信号的位掩码。
Spinlock_t sigmask_lock:信号掩码的自旋锁
Long blocked:信号掩码
Struct sem_undo semundo:为避免死锁而在信号量上设置的取消操作
Struct sem_queue
semsleeping:与信号量操作相关的等待队列
struct signal_struct *sig:信号处理函数

  • 4)上下文信息

struct desc_struct *ldt:进程关于CPU段式存储管理的局部描述符表的指针
struct thread_struct tss:任务状态段

  • 5)时间信息

Start_time:进程创建时间
Per_cpu_utime:进程在执行时在用户态上耗费的时间
Pre_cpu_stime:进程在执行时在系统态上耗费的时间
ITIMER_REAL:实时定时器,不论进程是否运行,都在实时更新
ITIMER_VIRTUAL:虚拟定时器,只有进程运行在用户态时才会更新
ITIMER_PROF:概况定时器,进程在运行处于用户态和系统态时更新

  • 6)地址空间/虚拟内存信息

struct mm_struct * mm:记录进程内存使用的信息

3.3 进程的内存分布

为了更加高效地使用内存,Linux系统采用了虚拟内存的方案,将物理内存和磁盘都映射为虚拟内存地址,为进程提供统一的地址空间。虚拟内存有三个优点:1)将内存当作磁盘的缓存,在内存中只保留常用数据,必要时从内存和磁盘之间交换数据。2)简化内存管理,为每个进程提供统一的地址空间。3)保护进程的内存空间不受其他进程影响。

每个进程都拥有独立的地址空间。一个进程无法访问另一个进程的变量和数据结构,如果想让一个进程访问另一个进程的资源,要使用进程间通信,比如管道、文件、套接字等。

进程的虚拟地址空间分为用户虚拟地址空间和内核虚拟地址空间,所有进程共享内核虚拟地址空间,每个进程有独立的用户虚拟地址空间。以32位Linux系统为例,共有4G的寻址能力。默认将高地址的1G空间分配给内核,称为内核空间,剩下的3G空间分配给进程使用,称为用户空间。用户空间从低地址到高地址空间包含5个部分:

进程的内存分布
进程的内存分布
  • 代码段(text segment):存放程序的可执行二进制代码。
  • 数据段(data segment):存放程序中已经初始化且初值不为0的全局变量和静态局部变量,数据段属于静态内存分配。
  • BSS段:存放未初始化的全局变量和静态局部变量;初值为0的全局变量和静态局部变量。
  • 堆(heap):用于存放程序运行时动态分配的内存段,可动态扩张或者缩减。
  • 栈(stack):由编译器自动分配释放,它存放函数内部声明的非静态局部变量,以及记录函数调用过程的相关维护信息(称为栈帧)。
3.4 进程的调度

每个CPU(或核心)在一个时间点上只能处理一个进程。操作系统调配CPU时间和其他资源,为进程分配一个状态,进程的状态随着环境要求而改变。进程运行的整个生命周期划分为六种状态:

  • Running:表示进程处在CPU的就绪队列中,运行态或者就绪态的进程
  • Disk Sleep:不可中断状态睡眠,一般表示进程正在跟硬件发送交互,并且交互过程中不允许被其他进程或中断打断
  • Zombie:僵尸进程,表示进程实际上已经结束了,但是父进程还没有回收它的资源(比如进程的描述符,PID等)
  • Interruptible Sleep:可中断状态睡眠,表示进程因等待某个事件而被系统挂起(阻塞态)。当进程等待的事件发生时,就会被唤醒并进入Running状态
  • Idle:空闲状态。用在不可中断睡眠的内核线程上。硬件交互导致的不可中断进程用D表示,但对某些内核线程来说,处在不可中断睡眠时有可能实际上并没有任何负载。
  • Stopped OR Traced:表示进程处于暂停或者跟踪状态。向一个进程发送SIGSTOP信号,它就会因响应这个号变成暂停状态(Stopped);向它发送SIGCONT信号,进程又会恢复运行(如果进程是终端里直接启动的,则需要用fg命令恢复到前台运行)。当用调试器调试一个进程时,在使用断点中断进程后,进程就会变成跟踪状态,这其实也是一种特殊的暂停状态,只不过可以用调试器来跟踪并按需要控制进程的运行。
  • EXIT:进程已经消亡
进程的内存分布
进程的调度

4 参考资料

https://www.linuxprobe.com/linux-system-programming.html
https://zhuanlan.zhihu.com/p/442909853
https://blog.csdn.net/qq_41209741/article/details/82870876

《彻底理解进程》的相关评论

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注