xuediao 回复于:2005-07-04 12:56:59 |
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进程数刷新。 8) 如果系统所有的任务数目超过了最大线程数,返回-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”; |
popmouse 回复于:2005-07-05 14:53:21 |
赞啊,好东西,谢谢分享!顶!! |
xuediao 回复于:2005-07-07 10:08:20 |
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值以及上次调度任务结构指针。 8) 获取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)重装载调试注册器。 |
law7890 回复于:2005-07-07 16:59:54 |
GOOD!~~~ |
xuediao 回复于:2005-07-11 16:07:57 |
呵呵,谢谢支持,下面还有 |
xuediao 回复于:2005-07-11 16:09:41 |
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); } |
我菜我怕谁 回复于:2005-07-11 16:12:42 |
pf~pf~,精神上严重支持!! :) |
xuediao 回复于:2005-07-13 13:40:06 |
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中完成,它完成了必须的等待队列操作。我们所要做的就是将其指向一个由我们设备相关的中断处理函数唤醒的等待队列。 |
friday182 回复于:2005-07-14 13:58:27 |
好帖呀,我得存下来看看 |
xuediao 回复于:2005-07-16 19:18:43 |
2.6. 内核时钟
现在我们来看看内核时钟。内核时钟常用来为细节函数(时钟处理函数)分派执行时间。主数据结构timer_list定义在include/linux/timer.h文件中: struct timer_list { struct list_head list; unsigned long expires; unsigned long data; void (*function)(unsigned long); volatile int running; }; list字段用于链接内部链表,操作时由timerlist_lock同步锁保护。expires字段表示了处理函数在以data数据为参数被调用的时间数值。running字段用于SMP机上标识时钟处理函数当前是否运行在其他CPU上。 函数add_timer()和del_timer()用于从链表中增加和删除一个时钟。当时钟终止时,它自动被移出。在时钟被使用之前,它必须被init_timer函数初始化;在其被添加到链表之前,function字段和expires字段必须被设置。 |
xuediao 回复于:2005-07-21 17:58:49 |
不好意思,最近太忙了。现在补上! |
xuediao 回复于:2005-07-21 18:01:43 |
2.7. 下半部机制
bottom half(又名下半部机制)是Linux提高系统中断响应和处理能力的有效机制。 发生中断时,处理器要停止当前正在执行的指令,而操作系统负责将中断发送到对应的设备驱动程序去处理。在中断的处理过程中,系统不能进行其他任何工作,因此,在这段时间内,设备驱动程序要以最快的速度完成中断处理,而其他大部分工作在中断处理过程之外进行。Linux 内核利用bottom half处理过程帮助实现中断的快速处理。在中断有效的情况下,有时需要将一个任务分割成小份,以便中断可以获取到资源及时处理,而任务则稍后再继续(例如,对数据进行后加工,唤醒等待该数据的进程等)。 Bottom halves是用于延缓内核任务执行的旧有机制,在linux 1.x中已经实现了。在2.0内核,提供了一个新机制,称为“任务队列”。 Bottom halves由global_bh_lock旋转锁控制,即是在任何CPU上每刻仅有一个bottom half工作。无论如何,当尝试执行处理程序时,如果global_bh_lock无效,则bottom half被标识为执行态,进程也就得以继续。 Linux中总共仅能注册32个bottom half,操作bottom half的函数如下: void init_bh(int nr, void (*routine)(void)):为运行状态宏注册bottom half处理函数。这些状态宏以XXXX_BH格式定义在include/linux/interrupt.h 文件中,例如TIMER_BH或者TQUEUE_BH。通常子系统初始化运行环境时就会调用此函数注册bottom half。 void remove_bh(int nr):执行init_bh()相反的操作,删除指定宏。这里没有错误校验过程,因此可能会影响系统。通常子系统清除运行环境时就会调用这个函数。 void mark_bh(int nr):将bottom half 标识为执行态。通常,一个中断处理函数就会这样做。 Bottom halves是一些全局的排斥锁,所以问题“什么时候bottom half处理函数会执行”就相当于“什么时候排斥锁会执行”。这个答案就是:a)在每一个schedule里面,b)在每一个中断或者系统调用返回时。 |
xuediao 回复于:2005-08-03 12:00:15 |
不好意思,最近忙着饭碗的事情,帖子中断了许久,给大家致歉!
这两周争取把这个章节全部贴上去 |
SirFang 回复于:2005-08-03 13:13:43 |
我怎么记得linux下进程数是有限制的,不仅仅要足够的内存分配task_struct结构,还关系到GDT表,Linux不使用LDT,所以,GDT中的entry个数也就决定了进程总数。其中第0项不用,1~4项被指定为Kernel CS/DS, User active CS/DS, 一共13位, 应该是(8192 - 5) / 2(每个进程都有CS/DS), 所以最大进程数4093个吧。 |
xuediao 回复于:2005-08-03 13:40:58 |
[quote:28e9953cd0="SirFang"]我怎么记得linux下进程数是有限制的,不仅仅要足够的内存分配task_struct结构,还关系到GDT表,Linux不使用LDT,所以,GDT中的entry个数也就决定了进程总数。其中第0项不用,1~4项被指定为Kernel CS/DS, User active ..........[/quote:28e9953cd0]
老兄说得没错,不过应该具体是这样的: 在Linux 2.2.x中,一些与进程管理相关的数据结构是在系统初始化的时候被初始化的。其中最重要的是gdt和进程表task。Gdt的初始化主要是确定需要为多少个进程保留空间,也就是需要多大的gdt。这是由一个宏定义NR_TASKS决定的。NR_TASKS的值就是系统的最大进程数,它是在编译的时候被确定的,gdt的大小是10+2+NR_TASKS*2。而全局描述符表寄存器gdtr长度域为16位,每项描述符为8字节,故可容纳的 最大描述符数 = 1《(16-3)=1《13 = 8192 个。 而在2.4以上,现在的2.6版本中,这个进程数事实上是可以更多的。不过大多数系统的默认依然是8191 |
SirFang 回复于:2005-08-03 18:15:58 |
:) 感谢纠正:) |
xuediao 回复于:2005-08-04 13:27:55 |
SirFang 兄对内部结构也是研究得比较深入啊,如果我这些咚咚里面还有什么地方需要修正,修改的,请你不要顾虑,随时提出来。大伙共同学习共同进步哈 :)
再次感谢SirFang兄 |
xuediao 回复于:2005-08-13 22:48:12 |
接下来的两节:
2.8 任务队列 2.9 I386体系中系统调用实现 2.8. 任务队列 任务队列可以看作是以前的下半部机制的动态扩展。在源代码里面,有时以新下半部机制来称呼他们。以前的下半部机制有一下的限制: 1) 他们仅有一个固定的数目; 2) 每个下半部仅仅能够关联一个处理函数; 3) 下半部可以被旋转锁结束,所以他们不能阻塞; 所以,对于任务队列,任意数目的函数可以被关联并前后连续的处理。通过DECLARE_TASK_QUEUE宏可以创建一个新的任务队列,采用queue_task函数可以向其中增加一个任务而调用run_task_queue函数则会执行一个任务队列。作为创建你自己的任务队列,你可以使用Linux系统提前定义的任务队列,如下所述: 1) tq_timer:定时器任务队列,在每个定时器中断或者释放tty设备时执行。当定时器处理函数在中断环境下运行时,tq_timer还运行于中断环境,并不能阻塞。 2) tq_scheduler:调度任务队列,由调度程序触发,同样关闭tty设备时也会运行。一旦调度程序运行于进程重设定的上下文后,tq_scheduler任务可以做任何想作的事情,比如阻塞,处理上下文数据等等。 3) tq_disk:用于底层阻塞设备启动真实的请求。这个任务队列为模块设计,除了设计本身的目的以外不能作为他用。 如果一个驱动使用它自身的任务队列,它就不需要调用run_tasks_queues函数去处理这个队列,除非是以下说明的情况。 tq_timer/tq_scheduler 任务队列不仅普通场合会被触发,而且其他场合(如关闭tty设备时)也会触发。如果我们记得驱动能够调度队列中的任务,并且这些任务仅仅在驱动的细节实例有效时做判断,,其原因非常简单的。这通常意味着应用关闭它。所以,驱动可能需要调用run_task_queue来激活它添加到队列中的任务,因为允许这些任务稍后执行是没有意义的,也就是说相关的数据结构可能被另一个实例释放或者重用。这就是run_task_queue在tq_timer 和tq_scheduler中多处使用而不是定时器中断和schedule分别调用。 2.9. I386体系中系统调用实现 linux实现系统调用有两种机制: lcall7/lcall27调用方式; 0x80号软中断; Linux附带的程序使用0x80方式,同时外来程序如UNIX (Solaris, UnixWare 7等)使用lcall7机制。由于历史原因,lcall7机制包含了lcall27机制,但是处理函数却命名为lcall7_func,所以这是令人误解的。 当系统启动时,arch/i386/kernel/traps.c:trap_init函数被调用,设置IDT,这样向量0x80就指向了arch/i386/kernel/entry.S文件中描述的系统调用向量表的地址。 当一个用户空间的应用触发系统调用时,参数由寄存器传递,并且应用执行'int 0x80'指令。这个指令进入内核模式,然后处理器跳转到system_cal入口。具体如下: 1) 保存寄存器; 2) 为KERNEL_DS设置%ds 和 %es,这样所有相关数据(和额外的段)就在内核地址空间被建立。 3) 如果%eax的值大于NR_syscalls (当前是 256),返回ENOSYS错误。 4) 如果任务满足tsk.>ptrace & PF_TRACESYS 条件,则执行专门处理。这用于支持strace 或者debugger这样的程序。 5) 调用sys_call_table+4*(syscall_number from %eax)。这个表在同一个文件arch/i386/kernel/entry.S中初始化,指向各自的系统调用处理函数,在linux下这些函数以sys_开头。这些C系统调用处理函数在堆栈中获取他们的参数。 6) 进入系统调用(system call return path)。这个部分不仅被0x80使用,而且被lcall7, lcall27使用。它处理任务tasklets,检查是否需要调用schedule,检查是否有信号待处理,如果是,则处理这些信号。 Linux系统为系统调用提供6个参数支持。他们分别寄存于%ebx, %ecx, %edx, %esi, %edi,%ebp。系统调用号存储到%eax。 |
xuediao 回复于:2005-08-14 21:40:01 |
再有一节:
2.10. 原子操作 2.10. 原子操作 目前有两种类型的原子操作:bitmaps 和 atomic_t。对于维护“已分配”或者“释放”单位(这些单位来自于某些大型的以数字标识每个单位的集合)的概念,bitmaps非常便利。释放inodes和块就是例子。这也在某些简单锁中有广泛的应用,例如提供打开设备的互斥访问。这样的例子可以在arch/i386/kernel/microcode.c中被找到。 /* * Bits in microcode_status. (31 bits of room for future expansion) */ #define MICROCODE_IS_OPEN 0 /* set if device is in use */ static unsigned long microcode_status; Linux下没有必要像清零BSS一样初始化microcode_status。 /* * We enforce only one user at a time here with open/close. */ static int microcode_open(struct inode *inode, struct file *file) { if (!capable(CAP_SYS_RAWIO)) return .EPERM; /* one at a time, please */ if (test_and_set_bit(MICROCODE_IS_OPEN, µcode_status)) return .EBUSY; MOD_INC_USE_COUNT; return 0; } 关于bitmaps的操作有: void set_bit(int nr, volatile void *addr):在由地址addr指向的bitmap中添加位nr。 void clear_bit(int nr, volatile void *addr):在由地址addr指向的bitmap中清除位nr。 void change_bit(int nr, volatile void *addr):在由地址addr指向的bitmap中绑定位nr。 int test_and_set_bit(int nr, volatile void *addr):设置位nr并返回以前的值。 int test_and_clear_bit(int nr, volatile void *addr):清除位nr并返回以前的值。 int test_and_change_bit(int nr, volatile void *addr):绑定位nr并返回以前的值。 这些操作都使用了LOCK_PREFIX宏,该宏在SMP内核上求总线锁指令前缀的值,在UP上不做任何实际操作。这保证了SMP环境下的访问原子数。 有时位操作不是便利的,作为替代我们采用算法操作,增加、减小、自增、自减。典型的案例就是引用计数。它由atomic_t数据类型和一下步骤实现: atomic_read(&v):返回atomic_t变量v的值; atomic_set(&v, i):将atomic_t变量v的值设置为整型i; void atomic_add(int i, volatile atomic_t *v):将atomic_t变量的值增加i; void atomic_sub(int i, volatile atomic_t *v):将atomic_t变量减少i; int atomic_sub_and_test(int i, volatile atomic_t *v):将atomic_t变量减少i,如果新的值是0则返回1,否则返回0; void atomic_inc(volatile atomic_t *v):atomic_t变量增加1; void atomic_dec(volatile atomic_t *v):atomic_t变量减少1; int atomic_dec_and_test(volatile atomic_t *v):atomic_t变量减少1,如果新的值是0则返回1,否则返回0; int atomic_inc_and_test(volatile atomic_t *v):atomic_t变量增加1,如果新的值是0则返回1,否则返回0; int atomic_add_negative(int i, volatile atomic_t *v):atomic_t变量增加i,如果结果是负数,返回1;否则返回0。这个操作用于实现semaphores。 |