【读书笔记】Linux内核设计与实现--内核同步介绍

并发访问共享数据是造成系统不稳定的一类隐患。
随着Linux内核2.6的出现,内核已经发展成抢占式内核,意味着在不加保护的情况下,调度程序可以在任何时刻抢占正在运行的内核代码,重新调度其他的进程执行。
内核代码中有不少部分都能够同步执行,这样就必须将它们妥善的保护起来。

1.临界区和竞争条件

Q:何为临界区?
A:访问和操作共享数据的代码段。

多个执行线程并发访问同一个资源通常是不安全的,为了避免在临界区中并发访问,必须保证这些代码原子地执行,即操作在执行结束之前不可被打断,就如同整个临界区是一个不可分割的指令一样。

如果两个执行线程(这里指任何正在执行的代码实例)有可能处于同一个临界区中同时执行,那么称之为竞争条件(race conditions)。

避免并发和防止竞争条件称之为同步(syncchronization)。

Q:为什么我们需要保护?
A:明白临界区无处不在。

2.加锁

当共享资源是一个复杂的数据结构时,竞争条件往往会使该数据结构遭到破坏。
如:允许并发访问队列,就会产生危害。

Q:如何允许并发访问队列,而不产生危害?
A:加锁–我们需要一种方法确保一次有且只有一个线程对数据结构进程操作,或者当另一个线程在对临界区标记时,就禁止(锁定)其他访问。 这样,线程持有锁,而锁保护了数据。

锁机制可以防止并发执行,并且保护队列不受竞争条件的影响。

任何访问队列的代码首先都要占住相应的锁,这样该锁就能阻止来自其他执行线程的并发访问,如:
在这里插入图片描述

2.1 造成并发执行的原因

用户空间之所以需要同步,是因为用户程序会被调度程序抢占和重新调度。
由于用户进程可能在任何时刻被抢占,而调度程序完成可能选择另一个高优先级的进程到处理器上执行,所以就会使得一个程序正处于临界区时,被非自愿的抢占了。

内核中有类似可能造成并发执行的原因。如下:

  1. 中断–中断几乎可以在任何时刻异步发生,也就是可能随时打断当前正在执行代码;
  2. 软中断和tasklet–内核能在任何时刻唤醒或调度软中断和tasklet,打断当前正在执行的代码;
  3. 内核抢占–因为内核具有抢占性,所以内核中的任务可能会被另一任务抢占;
  4. 睡眠以及用户空间的同步–在内核执行的进程可能会睡眠。这样就会唤醒调度程序,从而导致调度一个新的用户进程执行;
  5. 对称多处理–两个或多个处理器可以同时执行代码。

ps:辨认出真正需要共享的数据和相应的临界区,才是真正具有挑战性的地方,要记住,最开始设计代码的时候就要考虑加入锁,而不是事后才想到。

2.2 了解要保护些什么

找出哪些数据需要保护是关键所在
由于任何可能被并发访问的代码几乎无例外地需要保护,所以寻找哪些代码不需要保护反而相对更容易些。

如:执行线程的局部数据仅仅被它本身访问,显然不需要保护,比如局部变量(还有动态分配的数据结构,其地址仅存放在堆栈中)不需要任何形式的锁,因为它们独立存在于执行线程的栈中。如果数据只会被特定的进程访问,那么也不需要加锁(因为进程一次只在一个处理器上执行)。

在编写内核代码时,要问自己如下问题:

  1. 这个数据是不是全局的?除了当前线程外,其他线程能不能访问它?
  2. 这个数据会不会在进程上下文和中断上下文中共享?它是不是要在两个不同的中断处理程序中共享?
  3. 进程在访问数据时可不可能被抢占?被调度的新程序会不会访问同一数据?
  4. 当前进程是不是会睡眠(阻塞)在某些资源上,如果是,它会让共享数据处于何种状态?
  5. 怎么样防止数据失控?
  6. 如果这个函数又在另一个处理器上被调度会发生什么呢?
  7. 如何确保代码远离并发威胁呢?

3.死锁

Q:死锁产生的条件(什么是死锁)?
A:死锁的产生需要一定的条件:要有一个或多个执行线程和一个或多个资源,每个线程都在等待其中的一个资源,但所有的资源都已经被占用了。所有的线程都在相互等待,但它们永远不会释放已经占有的资源。-- 于是任何线程都无法继续,这便意味着死锁的发生。

自死锁:如果一个执行线程试图去获得一个自己已经持有的锁,它将不得不等待锁被释放,但因为它正在忙着等待这个锁,所以自己永远也不会有机会释放锁,最终造成自死锁。
在这里插入图片描述

ABBA死锁:考虑有n个线程和n个锁,如果每个线程都持有一把其他进程需要得到的锁,那么所有的线程都将阻塞地等待它们希望得到的锁重新可用。
在这里插入图片描述

如何预防死锁的发生,即使很难证明代码不会发生死锁,但是可以避免,如下规则对避免死锁大有帮助:

  1. 按顺序加锁。使用嵌套的锁时必须保证以相同的顺序获取锁,这样可以阻止致命拥抱类型的死锁。最好能记录下锁的顺序,以便其他人也能照此顺序使用;
  2. 防止发生饥饿。即这个代码的执行是否一定会结束?-- 如果“张”不发生?“王”要一直等待下去吗?(即超时机制)
  3. 不要重复请求同一个锁
  4. 设计应力求简单–越复杂的加锁方案越有可能造成死锁。

ps:获取锁–只要嵌套的使用多个锁,就必须按照相同的顺序去获取它们。尽管释放锁的顺序和死锁是无关的,但最好还是以获得锁的相反顺序来释放锁。

4.争用和扩展性

锁的争用(lock contention)简称争用。
争用是指正在被占用时,有其他线程试图获得该锁。
说一个锁处于高度争用状态,就是指有多个线程在等待获得该锁。
由于锁的作用是使程序以串行方式对资源进行访问,所以使用锁无疑会降低系统的性能。被高度争用(频繁被持有,或者长时间持有–两者都有就更糟糕)的锁会成为系统的瓶颈,严重降低系统性能。

但,相比于被几个相互抢夺共享资源的线程撕成碎片,引起内核崩溃,还是这种同步保护来得更好一点。

扩展性(scalability)是对系统可扩展程序的一个量度。
对于操作系统,在谈及可扩展性时就会和大量进程、大量处理器或是大量内存等联系起来,其实任何可以被计量的计算机组建都可以涉及可扩展性。

**加锁粒度用来描述加锁保护的数据规模。**粗锁保护大块数据,细锁保护小块数据。

许多锁的设计在开始阶段都很粗,但是当锁的争用问题变得严重时,设计就向更加精细的加锁方向进化。

一般来说,提高可扩展性是件好事,因为它可以提高Linux在更大型的、处理能力更强大的系统上的性能。
但一味的“提高“可扩展性,却会导致Linux在小型SMP和UP机器上的性能降低,因为小型机器可能用不到特别精细的锁,锁得过细只会增加复杂度,并加大开销。

因此,可扩展性是很重要的,需要慎重考虑。关键在于,在设计锁的开始阶段就应该考虑到保证良好的扩展性。因为即使在小型机器上,如果对重要资源锁的太粗,也容易造成系统性能瓶颈。
当锁争用严重时,加锁太粗会降低可扩展性;而锁争用不明显时,加锁过细会加大系统开销,带来浪费,都会造成系统性能下降。

以下是关于《Linux内核设计实现》的笔记: 1. 进程管理 - 进程控制块(Process Control Block, PCB):一个进程在内核中的表示,包含了进程的状态、各种计数器和指针,以及进程所需要的资源。 - 进程调度:内核必须在可能的情况下公平地分配CPU时间片给每个进程。Linux内核使用完全公平调度(Completely Fair Scheduler, CFS)来实现这一点。 - 进程同步:进程在访问共享资源时需要同步,以避免冲突。Linux内核提供了多种同步机制,如信号量、自旋锁和读写锁等。 2. 内存管理 - 虚拟内存:每个进程都拥有自己的虚拟内存空间,这使得每个进程都可以认为自己独占整个系统内存。 - 页面置换:当物理内存不足时,Linux内核会使用页面置换算法将一部分未使用的页面从物理内存中移出,以便为正在运行的进程腾出空间。 - 内存映射文件:Linux允许将磁盘上的文件映射到进程的虚拟地址空间中,这样就可以像访问内存一样访问文件。 3. 文件系统 - 虚拟文件系统(Virtual File System, VFS):Linux内核中的抽象层,它允许系统支持多种文件系统格式,如ext4、FAT32等。 - I/O管理:内核必须管理所有的I/O操作,包括磁盘读写和网络通信等。 - 文件描述符:Linux内核使用文件描述符来标识打开的文件,每个进程都有一个文件描述符表。 4. 网络协议栈 - TCP/IP协议栈:Linux内核支持多种网络协议,其中最常用的是TCP/IP协议栈。 - Socket:在Linux中,进程之间通信的主要方式是使用Socket。Socket是一种抽象的概念,它代表了一个网络连接。 5. 设备驱动程序 - 驱动程序开发:Linux内核的设备驱动程序通常是以模块的形式开发的,它们可以动态地加载和卸载。 - 设备文件:Linux内核将设备表示为文件,它们可以通过文件系统接口来访问。 以上是《Linux内核设计实现》的一些重点内容和笔记,希望对你有所帮助。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wang 恒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值