读书目的:了解内核编程基础,为学习《Linux设备驱动程序》和《深入理解Linux内核》做铺垫
读书收获:
心得
进程:处于执行期的程序以及相关资源的总称
进程描述符包含的数据能完整地描述一个正在执行的程序:打开的文件、进程的地址空间、挂起的信号、进程状态…;
内核把进程的列表存放在任务队列的双向循环链表中。
struct task_struct {
volatile long state;
...
}
TASK_RUNNING(运行)
TASK_INTERRUPTIBLE(信号可中断)
TASK_UNINTERRUPTIBLE(信号不可中断)
__TASK_TRACED(被跟踪)
__TASK_STOPPED(停止)
set_task_state(task, state);
/* 父进程和子进程链表 */
struct task_struct {
...
struct task_struct *parent;
struct list_head children;
...
}
/* 访问子进程 */
struct task_struct *task;
struct list_head *list;
list_for_each(list, ¤t->children) {
task = list_entry(list, struct task_struct, sibling);
}
Unix进程创建:fork()通过拷贝当前进程创建一个子进程;exec()读取可执行文件并将其载入地址空间开始运行
linux把所有线程都当做进程来实现,线程仅仅被视为与其他进程共享某些资源的进程
/* clone和fork差不多,只是父子进程共享地址空间、文件系统、文件描述符、信号处理 */
/* 新建的进程和它的父进程就是所谓的线程 */
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);
/* 从现有内核线程中创建一个新内核线程方法(需要wake_up_process唤醒) */
struct task_struct *kthread_create(int (*threadfn)(void *data),
void *data,
const char namefmt[],
...)
/* 从现有线程创建一个即刻运行的线程方法 */
struct task_struct *kthread_run(int (*threadfn)(void *data),
void *data,
const char namefmt[],
...)
/* 线程结束 */
int kthread_stop(struct task_struct *k)
显式结束:调用exit()系统调用
隐式结束:主函数返回
被动结束:接收到不能处理的信号或异常
调度程序决定将那个进程投入运行,何时运行以及运行运行多长时间
多任务系统分为两类:抢占式多任务(进程被动强制挂起)和非抢占式(进程主动挂起自己)
O(1)–>CFS
时间记账
调度器实体结构
调度器实体结构struct sched_entity作为名为se的成员变量,嵌入在进程描述符struct task_struct内
虚拟实时
struct sched_entity中的vruntime变量存放进程的虚拟运行时间,CFS使用vruntime变量来记录一个程序到底运行了多长时间以及它还应该再运行多长时间;
update_curr()函数实现了该记账功能,由系统定时器周期性调用,传递运行时间给vruntime。
/* q是希望休眠的等待队列 */
DEFINE_WAIT(wait); //创建等待队列项
add_wait_queue(q, &wait); //将q进程加入到等待队列
while (!condition) { //等待的事件
prepare_to_wait(&q, &wait, TASK_INTERRUPTIBLE); //将进程状态更变为TASK_INTERRUPTIBLE
if (signal_pending(current)) //进程信号被唤醒
/* 处理信号 */
schedule(); //执行其他进程
}
finish_wait(&q, &wait); //把q进程移出等待队列
2. 唤醒
wake_up(),它会唤醒制定的等待队列上的说有进程,它调用try_to_wake_up()将进程设置为TASK_RUNING,调用enqueue_task()将进程放入红黑树中
上下文切换:从一个可执行进程切换到另一个可执行进程,由context_switch()负责;
内核提供need_resched标志来表明是否需要重新执行一次调度,当某个进程应该被抢占时,scheduler_tick()就会设置这个标志。
内核提供两种实时调度策略:SCHED_FIFO和SCHED_RR;普通非实时调度策略为SCHED_NORMAL;SCHED_RR>SCHED_FIFO>SCHED_NORMAL
1. SCHED_FIFO:先入先出调度算法,不使用时间片,处于可执行状态的进程会一直执行,直到被阻塞、抢占或显示调用schedule()
2. SCHED_RR:带有时间片的SCHE_FIFO
Linux中,系统调用使用户空间访问内核的唯一手段;除异常和陷入外,它们是内核唯一的合法入口
POSIX是提供API和系统调用对应关系的一套标准,API由C库实现
系统调用在出现错误时,C库会把错误码写入errno全局变量,通过调用perror()将变量翻译成用户可以理解的字符串;
通过asmlinkage限定词声明系统调用函数:asmlinkage long sys_getpid(void),asmlinkage通知编译器仅从栈中提取该函数的参数。
用户程序通知内核执行系统调用是通过软中断实现:通过引发一个异常来促使系统切换到内核态去执行异常处理程序
- 指定恰当的系统调用
在x86上,系统调用号通过eax寄存器传递给内核,返回值通过eax寄存器传递给用户进程;
system_call()通过将给定的系统调用号与NR_syscalls作比较来检查其有效性,大于或等于NR_syscalls返回-ENOSYS,否则执行相应的系统调用。
- 参数传递
x86-32系统上,ebx、ecx、edx、esi、edi按照顺序存放前五个参数。
单向链表、双向链表、环形链表
//链表数据结构
struct list_head {
struct list_head *next;
struct list_head *prev;
}
//定义链表:通过嵌入list_head结构体到自己的数据结构中
struct fox {
unsigned long tail_length;
struct list_head list;
}
//定义链表头
static LIST_HEAD(fox_list)
//增加节点
list_add(struct list_head *new, struct list_head *head)//head->new
list_add_tail(struct list_head *new, struct list_head *head)//head-...->new
//删除节点
list_del(struct list_head *entry)
list_del_init(struct list_head *entry)
//移动节点
list_move(struct list_head *list, struct list_head *head)
list_move_tail(struct list_head *list, struct list_head *head)
//检查链表是否为空
list_empty(struct list_head *list)
//合并两个链表
list_splice(struct list_head *list, struct list_head *head)//head->list...
list_splice_init(struct list_head *list, struct list_head *head)
//基本方法
struct list_head *p;
list_for_each(p, list)
//基本方法+contain_of()
list_for_each_entry(pos, head, member)
list_for_each_entry_reverse(pos, head, member)
//遍历的同时删除
list_for_each_entry_safe(pos, next, head, member)
//动态创建队列
struct kfifo fifo;
int ret;
ret = kfifo_alloc(&fifo, PAGE_SIZE, GFP_KERNEL);
if (ret)
return ret;
unsigned int kfifo_in(struct kfifo *fifo, const void *from, unsigned int len);
unsigned int kfifo_out(struct kfifo *fifo, void *to, unsigned int len);
static inline unsigned int kfifo_size(struct kfifo *fifo);//返回字节数
static inline unsigned int kfifo_avail(struct kfifo *fifo);//查看队列可用空间
static inline unsigned int kfifo_is_empty(struct kfifo *fifo);//空则返回非0
static inline unsigned int kfifo_if_full(struct kfifo *fifo);//满则返回非0
static inline void kfifo_reset(struct kfifo *fifo);//抛弃队列中的内容
void kfifo_free(struct kfifo *fifo);//撤销一个使用kfifo_alloc()分配的队列
#include
#include
#include
#include
MODULE_LICENSE("Dual BSD/GPL");
struct kfifo fifo;
static int myfifo_init(void)
{
int ret;
unsigned int val;
unsigned int i;
ret = kfifo_alloc(&fifo, PAGE_SIZE, GFP_KERNEL);
if (ret)
return ret;
for (i = 0; i < 32; i++)
kfifo_in(&fifo, &i, sizeof(i));
ret = kfifo_out_peek(&fifo, &val, sizeof(val));
if (ret != sizeof(val))
return -EINVAL;
printk(KERN_INFO "%u\n", val);
while (kfifo_avail(&fifo)) {
unsigned int val;
ret = kfifo_out(&fifo, &val, sizeof(val));
if (ret != sizeof(val))
return -EINVAL;
printk(KERN_INFO "%u\n", val);
}
return 0;
}
static void myfifo_exit(void)
{
kfifo_free(&fifo);
}
module_init(myfifo_init);
module_exit(myfifo_exit);
映射即关联数组,是一个由唯一键组成的集合,每个键必须关联一个特定的值。
- 初始化一个idr
struct idr id_huh; //静态定义idr结构
idr_init(&id_huh); //初始化idr结构
int id;
do {
if (!idr_pre_get(&idr_huh, GFP_KERNEL)) //调整后备树大小,成功返回1
return -ENOSPC;
ret = idr_get_new(idr_huh, ptr, &id); //获取新UID,将其加到idr
} while (ret == -EAGAIN);
struct my_struct *ptr = idr_find(&idr_huh, id); //调用成功,则返回关联的指针
if (!ptr)
return -EINVAL;
void idr_remove(struct idr *idp, int id); //将id关联的指针一起从映射中删除
void idr_remove_all(struct idr *idp); //强制删除所有的UID
void idr_destroy(struct idr *idp); //释放idr中未使用的内存
struct rb_root root = RB_ROOT; //初始化根节点
struct rb_node node; //非根节点由rb_node描述
... //搜索、插入操作用户自己实现
对数据集合的主要操作是遍历数据就用链表
代码符合生产者/消费者模式,使用队列
需要映射一个UID到一个对象,使用映射
存储大量数据,要求检索迅速,使用红黑树
O(g(x)) | 名称 |
---|---|
1 | 恒量(理想的复杂度) |
log n | 对数的 |
n | 线性的 |
n^2 | 平方的 |
n^3 | 立方的 |
2^n | 指数的 |
n! | 阶乘 |
让硬件在需要的时候向内核发电信号,使高速处理器和低速外设协同工作
中断:异步中断,外部中断(cpu在执行某条指令时发生中断)
异常:同步中断,内部中断(cpu在执行完指令后异常才发生)
即中断的上半部,每个中断号对应一个中断处理程序,收到相应中断后就开始执行中断处理程序
上半部:即中断处理程序,执行紧急的任务,不可休眠
- 如果一个任务对时间非常敏感,将其放在中断处理程序中执行
- 如果一个任务和硬件相关,将其放在中断处理程序中执行
- 如果一个任务要保证不被其他中断打断,将其放在中断处理程序中执行
- 其他所有任务放在下半部执行
下半部:执行相对上半部不紧急的任务
request_irq()可能会休眠,不能再中断上下文中使用
int request_irq(unsigned int irq, //中断号
irq_handler_t handler, //中断处理函数指针
unsigned long flags, //中断处理程序标志
const char *name, //中断设备名
void *dev) //用于共享中断线
void free_irq(unsigned int irq, void *dev)
当执行一个中断处理程序时,内核处于中断上下文;中断处理程序拥有自己的中断栈,每个处理器一个,大小为1页。
中断线 中断计数器 中断控制器 设备名
中断控制是为了同步,通过禁止中断,确保某个中断处理程序不会抢占当前进程。获取锁既防止其他处理器对共享数据的并发访问,也禁止了中断对内核进程的抢占
- 禁止和激活中断
unsigned long flags; //存储中断状态的变量
local_irq_save(flags); //保存当前中断状态
local_irq_disabled(); //禁止中断
//local_irq_enabled(); //或者激活中断
local_irq_restore(flags); //恢复之前的中断状态
void disable_irq(unsigned int irq); //禁止中断控制器上的指定中断线
void disable_irq_nosync(unsigned int irq); //不会等待当前中断处理程序执行完毕
void enable_irq(unsigned int irq); //激活中断控制器上的制定中断线
void synchronize_irq(unsigned int irq); //等待一个特定的中断处理程序退出
irqs_disabled(); //本地中断传递被禁止,返回非0,否则返回0
in_interrupt(); //处于中断上下文中,返回非0,否则返回0
in_irq(); //当前正在执行中断处理程序,返回非0,否则返回0
下半部的任务就是执行中断处理密切相关但中断处理程序本身不执行的工作
- 为什么要用下半部
缩短中断被屏蔽的时间对系统响应能力至关重要;通常下半部在中断处理程序一返回就会马上运行,下半部执行期间允许所有中断
- 下半部的环境
性能:软中断 > tasklet > 工作队列
/*软中断结构体*/
struct softirq_action {
void (*action)(struct softirq_action *);
};
/*软中断处理程序*/
my_softirq->action(my_softirq); //my_softirq指向softirq_vec数组的某项
tasklet的实现
tasklet结构体
struct tasklet_struct {
struct tasklet_struct *next; //链表的下一个tasklet
unsigned long state; //tasklet的状态
atomic_t count; //引用计数器
void (*func)(unsigned long); //tasklet处理函数
unsigned long data; //tasklet处理函数的参数
}
调度tasklet
tasklet_schedule()和tasklet_hi_schedule()将tasklet调度到tasklet_vec和tasklet_hi_vec链表中,由do_softirq()执行
使用tasklet
声明自己的tasklet
DECLARE_TASKLET(name, func, data) //静态声明
tasklet_init(t, tasklet_handler, dev) //动态声明
编写自己的tasklet处理程序
tasklet处理程序中不能休眠
void tasklet_handler(unsigned long data)
调度自己的tasklet
调用tasklet_schedule()函数并传递给他相应的tasklet_struct指针,该tasklet就会被调度
tasklet_schedule(&my_tasklet);
ksoftirqd
每个处理器都有一组辅助处理软中断和tasklet的内核线程。当内核中出现大量软中断和tasklet时,这些内核进程就会辅助处理它们。
工作队列运行在进程上下文,他通常可以用内核线程替换。但是由于内核开发者们非常反对创建新的内核线程,所以我们也推荐使用工作队列。
工作队列的实现
工作队列子系统是一个用于创建内核线程的接口,通过它创建的进程负责执行由内核其他部分排到队列的任务。它创建的这些内核线程称为工作者线程。
表示线程的数据结构
struct workqueue_struct {
struct cpu_workqueue_struct cpu_wq[NR_CPUS];
struct list_head list;
const char *name;
int singlethread;
int freezeable;
int rt;
}
struct cpu_workqueue_struct {
spinlock_t lock;
struct list_head worklist;
wait_queue_head_t more_work;
struct work_struct *current_struct;
struct workqueue_struct *wq
task_t *thread;
}
表示工作的数据结构
这些结构体被连接成链表,当一个工作者线程被唤醒时,它会执行它的链表上的所有工作,工作完成继续休眠。
struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func;
};
使用工作队列
创建推后的工作
DECLARE_WORK(name, void(*func)(void *), void *data); //编译时静态创建
INIT_WORK(struct work_struct *work, void(*func)(void *), void *data); //运行时动态初始化
工作队列处理函数
运行在进程上下文,只有用户进程通过系统调用陷入内核,才能访问用户空间
void work_handler(void *data);
对工作进行调度
schedule_work(&work);
schedule_delayed_work(&work, delay);
刷新操作
排入队列的工作会在工作者线程下一次被唤醒时执行,为了保证不再有待处理的工作,需要调用以下函数
void flush_scheduled_work(void); //队列中所有工作执行完毕后返回
int cancel_delayed_work(struct work_struct *work); //取消任何与work_struct相关的挂起工作
创建新的工作队列
struct workqueue_struct *create_workqueue(const char *name);
int queue_work(struct workqueue_struct *wq, struct work_struct *work)
flush_workqueue(struct workqueue_struct *wq);
同步:避免并发、防止竞争条件
临界区:访问和操作共享数据的代码段
竞争条件:两个执行线程可能处于同一个临界区同时执行
在临界区并发操作共享数据会造成数据不一致
锁机制可以防止并发执行,防止共享数据遭到破坏。
- 造成并发执行的原因
中断、软中断和tasklet、内核抢占、睡眠(唤醒调度程序重新调度)、对称多处理
- 了解要保护些什么
如果由其他执行线程可以访问这些数据,就给这些数据加上某种形式的锁。记住:要给数据而不是代码加锁
要有一个或多个执行线程和一个或多个资源(锁),每个线程都在等待其中的一个资源(锁),但所有的资源(锁)都被占用了。
避免死锁规则:
1. 按顺序加锁。使用嵌套的锁时必须保证以相同的顺序获取锁
2. 防止发生饥饿。这个代码的执行是否一定会结束
3. 不要重复请求同一个锁
4. 设计力求简单
争用:当锁正在被占用时,有其他线程试图获取该锁。被高度争用的锁会成为系统瓶颈,严重降低系统性能。
扩展性:对系统可扩展性的一个度量,理想情况下,处理器数量加倍应该会使系统处理性能翻倍。而实际上由于锁机制是不可能达到的,锁粒度的精细化能提高系统性能。
原子操作是其他同步方法的基石,他通过读取和增加变量的行为包含在一个单步中执行。
typedef struct {
volatile int counter;
} atomic_t; //atomic_t类型定义
atomic_t v; //定义原子整数v
atomic_set(&v, 4); //设置v数值为4
atomic_add(2, &v); //v数值增加2
atomic_inc(&v); //v数值增加1
atomic_read(&v); //将v转换为int型
typedef struct {
volatile long counter;
} atomic64_t; //atomic64_t类型定义
void set_bit(int nr, void *addr)
void clear_bit(int nr, void *addr)
void change_bit(int nr, void *addr)
int test_bit(int nr, void *addr)
一个执行线程试图获得一个已经持有的自旋锁,那么该线程就会一直进行忙循环,检查自旋锁是否可用。自旋锁不应该被长时间持有。
DEFINE_SPINLOCK(mr_lock);
spin_lock(&mr_lock);
/* 临界区 */
spin_unlock(&mr_lock);
/* 用于中断处理程序 */
DEFINE_SPINLOCK(mr_lock);
unsigned long flags;
spin_lock_irqsave(&mr_lock, flags); //保存本地中断当前状态并获取锁
/* 临界区 */
spin_unlock_irqrestore(&mr_lock, flags);
临界区位置 | 注意事项 |
---|---|
中断处理程序&下半部 | 下半部获取锁并禁止中断 |
下半部&进程上下文 | 进程上下文加锁并禁止下半部 |
多处理器的软中断 | 获取自旋锁即可 |
同类tasklet之间 | 不需要保护 |
不同类tasklet之间 | 获取自旋锁即可 |
一个或多个读任务可以并发地持有读锁,用于写的锁最多只能被一个写任务持有,而且此时不能有并发的读操作。
DEFINE_RWLOCK(mr_rwlock);
read_lock(&mr_rwlock);
/* 临界区(只读) */
read_unlock(&mr_rwlock);
DEFINE_RWLOCK(mr_rwlock);
write_lock(&mr_rwlock);
/* 临界区(读写) */
write_unlock(&mr_rwlock);
/* 执行以下两个函数将会死锁 */
read_lock(&mr_rwlock);
write_lock(&mr_rwlock);
睡眠锁,争用信号量的进程会睡眠,被调度程序调度到等待队列。
struct semaphore mr_sem;
sema_init(&mr_sem, count);
struct semaphor name;
sema_init(&name, 1);
/* 试图获取信号量 */
if (down_interruptible(&mr_sem))
/* 信号被接收,信号量还未获取 */
/* 临界区 */
/* 释放信号量 */
up(&mr_sem);
static DECLARE_RWSEM(name); //静态声明读-写信号量
down_read(&mr_rwsem); //试图获取信号量用于读
/* 临界区(只读) */
up_read(&mr_rwsem); //释放信号量
down_write(&mr_rwsem); //试图获取信号量用于写
/* 临界区 */
up_write(&mr_rwsem); //释放信号量
其行为和互斥信号量相似,但操作接口更简单,实现更高效,使用限制更强。
相比信号量,应优先考虑使用互斥体
DEFINE_MUTEX(name); //静态定义
mutex_init(&mutex); //动态定义
mutex_lock(&mutex);
/* 临界区 */
mutex_unlock(&mutex);
系统定时器是一种可编程硬件芯片,它以固定频率产生中断。该中断就是定时器中断,对应的中断处理程序负责更新时间,执行需要周期性运行的任务
通过频率之间的时间间隔计算时间的流逝
系统定时器以某种频率自行触发时钟中断,该频率可以通过编程预定,称作节拍率(一般为100HZ)
全局变量jiffies用来记录自系统启动以来产生的节拍总数。
体系结构提供了两种设备进行计时——实时时钟和系统定时器。
定时器是管理内核流逝的时间的基础,用于推后执行某些代码。
//数据结构
struct timer_list {
struct list_head entry; //定时器链表入口
unsigned long expireds; //以jiffies为单位的定时值
void (*function)(unsigned long); //定时器处理函数
unsigned long data; //传给处理函数的长整型参数
struct tvec_t_base_s *base; //定时器内部值,用户不要使用
}
//定义定时器
struct timer_list my_timer;
//初始化定时器数据结构的内部值
init_timer(&my_timer);
//填充结构中需要的值
my_timer.expires = jiffies + delay; //定时器超时节拍数
my_timer.data = 0; //给定时器处理函数传入0值
my_timer.function = my_function; //定时器超时调用函数
//定义定时器处理函数
void my_timer_function(unsigned long data) {
...
}
//激活定时器
add_timer(&my_timer);
//更改已经激活的定时器超时时间
mod_timer(&my_timer, jiffies+new_delay);
//在超时前停止定时器
del_timer_sync(&my_timer);
unsigned long delay = jiffies + 2 * HZ;
while (timer_before(jiffies, delay))
;
void udelay(unsigned long usecs)
void ndelay(unsigned long usecs)
void mdelay(unsigned long usecs)
set_current_state(TASK_INTERRUPTIBLE); //将任务设置为可中断睡眠状态
schedule_timeout(s * HZ); //小睡一会儿,“s”秒后唤醒
处理器寻址单位:字节
MMU寻址单位:页
32位体系结构:4KB页
64位体系结构:8KB页
struct page {
unsigned long flags; //页状态
atomic_t _count; //引用计数器
atomic_t _mapcount;
unsigned long private;
struct address_space *mapping;
pgoff_t index;
struct list_head lru;
void *virtual; //页虚拟地址
}
该结构体描述物理内存本身,而非包含在其中的数据
分配内存页时创建struct page,释放时销毁struct page
系统中每个物理页都要分配该结构体
区(物理内存概念) | 物理内存<==>虚拟内存 | 描述 |
---|---|---|
ZONE_DMA | 0~16MB<==>3GB~3GB+16MB | 直接映射给DMA使用 |
ZONE_NORMAL | 16~896MB<==>3GB+16MB~3GB+896MB | 正常可寻址页,直接映射,内核直接访问 |
ZONE_HIGHMEM | 896MB~4GB<==>3GB+896MB~4GB or 0~3GB | 动态映射的页 |
//分配2^order个页,返回第一个页的page结构体
struct page *alloc_pages(gfp_t gfp_mask, unsigned int order)
//返回给定页的逻辑地址
void *page_address(struct page *page)
//分配2^order个页,返回第一个页的逻辑地址
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
//返回的页内容全为0
unsigned long get_zeroed_page(unsigned int gfp_mask)
void __free_pages(struct page *page, unsigned int order)
void free_pages(unsigned long addr, unsigned int order)
void free_pages(unsigned long addr)
void *kmalloc(size_t size, gfp_t flags)
void kfree(const void *ptr)
void *vmalloc(unsigned long size)
void vfree(const void *addr)
slab相当于通用数据结构的缓存层,由一个或多个物理上连续的页组成,每个高速缓存可以有多个slab组成。
//创建高速缓存
struct kmem_cache *kmem_cache_create(const char *name,
size_t size,
size_t align,
unsigned long flags,
void (*ctor)(void*));
//撤销高速缓存
int kmem_cache_destroy(struct kmem_cache *cachep);
//从高速缓存中分配
void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags);
//释放从高速缓存中分配的对象
void kmem_cache_free(struct kmem_cache *cachep, void *objp);
每个进程都有两页的内核栈
//映射一个给定的page结构到内核地址空间
void *kmap(struct page *page) //只能用在进程上下文,会睡眠
void kumap(struct page *page)
//建立临时映射
void *kmap_atomic(struct page *page, enum km_type type) //可以用在不能睡眠的地方
void kunmap_atomic(void *kvaddr, enum km_type type)