进程与线程

  • PCB是进程的唯一标志
  • 进程是系统进行资源分配和调度的一个独立单位
  • 动态性是进程最基本的特征
  • 阻塞是进程的主动行为
  • 高级调度即作业调度,会导致创建新进程
  • 设备分配不需要创建进程
  • 并发进程执行时具有
    • 间断性
    • 失去封闭性
    • 不可再现性

封闭性是指程序执行的结果只取决于程序本身,不收外界影响,即程序的执行速度不会影响它的执行结果,即有再现性

  • 增强进程安全性不是线程的优点

由于线程共享同一个进程的地址空间,一个线程出错,可能导致所有线程错误、崩溃

  • 线程可以便于进程内的通信,而不能便于进程间的通信
  • 线程又称为轻量级进程,但是当一个进程只有一个线程时,线程和进程一样大
  • 输入设备通常由中断和事件处理,不依赖于线程
  • 多线程的真正特长是并发处理,而非简单的事件响应
  • 进程之间的通信方式主要有:管道、共享内存、消息传递、套接字、文件映射
  • 处于运行态的进程被高优先级进程抢占,会使该进程回到就绪态
  • 系统调用,是用户进程处于用户态通过软中断陷入指令调用内核功能
  • 线程同步是指多个线程之间协调执行顺序机制

为了防止多个用户级线程同时访问共享资源而造成“混乱”或“数据错误”,需要用一种机制来协调他们的执行顺序,如互斥锁、信号量、条件变量等。在同步中,当某个线程如果需要等待资源,就会被程序中的调度器“暂停”,转而切换其他线程。所以线程同步可能引发用户级线程切换

  • 多线程:一个程序中可以定义多个线程并同时运行它们
    • 与多任务的区别:多任务是针对于操作系统而言的,代表操作系统可以同时执行的程序数;多线程是针对一个程序而言的,代表一个程序可以同时执行的线程个数,而每个线程可以完成不同的任务
父子进程
  • 父子进程并发执行
  • 父子进程间通信属于进程间通信,因为它们的内存空间是独立的,不像线程那样共享
  • 常见的通信方式:管道、共享内存、消息队列、套接字
  • 子进程会“复制”父进程的大部分资源:代码段、打开的文件、环境变量
  • 但子进程是独立的,它拥有自己的地址空间和自己的PCB
  • 为了保证进程的独立性和安全性,父子进程不共享虚拟地址空间,也不能同时使用同一临界资源
  • PPID是子进程保存的父进程的PID,用于指向父进程
  • 在 Linux/Unix 系统中,如果父进程终止,子进程不会立即被杀死,会被“领养”给进程号为 1 的 init 进程(现在通常是systemd)

从表面上看,复制一个一模一样的自己出来,好像有点多此一举,但实际上,父子进程模型是现代操作系统设计中极其强大和精妙的一块基石。它的用处主要体现在两大方面:

用途一:分工协作,提高并发与稳定性 (克隆出帮手)

这是最直观的用途:一个进程(父亲)可以创建一个或多个子进程,让它们同时处理不同的任务,或者分担同一个任务的不同部分。

最经典的例子:网络服务器(如网站服务器)

  1. 一个主服务器进程(父进程)启动后,它的唯一工作就是监听网络,等待用户的连接请求。
  2. 当第一个用户连接进来时,父进程马上 fork() 一个子进程。这个子进程的任务就是专门为这第一个用户提供服务(比如加载网页、处理数据)。
  3. 与此同时,父进程根本不管那个子进程在干嘛,它立刻回去继续监听,等待下一个用户的连接。
  4. 当第二个用户连接进来时,父进程fork() 一个新的子进程,专门为第二个用户服务。

这样做有什么巨大的好处?

  • 高并发:成千上万的用户可以同时访问网站。每个用户都由一个独立的子进程服务,大家互不干扰,实现了并行处理,极大地提高了服务器的吞吐能力。
  • 高稳定性:假设为某个用户服务的子进程因为代码Bug或者受到恶意攻击而崩溃了。没关系!死掉的只是这一个子进程,它完全不会影响到主服务器进程(父进程)和其他正在正常服务的子进程。父进程甚至可以检测到子进程的死亡,然后简单地清理一下“后事”,整个网站服务依然稳如泰山。

简单比喻:就像一个餐厅的总管(父进程),他只负责在前台接待客人。来一位客人,他就**克隆一个服务员(子进程)**去专门服务这位客人。这样总管可以不停地接待新客人,而每个服务员专心服务自己的客人,即使某个服务员打碎了盘子(崩溃),也不会影响餐厅的正常运营。

用途二:执行新任务,构建整个操作系统 (克隆后“变身”)

这是父子进程模型最核心、最根本的用途,也是整个Linux/Unix世界的基石。它通过 fork()exec() 的黄金组合来实现。

最经典的例子:你在命令行(Shell)中敲入任何命令

  1. 你打开一个终端,正在运行的那个程序叫 bash (或者 zsh 等),它就是父进程
  2. 你输入 ls -l 然后敲回车。
  3. bash 进程立刻调用 fork(),创建了一个和自己一模一样的子进程。此刻,内存里有两个 bash 进程。
  4. 紧接着,那个子进程立刻调用 exec("ls", ["-l"])exec 的作用是“变身”——它会把子进程自己的内存空间完全丢弃,然后加载全新的 ls 程序来取而代之。
  5. 于是,子进程就从一个 bash 的克隆体,摇身一变,成为了一个真真正正的 ls 进程,然后开始执行列出文件的任务。
  6. 与此同时,父进程(原来的 bash 通常会调用 wait(),安静地等待子进程(现在是 ls)执行完毕。
  7. ls 执行完后,子进程终止,父进程 wait() 结束,收回控制权,并打印出新的命令行提示符,等你输入下一条命令。

这样做有什么巨大的好处?

  • 模块化和解耦bash(外壳)和 ls(具体命令)是两个完全独立的程序。bash 不需要知道 ls 是怎么实现的,它的工作仅仅是创造一个新进程,然后让新进程自己“变身”成 ls 去执行。这使得操作系统可以轻松地添加任何新的命令和程序。
  • 保持上下文:父进程 bash 的状态(比如你当前所在的目录、环境变量等)在子进程执行期间被完好地保留了下来。如果 bash 是自己“变身”成 ls,那 ls 执行完后,bash 也就消失了,终端也就退出了。正是因为有父子进程的分离,才保证了你可以在一个连续的会话中执行无数条命令。

简单比喻:你(父进程)是一个魔法师,你的任务是完成“把木头变成椅子”的咒语。你不会亲自去变,而是先**fork() 一个自己的分身(子进程).然后,你对这个分身施法 exec("木匠"),分身立刻“变身”成了一个真正的木匠。木匠开始埋头做椅子,而你这个魔法师本体则在一旁等着他完工。

总结一下:
父子进程模型,一方面通过“克隆帮手”的方式实现了任务的并发与隔离,大大提高了系统的性能和稳定性;另一方面,通过“克隆后变身”的fork-exec模式,构建了整个操作系统执行新程序的基础,实现了无与伦比的灵活性和模块化

CPU调度

  • 暂时调到外存等待的进程的状态称为挂起态
  • 由中级调度(内存调度)按某种策略决定某个挂起进程重新调入内存
  • 一个进程可能多次调入内存、调出内存
  • 而一个高级调度(作业调度)的作业只调入调出一次
  • 注意区别挂起和阻塞,两者都暂时得不到CPU的服务,但前者在外存,后者在内存
  • 系统吞吐量:单位时间内完成的作业数量
  • 等待时间:指进程/作业处于等待处理机的状态时间之和
    • 对于进程来说,是指进程建立后等待被服务的时间之和,在等待I/O完成的期间进程也是在被服务的,所以不计入等待时间
    • 对于作业来说,不仅考虑建立进程后的等待时间,还要加上作业在外存后备队列中等待的时间
  • 进程在操作系统内核程序临界区中不能进行调度与切换
  • 进程处于临界区时可以进行调度和切换

临界区:访问临界资源的那段代码,普通临界区不会直接影响到操作系统内核的管理工作,因此可以在访问时进行调度与切换,以提高系统资料利用率
内核程序临界区:一般是用来访问某种内核数据结构的,比如进程的就绪队列,如果在访问期间进行调度与切换,极有可能影响操作系统内核的其他管理工作

  • 非抢占式调度方式:只运行进程主动放弃处理机资源
  • 抢占式:除了允许主动放弃处理机,也会强行被剥夺处理机