Linux 内核
1. 内核空间
现在来说一下内核空间以及用户空间,这是学习linux内核最基本的两个概念了,如果连这都不懂,,,,,,,,,,,,那就好好学吧。
我们先从这张图入手,32位操作系统的最大寻址空间是:2^32=4G,所以一个进程的虚拟空间大小为4G。但是这4G虚拟空间做了拆分,前面的3G是用户空间,后面1G是内核空间。Linux操作系统为什么要做这样的处理呢?原因很简单,Linux操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有直接访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核,保证内核的安全,操作系统将虚拟空间划分为两部分,内核空间以及用户空间。每个进程的用户空间都是独立的,每个进程的内核空间都是共享的。这也是为什么内核空间需要有同步管理的原因。
那么内核空间是通过什么方式被用户空间访问的呢?我们可以通过下图进一步了解Linux的内部结构。如下图所示:
从上图可以看出来,用户空间可以通过系统调用接口访问内核空间。用户空间必须通过内核空间访问或者控制硬件设备。当然硬件中断以及软中断的产生也会使得Linux操作系统立马进入内核态,但这些都是运行在中断上下文,和用户态进程没关系。
还有两个名词会经常提到,就是内核态和用户态。理解了内核空间和用户空间,内核态和用户态就很好理解了。
内核态:当一个进程执行系统调用而进到内核代码中执行或者中断触发,访问或者操控内核空间时,称该进程处于内核态。
用户态:当进程在执行用户自己的应用程序时,访问或者操控用户空间时,则称该进程处于用户态。
2. 上下文
对内核空间和用户空间理解后,我们再来理解一下上下文。上下文的意思其实就是Linux操作系统的某些环境信息,可分为进程上下文和中断上下文。
2.1 进程上下文
运行于内核空间,代表某个特定的进程或者内核线程执行。这里的进程上下文不可以从用户态的角度去理解,不是用户态的进程,因为进程上下文包括了用户态通过系统调用进入内核的情况以及内核线程的情况。总的来说,进程上下文其实就是需要包括进程控制块task_struct、内存管理信息mm_struct以及vm_area_struct、内核栈等系统级的上下文。
2.2 中断上下文
运行于内核空间,与任何进程以及内核线程无关,处理某个特定的中断(硬中断以及软中断)。这个时候包括的环境信息相对少很多,只需要将一些硬件参数、相关寄存器的值传给内核即可,内核通过这些环境信息进入相应的处理函数执行。中断时,内核处于中断上下文,但是此时的内核不代表任何特定的进程,中断处理函数产生的内核变动对所有进程都有效。
学习笔记,以上都是看张天飞老师的书以及网上查找资料后再结合自己的理解归纳出来的,如有不同理解的欢迎一起探讨,后面继续补充,待续。
3. 同步管理
3.1 原子操作
原子操作是指保证指令以原子的方式执行,执行过程不会被打断。举个例子:static int i = 0; …, i++,从CPU的角度来看,变量i作为一个静态全局变量存储在数据段中,首先读取变量的值到通用寄存器中,然后在通用寄存器里做 i++ 运算,最后把寄存器的数值写回变量 i 所在的内存中。所以一个变量的操作c语言实现是一步操作,汇编实现却进行了3步操作,这些操作有可能同时进行,因此需要对临界资源变量 i 加以保护。
针对上述例子,有的读者认为可以使用加锁的方式,例如用spinlock来保护 i++ 操作的原子性,但是加锁操作导致比较大的开销,用在这里有些浪费。Linux 内核提供了 atomic_t 类型的原子变量,它的实现依赖于不同的体系结构。atomic_t 类型的具体定义如下:[include/linux/types.h] typedef struct { int counter; } atomic_t;
Linux 内核提供了很多原子操作的函数:
#define ATOMIC_INIT(i) 声明一个原子变量并初始化为i #define atomic_read(v) 读取原子变量的值 #define atomic_set(v, i) 设置变量v的值为i #define atomic_inc(v) 原子地给v加1 #define atomic_dec(v) 原子地给v减1 #define atomic_add(i,v) 原子地给v增加i ...
3.2 内存屏障
Linux 内核中的内存屏障:传送门
在 ARM Linux 内核中,内存屏障函数实现代码路径:[arch/arm/include/asm/barrier.h]
内存屏障接口函数 描述 barrier() 编译优化屏障,阻止编译器为了性能优化而进行指令重排 mb() 内存屏障(包括读和写),用于SMP和UP rmb() 读内存屏障,用于SMP和UP wmb() 写内存屏障,用于SMP和UP 3.3 自旋锁
如果临界区只是一个变量,那么原子变量可以解决问题。但是临界区大多是一个数据操作集,比如临界区是一个链表操作。整个执行过程需要保证原子性,在数据被更新完毕前,不能有其他内核代码路径访问和改写这些数据。这个过程使用原子变量显然是不合适的,需要锁机制来完成。自旋锁(spinlock)是 Linux 内核中最常见的锁机制。自旋锁的特性如下:
- 忙等待的锁机制。操作系统中锁的机制分为两类,一类是忙等待,另一类是睡眠等待。自旋锁属于前者,当无法获取自旋锁时会不断轮询尝试,直到获取锁为止。
- 同一时刻只能有一个内核代码路径可以获取该锁。
- 要求自旋锁持有者尽快完成临界区的执行任务。如果临界区执行时间过长,在锁外面忙等待的CPU比较浪费,特别是自旋锁临界区里不能睡眠。
- 正是因为自旋锁临界区不能睡眠,所以自旋锁可以在中断上下文中使用。
自旋锁数据结构的定义如下:
[include/linux/spinlock_types.h] typedef struct spinlock { struct raw_spinlock rlock; } spinlock_t; typedef struct raw_spinlock { arch_spinlock_t raw_lock; } raw_spinlock_t; [arch/arm/include/asm/spinlock_types.h] typedef struct { union { u32 lock; struct __raw_tickets { /* little endian */ u16 owner; u16 next; } tickets; }; } arch_spinlock_t;
自旋锁操作函数接口:
- 定义自旋锁
动态: spinlock_t lock; spin_lock_init (&lock); 静态: DEFINE_SPINLOCK(lock);
- spin_lock():禁止内核抢占
[include/linux/spinlock_api_smp.h] static inline void __raw_spin_lock(raw_spinlock_t *lock) { preempt_disable(); // 禁止内核抢占 spin_acquire(&lock->dep_map, 0, 0, _RET_IP_); LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock); //调用 do_raw_spin_lock() }
- spin_lock_irq():禁止内核抢占、关闭硬中断,unlock时打开所有硬中断
static inline void __raw_spin_lock_irq(raw_spinlock_t *lock) { local_irq_disable(); // 关闭本地CPU中断 preempt_disable(); // 禁止内核抢占 spin_acquire(&lock->dep_map, 0, 0, _RET_IP_); LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock); //调用 do_raw_spin_lock() }
- spin_lock_irqsave():禁止内核抢占、关闭硬中断、保存硬中断的标志位,unlock时根据标志位打开
static inline unsigned long __raw_spin_lock_irqsave(raw_spinlock_t *lock) { local_irq_save(flags); //保存本地CPU当前的irq状态并且关闭本地CPU中断 preempt_disable(); // 禁止内核抢占 spin_acquire(&lock->dep_map, 0, 0, _RET_IP_); LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock); //调用 do_raw_spin_lock() }
- spin_lock_bh():禁止内核抢占、关闭软中断
/* * The preempt_count offset needed for things like: * * spin_lock_bh() * * Which need to disable both preemption (CONFIG_PREEMPT_COUNT) and * softirqs, such that unlock sequences of: * * spin_unlock(); * local_bh_enable(); * * Work as expected. */ #define SOFTIRQ_LOCK_OFFSET (SOFTIRQ_DISABLE_OFFSET + PREEMPT_LOCK_OFFSET) static inline void __raw_spin_lock_bh(raw_spinlock_t *lock) { __local_bh_disable_ip(_RET_IP_, SOFTIRQ_LOCK_OFFSET); //关闭软中断 spin_acquire(&lock->dep_map, 0, 0, _RET_IP_); LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock); }
3.4 信号量、互斥锁
信号量(semaphore)、互斥体(Mutex)都是操作系统中最常用的同步机制。自旋锁是实现一种忙等待的锁,信号量、互斥体则允许进程进入睡眠状态。
- 信号量
信号量数据结构体定义及相关操作API如下:
[include/linux/semaphore.h]
/* Please don't access any members of this structure directly */
struct semaphore {
raw_spinlock_t lock; //用于保护该结构体里的count和wait_list成员
unsigned int count; //表示允许进入临界区的内核执行路径个数
struct list_head wait_list; //没有成功获取锁的进程会睡眠在这个链表上
};
static inline void sema_init(struct semaphore *sem, int val); // 初始化信号量
[kernel/locking/semaphore.c]
extern void down(struct semaphore *sem); //获取信号量失败时进入不可中断的睡眠状态
extern int __must_check down_interruptible(struct semaphore *sem); //获取信号量失败时进入可中断的睡眠状态
extern int __must_check down_killable(struct semaphore *sem); //获取信号量失败时进入睡眠状态,睡眠可以被信号打断
extern int __must_check down_trylock(struct semaphore *sem); //尝试获取,0-成功但不会count--,1-失败但不会进入休眠
extern int __must_check down_timeout(struct semaphore *sem, long jiffies); //最大睡眠时间内获取不到信号量返回-ETIME
extern void up(struct semaphore *sem); //释放信号量
- 互斥体
信号量初始化count为1时,功能和互斥体是一致的,那为什么内核社区要重新开发互斥体呢?答案:互斥体的语义相对于信号量要简单、轻便一些,在锁争用激烈的测试场景下,互斥体比信号量执行速度更快。
信号量数据结构体定义及相关操作API如下:
[include/linux/mutex.h]
struct mutex {
atomic_long_t owner;
spinlock_t wait_lock;
#ifdef CONFIG_MUTEX_SPIN_ON_OWNER
struct optimistic_spin_queue osq; /* Spinner MCS lock */
#endif
struct list_head wait_list;
#ifdef CONFIG_DEBUG_MUTEXES
void *magic;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
struct lockdep_map dep_map;
#endif
};
静态初始化:
#define DEFINE_MUTEX(mutexname) struct mutex mutexname = __MUTEX_INITIALIZER(mutexname)
动态初始化:
struct mutex mutexname; mutex_init(mutexname);
回收锁资源:
void mutex_destroy(struct mutex *lock);
void __sched mutex_lock(struct mutex *lock); //上锁
void __sched mutex_unlock(struct mutex *lock); //解锁
3.5 读写锁
信号量有一个明显的缺点——没有区分临界区的读写属性。读写锁通常允许多个线程并发地读访问临界区,但是写访问只限制于一个线程。读写锁能有效提高并发性,在多处理器系统中允许同时有多个读者访问共享资源,但写者是排他性,读写锁具有一下特性:
- 允许多个读者同时进入临界区,但同一时刻写者不能进入。
- 同一时刻只允许一个写者进入临界区。
- 读者和写者不能同时进入临界区。
读写锁有两种,分别是自旋锁类型和信号量类型。
- 自旋锁类型读写锁
[include/linux/rwlock_types.h]
typedef struct {
arch_rwlock_t raw_lock;
} rwlock_t;
[arch/arm/include/asm/spinlock_types.h]
typedef struct {
u32 lock;
} arch_rwlock_t;
[include/linux/rwlock.h]
#define rwlock_init(lock) do { *(lock) = __RW_LOCK_UNLOCKED(lock); } while (0)
#define write_lock(lock) _raw_write_lock(lock)
#define write_unlock(lock) _raw_write_unlock(lock)
#define write_lock_irq(lock) _raw_write_lock_irq(lock)
#define write_unlock_irq(lock) _raw_write_unlock_irq(lock)
#define write_lock_bh(lock) _raw_write_lock_bh(lock)
#define write_unlock_bh(lock) _raw_write_unlock_bh(lock)
#define read_lock(lock) _raw_read_lock(lock)
#define read_unlock(lock) _raw_read_unlock(lock)
#define read_lock_irq(lock) _raw_read_lock_irq(lock)
#define read_unlock_irq(lock) _raw_read_unlock_irq(lock)
#define read_lock_bh(lock) _raw_read_lock_bh(lock)
#define read_unlock_bh(lock) _raw_read_unlock_bh(lock)
- 信号量类型读写锁
[include/linux/rwsem.h]
struct rw_semaphore {
atomic_long_t count;
struct list_head wait_list;
raw_spinlock_t wait_lock;
#ifdef CONFIG_RWSEM_SPIN_ON_OWNER
struct optimistic_spin_queue osq; /* spinner MCS lock */
/*
* Write owner. Used as a speculative check to see
* if the owner is running on the cpu.
*/
struct task_struct *owner;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
struct lockdep_map dep_map;
#endif
};
#define init_rwsem(sem)
[kernel/locking/rwsem.c]
void __sched down_read(struct rw_semaphore *sem)
void __sched down_write(struct rw_semaphore *sem)
void up_read(struct rw_semaphore *sem)
void up_write(struct rw_semaphore *sem)
int down_read_trylock(struct rw_semaphore *sem)
int down_write_trylock(struct rw_semaphore *sem)
3.6 RCU锁
3.7 等待队列
等待队列本质上是一个双向链表,当运行中的进程需要获取某一个资源而该资源暂时不能提供时,可以把进程挂入等待队列中等待该资源的释放,进程会进入睡眠状态。
等待队列数据结构定义及相关操作API如下:
1. 初始化
[include/linux/wait.h]
struct wait_queue_head {
spinlock_t lock;
struct list_head head;
};
typedef struct wait_queue_head wait_queue_head_t;
静态初始化:
#define DECLARE_WAIT_QUEUE_HEAD(name) \
struct wait_queue_head name = __WAIT_QUEUE_HEAD_INITIALIZER(name)
动态初始化:
init_waitqueue_head(q);
2. 定义等待队列项
/*
* A single wait-queue entry structure:
*/
struct wait_queue_entry {
unsigned int flags; //可以设置为 WQ_FLAG_EXCLUSIVE,表示等待的进程应该独占资源(解决惊群现象)
void *private; //用于保存等待进程的进程描述符 task_struct, 一般都是current进程
wait_queue_func_t func; //唤醒函数,一般设置为 default_wake_function() 函数,当然也可以设置为自定义的唤醒函数。
struct list_head entry; //用于连接其他等待资源的进程
};
/*
* Macros for declaration and initialisaton of the datatypes
*/
#define __WAITQUEUE_INITIALIZER(name, tsk) { \
.private = tsk, \
.func = default_wake_function, \
.entry = { NULL, NULL } }
//等待队列中的一项
#define DECLARE_WAITQUEUE(name, tsk) \
struct wait_queue_entry name = __WAITQUEUE_INITIALIZER(name, tsk)
可以通过调用 init_waitqueue_entry() 函数来初始化 wait_queue_t 结构变量:
static inline void init_waitqueue_entry(struct wait_queue_entry *wq_entry, struct task_struct *p)
{
wq_entry->flags = 0;
wq_entry->private = p;
wq_entry->func = default_wake_function;
}
也可以通过调用 init_waitqueue_func_entry() 函数来初始化为自定义的唤醒函数:
static inline void
init_waitqueue_func_entry(struct wait_queue_entry *wq_entry, wait_queue_func_t func)
{
wq_entry->flags = 0;
wq_entry->private = NULL;
wq_entry->func = func;
}
3. 添加删除等待队列
//添加到等待队列中去之后,不会进入睡眠等待
void add_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry);
//删除唤醒以后的等待项
void remove_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry);
4. 等待操作宏函数(sleep until a condition gets true)
/* 当前进程被置为不可中断地睡眠 */
wait_event(queue, condition);
/ * The function will return -ERESTARTSYS if it was interrupted by a signal and 0 if @condition evaluated to true. */
wait_event_interruptible(queue, condition);
/*
* Returns:
* 0 if the @condition evaluated to %false after the @timeout elapsed,
* 1 if the @condition evaluated to %true after the @timeout elapsed,
* or the remaining jiffies (at least 1) if the @condition evaluated
* to %true before the @timeout elapsed.
*/
wait_event_timeout(queue, condition, timeout);
wait_event_interruptible_timeout(queue, condition, timeout);
内核等待队列机制(比较全面):传送门
等待队列原理与实现(只介绍了其中一种操作方法):传送门