免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
123下一页
最近访问板块 发新帖
查看: 13138 | 回复: 21
打印 上一主题 下一主题

Linux 2.4 内核说明文档(进程与中断管理篇) [复制链接]

论坛徽章:
0
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2005-07-04 10:33 |只看该作者 |倒序浏览
[list=]
本文档是《Linux2.4 内核说明文档》中的第二部分。以下是整个文档大致目录:
1,启动 (http://bbs.chinaunix.net/forum/viewtopic.php?t=557946
2,进程和中断管理
3,虚拟文件系统
4,Linux 页缓冲
5,IPC机制

本篇文档的目录为:
2.1.        Tack结构和进程表       
2.2.        创建和中止任务与内核线程       
2.3.        调度程序       
2.4.        Linux执行链表       
2.5.        等待队列       
2.6.        内核时钟       
2.7.        Bottom Halves       
2.8.        任务队列       
2.9.        I386体系中系统调用实现       
2.10.        原子操作       
2.11.        旋转锁、读写旋转锁和Big-Reader旋转锁
2.12.        信号灯和读写信号灯       
2.13.        装载模块的内核支持

一下是正文:
2.        进程和中断管理

2.1.        Tack结构和进程表
  linux下的每个进程都是动态分配一个task_struct结构,整个系统可以创建的最大进程数仅由当前可用物理内存总数限制,并且等于(见kernel/fork.c:fork_init()函数):
  
   /*
    * The default maximum number of threads is set to a safe
    * value: the thread structures can take up at most half
    * of memory.
    */
    max_threads = mempages / (THREAD_SIZE/PAGE_SIZE) / 2;
   
    这个式子在IA32体系结构上主要意味着最大数为物理内存页数/4,例如:在一个512M内存机器上你可以创建32K线程。这对于旧版本(2.2或者更老)内核的4K限制是一个可观的改进,而且这可以在运行时使用系统调用sysctl(2)修改KERN_MAX_THREADS,或者简单使用procfs系统接口来调整。

# cat /proc/sys/kernel/threads.max
32764
# echo 100000 >; /proc/sys/kernel/threads.max
# cat /proc/sys/kernel/threads.max
100000
# gdb .q vmlinux /proc/kcore
Core was generated by `BOOT_IMAGE=240ac18 ro root=306 video=matrox:vesa:0x118'.
#0 0x0 in ?? ()
(gdb) p max_threads
$1 = 100000

  Linux系统上进程的关联表现为一个以下两个方式链接的task_struct结构的集合:
1)        以pid为键值的hash表。
2)        通过p->;next_task和p->;prev_task指针连接的双向链表。
这个hash表名为pidhash,并在include/linux/sched.h中定义:

/* PID hashing. (shouldnt this be dynamic?) */
#define PIDHASH_SZ (4096 >;>; 2)
extern struct task_struct *pidhash[PIDHASH_SZ];
#define pid_hashfn(x) ((((x) >;>; ^ (x)) & (PIDHASH_SZ . 1))

   所有的任务以他们的pid为键值存放到hash表中,并假定均匀地从(0 to PID_MAX-1)分布。这个hash表用来通过指定的pid快速的找到task结构,搜索函数find_task_pid()定义在include/linux/sched.h中:

static inline struct task_struct *find_task_by_pid(int pid)
{
struct task_struct *p, **htable = &pidhash[pid_hashfn(pid)];
for(p = *htable; p && p.>;pid != pid; p = p.>;pidhash_next)
;
return p;
}

  在每个hash链上的所有任务都通过p->;pidhash_next和p->;pidhash_pprev连接起来,这在hash_pid函数和unhash_pid函数将指定进程插入hash表或者移出hash表时使用。所有的操作都受到tasklist_lock写同步锁保护。
而双向链表则为系统遍历所有的任务提供了方便,这个操作由定义在include/linux/sched.h中的for_each_tack()宏来实现。

#define for_each_task(p) \
for (p = &init_task ; (p = p.>;next_task) != &init_task ; )

for_each_task()函数的使用者必须使用tasklist_lock读同步锁。注意for_each_task()函数采用init_task标识链表的起点,这样才是安全的,因为空任务(pid = 0) 是不在链表里的。进程hash表和进程链表的修改操作,特别是fork函数,exit函数和ptrace函数,必须调用tasklist_lock写同步锁。更有趣的是,所有的写操作还必须屏蔽当前CPU的中断,这个原因是显而易见的:send_sigio函数遍历了进程表,这样需要调用tasklist_lock读同步锁,并且该函数是kill_fasync函数在中断环境下调用的。
现在我们已经知道task_struct结构是怎样链接到一起的,现在让我们分析一下task_struct结构的成员。这些成员是UNIX系统的proc结构和user结构松散组合到一起的。
其他UNIX版本总是将进程状态信息作为单独一部分常驻内存,其他部分则作为进程运行时所需信息,如此简陋的设计仅仅因为内存时非常宝贵的资源。现代操作系统(如Linux或者FreeBSD)并不做如此区分,而是在内核常驻内存的数据结构中维护进程状态。
include/linux/sched.h中定义了task_struct结构,并且通常大写为1680字节,状态宏也定义同一个头文件中。

volatile long state; /* .1 unrunnable, 0 runnable, >;0 stopped */
#define TASK_RUNNING 0
#define TASK_INTERRUPTIBLE 1
#define TASK_UNINTERRUPTIBLE 2
#define TASK_ZOMBIE 4
#define TASK_STOPPED 8
#define TASK_EXCLUSIVE 32

为什么TASK_EXCLUSIVE宏定义为32而不是16呢?这是由于16被TASK_SWAPPING使用了,并且后来在移出TASK_SWAPPING时没有把TASK_EXCLUSIVE的值上调。
可变量p->;state的定义意味着它自身可以被中断处理者异步修改。
1)        TASK_RUNNING:含义是假定任务已经处于运行队列中。至于不是已经处于运行队列的原因是由于将一个任务标识为TASK_RUNNING和将该任务移动到运行队列不是一个原子操作。从运行队列角度考虑,操作时需要保持runqueue_lock读同步锁。如果这样操作,你将发现在运行队列的每个任务都处于TASK_RUNNING状态。然后,反过来却不一定。同样地,驱动程序可以标识他们自身状态为TASK_INTERRUPTIBLE,然后调用schedule()函数,这个函数将从运行队列移出它自己(除非当时有一个导致它滞留在运行队列的未处理信号)。
2)        TASK_INTERRUPTIBLE:含义是任务处于休眠状态但可以通过一个信号或者休眠中止时钟唤醒。
3)        TASK_UNINTERRUPTIBLE:含义类似于TASK_INTERRUPTIBL,但任务不能被唤醒。
4)        TASK_ZOMBIE:含义是任务已经被中止但它的状态还没被父进程获取。
5)        TASK_STOPPED:含义是由于任务控制信号或者ptrace系统调用,任务已经被停止。
6)        TASK_EXCLUSIVE:含义是这不是一个单独状态,但能够与TASK_INTERRUPTIBLE或者TASK_UNINTERRUPTIBLE状态并存(OR操作)。这意味着当任务与其他在等待队列休眠时,它可以单独被唤醒而不需要唤醒整个等待队列的任务。
任务标记包含了关于非互相排斥的进程状态信息。

unsigned long flags; /* per process flags, defined below */
/*
* Per process flags
*/
#define PF_ALIGNWARN 0x00000001 /* Print alignment warning msgs */
/* Not implemented yet, only for 486*/
#define PF_STARTING 0x00000002 /* being created */
#define PF_EXITING 0x00000004 /* getting shut down */
#define PF_FORKNOEXEC 0x00000040 /* forked but didn't exec */
#define PF_SUPERPRIV 0x00000100 /* used super.user privileges */
#define PF_DUMPCORE 0x00000200 /* dumped core */
#define PF_SIGNALED 0x00000400 /* killed by a signal */
#define PF_MEMALLOC 0x00000800 /* Allocating memory */
#define PF_VFORK 0x00001000 /* Wake up parent in mm_release */
#define PF_USEDFPU 0x00100000 /* task used FPU this quantum (SMP) */

p->;has_cpu, p->;processor, p->;counter, p->;priority, p->;policy and p->;rt_priority字段和调度程序关联,并将在后面描述。
p->;mm 和p->;active_mm字段分别指向mm_struct结构描述的进程地址空间和有效地址空间(如果这个进程不是内核进程的话)。这使得当任务被调度离开时TLB能够在地址空间自由切换。所以,如果当前正在执行内核任务(没有p->;mm),则它的next->;active_mm将被设置为已经被调度离开的任务的prev->;active_mm,如果pre-mm != NULL,则这个地址将和prev->;mm相同。如果CLONE_VM标识传递到了clone系统调用或者依靠vfork系统调用,则地址空间可以在任务之间共享。
p->;exec_domain和p->;personality字段与任务的特性相关,也就是为了模仿UNIX特性的唯一系统调用。
p->;fs字段包含了文件系统信息,在linux下有三个方面的含义:
1)        root目录实体结构和挂载点;
2)        预备的root目录实体和挂载点;
3)        当前工作目录实体和挂载点;
这个结构同样包含了一个引用计数,因为当进行带CLONE_FS标识的clone系统调用时,它是共享的。
p->;files字段包含了文件句柄表,这在进行带CLONE_FILES标识clone系统调用时也是多任务共享的。
p->;sig字段包含了信号处理函数入口,以CLONE_SIGHAND参数执行clone操作后页可以在进程间共享。

论坛徽章:
0
2 [报告]
发表于 2005-07-04 12:56 |只看该作者

Linux 2.4 内核说明文档(进程与中断管理篇)

2.2.        创建和中止任务与内核线程
不同的操作系统书籍,从一个“正在执行的程序的实例”到“由clone或者fork系统调用产生的任务”等不同方式定义了“进程”。在linux下,共有三种类型程序:
        空线程;
        内核线程;
        用户任务;
空线程在为第一个CPU引导时创建,然后依靠定义在arch/i386/kernel/smpboot.c的fork_by_hand()函数手工为每个CPU创建这个线程。所有的空线程共享一个init_task结构,但都拥有各自的存放在CPU队列里的init_tss表示的TSS结构。他们以CLONE_PID方式clone,PID都为零,其他任务都不能共享这个PID。
内核模式下,kernel_thread函数调用clone系统调用创建了内核线程。内核线程通常没有用户地址空间,也就是p->;mm=NULL,因为他们明确通过daemonize()函数执行exit_mm()函数。内核线程通常可以直接操作内核地址空间,并被分配低范围的pid号。当在处理器模式下运行时意味着内核线程将享用所有的I/O特权并不能被调用程序预清空。
用户任务通过clone或者fork系统调用创建,他们都在内部调用了kernel/fork.c的do_fork()函数。
让我们分析一下当用户进程调用fork系统调用时发生了什么。虽然fork操作在传递用户堆栈和寄存器时依赖于体系架构,但在下面真实执行这个操作的do_fork()函数确实简洁的,并位于kernel/fork.c文件。
以下步骤将被执行:
1)        本地变量被设置为-ENOMEM,当fork创建一个新任务结构失败时将作为错误代码返回。
2)        .如果CLONE_PID标识被设置,则返回-EPERM错误,除非调用者是空线程。普通用户线程clone时不能传递CLONE_PID标识并期待操作成功。SIFCHLDclone标识,对于fork来说,它被认为是不相关的,仅在sys_clone调用do_fork时才被认为是相关的。
3)        初始化current->;vfork_sem。它将在sys_vfork函数为了休眠父进程直到子进程执行mm_release函数时使用,就像执行其他程序或者中止其他程序一样。
4)        通过alloc_task_struct()宏分配一个新的任务结构。在x86系统上,它仅是一个GFP_KERNEL优先级的gfp。这酒是为何fork系统调用可能休眠的第一个原因。如果分配失败,返回-ENOMEM错误。
5)        通过结构拷贝*p = *current,将所有当前进程结构的数据都拷贝到新进程,或许这个操作应该被memcpy替换。然后,所有不能被子进程修改的字段将被设置为正确的值。
6)        大范围的内核锁被采用以防止其他部分执行本段代码。
7)        如果父进程拥有用户资源则校验是否超出了RLIMIT_NPROC限制。如果是这样,则返回-EAGAIN错误;如果没有,则通过指定的uid将计数器p->;user->;count进程数刷新。
        如果系统所有的任务数目超过了最大线程数,返回-EAGAIN错误。
9)        如果进程是依赖预模块执行的,则增加依赖模块的引用计数。
10)        如果进程是依赖预模块二进制格式的,也增加依赖模块的引用计数。
11)        子进程被标识为“没有被执行”(p->;did_exec=0)。
12)        子进程被标识为'not.swappable' (p->;swappable = 0)。
13)        子进程被置为TASK_UNINTERRUPTIBLE状态,即p->;state = TASK_UNINTERRUPTIBLE。
14)        依照clone_flags的数值设置子进程的p->;flags,如果是简单fork,p->;flags= PF_FORKNOEXEC。
15)        通过快速算法kernel/fork.c的get_pid()函数设置子进程号p->;pid。
16)        初始化子进程其他任务结构。最后子进程结构被插入到pidhash表中,并且被唤醒。
这样任务就被创建了。停止任务有很多方式。
1)        通过exit系统调用;
2)        收到一个中止信号;
3)        被确定异常强制中止;
4)        以func == 1参数调用bdflush。
系统调用的实现函数都有sys_前缀,当他们通常仅与参数检测或者以细节方式传递信息,真正的操作是由do_**函数完成的。所以sys_exit()函数调用了do_exit来完成操作。尽管如此,内核其他部分有时也通过调用sys_exit实现堆do_exit的调用。
do_exit函数定义在kernel/exit.c中,按照以下几个步骤执行:
        获取内核全局锁;
        在最后调用一直循环的schedule()函数;
        设置任务状态为TASK_ZOMBIE;
        以current->;pdeath_signa信号通知所有的子进程;
        以等同于SIGCHLD的信号current.>;exit_signal通知父进程;
        释放fork函数分配的资源,关闭已经打开的文件;
        在采用少量FPU切换的体系中,不管硬件设备要求什么都向FPU的所有者传递一个“none”;

论坛徽章:
0
3 [报告]
发表于 2005-07-05 14:53 |只看该作者

Linux 2.4 内核说明文档(进程与中断管理篇)

赞啊,好东西,谢谢分享!顶!!

论坛徽章:
0
4 [报告]
发表于 2005-07-07 10:08 |只看该作者

Linux 2.4 内核说明文档(进程与中断管理篇)

2.3.        Linux 调度程序

调度程序的任务就是从多个进程中挑选一个访问当前CPU,它在kernel/sched.c中实现,对应的被每个内核源文件都引用的头文件定义在include/linux/sched.h。

任务结构中对应调度的字段为:
        p->;need_resched:如果schedule()函数需要在下次唤醒,则设置本字段。
        p->;counter:到下次运行调度时间片剩余的时间嘀哒,由定时器递减。当这个字段的值小于或者等于零时,它被重置为零,同时设置p->;need_resched。由于这个可以被进程自身修改,有时也称之为“动态优先级”。
        p->;priority:进程的静态优先级,仅能被系统调用如nice函数,POSIX.1b 的sched_setparam函数,4.4BSD/SVR4 的setpriority函数修改。
        p->;rt_priority:实时优先权。
        p->;policy: 调度策略,指定任务所属的调度种类,任务可以通过调用sched_setscheduler函数修改它。有效的值为SCHED_OTHER (传统UNIX进程),SCHED_FIFO (POSIX.1b的FIFO 实时进程)和SCHED_RR (POSIX循环实时进程)。如果要标识进程绑定到某个CPU,则可以将SCHED_YIELD与其他任何值“或”操作。例如通过调用sched_yield系统调用,一个FIFO实时进程将运行于○A处于I/O阻塞,○b明确绑定到某个CPU或者○c被另一个高优先级的实时进程占用。SCHED_RR和SCHED_FIFO相同,除非时间片中止后它回到运行队列末尾。

尽管看起来schedule()函数非常复杂,然而调度算法是简单的。该函数看起来复杂的原因是由于它在一个函数中实现了三个调度算法并和精细的SMP处理细节相关。

显然,“空”跳转到schedule()有这个目的:产生最优代码。同样,标识了调度程序是为2.4重写的。因此,下面的讨论是适合2.2或者更早的内核。
让我们看看函数细节:
1)        如果current.>;active_mm == NULL,则出现了错误。当前进程即使是一个内核进程,都必须在任何时候有一个有效的p->;active_mm。
2)        如果tq_scheduler任务队列需要执行,则处理它。任务队列为调度程序稍后执行提供了一个内核机制。
3)        将本地变量prev 和this_cpu分别初始化为当前任务和当前CPU。
4)        检查schedule()是否由中断处理函数调用的,如果出现这种情况是没有理由的。
5)        释放内核全局锁。
6)        如过softirq机制有任务执行,则立即执行。
7)        将本地schedule_data结构指针*sched_data初始化为pre-CPU调度数据区首指针,这个包含了last_schedule 的TSC值以及上次调度任务结构指针。
        获取runqueue_lock旋转锁。注意,采用spin_lock_irq()是由于在调度函数中我们保证了中断可用。所以,当释放runqueue_lock时,可以通过重新使其生效替代保存或者恢复eflags。
9)        任务状态处理:如果处于TASK_RUNNING状态,任务继续;如果处于TASK_INTERRUPTIBLE状态且有未处理信号,则任务调整到TASK_RUNNING状态;处于其他任何状态,任务都将从执行队列中移出。
10)        本CPU的空任务被设置为下一步(最优候调度者),可是这个候选者被设置为非常低的优先级,以期待有比它更好的出现。
11)        如果上一个任务处于TASK_RUNNING状态,则当前任务优先级被设置为该任务的优先级,并被标识为比空线程更优的候选者。
12)        现在开始处理运行队列,这个CPU上可被调度的每个进程的优先级都以当前值进行比较,高优先级的活得执行机会。“这个CPU上可被调度”的概念必须被理解为:在单CPU机型上,运行队列的每个进程是合格的可调度的;在多CPU机型上,仅仅在其他CPU非已经执行的进程在当前CPU是合格可调度的。优先级由goodness()函数计算,这个函数为所有的实时进程标识非常高的优先级,这个优先级高于1000,以至于超过所有的SCHED_OTHER进程;这样,它就只与其他拥有高优先级的实时进程竞争。如果进程时间片结束,goodness函数返回0。对于非实时进程,goodness的初始值设置为p->;counter,这样进程就近似于在CPU空闲时获得资源,也就是说交互进程较CPU范围计数器更有特权。常量PROC_CHANGE_PENALTY试图表达了“cpu affinity”。这也为内核进程提供了微小的优势。
13)        如果goodness返回值为零,则遍历进程入口列表,并通过简单的算法重新计算他们的动态优先级:
recalculate:
{
struct task_struct *p;
spin_unlock_irq(&runqueue_lock);
read_lock(&tasklist_lock);
for_each_task(p)
p.>;counter = (p.>;counter >;>; 1) + p.>;priority;
read_unlock(&tasklist_lock);
spin_lock_irq(&runqueue_lock);
}

注意,在重新计算前停止了runqueue_lock锁。这个原因是我们将进入到进程集合中,这需要较长的时间,当我们在这个CPU计算时,schedule函数可以在另外的CPU上被调用,并为其挑选一个合适的进程。无可否认的,这就是一个矛盾,因为当我们在这个CPU上挑选合适优先权的进程时,其他CPU正运行schedule计算动态的优先级。
14)        毫无疑问,下一个任务将会被调度,所以初始化next.>;has_cpu为1,next.>;processor为this_cpu。现在runqueue_lock可以被释放了。
15)        如果该任务有一次被调用(next == prev),那么可以简单获取内核全局锁并返回,也就是跳过所有的硬件层(registers, stack 等)和虚拟内存相关操作。
16)        switch_to宏依赖于体系结构。在i386上,它与这些相关:a) FPU 处理; b)LDT 处理; c) 重装载段注册器;d) TSS处理 和 e)重装载调试注册器。

论坛徽章:
0
5 [报告]
发表于 2005-07-07 16:59 |只看该作者

Linux 2.4 内核说明文档(进程与中断管理篇)

GOOD!~~~

论坛徽章:
0
6 [报告]
发表于 2005-07-11 16:07 |只看该作者

Linux 2.4 内核说明文档(进程与中断管理篇)

呵呵,谢谢支持,下面还有

论坛徽章:
0
7 [报告]
发表于 2005-07-11 16:09 |只看该作者

Linux 2.4 内核说明文档(进程与中断管理篇)

2.4.        Linux执行链表

在遍历等待队列之前,首先执行Linux标准的双向执行链表。等待队列用起来繁杂,行话称之为“list.h 实现”,因为它最相关的文件是include/linux/list.h。
基础数据结构是list_head结构:
struct list_head {
struct list_head *next, *prev;
};
#define LIST_HEAD_INIT(name) { &(name), &(name) }
#define LIST_HEAD(name) \
struct list_head name = LIST_HEAD_INIT(name)
#define INIT_LIST_HEAD(ptr) do { \
(ptr).>;next = (ptr); (ptr).>;prev = (ptr); \
} while (0)
#define list_entry(ptr, type, member) \
((type *)((char *)(ptr).(unsigned long)(&((type *)0).>;member)))
#define list_for_each(pos, head) \
for (pos = (head).>;next; pos != (head); pos = pos.>;next)
前三个宏用来初始化一个next和prev指针皆指向自身的空链表,宏可以用到的地方明显受限于c语法约束。例如,LIST_HEAD_INIT()用来初始化结构元素,第二个宏用来初始化静态变量,第三个用于函数内部。
list_entry()宏提供了对单独链表元素的操作,例如(fs/file_table.c:fs_may_remount_ro()函数):
struct super_block {
...
struct list_head s_files;
...
} *sb = &some_super_block;
struct file {
...
struct list_head f_list;
...
} *file;
struct list_head *p;

for (p = sb.>;s_files.next; p != &sb.>;s_files; p = p.>;next) {
struct file *file = list_entry(p, struct file, f_list);
// do something to 'file'
}
一个使用list_for_each()宏的最好例子就是任务调度函数中遍历运行队列以查找高优先级的进程部分。
static LIST_HEAD(runqueue_head);
struct list_head *tmp;
struct task_struct *p;

list_for_each(tmp, &runqueue_head) {
p = list_entry(tmp, struct task_struct, run_list);
if (can_schedule(p)) {
int weight = goodness(p, this_cpu, prev.>;active_mm);
if (weight >; c)
c = weight, next = p;
}
}
p.>;run_list 被定义成task_struct结构内的list_head结构成员run_list,serves为指向链表的指针。向链表里面删除或者添加一个元素由list_del()/list_add()/list_add_tail()三个宏完成。下面的例子向运行队列添加并删除一个任务。
static inline void del_from_runqueue(struct task_struct * p)
{
nr_running..;
list_del(&p.>;run_list);
p.>;run_list.next = NULL;
}
static inline void add_to_runqueue(struct task_struct * p)
{
list_add(&p.>;run_list, &runqueue_head);
nr_running++;
}
static inline void move_last_runqueue(struct task_struct * p)
{
list_del(&p.>;run_list);
list_add_tail(&p.>;run_list, &runqueue_head);
}
static inline void move_first_runqueue(struct task_struct * p)
{
list_del(&p.>;run_list);
list_add(&p.>;run_list, &runqueue_head);
}

论坛徽章:
0
8 [报告]
发表于 2005-07-11 16:12 |只看该作者

Linux 2.4 内核说明文档(进程与中断管理篇)

pf~pf~,精神上严重支持!!

论坛徽章:
0
9 [报告]
发表于 2005-07-13 13:40 |只看该作者

Linux 2.4 内核说明文档(进程与中断管理篇)

2.5.        等待队列

当进程要求内核完成一件当前不会发生但稍后可能发生的事情时,它就进入休眠并在事件条件符合时被唤醒。内核实现机制的其中之一就被称为“等待队列”。
Linux实现允许通过TASK_EXCLUSIVE标记来唤醒。对于等待队列,你可以采用通用的队列,然后简单地sleep_on /sleep_on_timeout /interruptible_sleep_on /interruptible_sleep_on_timeout;也可以定义自己的队列,并通过add/remove_wait_queue向它添加删除自己,在需要时通过wake_up/wake_up_interruptible来唤醒。
等待队列的第一种用法的例子就是页分配(mm/page_alloc.c:__alloc_pages()函数)与kswapd内核守护进程(mm/vmscan.c:kswap()函数)的交互,也就是定义在mm/vmscan.c里的kswapd_wait等待队列。Kswapd守护进程在这个队列休眠,当页分配函数需要释放一些内存页的时候它就被唤醒。
自治等待队列用法的例子就是用户进程通过read系统调用请求数据与中断环境的内核提供数据之间的交互。中断处理可能如下(drivers/char/rtc_interrupt()):

static DECLARE_WAIT_QUEUE_HEAD(rtc_wait);
void rtc_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
spin_lock(&rtc_lock);
rtc_irq_data = CMOS_READ(RTC_INTR_FLAGS);
spin_unlock(&rtc_lock);
wake_up_interruptible(&rtc_wait);
}

这样,中断处理函数通过读取一些设备指定的I/O端口获得了数据,然后唤醒rtc_wait等待队列中的任务。现在,read系统调用可以实现为:

ssize_t rtc_read(struct file file, char *buf, size_t count, loff_t *ppos)
{
DECLARE_WAITQUEUE(wait, current);
unsigned long data;
ssize_t retval;

add_wait_queue(&rtc_wait, &wait);
current.>;state = TASK_INTERRUPTIBLE;
do {
spin_lock_irq(&rtc_lock);
data = rtc_irq_data;
rtc_irq_data = 0;
spin_unlock_irq(&rtc_lock);

if (data != 0)
break;

if (file.>;f_flags & O_NONBLOCK) {
retval = .EAGAIN;
goto out;
}

if (signal_pending(current)) {
retval = .ERESTARTSYS;
goto out;
}

schedule();
} while(1);
retval = put_user(data, (unsigned long *)buf);
if (!retval)
retval = sizeof(unsigned long);

out:
current.>;state = TASK_RUNNING;
remove_wait_queue(&rtc_wait, &wait);
return retval;
}

rtc_read将执行以下步骤操作:
1)        定义一个指向当前进程环境的等待队列元素;
2)        将此元素添加倒rtc_wait等待队列;
3)        将当前环境标识为TASK_INTERRUPTIBLE,这意味着他下次休眠后不再被调度;
4)        检查是否有数据有效?如果有则跳出,拷贝数据到用户缓冲区,自身任务状态调整为TASK_RUNNING,并从等待队列中移出,然后返回。
5)        如果目前没有数据,则检查用户是否指定了非阻塞I/O。
6)        同样检查是否有信号等待处理。如果是,则通知上层必要时重启系统调用。
7)        最后休眠,直到被中断处理函数唤醒。如果任务自身状态不是TASK_INTERRUPTIBLE,则调度函数会在稍后有数据可用时调度该任务,这会导致不必要的处理。
这非常有价值的一点就是,采用等待队列就比较容易的实现poll系统调用:

static unsigned int rtc_poll(struct file *file, poll_table *wait)
{
unsigned long l;

poll_wait(file, &rtc_wait, wait);

spin_lock_irq(&rtc_lock);
l = rtc_irq_data;
spin_unlock_irq(&rtc_lock);

if (l != 0)
return POLLIN | POLLRDNORM;
return 0;
}

所有的工作都在独立于设备的函数poll_wait中完成,它完成了必须的等待队列操作。我们所要做的就是将其指向一个由我们设备相关的中断处理函数唤醒的等待队列。

论坛徽章:
0
10 [报告]
发表于 2005-07-14 13:58 |只看该作者

Linux 2.4 内核说明文档(进程与中断管理篇)

好帖呀,我得存下来看看
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP