从一个简单的时间片轮转多道程序内核代码看操作系统如何运行
试验要求:
完成一个简单的时间片轮转多道程序内核代码,需要仔细分析进程的启动和进程的切换机制,总结部分需要阐明自己对“操作系统是如何工作的”理解。
本次试验使用的工具是QEMU。QEMU是一套由Fabrice Bellard所编写的以GPL许可证分发源码的模拟处理器,在GNU/Linux平台上使用广泛。QEMU除了仿真处理器外,还允许仿真所有必要的子系统,如连网硬件和视频硬件。它还允许实现高级概念上的仿真(如对称多处理系统(多达 255 个 CPU)和其他处理器架构(如 ARM 和 PowerPC)。
首先进入实验楼,打开shell,输入命令:
cd LinuxKernel/linux-3.9.4进入内核代码目录
qemu -kernel arch/x86/boot/bzImage搭建目标环境
在QEMU窗口看以看到在程序执行过程中不断有中断程序的输出:
my_start_kernel here是程序顺序执行过程中的输出
>>>>>>>>>>>>>>>>>my_timer_handler here<<<<<<<<<<<<<<<<<<是中断程序的输出内容
具体代码:
首先是中断处理程序的代码
/* * myinterrupt.c * */ #include <linux/kernel_stat.h> #include <linux/export.h> #include <linux/interrupt.h> #include <linux/percpu.h> #include <linux/init.h> #include <linux/mm.h> #include <linux/swap.h> #include <linux/pid_namespace.h> #include <linux/notifier.h> #include <linux/thread_info.h> #include <linux/time.h> #include <linux/jiffies.h> #include <linux/posix-timers.h> #include <linux/cpu.h> #include <linux/syscalls.h> #include <linux/delay.h> #include <linux/tick.h> #include <linux/kallsyms.h> #include <linux/irq_work.h> #include <linux/sched.h> #include <linux/sched/sysctl.h> #include <linux/slab.h> #include <asm/uaccess.h> #include <asm/unistd.h> #include <asm/div64.h> #include <asm/timex.h> #include <asm/io.h> #define CREATE_TRACE_POINTS #include <trace/events/timer.h> /* * Called by timer interrupt. */ void my_timer_handler(void) { printk(KERN_NOTICE "\n>>>>>>>>>>>>>>>>>my_timer_handler here<<<<<<<<<<<<<<<<<<\n\n"); }
接着是主函数的代码:
/* * mymain.c * */ #include <linux/types.h> #include <linux/module.h> #include <linux/proc_fs.h> #include <linux/kernel.h> #include <linux/syscalls.h> #include <linux/stackprotector.h> #include <linux/string.h> #include <linux/ctype.h> #include <linux/delay.h> #include <linux/ioport.h> #include <linux/init.h> #include <linux/initrd.h> #include <linux/bootmem.h> #include <linux/acpi.h> #include <linux/tty.h> #include <linux/percpu.h> #include <linux/kmod.h> #include <linux/vmalloc.h> #include <linux/kernel_stat.h> #include <linux/start_kernel.h> #include <linux/security.h> #include <linux/smp.h> #include <linux/profile.h> #include <linux/rcupdate.h> #include <linux/moduleparam.h> #include <linux/kallsyms.h> #include <linux/writeback.h> #include <linux/cpu.h> #include <linux/cpuset.h> #include <linux/cgroup.h> #include <linux/efi.h> #include <linux/tick.h> #include <linux/interrupt.h> #include <linux/taskstats_kern.h> #include <linux/delayacct.h> #include <linux/unistd.h> #include <linux/rmap.h> #include <linux/mempolicy.h> #include <linux/key.h> #include <linux/buffer_head.h> #include <linux/page_cgroup.h> #include <linux/debug_locks.h> #include <linux/debugobjects.h> #include <linux/lockdep.h> #include <linux/kmemleak.h> #include <linux/pid_namespace.h> #include <linux/device.h> #include <linux/kthread.h> #include <linux/sched.h> #include <linux/signal.h> #include <linux/idr.h> #include <linux/kgdb.h> #include <linux/ftrace.h> #include <linux/async.h> #include <linux/kmemcheck.h> #include <linux/sfi.h> #include <linux/shmem_fs.h> #include <linux/slab.h> #include <linux/perf_event.h> #include <linux/file.h> #include <linux/ptrace.h> #include <linux/blkdev.h> #include <linux/elevator.h> #include <asm/io.h> #include <asm/bugs.h> #include <asm/setup.h> #include <asm/sections.h> #include <asm/cacheflush.h> #ifdef CONFIG_X86_LOCAL_APIC #include <asm/smp.h> #endif void __init my_start_kernel(void) { int i = 0; while(1) { i++; if(i%100000 == 0) printk(KERN_NOTICE "my_start_kernel here %d \n",i); } }
这两段代码的开头都是包含一些头文件,用作编译时期。
下面整理一些常用的Linux内核头文件:
#include <linux/kernel.h> // printk()
#include <linux/slab.h> // kmalloc()
#include <linux/fs.h> // file_operation,inode_operations,super_operations
#include <linux/errno.h> // error codes */
#include <linux/types.h> // size_t等各种系统typedef的数据类型 */
#include <linux/fcntl.h> // O_ACCMODE */
#include <linux/poll.h> // COPY_TO_USER */
#include <linux/init.h> // #define module_init(x) __initcall(x);
// #define module_exit(x) __exitcall(x);
#include <linux/miscdevice.h> // extern int misc_register();
// 注册miscdevice结构体成员变量设备
// extern int misc_deregister();
// 注销miscdevice结构体成员变量设备
#include <linux/cdev.h> // void cdev_init()
// struct cdev *cdev_alloc(void);
// void cdev_put(struct cdev *p);
// int cdev_add(struct cdev *, dev_t, unsigned);
// void cdev_del(struct cdev *);
// void cd_forget(struct inode *);
#include <mach/hardware.h> // 和GPIO口操作相关的函数定义的头文件对应的源文件在
// /arch/arm/plat-s3c24xx/gpio.c
// void s3c2410_gpio_cfgpin(pin,function);
// unsigned int s3c2410_gpio_getcfg(pin);
// void s3c2410_gpio_pullup(pin,to);
// int s3c2410_gpio_getpull(pin);
// void s3c2410_gpio_setpin(lin,to);
// unsigned int s3c2410_gpio_getpin(pin);
// unsigned int s3c2410_modify_misccr(clear,chang)
// int s3c2410_gpio_getirq(pin);
// int s3c2410_gpio_irq2pin(pin);
#include <mach/regs-gpio.h> // 和GPIO口相关的一些宏定义
//(在arch/arm/mach-s3c2410/mach)
#include <linux/platform_device.h>
#include <linux/interrupt.h> // typedef irqreturn_t (*irq_handler_t)
// (int, void *);
// extern void free_irq(unsigned int, void *);
// extern void disable_irq(unsigned int irq);
// extern void enable_irq(unsigned int irq);
// 等和中断相关函数的声明
#include <linux/wait.h> // wake_up_interruptible唤醒 q 指定的注册在等待队列
//上的进/程。该函数不能直接的立即唤醒进程,而是由调
// 度程序转换上下文,调整为可运行状态。
// DECLARE_WAIT_QUEUE_HEAD(button_waitq);声明队列为
// 全局变量
// wait_event_interruptible该函数修改task的状态
// 为 TASK_INTERRUPTIBLE,意味着改进程将不会继续运
// 行直到被唤醒,然后被添加到等待队列wq中
#include <linux/poll.h> // poll_wait
#include <asm/uaccess.h> // copy_to_user和copy_from_user
#include <asm/io.h> // __raw_readl __raw_writel
#include <linux/clk.h> // struct clk *clk_get
// (struct device *dev, const char *id);
// unsigned long clk_get_rate(struct clk *clk);
#include <plat/clock.h> // struct clk;结构体的定义(在arch/arm/plat-
// s3c/plat/include/clock.arch中,对应的源文件在
// /arm/plat-s3c/clock.c)
#include <linux/device.h> //包含了device、class 、device_driver等结构的定义
#include <mach/regs-mem.h> // 和bank相关的寄存器的宏定义
#include <asm/div64.h> // Linux内核中64位除法函数do_div
#include <linux/fb.h> // struct fb_info 结构体的定义
#include <mach/fb.h> // struct s3c2410fb_display
//(在arch/arm/mach-s3c2410/include/mach/fb.h)
#include <linux/module.h> //MODULE_LICENSE("GPL");内核认识的特定许可有,
"GPL"( 适用 GNU 通用公共许可的任何版本 ),
"GPL v2"( 只适用 GPL 版本 2 ),
"GPL and additional rights",
"Dual BSD/GPL",
"Dual MPL/GPL",
"Proprietary".
除非你的模块明确标识是在内核认识的一个自由许可
下,否则就假定它是私有的,内核在模块加载时被"弄污
浊"了. 象我们在第 1 章"许可条款"中提到的, 内核开
发者不会热心帮助在加载了私有模块后遇到问题的用户.
MODULE_AUTHOR ( 声明谁编写了模块 ),
MODULE_DESCRIPION( 一个人可读的关于模块做什么的声明 ),
MODULE_VERSION(一个代码修订版本号;看 <linux/module.h>
的注释以便知道创建版本字串使用的惯例),
MODULE_ALIAS ( 模块为人所知的另一个名子 ),
MODULE_DEVICE_TABLE(来告知用户空间, 模块支持那些设备 )
下面看一些文件中的函数:
void my_timer_handler(void) { printk(KERN_NOTICE "\n>>>>>>>>>>>>>>>>>my_timer_handler here<<<<<<<<<<<<<<<<<<\n\n"); }这一个函数是内核的中断处理函数,函数调用了printk()内核函数,内核通过 printk() 输出的信息具有日志级别,printk()的使用方法是:printk(日志级别 "消息文本");其中日志级别KERN_NOTICE代表正常但又重要的条件,用于提醒。常用于与安全相关的消息。当发生中断时,CPU转入执行此函数。
void __init my_start_kernel(void) { int i = 0; while(1) { i++; if(i%100000 == 0) printk(KERN_NOTICE "my_start_kernel here %d \n",i); } }这个函数是内核开始执行的主函数,while循环中变量i不断增加,每增加100000时内核输出一条消息:my_start_kernel here,代表内核正在执行中,i用于时间片轮转计数,即每100000为一个时间片。
进程在其生命周期内,由于系统中各进程之间的相互制约关系及系统的运行环境的变化,使得进程的状态也在不断地发生变化(一个进程会经历若干种不同状态)。通常进程有以下五种状态,前三种是进程的基本状态。
1) 运行状态:进程正在处理机上运行。在单处理机环境下,每一时刻最多只有一个进程处于运行状态。
2) 就绪状态:进程已处于准备运行的状态,即进程获得了除处理机之外的一切所需资源,一旦得到处理机即可运行。
3) 阻塞状态,又称等待状态:进程正在等待某一事件而暂停运行,如等待某资源为可用(不包括处理机)或等待输入/输出完成。即使处理机空闲,该进程也不能运行。
4) 创建状态:进程正在被创建,尚未转到就绪状态。创建进程通常需要多个步骤:首先申请一个空白的PCB,并向PCB中填写一些控制和管理进程的信息;然后由系统为该进程分配运行时所必需的资源;最后把该进程转入到就绪状态。
5) 结束状态:进程正从系统中消失,这可能是进程正常结束或其他原因中断退出运行。当进程需要结束运行时,系统首先必须置该进程为结束状态,然后再进一步处理资源释放和回收等工作。
注意区别就绪状态和等待状态:就绪状态是指进程仅缺少处理机,只要获得处理机资源就立即执行;而等待状态是指进程需要其他资源(除了处理机)或等待某一事件。之所以把处理机和其他资源划分开,是因为在分时系统的时间片轮转机制中,每个进程分到的时间片是若干毫秒。也就是说,进程得到处理机的时间很短且非常频繁,进程在运行过程中实际上是频繁地转换到就绪状态的;而其他资源(如外设)的使用和分配或者某一事件的发生(如I/O操作的完成)对应的时间相对来说很长,进程转换到等待状态的次数也相对较少。这样来看,就绪状态和等待状态是进程生命周期中两个完全不同的状态,很显然需要加以区分。
图2-1说明了五种进程状态的转换,而三种基本状态之间的转换如下:
图2-1 五种进程状态的转换
就绪状态 -> 运行状态:处于就绪状态的进程被调度后,获得处理机资源(分派处理机时间片),于是进程由就绪状态转换为运行状态。
运行状态 -> 就绪状态:处于运行状态的进程在时间片用完后,不得不让出处理机,从而进程由运行状态转换为就绪状态。此外,在可剥夺的操作系统中,当有更高优先级的进程就 、 绪时,调度程度将正执行的进程转换为就绪状态,让更高优先级的进程执行。
运行状态 -> 阻塞状态:当进程请求某一资源(如外设)的使用和分配或等待某一事件的发生(如I/O操作的完成)时,它就从运行状态转换为阻塞状态。进程以系统调用的形式请求操作系统提供服务,这是一种特殊的、由运行用户态程序调用操作系统内核过程的形式。
阻塞状态 -> 就绪状态:当进程等待的事件到来时,如I/O操作结束或中断结束时,中断处理程序必须把相应进程的状态由阻塞状态转换为就绪状态。
(以上内容摘自:http://c.biancheng.net/cpp/html/2589.html)
操作系统有很多种,现在多用的操作系统是多道批处理操作系统,内存中同时存在多个作业,要使得每个作业都能分配到运行时间,操作系统多采用时间片轮转的机制实现进程的调度,即在操作系统内设置一个进程队列,每个进程分配一段时间片,操作系统控制器每次从队列头选择处于就绪态的进程运行,当时间片用完时,把该进程挂在队列尾,在从队列头选择一个程序运行,如此往复。现在计算机的一个重要的机制就是中断机制,可以说现在的计算机是建立于中断机制之上的,当程序执行过程中有中断信号传来,操作系统负责保存当前程序执行状态,将有关程序执行的数据压栈,接着转入中断处理程序执行,当中断处理完成,操作系统吧堆栈中的数据弹出,恢复程序执行现场,继续原程序的执行。
以上是本次试验的全过程,更加深入的内容有待于继续学习。
Allen原创作品转载请注明出处《Linux内核分析》MOOC课