实践 一.准备工作 1.代码下载 http://oldlinux.org/Linux.old/kernel/0.1x/linux-0.12.tar.gz 读书笔记 第六章 引导启动程序 Boot 1.PC在电源开启后,80x86 CPU将进入实模式,并从地址0xFFFF0开始自动执行程序代码,这里通常是BIOS的地址,它首先执行某些系统检测,然后在0地址处初始化中断向量,注意这里的中断向量号是BIOS定义的,与操作系统,即CPU指定的向量号是不同的;然后BIOS读取启动设备的第一个扇区的512字节到地址0x7C00(即07C0:0000)处,并跳转到此处开始执行。 2.boot/bootsect.S 1)把0x7c00开始的512字节代码,复制到0x90000开始的512字节处 2)设置软驱最大读取扇区数目,BIOS中断0x1E的中断向量值是软驱参数表地址,其向量值位于内存0x0000:0x78处,该地址处存储的一个四字节是一个地址,该地址处的12字节就是软驱参数表,把该地址的12字节复制到0x9FeF4处,然后把0x1E的中断向量值指向此地址(以little endian的形式) ------------------------------------------------------ | 512 Byte | 4 * 512 Byte | Stack Size | 12 Byte | ------------------------------------------------------ 0x90000 setup.S 0x9FeF4 0x9FF00 3)使用BIOS中断0x13从磁盘第2扇区读取4个扇区到0x90200处,这就是setup模块 4)使用BIOS中断0x13获得每磁道扇区数 5)从磁盘中第5个扇区开始读取system模块,到地址0x10000,长度是0x30000 6)判断root_dev是否被指定,root_dev表示根文件系统的设备号,它的具体格式可以参考P213,表6-2,如果它没有被指定,要根据每磁道扇区数来判断是1.44M驱动器,还是1.2M驱动器来确定设备号,设备号的格式参考P207的注释 7)跳转到setup模块的地址处 http://oldlinux.org/Linux.old/docs 从硬盘启动的问题:boot程序要识别活动分区的文件系统类型,并能够访问到内核Image 3.setup.S 1)使用BIOS中断0x15获取扩展内存的大小,并存储在0x90002开始的2字节处,所谓扩展内存是从0x100000(1M)开始的内存 2)使用BIOS中断0x10,获取屏幕光标位置,并保存在内存0x90000处的2字节处 3)获取当前显卡的显示模式 4)获取第一块硬盘的硬盘参数,存储到0x90080处,获取第二块硬盘的硬盘参数,存储到0x90090处 5)判断是否有第二块硬盘,如果没有,把第二块硬盘参数清零 6)把system模块从0x10000移动到0x00000,即内存0地址处,长度为0x80000,注意0x80000超过了实模式下段长度的最大值,所以这里是采用每次复制0x8000,即32k的方式 7)加载idt,加载gdt,打开内存的第20位地址线 8)重新编程8259A,设置中断向量号,这里说明一下对于中断的理解,外部设备发生中断后,通过中断控制器8259A通知CPU(通过一个信号),CPU产生中断,在保护模式下,会调用相应的中断门进行处理。但是IBM PC BIOS把中断向量号放在了0x08-0x0F,0x70-0x77,详见P19表2-2,与Intel指定的中断向量号不同,所以在操作系统中需要重新对中断控制器的中断请求号与中断向量号的对应关系进行编程,按照80x86的说明,中断向量号被设置在0x20-0x2f,详见P165表5-2 9)打开CR0的PE位,进入保护模式 10)jmpi 0,8,其中8是段选择符,0是段中的偏移量,8所指定的段就是system所在的位置0x0 进入setup.S后,bootsect.S所在的0x90000的512字节就没用了,setup.S通过BIOS读取系统参数放置到该地址处,然后把system模块复制到0地址处,最后进入保护模式,并跳转到0地址。 8259A的编程方法 详见P235 4.head.s 1)重新加载idt和gdt,idt中有256个表项,在后面各个模块初始化的时候,会安装各自的中断处理程序;gdt也有256项。在加载完成之后,需要重新加载各个段寄存器,尤其是CS段,需要通过一个jmp指令来加载它,让它们的描述符使用新的gdt 2)通过向0地址写数据,然后和0x10000比较的方法,来判断是否成功打开了A20地址线 3)检查是否存在协处理器,方法是执行协处理器(80287/80387)初始化命令,然后读取协处理器的状态字,如果为0,说明协处理器执行命令正常,否则说明协处理器不存在,需要设置cr0寄存器的EM仿真位,并复位MP协处理器存在位 4)启动分页机制,执行这一步会导致从0地址开始5页,即5*4K范围内的代码段被覆盖,作为页目录和页表,页表中填写的最后一个页基地址是从0xFFF000,0xFFF000+0x1000(4K)正好等于0x1000000,即16M,也就是说,4个页表覆盖了16M的地址范围。这里一直有一个疑问,在页表中的基地址指的是物理地址还是虚拟地址呢? 5)跳转到main函数 中断对于堆栈的影响 中断发生的时候,如果没有发生特权级变换,那么会把eflags,cs,eip,error_code依次入栈。如果发生了特权级变换,那么需要把ss,esp,elfags,cs,eip,error_code依次入栈,同时使用iretd作为中断返回的语句。该语句将把栈中内容,存入对应的寄存器中,但是对于eflags有些不同,只有当CPL=0的时候,elfags寄存器中的IOPL才能被改变,只有当CPL>2 ] ; struct { long * a; short b; } stack_start = { & user_stack [PAGE_SIZE>>2] , 0x10 }; 按照little-endian的顺序,0x10应该是ss段的选择符,& user_stack [PAGE_SIZE>>2] 则是esp的值,它指向stack数组的最高位,向低地址扩展 ds则被设置为0x10选择符,即在head.s中定义的gdt中的段描述符 _gdt: .quad 0x0000000000000000 /* NULL descriptor */ .quad 0x00c09a0000000fff /* 16Mb 代码段*/ .quad 0x00c0920000000fff /* 16Mb 数据段*/ .quad 0x0000000000000000 /* TEMPORARY - don't use */ .fill 252,8,0 /* space for LDT's and TSS's etc */ 进入main.c之后,调用move_to_user_mode,它在system.h中定义 #define move_to_user_mode() \ __asm__ ("movl %%esp,%%eax\n\t" \ "pushl $0x17\n\t" \ //指定ss描述符,0x17指定ldt中的第二个描述符,但是ldt是在哪里加载的呢? "pushl %%eax\n\t" \ //esp入栈 "pushfl\n\t" \ //eflags入栈 "pushl $0x0f\n\t" \ //cs 入栈,这里同样是加载ldt中描述符,而且是第一个描述符 "pushl $1f\n\t" \ //eip入栈,$1f表示前向标号1 "iret\n" \ //经过iret后,从特权级0转换到任务0的特权级3,同时跳转到标号1处 "1:\tmovl $0x17,%%eax\n\t" \ "mov %%ax,%%ds\n\t" \ "mov %%ax,%%es\n\t" \ "mov %%ax,%%fs\n\t" \ "mov %%ax,%%gs" \ :::"ax") 在sched_init中,调用了lldt(0),所以使用0x17作为ss的选择符是有道理的,在sched.h中有如下的定义: /* * Entry into gdt where to find first TSS. 0-nul, 1-cs, 2-ds, 3-syscall * 4-TSS0, 5-LDT0, 6-TSS1 etc ... */ #define FIRST_TSS_ENTRY 4 #define FIRST_LDT_ENTRY (FIRST_TSS_ENTRY+1) #define _TSS(n) ((((unsigned long) n)get_free_page(swap.c)//从0x100000即1M开始的15M内存,是所谓的主内存,由操作系统进行管理,它把这些内存分成页,并使用一个数组mem_map来管理,如果某页使用了,那么设置数组中对应序号为1,如果没有使用设置为0 //get_free_page的逻辑是根据mem_map找到一个没有使用过的页,然后计算出它对应的物理地址(LOW_MEM+4K*index),最后把这4K的地址清零,如果没有找到一个空闲的页,执行swap_out操作 ----->swap_out()//该函数用来从内存中换出一个空闲页,在线性地址空间中,低64M内存是代码和数据等存放的地址,所以虚拟内存是从64M开始的,也就是说FIRST_VM_PAGE=(64M)>>12,线性地址空间范围是4G,除了64M以外,还有可用的虚拟页数为1024*1024-FIRST_VM_PAGE;pg_dir是在head.s中定义的标号,它是0地址,在0地址中存放的是页目录表 //从FIRST_VM_PAGE中可以算出它对应页目录中的第几项,从这项开始在pg_dir中查找页目录项是否有P位置位的,如果有,那么从这个页目录项的高20位中可以得到一个物理地址,它指向一个页表,该页表一共有1024项,每项指向一个4K的页面,对这个页表的每一项调用try_to_swap_out函数,尝试把它换出内存 ----->try_to_swap_out()//该函数检查当前页表项的P位是否置位,检查mem_map中对应的字节是否为1,如果都确认,那么就可以交换了,首先得到一个交换号,然后把页面数据写到交换空间中。 //write_swap_page这个函数用于写交换页,它使用到了SWAP_DEV设备号,从前面的分析我们知道它实际上是一个硬盘设备。 /* 其实分页管理把4G的线性地址分成了1024*1024个页,每个页都有一个页表项对应,每1024个页表项使用一个页目录项来对应,也就是说只要指定了页目录项,和对应的页表中的项目,就确定了线性地址空间中的页的线性地址,而存储在页表项中的内容,要么是这个虚拟页对应的物理地址,要么是交换页号。而如果要把所有的页目录表和页表项都放在内存中的话需要占用1024*1024*4 + 1024*4的大小 */ ------------------------------------------------------------- 有个疑问,CPU是如何区分线性地址和物理地址的,在访问线性地址的时候,会发生地址转换,在转换的过程中,使用的是物理地址,CPU怎么知道此时用的就是物理地址,而不是线性地址呢? 在内核态,在构建页目录和页表的时候,线性地址与物理地址是一一对应的,也就是说线性地址等于物理地址 ------------------------------------------------------------- copy_process中还保存把用于任务切换的寄存器的值写入到task的tss中,同时指定了任务在内核态执行的时候ss和esp指向task结构所在页的顶端 至此fork过程算是结束了 从move_to_user_mode函数开始,该函数会从内核态,即特权级0,切换到特权级为3的任务0,任务0通过静态变量init_task说明了ldt和tss。 接着int 80系统调用,使得后面的入栈出栈操作都是在任务0的内核态堆栈进行的,并不影响任务0的用户态堆栈,在fork函数中复制了任务0的task_struct,并把当前的寄存器保存在新创建的task_struct的tss中,其中eax保存的值为0,这正是fork之后,子进程返回值为0的关键。在创建新的task_struct之后,int 80把新创建的pid作为返回值,返回到sys_call之后会进行任务调度,如果它导致任务切换至新创建的任务1执行,那么CPU将会把TSS中保存的各个寄存器中的值恢复到相应寄存器中并载入相应的ldt,此时eax作为返回值被返回给main函数,这时的返回值就成为0,而不同于其父进程任务0的值 ------------------------------------------------------------- 为什么任务块在线性地址空间的地址都是以64M逐渐增加的? ------------------------------------------------------------- ldt,gdt中存放的基地址都是线性地址 memory.c中的copy_page_tables函数,其主要作用是复制页目录项及其页表,其中比较费解的是如下的几句: from_dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */ to_dir = (unsigned long *) ((to>>20) & 0xffc); size = ((unsigned) (size+0x3fffff)) >> 22; 其中from和to都是线性地址,计算from_dir的这一句,如果写成如下的这句可能更好理解: from_dir = (unsigned long *) ((from>>22)>22得到的是from线性地址对应的是第几个页目录项,然后左移2位,则是计算这个目录项的地址,因为每个目录项都占用4字节,所以from_dir得到的是from线性地址对应的页目录项的地址 ------------------------------------------------------------- 从这里看所有的任务块的页目录项显然也是存放在0地址开始的4K的,那么就有一个问题,难道每个任务的线性地址空间就只有64M么? ------------------------------------------------------------- ------------------------------------------------------------- 一个进程的页目录和页表是否可能缺页 完全可能,此时需要第一次加载数据到物理页中 ------------------------------------------------------------- mm目录中给出了两个层次的映射,一个是线性地址到物理地址的映射,一个是线性地址到交换页的映射。只要给定了一个线性地址,那么就确定了其页目录项的地址,如果该地址没有内容,那么就会发生缺页中断,如果有内容,那么它就指向了一个页表,通过线性地址的中间10bit又可以得到一个页表项的地址,该地址中的内容可能有值,也可能没有值 交换页的机制是分配一个页作为映射位图,把其中的每一个bit用来表示一个交换页4k是否使用,把这个bit的序号作为交换页的序号保存在页表项中,并且序号为0,即映射位图的序号 也就是说一个线性地址确定了页表项的内容之后,该内容要么是一个物理页地址,要么是一个交换页的序号 signal.c fork.c文件中有一个verify_area函数,它的作用是检查一段内存区域是否可写,因为在80386中,特权级0在写特权级3的页面的时候,不会产生页写保护,这导致用户进程写保护失效,但是在80486以后,intel加上了特权级0在写特权级3的页面的时候,也会产生页写保护的功能。verify_area函数就是实现这个功能的,它的参数是进程中的内存地址,及长度。 这里特别需要说明用户进程中的内存地址,实际上就是所谓的逻辑地址,由于可执行文件在编译的时候,其中的变量,函数地址都是相对地址,这样可以在加载可执行文件的时候,把它加载到任意地址,这个所谓的相对地址就是用户进程的逻辑地址,在内核中,需要根据进程所在的线性空间地址,计算出这个逻辑地址的线性地址,继而算出它所在页首的线性地址。 任务结构中blocked字段,表示信号阻塞位图,如果1置位,表示对应的信号将被阻塞,如果0置位表示对应的信号不被阻塞,其中SIGKILL和SIGSTOP是不能被阻塞的,也就是说它们在blocked字段中相应位中为0 下面函数中参数addr是用户进程的地址,我们可以看到在汇编代码中使用的是fs:offset,段加偏移量的方式,这样就访问到逻辑地址的线性地址。 extern inline void put_fs_long(unsigned long val,unsigned long * addr) { __asm__ ("movl %0,%%fs:%1"::"r" (val),"m" (*addr)); } 为什么使用fs段寄存器呢,因为在int80系统调用中,sys_call.s文件中,把ds,es指向了内核的数据段,而fs指向了当前进程ldt的数据段,所以这里可以使用fs来引用用户空间数据段。 好不容易看懂了signal.c中的do_signal函数, 1.调用与被调用 首先该函数是在sys_call.s中的ret_from_sys_call调用的,该函数是所有中断调用退出的时候调用的函数,其中也包括系统调用system_call,它是通过入栈的eax与其他调用区分的,system_call函数在根据调用号调用了相应的c函数之后,把返回的结果eax入栈,而其他的中断调用则是直接把-1入栈。 2.被中断的系统调用重新启动的问题 ret_from_sys_call函数会对signal进行处理,即调用do_signal函数,但是system_call系统调用调用c函数的过程中,有可能导致所在进程sleep,在收到非阻塞的信号唤醒该进程的时候,这个系统调用就要告诉do_signal是否要重新启动这个系统调用了,因为它被中断了,do_signal中的代码如下; if ((orig_eax != -1) && //就是通过这个eax来区分是否系统调用int80 ((eax == -ERESTARTSYS) || (eax == -ERESTARTNOINTR))) {//这表示系统调用的处理函数要求重新启动系统调用 if ((eax == -ERESTARTSYS) && ((sa->sa_flags & SA_INTERRUPT) || signr < SIGCONT || signr > SIGTTOU))//这种情况下,不用重新启动系统调用 *(&eax) = -EINTR; else {//这是重新启动系统调用的代码,eip会用户态的ip,它减掉2,表示eip移动到指向系统调用的代码 *(&eax) = orig_eax; *(&eip) = old_eip -= 2; } } 3.对调用信号句柄的预处理 首先需要明白两件事情 1)由于do_signal是在中断调用中调用的,所以它是在内核态执行的,它用的栈是用户进程的内核栈,而不是用户进程的用户栈 2)所谓的调用信号句柄,是通过设置内核栈上的eip来实现的,因为在中断调用结束后,栈中的值会被弹回到用户进程的相应寄存器中,所以只需要设置栈中相应寄存器的值就可以实现对信号句柄的调用 下面来说明信号句柄的问题,用户实际上是通过Libc库中的函数来调用int 80系统调用,进而实现各种功能的,对于设置信号句柄来说,代码如下: void (*signal)(int sig,__sighandler_t func)(int) { void (*res)(); register int __fooebx __asm ("bx") = sig; __asm__("int $80":"=a" (res): "0" (__NR_signal), "r" (__fooebx), "c" (func), "d" ((long)__sig_restore)); return res; } 从该函数中,我们可以看到eax寄存器保存了功能号__NR_signal,ebx保存了信号号,ecx保存了信号句柄,edx保存了__sig_restore,这是一个用来恢复寄存器的函数,后面会详细说明。 在sys_call.s中,我们可以看到如下的代码: _system_call: push %ds push %es push %fs pushl %eax # save the orig_eax pushl %edx pushl %ecx # push %ebx,%ecx,%edx as parameters pushl %ebx # to the system call movl $0x10,%edx # set up ds,es to kernel space mov %dx,%ds mov %dx,%es movl $0x17,%edx # fs points to local data space mov %dx,%fs cmpl _NR_syscalls,%eax jae bad_sys_call call _sys_call_table(,%eax,4) pushl %eax 这里是根据功能号,调用相应的c函数的代码,在调用c函数之前,把eax,edx(__sig_restore),ecx(handler),ebx(signr)都入栈了,而signal的c函数原型是 int sys_signal(int signum, long handler, long restorer) 根据c函数的入栈规则,sys_signal函数可以取到它的参数 信号处理后寄存器的恢复 我们先来说说信号处理在整个系统调用中的位置 1.进入系统调用 ----- 2.系统调用调用c函数处理 \____这里都是处于内核态 3.do_signal处理信号 -----/ 4.调用signal_handler -----\____这里都是处于用户态 5.调用sig_restore -----/ 6.返回到用户进程调用系统调用之后的一条指令 从代码入手看看这些调用是如何跳转的 *(&eip) = sa_handler; //这里把要调用的信号句柄复制给栈参数eip,在中断调用结束的时候,这个栈参数会弹出给eip寄存器,实现从内核态到用户态的转换,从而进入用户进程的信号句柄处理函数 longs = (sa->sa_flags & SA_NOMASK)?7:8; *(&esp) -= longs;//这里移动用户态栈的esp,7个或者8个4字节 verify_area(esp,longs*4); tmp_esp=esp; put_fs_long((long) sa->sa_restorer,tmp_esp++); put_fs_long(signr,tmp_esp++); if (!(sa->sa_flags & SA_NOMASK)) put_fs_long(current->blocked,tmp_esp++); put_fs_long(eax,tmp_esp++); put_fs_long(ecx,tmp_esp++); put_fs_long(edx,tmp_esp++); put_fs_long(eflags,tmp_esp++); put_fs_long(old_eip,tmp_esp++); current->blocked |= sa->sa_mask; return(0); /* Continue, execute handler */ 经过上述调整之后,用户态栈的状态如下: ------------- high address | old_eip | | eflags | | edx | | ecx | | eax | | blocked |(可能没有这个四字节) | signr | low address | restorer | blocked = old_mask; return -EINTR; } /* we're not restarting. do the work */ *(&restart) = 1; *(&old_mask) = current->blocked; current->blocked = set; (void) sys_pause(); /* return after a signal arrives */ return -ERESTARTNOINTR; /* handle the signal, and come back */ } 该函数实际上是包在Libc的库函数中,通过int80系统调用调用的,其调用代码如下: int sigsuspend(sigset_t *sigmask) { int res; register int __fooebx __asm__ ("bx") = 0; __asm__("int $0x80" :"=a" (res) :"0" (__NR_sigsuspend), "r" (__fooebx), "c" (0), "d" (*sigmask) :"bx","cx"); if (res >= 0) return res; errno = -res; return -1; } 前面说过system_call首先把edx,ecx,ebx入栈作为调用的c函数sys_sigsuspend的参数,显然这里的寄存器edx对应set,ecx对应old_mask,ebx对应restart,在进程第一次调用sigsuspend的时候,进入sys_sigsuspend,因为restart为0,所以不会进入if语句,而会到后面的代码,restart栈变量被赋值为1,old_mask栈变量的值被赋值为当前进程的blocked,当前线程的blocked被赋值为set,然后调用sys_pause,该函数将导致系统重新调度,当前进程就此被挂起,当当前进程收到一个信号的时候,它被唤醒,并从return语句开始往后执行。由于该c函数返回了-ERESTARTNOINTR,所以do_signal函数会重新启动该系统调用,也就是说,在信号处理函数执行完成后,返回到用户态的时候,sigsuspend函数中的嵌入式汇编代码会被再次执行 下面我们再来看参数的问题: 从system_call函数看起,函数调用及参数的情况如下: 1.system_call pushl %edx pushl %ecx # push %ebx,%ecx,%edx as parameters pushl %ebx 2.调用_sys_call_table call _sys_call_table(,%eax,4) // sys_suspend函数被调用,栈变量被修改 pushl %eax 3.调用do_signal 4. popl %eax popl %ebx popl %ecx popl %edx 执行这段代码后,栈上被修改的值就被弹入到相应的寄存器中 5.系统调用被重新启动,即第二次执行sigsuspend中的嵌入式汇编代码 此时的ebx,ecx,edx已经不是开始原来的初值了,而是已经修改过的值了,在第二次进入到suspend系统调用中的时候,发现是restart,会立即返回-EINTR,在do_signal函数中也因为这个返回值不会再次修改eip的值而导致重新启动,最终该进程因为收到一个信号而被唤醒。 signal与sigaction的区别 两个函数的定义如下: void (*signal(int _sig, void (*_func)(int)))(int); 这个函数定义看起来还是挺复杂的,其实复杂的地方在于当把一个函数指针作为一个函数的返回值的时候,同时又要把这个函数指针的原型表现出来。 使用如下的定义可以简化该定义: typedef void (*sigfunc)(int);//这里显然定义了一种函数指针类型 sigfunc signal(int _sig,sigfunc); struct sigaction { void (*sa_handler)(int); sigset_t sa_mask; int sa_flags; void (*sa_restorer)(void); }; int sigaction(int sig, struct sigaction *act, struct sigaction *oldact); 我们先来看看跟signal相关的一些过程 1.中断发生 2.ret_from_sys_call函数被调用 3.取当前进程的signal和block,判断可以进行处理的signal,block是信号屏蔽mask,其中位设置为1表示该信号被屏蔽,设置为0表示该信号可以进行处理 4.把当前要处理的signal,在当前进程的signal位图中复位 5.调用do_signal函数,其中会把处理这个signal的handler保存起来,然后把当前进程的这个signal的handler恢复默认,设置为NULL 6.进入用户态,调用signal的handler,在handler的内部一般会把自己重新设置给这个signal作为signal handler 这里可能出现的问题是第6步,因为在进入到用户态以后,该函数可能在把自己重新设置为signal handler的之前,又发生了同样的signal信号,此时实际上发生了信号处理的嵌套,这时对signal信号调用它的信号处理handler,将是默认的操作,而不是用户定义的,由此导致信号的丢失。 那么sigaction函数是如何避免这种情况的发生的呢? 在sigaction结构中有一个sa_mask字段,在这个字段中可以设置信号屏蔽位,注意这个结构在进程中是每个信号都有一个的,在do_signal函数中,默认情况下会把自己的信号加入到这里,同时会把这个sa_mask加入到进程的blocked字段中,这样一来,在第6步进入到用户态调用处理handler的时候,又发生了同样的信号,那么它虽然被设置到进程的signal上,但是将会被blocked,也就是被阻塞发生,同时,在当前的handler处理结束之后,sa_restorer将会把进程原来的blocked,也就是没有合并当前信号的sa_mask的blocked字段重新恢复到进程中,这样进程将在下次调用的时候发现又发生了signal信号,从而避免了该信号的丢失。 块设备 1.块设备的读写是靠中断驱动的,而不是在单个进程中进行的 2.块设备的几种情况 1)正常读写 2)发生异常 3)读写错误 4)超时 3.块设备的reset 4.扇区号到磁头号,柱面号,当前磁道扇区号的转换 软盘驱动程序 1.基本流程 1)触发方式与硬盘是相同的,即使用kernel/blk_drv/ll_rw_blk.c文件中的ll_rw_page和ll_rw_block函数来启动软盘设备的请求处理 2)软盘的读写,首先需要启动马达,然后寻道,然后启动DMA进行读写,读写完成之后,DMA设备会通过中断控制器向CPU发送中断请求。这里的问题是启动马达和寻道操作都是异步进行的,驱动程序中采用了两种方式来处理,对于启动马达操作,是启动了一个定时器,在定时器超时之后,查询软盘控制器的状态;对于寻道操作,则是设置一个中断函数,软盘驱动器在执行完成寻道操作之后,会产生一个中断,CPU根据中断号会调用中断函数 3)几种情况的处理 a.正常情况的处理 正常情况下,启动马达,并启动定时器,定时器超时后,判断是否需要寻道,如果不需要寻道,则马上启动DMA,如果需要寻道,则设置寻道完成后的中断函数,该中断函数检查控制器状态,并启动DMA开始读写数据;DMA传输数据完成之后,会发生中断,中断函数将检查驱动器状态,并做出相应处理。 b.错误状态 在通过result检查驱动器状态,并发现状态错误的情况下,会给错误计数加一,在错误计数小于MAX_ERRORS/2的时候,将进入recalibrate状态进行校准,如果错误技术大于MAX_ERRORS/2,那么将进入reset状态进行驱动器重置。在错误次数超过MAX_ERRORS的时候,将会终止当前请求,启动下一个请求 c.recalibrate的处理 重新校正命令是软盘驱动器发送的定位命令,它将导致磁头移动到0磁道,由于这个过程较长,所以它是一个异步操作,第一步是发送校准命令,第二步是在校准完成后,发送中断请求。 d.reset的处理 reset处理是要求软盘驱动器重新设置驱动器的各项参数 e.选定驱动器 由于支持4个软盘驱动器,所以有一个选定的操作,在启动某个驱动器马达之后,将设置selected标志,表示已经有驱动器在处理了,在达到错误计数最大数目,读写过程中写保护,或者DMA读写完成的时候,将调用deselect操作,该操作将把selected标志复位,并唤醒那些wait_on_floppy_select等待软盘驱动器选定的进程。这里又涉及另外一个函数floppy_change f. floppy_change 该函数调用了floppy_on函数,该函数通过设置DOR,启动指定的驱动器的马达,并关闭其他的驱动器,同时启动了一个计时器,在启动马达,即计时器没有超时的时候,当前进程休眠,在计时器超时之后,唤醒进程,如果进程发现当前已经有驱动器被selected,那么就sleep在wait_on_floppy_select上,直到deselect操作把该进程唤醒 g.关于马达的启动和关闭 马达的启动和关闭是通过设置DOR寄存器中的高四位来实现的,ticks_to_floppy_on函数设置了moff_timer和mon_timer,分别用于关闭和启动定时器超时,在moff_timer超时的时候,将关闭对应的马达,在mon_timer超时的时候,将唤醒等待启动马达完成的进程。 4)软盘驱动的几种命令 软盘驱动器主要用到三个寄存器,分别是主状态寄存器,数据寄存器和数字输出寄存器(DOR),其中DOR控制马达的启动和关闭,驱动器的选择,是否允许使用DMA等操作。主状态寄存器和数据寄存器是配对使用的,通过数据寄存器发送命令和参数数据,通过主状态寄存器查询命令的执行结果,使用到的命令有:recalibrate命令,寻道命令,读数据命令,写数据命令,检测中断状态命令,设置驱动器参数命令 大致分为三种命令,一种是查询状态命令,如查询主状态寄存器,检测中断状态,一种是阻塞式命令,即发送命令后,即时查询驱动器状态寄存器,就可以得到命令结果的,例如:读写命令,一种是异步式的命令,例如,启动关闭马达,校准,重置,寻道,这种异步命令,一般通过中断或者定时器的方式,并通过查询状态判断是否正确完成了 由于软盘驱动程序较为复杂,所以从这里我们也可以看到一般块设备驱动程序的编写方法,首先要掌握寄存器的使用,其次要了解驱动程序读写数据的一般步骤,最后,要对意外情况进行处理,例如,异步操作,超时,重新校准,重置等 字符设备 tty实际上是对一组成对的输入输出设备的抽象,tty既包含输入,也包含输出,所以这个概念可以应用在任何慢速的输入输出逻辑中 uart uart芯片的这个结构图很有意思,它包括了读写信号选通引脚,8根数据引脚,复位引脚,中断引脚,中断屏蔽引脚,提供时钟的晶振引脚,很有代表性哦 波特率是一种描述调制信号能力的单位,它表示每秒调制信号的个数,比特率表示每秒传输数据的bit数,在两相调制下,二者是相等的,即码元为0或者1,即bit 只要是设备,都有一个等待的进程队列,在设备忙的时候需要休眠,在设备空闲的时候需要唤醒 fs inode_table是对存储在设备中的文件系统的inode的缓存,inode结构中的i_count表示系统对这个缓存节点的引用,当这个值为0的时候,表示inode_table中的这个inode节点无效,而i_nlinks是文件系统中目录项对inode的引用次数,当这个值为0的时候,表示在文件系统中这个inode节点可以删除了 inode.c中对外的接口包括:invalidate_inodes,sync_inodes,bmap,create_block,iput,get_empty_inode,get_pipe_inode,iget.实际上,inode通过在内存中缓存inode节点来达到快速访问inode的目的,inode_table即是这个缓冲区。所以,这就涉及缓冲区与设备的同步问题,在提供上述接口的时候,首先是针对缓冲区的操作,如果发现缓冲区的inode与设备的inode不同步,则需要同步之,这可能就涉及设备操作. _bmap是其中很重要的一个函数,它根据数据序号,返回某个inode节点数据块的逻辑块号。这里就涉及两个问题,一个是可能需要设备操作,如果这个inode节点的数据序号对应的逻辑块在设备上还没有分配,或者甚至用来索引数据的数据块都没有分配,那么就需要设备分配数据块,同时当前进程必须等待这个过程的完成而进入sleep状态,另外一个是inode包含的数据块序号与逻辑块号的对应问题 inode节点中的i_dev表示inode节点所在的设备,而字符设备和块设备使用inode节点的i_zone[0]来表示这个inode节点所代表的设备号 在超级块或者逻辑块dirty之后,什么时候同步数据,在哪个进程同步数据 bitmap.c 超级块的数据字段 s_ninods 表示inode的块数 s_nzones 表示逻辑块的块数,包括超级块,inode映射块和数据映射块 s_firstdatazone 表示第一个数据块对应的盘块号,这里涉及两个序号空间,一个是数据块序号空间,一个是盘块号空间,数据块是在inode块之后的,它是从0开始的,同时序号0又是不使用的序号,盘块也是从0开始的,0对应引导块,后面依次是超级块,inode映射块,数据块映射块,inode块,数据块,这两个序号空间之间的对应关系是序号为1的数据块,对应序号为s_firstdatazone的盘块,那么序号为nr的数据块,对应nr+s_firstdatazone-1的盘块号 之所以存在这样一个转换是因为,某个数据块的序号就是该数据块在数据块映射块中的位移,通常情况下,首先可以得到这个位移,也就知道了数据块的序号,通过转换就可以得到它对应的盘块号,继而可以读写盘块数据 另外一个序号空间就是inode序号,同样的,inode序号也是从0开始的,序号0不使用,inode序号就是该inode在inode映射块中的位移,得到这个位移,就得到了inode的序号,由于每个inode节点只有32字节,所以通过类似上面的算法,可以算出该inode节点所在的盘块号,继而读写该块数据 还有一个序号空间就是inode节点中的数据序号,这里实际上是一个映射表,inode节点中给出了数据序号0到6,对应的盘块号,同时给出了一级索引块的盘块号,和二级索引块的盘块号 分配一个数据块的过程: 1.超级块的s_zmap字段中每一位表示对应序号的数据块是否使用,通过这个字段得到一个没有使用的数据块的盘块号 2.从buffer的hash表中获取一个空闲的buffer块 3.把这个空闲块的b_blocknr字段设置为得到的数据块序号,即建立buffer缓冲与设备上数据的盘块号的映射 分配一个inode的过程: 一般都是在得知一个inode的序号之后,要求一个inode节点 1.从inode_table中查找是否有对应设备和对应序号的inode,如果有就把该节点的引用数加一 2.如果没有就在inode_table中,找一个空闲节点,并根据inode的序号读取设备上该序号的inode内容到空闲节点上。 buffer.c 从breada函数可以看出,buffer_head结构中的b_count字段用来表示引用次数,在提前预读,而不是使用该buffer的情况下,这个次数是要恢复回去的,而b_uptodate用来表示该字段是否与设备是相同的,也就是说在b_count为0的情况下,b_uptodate仍然可能是1 另外一点,在buffer初始化的时候,在高速缓冲中分配buffer,buffer的数目可能小于总的数据块个数 namei.c 基本概念 文件方式字 在inode中有一个i_mode的字段,它表示文件的存取权限,文件类型和set-user-id,set-group-id 有效用户ID,有效组ID 这两个概念首先是进程里的概念,即是进程的有效用户ID,进程的有效组ID。在执行一个程序文件的时候,一般情况下,有效用户ID就是实际用户ID,即当前登录的用户ID,有效组ID就是当前用户所属的组ID,但是如果该文件的文件方式字中set-user-id置位,那么进程的有效用户ID就会设置为文件的宿主ID,同样,如果set-group-id置位,那么进程的有效组ID就会设置为文件宿主所在组的ID。 关于namei.c中find_entry中一段代码的解释: if (namelen==2 && get_fs_byte(name)=='.' && get_fs_byte(name+1)=='.') { /* '..' in a pseudo-root results in a faked '.' (just change namelen) */ if ((*dir) == current->root) namelen=1; else if ((*dir)->i_num == ROOT_INO) { ---------------------------------------------------------begin主要是这一段 /* '..' over a mount-point results in 'dir' being exchanged for the mounted directory-inode. NOTE! We set mounted, so that we can iput the new dir */ sb=get_super((*dir)->i_dev); if (sb->s_imount) { iput(*dir); (*dir)=sb->s_imount; (*dir)->i_count++; } }-------------------------------------------------------------------------------------------------end } find_entry的函数原型如下: static struct buffer_head * find_entry(struct m_inode ** dir, const char * name, int namelen, struct dir_entry ** res_dir) 它的具体功能是在dir这个node上查找名字为name的direntry,上面截取的代码是为了处理一种特殊情况,当前所在目录是mount过的一个文件系统,而dir指向这个文件系统的根节点,而name为字符串"..",我们先来看看mount一个文件系统的过程 super.c文件的sys_mount函数,其过程如下: 从设备文件路径得到设备文件对应的inode,从这个inode的i_zone[0]得到设备号,从这个设备号中读取文件系统的超级块,从要mount到的路径得到对应的inode,把超级块的s_imount指针指向这个inode 从这里我们可以看到宿主文件系统中的inode与新mount上的文件系统是对应起来的,如果要取mount上的文件系统的根目录的上一级,那么就是取宿主文件系统中的inode的上一级,恰好在创建目录inode的时候,它必然有两个entry,一个是.,一个是..,所以上面的代码使用超级块指向的宿主文件系统中的inode替换原来的inode是没问题的。 实际上inode.c中的iget函数中的如下的代码和上面是一个意思: if (inode->i_mount) { int i; for (i = 0 ; i= NR_SUPER) { printk("Mounted inode hasn't got sb\n"); if (empty) iput(empty); return inode; } iput(inode); dev = super_block[i].s_dev; nr = ROOT_INO; inode = inode_table; continue; } follow_link函数 首先说明一下软连接和硬连接的区别,软连接是这样一个inode,它的数据内容实际上是一个字符串表示的路径,所以它可以是跨文件系统的;硬连接实际上是一个目录中的entry,该entry指定了一个inode和一个名字,名字即是硬连接的名字,inode即是它所指向的文件,该文件的nlink连接数因此被加一,由于指定了一个inode的序号,所以硬连接必须是指向同一个文件系统内部的。总的来说,软连接是一个inode,而硬连接只是一个entry follow_link函数就是从inode中找到数据中的字符串路径,并根据这个路径找到相应inode的函数,第一步找到数据中的路径是好理解的,第二部根据路径找相应的inode就有点说法,一般来说,根据路径查找inode是通过namei函数进行的,但是namei的参数路径,一般都是用户空间的数据,而这里得到的路径是内核空间的,在follow_link函数中通过修改fs来达到目的 get_dir函数是从一个基准目录的inode节点和一个给定的path,得到这个给定path中目录部分的inode节点 dir_namei函数调用get_dir函数,返回给定path中目录部分的inode,和path中除了目录部分剩余的字符串 _namei函数调用dir_name函数,得到给定path的inode,如果有剩余的字符串,则在这个inode的entry中查找是否有对应的entry,得到entry后,根据entry中的inode序号得到inode节点 umask基础知识 umask是创建文件的时候,赋予文件的默认权限。对于文件来说,系统不允许默认执行权限,必须使用chmod修改,所以它的最大权限只能是6,即只有读写权限,而对于目录,则可以赋予执行权限,umask的作用是设置在创建文件的时候,屏蔽掉的权限,例如umask设置为002,表示把other的写权限屏蔽,那么文件对应的权限就是664,对于目录则是775.