1. tasklet只可以在一个CPU上同步地执行,不同的tasklet可以在不同地CPU上同步地执行.
2. tasklet的实现是建立在两个软件中断的基础之上的,即HI_SOFTIRQ和TASKLET_SOFTIRQ,本质上没有什么区别,只不过HI_SOFTIRQ的优先级更高一些.
3. 由于tasklet是在软中断上实现的,所以像软中断一样不能睡眠、不能阻塞,处理函数内不能含有导致睡眠的动作,如减少信号量、从用户空间拷贝数据或手工分配内存等.
4. 一个 tasklet 能够被禁止并且之后被重新使能; 它不会执行直到它被使能的次数与被禁止的次数相同.
5. tasklet的串行化使tasklet函数不必是可重入的,因此简化了设备驱动程序开发者的工作.
6. 每个cpu拥有一个tasklet_vec链表.
结构体:
- struct tasklet_struct
- {
- struct tasklet_struct *next; //tasklet结构变量是tasklet_vec链表的一个节点,next是链表的下一节点。
- unsigned long state;
- state使用了两个位如下 :
- enum
- {
- TASKLET_STATE_SCHED, /* 1已经被调度,0表示还没调度*/
- TASKLET_STATE_RUN /* 1tasklet正在执行,0表示尚未执行,只针对SMP有效,单处理器无意义 */
- };
- state使用了两个位如下 :
- atomic_t count;
- void (*func)(unsigned long);
- unsigned long data; //函数的参数
- };
工作队列是一种将工作推后执行的形式,交由一个内核线程去执行在进程上下文执行,其不能访问用户空间。最重要特点的就是工作队列允许重新调度甚至是睡眠。工作队列子系统提供了一个默认的工作者线程来处理这些工作。默认的工作者线程叫做events/n,这里n是处理器的编号,每个处理器对应一个线程,也可以自己创建工作者线程。
内核定时器是内核用来控制在未来某个时间点调度执行某个函数的一种机制,而且是处于中断上下文中,所以调度函数必须遵守以下规则:
1. 没有 current 指针、不允许访问用户空间。因为没有进程上下文,相关代码和被中断的进程没有任何联系。
2. 定时器函数必须是原子的,不能进行睡眠或者调度。 原子代码不能调用 schedule 或者某种 wait_event, 也不能调用任何其他可能睡眠的函数。
3. 内核定时器的调度函数运行过一次后自动注销,但可以通过在被调度的函数中重新调度自己来周期运行。
内核线程和普通的进程间的区别在于内核线程没有独立的地址空间,它只在内核空间运行,从来不切换到用户空间去;并且和普通进程一样,可以被调度,也可以被抢占。
有个kthread后台进程用于管理调度其它的内核线程。其创建函数:
- static __init int helper_init(void)
- {
- //创建一个单线程的共享列队
- helper_wq = create_singlethread_workqueue("kthread");
- BUG_ON(!helper_wq);
- return 0;
- }
- core_initcall(helper_init);
- struct kthread_create_info
- {
- /* Information passed to kthread() from keventd. */
- int (*threadfn)(void *data); //线程处理函数
- void *data; //线程参数
- struct completion started; //在工作中等待kernel_thread创建线程完成,线程创建完后线程会通知工作继续。
- /* Result passed back to kthread_create() from keventd. */
- struct task_struct *result; // started当收到线程创建完信号started后,用来存放创建的任务结构体。
- struct completion done; // 工作者线程加入一个工作后会等待工作做完,这个工作只是创建线程。
- struct work_struct work; // 创建线程的工作,具体工作看后面源码 。
- };
在驱动程序中,当多个线程同时访问相同的资源时(全局变量或硬件资源),可能会引发竞态,因此我们必须对共享资源进行并发控制。linux内核中解决并发控制的最常用方法是自旋锁,信号量(互斥体),完成量,原子变量,位变量。
自旋锁可以确保在同时只有一个线程进入临界区,所要保护的临界资源访问时间比较短时,用自旋锁是非常方便的和高效的,自选锁有如下特点:
1. 内核代码持有一个自旋锁的任何时间, 抢占在相关处理器上被禁止。
2. 如果访问临界资源的时间较长,则选用信号量,否则选用自旋锁。
3. 保护临界区代码必须是原子的,自旋锁后进行调度、抢占以及在等待队列上睡眠都是非法的。
4. 在中断处理函数中,只能使用自旋锁,可选择持有锁时禁止中断。
内核抢占(可抢占式内核):即当进程位于内核空间时,有一个更高优先级的任务出现时,如果当前内核允许抢占,则可以将当前任务挂起,执行优先级更高的进程。
非抢占式内核:高优先级的进程不能中止正在内核中运行的低优先级的进程而抢占CPU运行。进程一旦处于核心态(例如用户进程执行系统调用),则除非进程自愿放弃CPU,否则该进程将一直运行下去,直至完成或退出内核。
抢占式内核的意义:首先,这是将Linux应用于实时系统所必需的。实时系统对响应时间有严格的限定,当一个实时进程被实时设备的硬件中断唤醒后,它应在限定的时间内被调度执行。而Linux不能满足这一要求,因为Linux的内核是不可抢占的,不能确定系统在内核中的停留时间。事实上当内核执行长的系统调用时,实时进程要等到内核中运行的进程退出内核才能被调度,由此产生的响应延迟,在如今的硬件条件下,会长达100ms级。这对于那些要求高实时响应的系统是不能接受的。而可抢占的内核不仅对Linux的实时应用至关重要,而且能解决Linux对多媒体(video, audio)等要求低延迟的应用支持不够好的缺陷。
CPU在内核中运行时并不是处处不可抢占的,内核中存在一些空隙,在这时进行抢占是安全的,内核抢占补丁的基本原理就是将SMP可并行的代码段看成是可以进行内核抢占的区域。Linux 2.4内核正好细化了多CPU下的内核线程同步机构,对不可并行的指令块用spinlock和rwlock作了细致的表示,该补丁的实现可谓水到渠成。具体的方法就是在进程的任务结构上增加一个preempt_count变量作为内核抢占锁,它随着spinlock和rwlock一起加锁和解锁。当preempt_count为0时表示可以进行内核调度。内核调度器的入口为preempt_schedule(),它将当前进程标记为TASK_PREEMPTED状态再调用schedule(),在TASK_PREEMPTED状态,schedule()不会将进程从运行队列中删除。
自旋锁保持期间是抢占失效的(内核不允许被抢占),而信号量和读写信号量保持期间是可以被抢占的。自旋锁只有在内核可抢占或SMP的情况下才真正需要,在单CPU且不可抢占的内核下,自旋锁的所有操作都是空操作。
(问题:在单CPU可抢占内核中,如果一个进程请求自旋锁,那么他一直占用CPU循环等待自旋锁可用,那么原来占用自旋锁的进程得不到使用CPU的机会,怎么释放它自己占用的自旋锁呢?
回答:这种情况应该是在实际中不会发生的。因为考虑最开始的情况,一个进程需要自旋锁,而且这个时候自旋锁没有被占用,所以他加锁,进入临界区。因为这个时候内核已经被自旋锁设置成了非抢占式,所以正在运行的进程不会被调用处CPU,因此也不会再有其他的进程来请求这个已经被占用的自旋锁,当临界区代码处理完成后,释放了自旋锁,此时内核又被自旋锁设置成了可抢占模式,这个时候其他的进程可以被调度,因此可以再请求自旋锁等操作。因此问题中的情况应该在实际中不会发生。
注意,如果在一个已经对某个自旋锁加锁的进程的临界区中又申请对这个自旋锁加锁,则会导致进程自旋在那里,引起死机。)
因为自旋锁在同一时刻至多被一个执行线程持有,所以一个时刻只能有一个线程位于临界区,这就为多处理器提供了防止并发访问所需的保护机制,但是在单处理器上,编译的时候不会加入自旋锁。它仅仅被当作一个设置内核抢占机制是否被启用的开关(gx自己的理解:在单内核可抢占式内核中,对自旋锁加锁导致禁止抢占,对自旋锁解锁导致恢复抢占模式。)。