1、课 程 设 计课 程 名 称 操 作 系 统 开 课 学 院 计算机科学与技术学院 指导老师姓名 刘 军 学 生 姓 名 李发禹 学生专业班级 软件工程 0905 2011 2012 学年 第 1 学期学生学号 0120910680526 课程成绩目录课程设计任务书.1摘要 .21 设计题目与要求 311 设计题目:内核定时器 312 设计要求:通过研究内核的时间管理算法,学习内核源代码;然后应用这些知识并且使用“信号”建立一种用户空间机制来测量一个多线程程序的执行时间。 .32 总的设计思想及系统平台、语言、工具 .32.1 设计思想: .32.1.1Linux 内核对定时器的描述 .32.
2、1.2Linux 内核定时器 42.1.3Linux 信号 signal 处理机制 .72.1.4 多线程编程 .82.1.5 内核定时器机制的实现 .102.2. 系统平台: .132.3. 编程工具: 133数据结构与模块说明(功能与流程图) .133.1 定时器使用: 133.2 多线程程序: 143.3 程序流程图: .154. 源程序: .155运行结果与运行情况 .166调试记录: .177自我评析和总结: .188.参考文献 .189.评分标准19课程设计任务书学生姓名: 李发禹 专业班级: 软件工程 0905 指导教师: 刘军 工作单位:计算机科学与技术学院 题 目: 内核定时
3、器初始条件:理论:学习了操作系统课程,掌握对操作系统的了解。实践:计算机技术系实验中心提供计算机及软件开发环境。要求完成的主要任务: (包括课程设计工作量及其技术要求,以及说明书撰写等具体要求)通过研究内核的时间管理算法学习内核源代码。然后应用这些知识并且使用“信号”建立一种用户空间机制来测量一个多线程程序的执行时间。实验条件要求:每人一台 Linux 主机且有超级用户权限。时间安排: 2011 年 12 月 26 日30 日 (第 17 周)序号 阶段内容 所需时间1 查阅资料、系统设计 1 天2 编程、调试 天3 撰写报告 1 天合计 天指导教师签名: 2011 年 12 月 24日系主任
4、(或责任教师)签名: 年 月 日内核定时器摘要每个正在系统上运行的程序都是一个进程。每个进程包含一到多个线程。进程也可能是整个程序或者是部分程序的动态执行。线程是一组指令的集合,或者是程序的特殊段,它可以在程序里独立执行。也可以把它理解为代码运行的上下文。内核时间指明线程执行操作系统代码已经经过了多少个 100ns 的CPU 时间,linux 是一个具有保护模式的操作系统。它一直工作在 i386 cpu 的保护模式之下。内存被分为两个单元: 内核区域和用户区域。一般地,在使用虚拟内存技术的多任务系统上,内核和应用有不同的地址空间,因此,在内核和应用之间以及在应用与应用之间进行数据交换需要专门的
5、机制来实现,本文站在用户空间的角度,测试一个多线程程序的程序执行时间。当一个进程希望获得信号量时, 如果信号量已经被占有 , 则该进程将会被放到等待队列上 sleep 直到 cpu 将其唤醒。相对于 spinlock 来说开销太大,适用于长时间占有的 lock。不可用于中断状态,因为它拥有信号量的进程可以 sleep, 可以被抢占,信号量可以设置为同时允许的进程数。1 设计题目与要求11 设计题目:内核定时器12 设计要求:通过研究内核的时间管理算法,学习内核源代码;然后应用这些知识并且使用“信号”建立一种用户空间机制来测量一个多线程程序的执行时间。2 总的设计思想及系统平台、语言、工具2.1
6、 设计思想:2.1.1Linux 内核对定时器的描述 Linux 在 include/linux/timer.h 头文件中定义了数据结构 timer_list 来描述一个内核定时器: struct timer_list struct list_head list; unsigned long expires; unsigned long data; void (*function)(unsigned long); ; 各数据成员的含义如下: (1)双向链表元素 list:用来将多个定时器连接成一条双向循环队列。 (2)expires:指定定时器到期的时间,这个时间被表示成自系统启动以来的时钟滴
7、答计数(也即时钟节拍数)。当一个定时器的 expires 值小于或等于 jiffies 变量时,我们就说这个定时器已经超时或到期了。在初始化一个定时器后,通常把它的 expires 域设置成当前 expires变量的当前值加上某个时间间隔值(以时钟滴答次数计)。 (3)函数指针 function:指向一个可执行函数。当定时器到期时,内核就执行function所指定的函数。而 data 域则被内核用作 function 函数的调用参数。 内核函数 init_timer()用来初始化一个定时器。实际上,这个初始化函数仅仅将结构中的list 成员初始化为空。如下所示(include/linux/ti
8、mer.h): static inline void init_timer(struct timer_list * timer) timer-list.next = timer-list.prev = NULL; 由于定时器通常被连接在一个双向循环队列中等待执行(此时我们说定时器处于 pending状态)。因此函数 time_pending()就可以用 list 成员是否为空来判断一个定时器是否处于pending 状态。如下所示 (include/linux/timer.h): static inline int timer_pending (const struct timer_list *
9、 timer) return timer-list.next != NULL; 时间比较操作 在定时器应用中经常需要比较两个时间值,以确定 timer 是否超时,所以 Linux内核在timer.h 头文件中定义了 4 个时间关系比较操作宏。这里我们说时刻 a 在时刻 b之后,就意味着时间值 ab。 Linux 强烈推荐用户使用它所定义的下列 4 个时间比较操作宏(include/linux/timer.h): #define time_after(a,b) (long)(b) - (long)(a) = 0) #define time_before_eq(a,b) time_after_eq
10、(b,a)2.1.2Linux 内核定时器定时器是管理内核时间的基础,用来计算流逝的时间,它以某种频率(节拍率)自行触发时钟中断,当时钟中断发生时,内核就通过一种特殊中断处理程序对其进行处理。但是原来的实现只能是 time_t mytime 形式的,经过简单的 localtime(mytime)和ctime( do_gettimeofday( if (copy_to_user(tv, if (tz) if (copy_to_user(tz, return 0; 显然,函数的实现主要分成两个大的方面: (1)如果 tv 指针有效,则说明用户要以 timeval 格式来检索系统当前时间。为此,先调
11、用 do_gettimeofday()函数来检索系统当前时间并保存到局部变量 ktv 中。然后再调用 copy_to_user()宏将保存在内核空间中的当前时间信息拷贝到由参数指针 tv 所指向的用户空间缓冲区中。 (2)如果 tz 指针有效,则说明用户要检索当前时区信息,因此调用copy_to_user()宏将全局变量 sys_tz 中的时区信息拷贝到参数指针 tz 所指向的用户空间缓冲区中。 (3)最后,返回 0 表示成功。 函数 do_gettimeofday()的源码如下( arch/i386/kernel/time.c): /* * This version of gettimeof
12、day has microsecond resolution * and better than microsecond precision on fast x86 machines with TSC. */ void do_gettimeofday(struct timeval *tv) unsigned long flags; unsigned long usec, sec; read_lock_irqsave( usec = do_gettimeoffset(); unsigned long lost = jiffies - wall_jiffies; if (lost) usec +=
13、 lost * (1000000 / HZ); sec = xtime.tv_sec; usec += xtime.tv_usec; read_unlock_irqrestore( while (usec = 1000000) usec -= 1000000; sec+; tv-tv_sec = sec; tv-tv_usec = usec; 该函数的完成实际的当前时间检索工作。由于 gettimeofday()系统调用要求时间精度要达到微秒级,因此 do_gettimeofday()函数不能简单地返回 xtime 中的值即可,而必须精确地确定自从时钟驱动的 Bottom Half 上一次更新
14、 xtime 的那个时刻到 do_gettimeofday()函数的当前执行时刻之间的具体时间间隔长度 ,以便精确地修正 xtime 的值.假定被 do_gettimeofday()用来修正 xtime 的时间间隔为 fixed_usec,而从wall_jiffies 到 jiffies 之间的时间间隔是 lost_usec,而从 jiffies 到do_gettimeofday()函数的执行时刻的时间间隔是 offset_usec。则下列三个等式成立: fixed_usec( lost_usecoffset_usec ) lost_usec(jiffieswall_jiffies)TICK_
15、SIZE(jiffieswall_jiffies)(1000000HZ) 由于全局变量 last_tsc_low 表示上一次时钟中断服务函数 timer_interrupt()执行时刻的 CPU TSC 寄存器的值,因此我们可以用 X86 CPU 的 TSC 寄存器来计算offset_usec 的值。也即: offset_usec=delay_at_last_interrupt(current_tsc_lowlast_tsc_low )fast_gettimeoffset_quotient 其中,delay_at_last_interrupt 是从上一次发生时钟中断到 timer_interr
16、upt()服务函数真正执行时刻之间的时间延迟间隔。每一次 timer_interrupt()被执行时都会计算这一间隔,并利用 TSC 的当前值更新 last_tsc_low 变量(可以参见 7.4 节) 。假定 current_tsc_low 是 do_gettimeofday()函数执行时刻 TSC 的当前值,全局变量 fast_gettimeoffset_quotient 则表示 TSC 寄存器每增加 1 所代表的时间间隔值,它是由 time_init()函数所计算的。 根据上述原理分析,do_gettimeofday() 函数的执行步骤如下: (1)调用函数 do_gettimeoffs
17、et()计算从上一次时钟中断发生到执行do_gettimeofday()函数的当前时刻之间的时间间隔 offset_usec。 (2)通过 wall_jiffies 和 jiffies 计算 lost_usec 的值。 (3)然后,令 sec=xtime.tv_sec,usec=xtime.tv_usec+lost_usec+offset_usec。显然,sec 表示系统当前时间在秒数量级上的值,而 usec 表示系统当前时间在微秒量级上的值。 (4)用一个 while循环来判断 usec 是否已经溢出而超过 106us1 秒。如果溢出,则将 usec 减去 106us 并相应地将 sec 增
18、加 1,直到 usec 不溢出为止。 (5)最后,用 sec 和 usec 分别更新参数指针所指向的 timeval 结构变量。至此,整个查询过程结束。 函数 do_gettimeoffset()根据 CPU 是否配置有 TSC 寄存器这一条件分别有不同的实现。其定义如下(arch/i386/kernel/time.c): #ifndef CONFIG_X86_TSC static unsigned long do_slow_gettimeoffset(void) static unsigned long (*do_gettimeoffset)(void) = do_slow_gettimeo
19、ffset; #else #define do_gettimeoffset() do_fast_gettimeoffset() #endif 显然,在配置有 TSC 寄存器的 i386 平台上,do_gettimeoffset ()函数实际上就是 do_fast_gettimeoffset()函数。它通过 TSC 寄存器来计算 do_fast_gettimeoffset()函数被执行的时刻到上一次时钟中断发生时的时间间隔值。其源码如下(arch/i386/kernel/time.c): static inline unsigned long do_fast_gettimeoffset(void
20、) register unsigned long eax, edx; /* Read the Time Stamp Counter */ rdtsc(eax,edx); /* relative to previous jiffy (32 bits is enough) */ eax -= last_tsc_low; /* tsc_low delta */ /* * Time offset = (tsc_low delta) * fast_gettimeoffset_quotient * = (tsc_low delta) * (usecs_per_clock) * = (tsc_low del
21、ta) * (usecs_per_jiffy / clocks_per_jiffy) * * Using a mull instead of a divl saves up to 31 clock cycles * in the critical path. */ _asm_(“mull %2“ :“=a“ (eax), “=d“ (edx) :“rm“ (fast_gettimeoffset_quotient), “0“ (eax); /* our adjusted time offset in microseconds */ return delay_at_last_interrupt +
22、 edx; 对该函数的注释如下: (1)先调用 rdtsc()函数读取当前时刻 TSC 寄存器的值,并将其高 32 位保存在edx 局部变量中,低 32 位保存在局部变量 eax 中。 (2)让局部变量 eax tsc_loweaxlast_tsc_low;也即计算当前时刻的TSC 值与上一次时钟中断服务函数 timer_interrupt()执行时的 TSC 值之间的差值。(3)显然,从上一次 timer_interrupt()到当前时刻的时间间隔就是(tsc_lowfast_gettimeoffset_quotient) 。因此用一条 mul 指令来计算这个乘法表达式的值。 (4)返回值
23、delay_at_last_interrupt(tsc_lowfast_gettimeoffset_quotient)就是从上一次时钟中断发生时到当前时刻之间的时间偏移间隔值。2.1.3Linux 信号 signal 处理机制信号 signal 机制是进程之间相互传递消息的一种方法,全称为软中断信号。系统调用 signal 用来设定某个信号的处理方法,其调用声明的格式如下: void (*signal(int signum, void (*handler)(int)(int); 成功则返回该信号以前的处理配置,出错则返回 SIG_ERR。在使用该调用的进程中加入以下头文件:几个常见信号:SIG
24、INT: 当用户按某些终端键时, 引发终端产生的信号. 如 Ctrl+C 键, 这将产生中断信号(SIGINT),它将停止一个已失去控制的程序。SIGSEGV: 由硬件异常(除数为 0, 无效的内存引用等等 )产生的信号。这些条件通常由硬件检测到, 并将其通知内核,然后内核为该条件发生时正在运行的进程产生该信号。SIGURG: 在网络连接上传来带外数据时产生。SIGPIPE: 在管道的读进程已终止后 , 一个进程写此管道时产生,当类型为SOCK_STREAM 的 socket 已不再连接时, 进程写到该 socket 也产生此信号。SIGALRM: 进程所设置的闹钟时钟超时的时候产生。SIGA
25、BRT: 进程调用 abort 函数时产生此信号 , 进程异常终止。SIGCHLD: 在一个进程终止或停止时, 它将把该信号发送给其父进程。 按系统默认, 将忽略此信号,如果父进程希望被告知其子进程的这种状态改变, 则应该捕捉此信号。通常是用 wait 系列函数捕捉, 如果不 wait 的话, 子进程将成为一个僵尸进程。SIGIO: 此信号指示一个异步 I/O 事件。SIGSYS: 该信号指示一个无效的系统调用。SIGTSTP: 交互式停止信号. Ctrl+Z, 按下时, 终端将产生此信号, 进程被挂起。2.1.4 多线程编程多线程是计算机同时运行多个执行线程的能力(这些线程可以是同一程序的组
26、成部分,或者也可以是完全不同的程序) 。Linux 系统下的多线程遵循POSIX 线程接口,称为 pthread。编写 Linux 下的多线程程序,需要使用头文件pthread.h,连接时需要使用库 libpthread.a。而 Linux 下 pthread 的实现是通过系统调用 clone()来实现的。 clone()是 Linux 所特有的系统调用,它的使用方式类似 fork。下面展示多线程程序部分 050119.c。/* 050119.c */#include #include void thread(void)int i;for(i=0;iexpires; unsigned long
27、 idx = expires - timer_jiffies; struct list_head * vec; if (idx TVR_BITS) vec = tv2.vec + i; else if (idx (TVR_BITS + TVN_BITS) vec = tv3.vec + i; else if (idx (TVR_BITS + 2 * TVN_BITS) vec = tv4.vec + i; else if (signed long) idx (TVR_BITS + 3 * TVN_BITS) vec = tv5.vec + i; else /* Can only get her
28、e on architectures with 64-bit jiffies */ INIT_LIST_HEAD( return; /* * Timers are FIFO! */ list_add( 对该函数的注释如下: (1)首先,计算定时器的 expires 值与 timer_jiffies 的插值(注意!这里应该使用动态定时器自己的时间基准) ,这个差值就表示这个定时器相对于上一次运行定时器机制的那个时刻还需要多长时间间隔才到期。局部变量 idx 保存这个差值。 (2)根据 idx 的值确定这个定时器应被插入到哪一个定时器向量中。其具体的确定方法我们在 7.6.2 节已经说过了,这里不
29、再详述。最后,定时器向量的头部指针 vec 表示这个定时器应该所处的定时器向量链表头部。 (3)最后,调用 list_add()函数将定时器插入到 vec 指针所指向的定时器队列的尾部。 2.1.5.3 修改一个定时器的 expires 值 当一个定时器已经被插入到内核动态定时器链表中后,我们还可以修改该定时器的 expires值。函数 mod_timer()实现这一点。如下所示(kernel/timer.c): int mod_timer(struct timer_list *timer, unsigned long expires) int ret; unsigned long flags
30、; spin_lock_irqsave( timer-expires = expires; ret = detach_timer(timer); internal_add_timer(timer); spin_unlock_irqrestore( return ret; 该函数首先根据参数 expires 值更新定时器的 expires 成员。然后调用detach_timer()函数将该定时器从它原来所属的链表中删除。最后调用 internal_add_timer()函数将该定时器根据它新的 expires 值重新插入到相应的链表中。 函数 detach_timer()首先调用 timer_p
31、ending()来判断指定的定时器是否已经处于某个链表中,如果定时器原来就不处于任何链表中,则 detach_timer()函数什么也不做,直接返回 0值,表示失败。否则,就调用 list_del()函数将定时器从它原来所处的链表中摘除。如下所示(kernel/timer.c): static inline int detach_timer (struct timer_list *timer) if (!timer_pending(timer) return 0; list_del( return 1; 2.2. 系统平台:一台 Linux 主机且有超级用户权限2.3. 编程工具:VI 编辑器
32、,Gedit 编辑器3数据结构与模块说明(功能与流程图)3.1定时器使用:int gettimeofday(struct timeval *tv,struct timezone *tz); strut timevallong tv_sec; /*秒数*/ long tv_usec; /*微秒数*/ ;这个 syscall 用来供用户获取 timeval 格式的当前时间信息(精确度为微秒级) ,以及系统的当前时区信息(timezone) 。结构类型 timeval 的指针参数 tv 指向接受时间信息的用户空间缓冲区,参数 tz 是一个 timezone 结构类型的指针,指向接收时区信息的用户空间
33、缓冲区。这两个参数均为输出参数,返回值 0 表示成功,返回负值表示出错。实现过程如下:main() struct timeval tpstart,tpend; /*申请 struct timeval 的变量,tv_sec 返回的是秒数,tv_usec 返回的是微秒数*/float timeuse; gettimeofday( pthread(); gettimeofday( timeuse=1000000*(tpend.tv_sec-tpstart.tv_sec)+ tpend.tv_usec-tpstart.tv_usec; timeuse/=1000000; printf(“Used Ti
34、me:%f secn“,timeuse); exit(0); 3.2 多线程程序:进行多线程程序设计时,我们使用到了两个函数,pthread_create 和pthread_join,并声明了一个 pthread_t 型的变量。pthread_t 在头文件/usr/include/bits/pthreadtypes.h 中定义,它是一个线程的标识符。函数pthread_create 用来创建一个线程,函数 pthread_join 用来等待一个线程的结束。实现过程如下:int pthread_create(pthread_join(id,NULL);void thread(void)int i
35、;for(i=0;i#include #include int gettimeofday(struct timeval *tv,struct timezone *tz); int pthread_create(/pthread_join(id,NULL);strut timevallong tv_sec; /*秒数*/ long tv_usec; /*微秒数*/ ;void thread(void)int i;for(i=0;i3;i+)printf(“This is a pthread.n“);int pthread(void)pthread_t id; /* 声明了一个pthread_t型
36、的变量*/int i,ret;ret=pthread_create(if(ret!=0)printf(“Create pthread error!n“);exit(1);for(i=0;i3;i+)printf(“This is the main process.n“);pthread_join(id,NULL);return(0);main() struct timeval tpstart,tpend; /*申请struct timeval的变量,tv_sec返回的是秒数,tv_usec返回的是微秒数*/float timeuse; gettimeofday( pthread(); gett
37、imeofday( timeuse=1000000*(tpend.tv_sec-tpstart.tv_sec)+ tpend.tv_usec-tpstart.tv_usec; timeuse/=1000000; printf(“Used Time:%f secn“,timeuse); exit(0); 5运行结果与运行情况运行截图如下:运行结果记录如下:rootlocalhost # gcc 050119.c -lpthread -o 050119.out050119.c: In function pthread:050119.c:17: 警告:隐式声明与内建函数 exit 不兼容rootlo
38、calhost # ./050119.outThis is the main process.This is the main process.This is the main process.This is a pthread.This is a pthread.This is a pthread.Used Time:0.000532secrootlocalhost # gcc 050119.c -lpthread -o 050119.out050119.c: In function pthread:050119.c:17: 警告:隐式声明与内建函数 exit 不兼容rootlocalhos
39、t # ./050119.outThis is a pthread.This is a pthread.This is a pthread.This is the main process.This is the main process.This is the main process.Used Time:0.000545sec6调试记录:rootlocalhost # gcc 050119.c050119.c: In function pthread:050119.c:17: 警告:隐式声明与内建函数 exit 不兼容/tmp/ccarv1uC.o: In function pthread
40、:050119.c:(.text+0x4d): undefined reference to pthread_create050119.c:(.text+0xa0): undefined reference to pthread_joincollect2: ld 返回 1分析得知,编写 Linux 下的多线程程序,需要使用头文件 pthread.h,连接时需要使用库 libpthread.a,加编译参数 -lpthread。7自我总结:经过一个多星期的努力,总算顺利的完成了这次的课程设计,在做实验的过程中遇到许多的困难,但通过自己网上查阅资料及向同学的请教,终于完成了自己的任务,通过这次的实验
41、,学到了很多之前没学会的东西,并使得自己一学期以来学到的东西得到实践。本程序是在 Linux 系统平台下运行的。但是原来的实现只能是 time_t mytime 形式的,经过简单的 localtime(mytime)和 ctime(&mytime)处理.精度是不够的,为了返回高精度的时间,这里使用了 gettimeofday 函数。其程序结构相对简单,实现了题目的基本要求,测量了多线程的进程执行时间。进行多线程程序设计时,我们使用到了两个函数,pthread_create 和pthread_join,并声明了一个 pthread_t 型的变量。pthread_t 在头文件/usr/includ
42、e/bits/pthreadtypes.h 中定义,它是一个线程的标识符。函数pthread_create 用来创建一个线程,函数 pthread_join 用来等待一个线程的结束。操作系统编程实现起来,相对比较复杂,很多函数用起来很抽象。经过查一些资料,和测试函数功能渐渐明白了基本用法。进程是一个很难在用户态理解的,只能通过截取它的运行状态来了解它。8.参考文献1Operating System Concepts(Sixth Edition)(操作系统概念)影印版 Abraham Silberschatz 编 高等教育出版社 2003 年2计算机操作系统教程(第三版) 张尧学编 清华大学出版
43、社 2001 年3 操作系统 罗宇 邹鹏 吴刚 电子工业出版社4操作系统概念 高等教育出版社本科课程论文评分标准班级 0905 学号 姓名课设题目评阅点 评分标准(细则) 分值 给分正确实现本程序所需全部功能,算法设计正确合理且有一定创意40 分实现所需功能,算法正确 30 分基本实现所需功能 15 分有明显重大错误 5 分功能及算法(40 分)无法实现程序功能 0 分界面美观、合理,可操作性强 20 分界面合理,可操作 15 分界面尚可,基本可操作 10 分界面和操作性(20 分)可操作较差 5 分程序可读性好、逻辑清晰,程序完整,可维护性好,15 分程序可读、可维护 10 分基本可读可维护 5 分程序可读、可维护性(15 分)逻辑混乱、不可读 0 分论文规范,行文流畅,层次清晰 25 分论文书写基本规范,文理较通畅 20 分结构较合理,层次较清楚,基本符合要求15 分论文质量(25 分)结构混乱,文不对题目,或者有明显抄袭现象5 分总分教师签名: