Linux 基本概念
并发(Concurrency)
线程、进程,线程安全,进程同步,可见性,一致性,锁,信号量,并发,并行
线程和进程
从操作系统概念上说,线程是最小的可执行单位,也就是系统调度的最小单位。进程是资源分配的最小单位。线程是依赖进程存在的,共享进程内的资源,如内存,cpu,io 等。在操作系统的发展过程中,为了提高系统的稳定性、吞吐量和安全性,操作系统内核和用户态做了隔离,例如 Linux 有内核线程,用户线程,内核进程,用户进程,从根本上 Linux 是没有线程的,线程对 Linux 系统来说是个特殊的进程。那么用户线程和内核线程是一一对应呢?从宏观上看是一一对应的,在用户态的每一个线程,都会在内核有对应的执行线程,但是由于资源的限制,用户态的线程和内核线程是多对一的关系。用户进程和内核进程也类似。具体怎样对应的,这里就不探讨了。
为了提高操作系统的实时性,操作系统是以时间片轮转来实现任务调度的。理论上时间片内是不可以被中断的,可认为是 cpu 最小的单位执行时间。现代操作系统为了提高用户体验,线程都是抢占式的,而中断一般在时间片用完的时候发生。线程、进程和 CPU 都是多对一的关系,所以存在进程线程切换的问题。
线程内部还是有自己内存空间的,所以有个概念叫线程内存模型。线程内部有自己私有的本地内存,故线程和线程之间的本地内存存在可见性问题。例如全局变量 A 在线程 1 修改后,线程 2 并不一定能拿到 A 的修改值,因为线程 1 会把全局变量 A 拷贝到本地内存,修改后并不会马上同步。在编译的时候,编译器为了优化,(例如利用超线程技术)可能会重排指令的执行顺序,这就会存在一致性了。
线程安全
在线程安全里面经常要讨论的两个问题就是:可见性和一致性。锁是什么东西呢?锁就是一道内存屏障,保证可见性和一致性的一种策略,由操作系统甚至更底层的硬件提供。加锁是消耗资源的,特别是在多核 CPU 上,现在多核 CPU 一般有 3 级缓存,一级缓存通常是单核独占的,而线程的本地内存很可能就保存在 cpu 的缓存里面,然而加锁就意味着保证可见性和一致性,需要中断同步数据,保证别人拿到的是最新修改值。由于用途不同,锁被设计成各种各样的,如互斥锁,读写锁,自旋锁,同步块,数据库的事务等,如果只要保证可见性的,可以不使用锁,在 java 里面可以使用 volatile 修饰全局变量。虽然在 c/c++,都有同样的修饰符,但是是不是一样的意思呢,请参考其他文章。
死锁(deadlock)
定义
多个进程竞争资源造成的互相等待情况。
资源
可重用性资源:可供重复使用多次的资源
不可抢占性资源:一旦系统把某资源分配给该进程后,就不能将它强行收回,只能在进程使用完后自动释放
可消耗资源:又叫临时性资源,它是在进程运行期间,由进程动态的创建和消耗的
死锁产生的原因
- 系统资源的竞争
系统资源的竞争导致系统资源不足,以及资源分配不当,导致死锁。
主要是竞争可重用不可抢占式的资源和可消耗的资源。 - 进程运行推进顺序不合适
进程在运行过程中,请求和释放资源的顺序不当,会导致死锁。
死锁产生的条件
互斥条件 一个资源每次只能被一个进程使用,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
请求与保持条件 进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
不可剥夺条件 进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。
循环等待条件 若干进程间形成首尾相接循环等待资源的关系
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
形象地说,就是有两个酒鬼,一个有开瓶器,一个有酒,这两种资源都只能被一个人占有(互斥),且用完之前不能被另一个人抢去(不可剥夺),他们互相等对方手上的资源(循环等待),但又不肯放开自己手上的资源(请求与保持),因此陷入了死锁。
死锁避免
系统对进程发出每一个系统能够满足的资源申请进行动态检查,并根据检查结果决定是否分配资源,如果分配后系统可能发生死锁,则不予分配,否则予以分配。
书上给出了两种死锁避免策略
- 进程启动拒绝
若对每个资源,能满足现有所有进程再加上新进程的需求,则可以启动这个进程,否则拒绝 - 资源分配拒绝(银行家算法)
死锁预防
死锁预防是设法至少破坏产生死锁的四个必要条件之一,严格的防止死锁的出现。
- 互斥
不可能禁止,比如文件只允许互斥的写访问 - 占有且等待
可以要求进程一次性请求所有需要的资源,并且阻塞这个进程直到所有请求都同时满足,这样就不会再请求新资源了。 - 不可抢占
- 循环等待
持久化(Persistent)
Linux IO 模型
应用程序调用内核 IO 函数的过程如下图所示:
处于 OS 的安全性等的考虑,进程无法直接操作 I/O 设备,必须通过系统调用来请求内核完成 I/O 动作,而内核会为每个 I/O 设备维护一个 Buffer。
- 用户进程发起请求;
- 内核接收到请求后,从 I/O 设备中获取数据到 Buffer 中;
- 将 Buffer 中的数据拷贝到用户进程的地址空间,该用户进程获取到数据后响应给客户端。
在整个请求过程中,数据输入至 Buffer 需要时间,从 Buffer 复制数据到进程也需要时间,这个等待时间是限制 I/O 效率的罪魁祸首,根据等待方式的不同,I/O 动作可以分为以下五种模式:
- 阻塞 I/O(Blocking I/O)
- 非阻塞 I/O(Non-Blocking I/O)
- I/O 复用(I/O Multiplexing)
- 信号驱动的 I/O(Signal Driven I/O)
- 异步 I/O(Asynchronous I/O)
存储器管理
文件系统
IO(pipe)
1 | #include <stdio.h> |
虚拟内存
swap
虚拟化(Virtualization)
驱动管理
参考
Linux 应用
- 虚拟内存
All about Linux swap space
Swap
Linux Performance: Why You Should Almost Always Add Swap Space - 演进
Linux 内核的发展 介绍 2.6.28 和 2.6.29 版本中的新特性
对 Linux 内核的发展方向的展望 - Linux 4.2 - 运维
老司机告诉你:正规的运维工作是什么的? - 并发
Linux 原子操作 atomic_cmpxchg()/Atomic_read()/Atomic_set()/Atomic_add()/Atomic_sub() - 隔离
cgroup - jerry017cn
操作系统
- 分时和实时操作系统
List of open source real-time operating systems - 操作系统概念
【操作系统】操作系统综述(一) - Operating Systems: Three Easy Pieces