2_进程与线程
进程与线程
PCB是进程的唯一标志- 进程是系统进行资源分配和调度的一个独立单位
- 动态性是进程最基本的特征
- 阻塞是进程的主动行为
- 高级调度即作业调度,会导致创建新进程
- 设备分配不需要创建进程
- 并发进程执行时具有
- 间断性
- 失去封闭性
- 不可再现性
封闭性是指程序执行的结果只取决于程序本身,不收外界影响,即程序的执行速度不会影响它的执行结果,即有再现性
- 增强进程安全性不是线程的优点
由于线程共享同一个进程的地址空间,一个线程出错,可能导致所有线程错误、崩溃
- 线程可以便于进程内的通信,而不能便于进程间的通信
- 线程又称为轻量级进程,但是当一个进程只有一个线程时,线程和进程一样大
- 输入设备通常由中断和事件处理,不依赖于线程
- 多线程的真正特长是并发处理,而非简单的事件响应
- 进程之间的通信方式主要有:管道、共享内存、消息传递、套接字、文件映射
- 处于运行态的进程被高优先级进程抢占,会使该进程回到就绪态
- 系统调用,是用户进程处于用户态通过软中断陷入指令调用内核功能
- 线程同步是指多个线程之间协调执行顺序机制
为了防止多个用户级线程同时访问共享资源而造成“混乱”或“数据错误”,需要用一种机制来协调他们的执行顺序,如互斥锁、信号量、条件变量等。在同步中,当某个线程如果需要等待资源,就会被程序中的调度器“暂停”,转而切换其他线程。所以线程同步可能引发用户级线程切换
- 多线程:一个程序中可以定义多个线程并同时运行它们
- 与多任务的区别:多任务是针对于操作系统而言的,代表操作系统可以同时执行的程序数;多线程是针对一个程序而言的,代表一个程序可以同时执行的线程个数,而每个线程可以完成不同的任务
父子进程
- 父子进程并发执行
- 父子进程间通信属于进程间通信,因为它们的内存空间是独立的,不像线程那样共享
- 常见的通信方式:管道、共享内存、消息队列、套接字
- 子进程会“复制”父进程的大部分资源:代码段、打开的文件、环境变量
- 但子进程是独立的,它拥有自己的地址空间和自己的
PCB - 为了保证进程的独立性和安全性,父子进程不共享虚拟地址空间,也不能同时使用同一临界资源
PPID是子进程保存的父进程的PID,用于指向父进程- 在 Linux/Unix 系统中,如果父进程终止,子进程不会立即被杀死,会被“领养”给进程号为 1 的 init 进程(现在通常是systemd)
从表面上看,复制一个一模一样的自己出来,好像有点多此一举,但实际上,父子进程模型是现代操作系统设计中极其强大和精妙的一块基石。它的用处主要体现在两大方面:
用途一:分工协作,提高并发与稳定性 (克隆出帮手)
这是最直观的用途:一个进程(父亲)可以创建一个或多个子进程,让它们同时处理不同的任务,或者分担同一个任务的不同部分。
最经典的例子:网络服务器(如网站服务器)
- 一个主服务器进程(父进程)启动后,它的唯一工作就是监听网络,等待用户的连接请求。
- 当第一个用户连接进来时,父进程马上
fork()一个子进程。这个子进程的任务就是专门为这第一个用户提供服务(比如加载网页、处理数据)。 - 与此同时,父进程根本不管那个子进程在干嘛,它立刻回去继续监听,等待下一个用户的连接。
- 当第二个用户连接进来时,父进程再
fork()一个新的子进程,专门为第二个用户服务。
这样做有什么巨大的好处?
- 高并发:成千上万的用户可以同时访问网站。每个用户都由一个独立的子进程服务,大家互不干扰,实现了并行处理,极大地提高了服务器的吞吐能力。
- 高稳定性:假设为某个用户服务的子进程因为代码Bug或者受到恶意攻击而崩溃了。没关系!死掉的只是这一个子进程,它完全不会影响到主服务器进程(父进程)和其他正在正常服务的子进程。父进程甚至可以检测到子进程的死亡,然后简单地清理一下“后事”,整个网站服务依然稳如泰山。
简单比喻:就像一个餐厅的总管(父进程),他只负责在前台接待客人。来一位客人,他就**克隆一个服务员(子进程)**去专门服务这位客人。这样总管可以不停地接待新客人,而每个服务员专心服务自己的客人,即使某个服务员打碎了盘子(崩溃),也不会影响餐厅的正常运营。
用途二:执行新任务,构建整个操作系统 (克隆后“变身”)
这是父子进程模型最核心、最根本的用途,也是整个Linux/Unix世界的基石。它通过 fork() 和 exec() 的黄金组合来实现。
最经典的例子:你在命令行(Shell)中敲入任何命令
- 你打开一个终端,正在运行的那个程序叫
bash(或者zsh等),它就是父进程。 - 你输入
ls -l然后敲回车。 bash进程立刻调用fork(),创建了一个和自己一模一样的子进程。此刻,内存里有两个bash进程。- 紧接着,那个子进程立刻调用
exec("ls", ["-l"])。exec的作用是“变身”——它会把子进程自己的内存空间完全丢弃,然后加载全新的ls程序来取而代之。 - 于是,子进程就从一个
bash的克隆体,摇身一变,成为了一个真真正正的ls进程,然后开始执行列出文件的任务。 - 与此同时,父进程(原来的
bash) 通常会调用wait(),安静地等待子进程(现在是ls)执行完毕。 ls执行完后,子进程终止,父进程wait()结束,收回控制权,并打印出新的命令行提示符,等你输入下一条命令。
这样做有什么巨大的好处?
- 模块化和解耦:
bash(外壳)和ls(具体命令)是两个完全独立的程序。bash不需要知道ls是怎么实现的,它的工作仅仅是创造一个新进程,然后让新进程自己“变身”成ls去执行。这使得操作系统可以轻松地添加任何新的命令和程序。 - 保持上下文:父进程
bash的状态(比如你当前所在的目录、环境变量等)在子进程执行期间被完好地保留了下来。如果bash是自己“变身”成ls,那ls执行完后,bash也就消失了,终端也就退出了。正是因为有父子进程的分离,才保证了你可以在一个连续的会话中执行无数条命令。
简单比喻:你(父进程)是一个魔法师,你的任务是完成“把木头变成椅子”的咒语。你不会亲自去变,而是先**fork() 一个自己的分身(子进程).然后,你对这个分身施法 exec("木匠"),分身立刻“变身”成了一个真正的木匠。木匠开始埋头做椅子,而你这个魔法师本体则在一旁等着他完工。
总结一下:
父子进程模型,一方面通过“克隆帮手”的方式实现了任务的并发与隔离,大大提高了系统的性能和稳定性;另一方面,通过“克隆后变身”的fork-exec模式,构建了整个操作系统执行新程序的基础,实现了无与伦比的灵活性和模块化。
CPU调度
- 暂时调到外存等待的进程的状态称为挂起态
- 由中级调度(内存调度)按某种策略决定某个挂起进程重新调入内存
- 一个进程可能多次调入内存、调出内存
- 而一个高级调度(作业调度)的作业只调入调出一次
- 注意区别挂起和阻塞,两者都暂时得不到CPU的服务,但前者在外存,后者在内存
- 系统吞吐量:单位时间内完成的作业数量
- 等待时间:指进程/作业处于等待处理机的状态时间之和
- 对于进程来说,是指进程建立后等待被服务的时间之和,在等待I/O完成的期间进程也是在被服务的,所以不计入等待时间
- 对于作业来说,不仅考虑建立进程后的等待时间,还要加上作业在外存后备队列中等待的时间
- 进程在操作系统内核程序临界区中不能进行调度与切换
- 进程处于临界区时可以进行调度和切换
临界区:访问临界资源的那段代码,普通临界区不会直接影响到操作系统内核的管理工作,因此可以在访问时进行调度与切换,以提高系统资料利用率
内核程序临界区:一般是用来访问某种内核数据结构的,比如进程的就绪队列,如果在访问期间进行调度与切换,极有可能影响操作系统内核的其他管理工作
- 非抢占式调度方式:只运行进程主动放弃处理机资源
- 抢占式:除了允许主动放弃处理机,也会强行被剥夺处理机

