转载地址:http://blog.csdn.net/williamwanglei/article/details/10518811
启动
当PC启动时,Intel系列的CPU首先进入的是实模式,并开始执行位于地址0xFFFF0处的代码,也就是ROM-BIOS起始位置的代码。BIOS先进行一系列的系统自检,然后初始化位于地址0的中断向量表。最后BIOS将启动盘的第一个扇区装入到0x7C00,并开始执行此处的代码.这就是对内核初始化过程的一个最简单的描述。
最初,Linux核心的最开始部分是用8086汇编语言编写的。当开始运行时,核心将自己装入到绝对地址0x90000,再将其后的2k字节装入到地址0x90200处,最后将核心的其余部分装入到0x10000。
当系统装入时,会显示Loading...信息。装入完成后,控制转向另一个实模式下的汇编语言代码boot/Setup.S。Setup部分首先设置一些系统的硬件设备,然后将核心从0x10000处移至0x1000处。这时系统转入保护模式,开始执行位于0x1000处的代码。
接下来是内核的解压缩。0x1000处的代码来自于文件Boot/head.S,它用来初始化寄存器和调用decompress_kernel( )程序。decompress_kernel( )程序由Boot/inflate.c, Boot/unzip.c 和Boot/misc.c组成。解压缩后的数据被装入到了0x100000处,这也是Linux不能在内存小于2M的环境下运行的主要原因。
解压后的代码在0x1010000处开始执行,紧接着所有的32位的设置都将完成: IDT、GDT和LDT将被装入,处理器初始化完毕,设置好内存页面,最终调用start_kernel过程。这大概是整个内核中最为复杂的部分。
[系统开始运行]
Linux kernel 最早的C代码从汇编标记startup_32开始执行
|startup_32:
|start_kernel
|lock_kernel
|trap_init
|init_IRQ
|sched_init
|softirq_init
|time_init
|console_init
|#ifdef CONFIG_MODULES
|init_modules
|#endif
|kmem_cache_init
|sti
|calibrate_delay
|mem_init
|kmem_cache_sizes_init
|pgtable_cache_init
|fork_init
|proc_caches_init
|vfs_caches_init
|buffer_init
|page_cache_init
|signals_init
|#ifdef CONFIG_PROC_FS
|proc_root_init
|#endif
|#if defined(CONFIG_SYSVIPC)
|ipc_init
|#endif
|check_bugs
|smp_init
|rest_init
|kernel_thread
|unlock_kernel
|cpu_idle
·startup_32 [arch/i386/kernel/head.S]
·start_kernel [init/main.c]
·lock_kernel [include/asm/smplock.h]
·trap_init [arch/i386/kernel/traps.c]
·init_IRQ [arch/i386/kernel/i8259.c]
·sched_init [kernel/sched.c]
·softirq_init [kernel/softirq.c]
·time_init [arch/i386/kernel/time.c]
·console_init [drivers/char/tty_io.c]
·init_modules [kernel/module.c]
·kmem_cache_init [mm/slab.c]
·sti [include/asm/system.h]
·calibrate_delay [init/main.c]
·mem_init [arch/i386/mm/init.c]
·kmem_cache_sizes_init [mm/slab.c]
·pgtable_cache_init [arch/i386/mm/init.c]
·fork_init [kernel/fork.c]
·proc_caches_init
·vfs_caches_init [fs/dcache.c]
·buffer_init [fs/buffer.c]
·page_cache_init [mm/filemap.c]
·signals_init [kernel/signal.c]
·proc_root_init [fs/proc/root.c]
·ipc_init [ipc/util.c]
·check_bugs [include/asm/bugs.h]
·smp_init [init/main.c]
·rest_init
·kernel_thread [arch/i386/kernel/process.c]
·unlock_kernel [include/asm/smplock.h]
·cpu_idle [arch/i386/kernel/process.c]
start_kernel( )程序用于初始化系统内核的各个部分,包括:
*设置内存边界,调用paging_init( )初始化内存页面。
*初始化陷阱,中断通道和调度。
*对命令行进行语法分析。
*初始化设备驱动程序和磁盘缓冲区。
*校对延迟循环。
最后的function'rest_init' 作了以下工作:
·开辟内核线程'init'
·调用unlock_kernel
·建立内核运行的cpu_idle环, 如果没有调度,就一直死循环
实际上start_kernel永远不能终止.它会无穷地循环执行cpu_idle.
最后,系统核心转向move_to_user_mode( ),以便创建初始化进程(init)。此后,进程0开始进入无限循环。
初始化进程开始执行/etc/init、/bin/init 或/sbin /init中的一个之后,系统内核就不再对程序进行直接控制了。之后系统内核的作用主要是给进程提供系统调用,以及提供异步中断事件的处理。多任务机制已经建立起来,并开始处理多个用户的登录和fork( )创建的进程。
[init]
init是第一个进程,或者说内核线程
|init
|lock_kernel
|do_basic_setup
|mtrr_init
|sysctl_init
|pci_init
|sock_init
|start_context_thread
|do_init_calls
|(*call())->; kswapd_init
|prepare_namespace
|free_initmem
|unlock_kernel
|execve
[目录]
--------------------------------------------------------------------------------
启动步骤
系统引导:
涉及的文件
./arch/$ARCH/boot/bootsect.s
./arch/$ARCH/boot/setup.s
bootsect.S
这个程序是linux kernel的第一个程序,包括了linux自己的bootstrap程序,
但是在说明这个程序前,必须先说明一般IBM PC开机时的动作(此处的开机是指
"打开PC的电源":
一般PC在电源一开时,是由内存中地址FFFF:0000开始执行(这个地址一定
在ROM BIOS中,ROM BIOS一般是在FEOOOh到FFFFFh中),而此处的内容则是一个
jump指令,jump到另一个位於ROM BIOS中的位置,开始执行一系列的动作,包
括了检查RAM,keyboard,显示器,软硬磁盘等等,这些动作是由系统测试代码
(system test code)来执行的,随着制作BIOS厂商的不同而会有些许差异,但都
是大同小异,读者可自行观察自家机器开机时,萤幕上所显示的检查讯息。
紧接着系统测试码之后,控制权会转移给ROM中的启动程序
(ROM bootstrap routine),这个程序会将磁盘上的第零轨第零扇区读入
内存中(这就是一般所谓的boot sector,如果你曾接触过电脑病
毒,就大概听过它的大名),至於被读到内存的哪里呢? --绝对
位置07C0:0000(即07C00h处),这是IBM系列PC的特性。而位在linux开机
磁盘的boot sector上的正是linux的bootsect程序,也就是说,bootsect是
第一个被读入内存中并执行的程序。现在,我们可以开始来
看看到底bootsect做了什么。
第一步
首先,bootsect将它"自己"从被ROM BIOS载入的绝对地址0x7C00处搬到
0x90000处,然后利用一个jmpi(jump indirectly)的指令,跳到新位置的
jmpi的下一行去执行,
第二步
接着,将其他segment registers包括DS,ES,SS都指向0x9000这个位置,
与CS看齐。另外将SP及DX指向一任意位移地址( offset ),这个地址等一下
会用来存放磁盘参数表(disk para- meter table )
第三步
接着利用BIOS中断服务int 13h的第0号功能,重置磁盘控制器,使得刚才
的设定发挥功能。
第四步
完成重置磁盘控制器之后,bootsect就从磁盘上读入紧邻着bootsect的setup
程序,也就是setup.S,此读入动作是利用BIOS中断服务int 13h的第2号功能。
setup的image将会读入至程序所指定的内存绝对地址0x90200处,也就是在内存
中紧邻着bootsect 所在的位置。待setup的image读入内存后,利用BIOS中断服
务int 13h的第8号功能读取目前磁盘的参数。
第五步
再来,就要读入真正linux的kernel了,也就是你可以在linux的根目录下看
到的"vmlinuz" 。在读入前,将会先呼叫BIOS中断服务int 10h 的第3号功能,
读取游标位置,之后再呼叫BIOS 中断服务int 10h的第13h号功能,在萤幕上输
出字串"Loading",这个字串在boot linux时都会首先被看到,相信大家应该觉
得很眼熟吧。
第六步
接下来做的事是检查root device,之后就仿照一开始的方法,利用indirect
jump 跳至刚刚已读入的setup部份
第七步
setup.S完成在实模式下版本检查,并将硬盘,鼠标,内存参数写入到 INITSEG
中,并负责进入保护模式。
第八步
操作系统的初始化。
[目录]
--------------------------------------------------------------------------------
bootsect.S
1.将自己移动到0x9000:0x0000处,为内核调入留出地址空间;
2.建立运行环境(ss=ds=es=cs=0x9000, sp=0x4000-12),保证起动程序运行;
3.BIOS初始化0x1E号中断为软盘参数表,将它取来保存备用;
4.将setup读到0x9000:0x0200处;
5.测试软盘参数一个磁道有多少个扇区(也没有什么好办法,只能试试36, 18, 15, 9对不对了);
6.打印“Loading”;
7.读入内核到0x1000:0000(如果是bzImage, 则将每个64K移动到0x100000处,在实模式下,只能调用0x15号中断了,这段代码无法放在bootsect中所以只能放在setup中,幸好此时setup已经读入了);
8.到setup去吧
发发信人: seis (矛), 信区: Linux
标 题: Linux操作系统内核引导程序详细剖析
发信站: BBS 水木清华站 (Fri Feb 2 14:12:43 2001)
! bootsect.s (c) 1991, 1992 Linus Torvalds 版权所有
! Drew Eckhardt修改过
! Bruce Evans (bde)修改过
!
! bootsect.s 被bios-启动子程序加载至0x7c00 (31k)处,并将自己
! 移到了地址0x90000 (576k)处,并跳转至那里。
!
! bde - 不能盲目地跳转,有些系统可能只有512k的低
! 内存。使用中断0x12来获得(系统的)最高内存、等。
!
! 它然后使用BIOS中断将setup直接加载到自己的后面(0x90200)(576.5k),
! 并将系统加载到地址0x10000处。
!
! 注意! 目前的内核系统最大长度限制为(8*65536-4096)(508k)字节长,即使是在
! 将来这也是没有问题的。我想让它保持简单明了。这样508k的最大内核长度应该
! 是足够了,尤其是这里没有象minix中一样包含缓冲区高速缓冲(而且尤其是现在
! 内核是压缩的
!
! 加载程序已经做的尽量地简单了,所以持续的读出错将导致死循环。只能手工重启。
! 只要可能,通过一次取得整个磁道,加载过程可以做的很快的。
#include /* 为取得CONFIG_ROOT_RDONLY参数 */
!! config.h中(即autoconf.h中)没有CONFIG_ROOT_RDONLY定义!!!?
#include
.text
SETUPSECS = 4 ! 默认的setup程序扇区数(setup-sectors)的默认值;
BOOTSEG = 0x7C0 ! bootsect的原始地址;
INITSEG = DEF_INITSEG ! 将bootsect程序移到这个段处(0x9000) - 避开;
SETUPSEG = DEF_SETUPSEG ! 设置程序(setup)从这里开始(0x9020);
SYSSEG = DEF_SYSSEG ! 系统加载至0x1000(65536)(64k)段处;
SYSSIZE = DEF_SYSSIZE ! 系统的大小(0x7F00): 要加载的16字节为一节的数;
!! 以上4个DEF_参数定义在boot.h中:
!! DEF_INITSEG 0x9000
!! DEF_SYSSEG 0x1000
!! DEF_SETUPSEG 0x9020
!! DEF_SYSSIZE 0x7F00 (=32512=31.75k)*16=508k
! ROOT_DEV & SWAP_DEV 现在是由"build"中编制的;
ROOT_DEV = 0
SWAP_DEV = 0
#ifndef SVGA_MODE
#define SVGA_MODE ASK_VGA
#endif
#ifndef RAMDISK
#define RAMDISK 0
#endif
#ifndef CONFIG_ROOT_RDONLY
#define CONFIG_ROOT_RDONLY 1
#endif
! ld86 需要一个入口标识符,这和通常的一样;
.globl _main
_main:
#if 0 /* 调试程序的异常分支,除非BIOS古怪(比如老的HP机)否则是无害的 */
int 3
#endif
mov ax,#BOOTSEG !! 将ds段寄存器置为0x7C0;
mov ds,ax
mov ax,#INITSEG !! 将es段寄存器置为0x9000;
mov es,ax
mov cx,#256 !! 将cx计数器置为256(要移动256个字, 512字节);
sub si,si !! 源地址 ds:si=0x07C0:0x0000;
sub di,di !! 目的地址es:di=0x9000:0x0000;
cld !! 清方向标志;
rep !! 将这段程序从0x7C0:0(31k)移至0x9000:0(576k)处;
movsw !! 共256个字(512字节)(0x200长);
jmpi go,INITSEG !! 间接跳转至移动后的本程序go处;
! ax和es现在已经含有INITSEG的值(0x9000);
go: mov di,#0x4000-12 ! 0x4000(16k)是>;=bootsect + setup 的长度 +
! + 堆栈的长度 的任意的值;
! 12 是磁盘参数块的大小 es:di=0x94000-12=592k-12;
! bde - 将0xff00改成了0x4000以从0x6400处使用调试程序(bde)。如果
! 我们检测过最高内存的话就不用担心这事了,还有,我的BIOS可以被配置为将wini驱动
表
! 放在内存高端而不是放在向量表中。老式的堆栈区可能会搞乱驱动表;
mov ds,ax ! 置ds数据段为0x9000;
mov ss,ax ! 置堆栈段为0x9000;
mov sp,di ! 置堆栈指针INITSEG:0x4000-12处;
/*
* 许多BIOS的默认磁盘参数表将不能
* 进行扇区数大于在表中指定
* 的最大扇区数( - 在某些情况下
* 这意味着是7个扇区)后面的多扇区的读操作。
*
* 由于单个扇区的读操作是很慢的而且当然是没问题的,
* 我们必须在RAM中(为第一个磁盘)创建新的参数表。
* 我们将把最大扇区数设置为36 - 我们在一个ED 2.88驱动器上所能
* 遇到的最大值。
*
* 此值太高是没有任何害处的,但是低的话就会有问题了。
*
* 段寄存器是这样的: ds=es=ss=cs - INITSEG,(=0X9000)
* fs = 0, gs没有用到。
*/
! 上面执行重复操作(rep)以后,cx为0;
mov fs,cx !! 置fs段寄存器=0;
mov bx,#0x78 ! fs:bx是磁盘参数表的地址;
push ds
seg fs
lds si,(bx) ! ds:si是源地址;
!! 将fs:bx地址所指的指针值放入ds:si中;
mov cl,#6 ! 拷贝12个字节到0x9000:0x4000-12开始处;
cld
push di !! 指针0x9000:0x4000-12处;
rep
movsw
pop di !! di仍指向0x9000:0x4000-12处(参数表开始处);
pop si !! ds =>; si=INITSEG(=0X9000);
movb 4(di),*36 ! 修正扇区计数值;
seg fs
mov (bx),di !! 修改fs:bx(0000:0x007处磁盘参数表的地址为0x9000:0x4000-12;
seg fs
mov 2(bx),es
! 将setup程序所在的扇区(setup-sectors)直接加载到boot块的后面。!! 0x90200开始处
;
! 注意,es已经设置好了。
! 同样经过rep循环后cx为0
load_setup:
xor ah,ah ! 复位软驱(FDC);
xor dl,dl
int 0x13
xor dx,dx ! 驱动器0, 磁头0;
mov cl,#0x02 ! 从扇区2开始,磁道0;
mov bx,#0x0200 ! 置数据缓冲区地址=es:bx=0x9000:0x200;
! 在INITSEG段中,即0x90200处;
mov ah,#0x02 ! 要调用功能号2(读操作);
mov al,setup_sects ! 要读入的扇区数SETUPSECS=4;
! (假释所有数据都在磁头0、磁道0);
int 0x13 ! 读操作;
jnc ok_load_setup ! ok则继续;
push ax ! 否则显示出错信息。保存ah的值(功能号2);
call print_nl !! 打印换行;
mov bp,sp !! bp将作为调用print_hex的参数;
call print_hex !! 打印bp所指的数据;
pop ax
jmp load_setup !! 重试!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!!INT 13 - DISK - READ SECTOR(S) INTO MEMORY
!! AH = 02h
!! AL = number of sectors to read (must be nonzero)
!! CH = low eight bits of cylinder number
!! CL = sector number 1-63 (bits 0-5)
!! high two bits of cylinder (bits 6-7, hard disk only)
!! DH = head number
!! DL = drive number (bit 7 set for hard disk)
!! ES:BX ->; data buffer
!! Return: CF set on error
!! if AH = 11h (corrected ECC error), AL = burst length
!! CF clear if successful
!! AH = status (see #00234)
!! AL = number of sectors transferred (only valid if CF set for some
!! BIOSes)
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
ok_load_setup:
! 取得磁盘驱动器参数,特别是每磁道扇区数(nr of sectors/track);
#if 0
! bde - Phoenix BIOS手册中提到功能0x08只对硬盘起作用。
! 但它对于我的一个BIOS(1987 Award)不起作用。
! 不检查错误码是致命的错误。
xor dl,dl
mov ah,#0x08 ! AH=8用于取得驱动器参数;
int 0x13
xor ch,ch
!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! INT 13 - DISK - GET DRIVE PARAMETERS (PC,XT286,CONV,PS,ESDI,SCSI)
!! AH = 08h
!! DL = drive (bit 7 set for hard disk)
!!Return: CF set on error
!! AH = status (07h) (see #00234)
!! CF clear if successful
!! AH = 00h
!! AL = 00h on at least some BIOSes
!! BL = drive type (AT/PS2 floppies only) (see #00242)
!! CH = low eight bits of maximum cylinder number
!! CL = maximum sector number (bits 5-0)
!! high two bits of maximum cylinder number (bits 7-6)
!! DH = maximum head number
!! DL = number of drives
!! ESI ->; drive parameter table (floppies only)
!!!!!!!!!!!!!!!!!!!!!!!!!!!!
#else
! 好象没有BIOS调用可取得扇区数。如果扇区36可以读就推测是36个扇区,
! 如果扇区18可读就推测是18个扇区,如果扇区15可读就推测是15个扇区,
! 否则推测是9. [36, 18, 15, 9]
mov si,#disksizes ! ds:si->;要测试扇区数大小的表;
probe_loop:
lodsb !! ds:si所指的字节 =>;al, si=si+1;
cbw ! 扩展为字(word);
mov sectors, ax ! 第一个值是36,最后一个是9;
cmp si,#disksizes+4
jae got_sectors ! 如果所有测试都失败了,就试9;
xchg ax,cx ! cx = 磁道和扇区(第一次是36=0x0024);
xor dx,dx ! 驱动器0,磁头0;
xor bl,bl !! 设置缓冲区es:bx = 0x9000:0x0a00(578.5k);
mov bh,setup_sects !! setup_sects = 4 (共2k);
inc bh
shl bh,#1 ! setup后面的地址(es=cs);
mov ax,#0x0201 ! 功能2(读),1个扇区;
int 0x13
jc probe_loop ! 如果不对,就试用下一个值;
#endif
got_sectors:
! 恢复es
mov ax,#INITSEG
mov es,ax ! es = 0x9000;
! 打印一些无用的信息(换行后,显示Loading)
mov ah,#0x03 ! 读光标位置;
xor bh,bh
int 0x10
mov cx,#9
mov bx,#0x0007 ! 页0,属性7 (normal);
mov bp,#msg1
mov ax,#0x1301 ! 写字符串,移动光标;
int 0x10
! ok, 我们已经显示出了信息,现在
! 我们要加载系统了(到0x10000处)(64k处)
mov ax,#SYSSEG
mov es,ax ! es=0x01000的段;
call read_it !! 读system,es为输入参数;
call kill_motor !! 关闭驱动器马达;
call print_nl !! 打印回车换行;
! 这以后,我们来检查要使用哪个根设备(root-device)。如果已指定了设备(!=0)
! 则不做任何事而使用给定的设备。否则的话,使用/dev/fd0H2880 (2,32)或/dev/PS0
(2,2
! 或者是/dev/at0 (2,之一,这取决于我们假设我们知道的扇区数而定。
!! |__ ps0?? (x,y)--表示主、次设备号?
seg cs
mov ax,root_dev
or ax,ax
jne root_defined
seg cs
mov bx,sectors !! sectors = 每磁道扇区数;
mov ax,#0x0208 ! /dev/ps0 - 1.2Mb;
cmp bx,#15
je root_defined
mov al,#0x1c ! /dev/PS0 - 1.44Mb !! 0x1C = 28;
cmp bx,#18
je root_defined
mov al,0x20 ! /dev/fd0H2880 - 2.88Mb;
cmp bx,#36
je root_defined
mov al,#0 ! /dev/fd0 - autodetect;
root_defined:
seg cs
mov root_dev,ax !! 其中保存由设备的主、次设备号;
! 这以后(所有程序都加载了),我们就跳转至
! 被直接加载到boot块后面的setup程序去:
jmpi 0,SETUPSEG !! 跳转到0x9020:0000(setup程序的开始位置);
! 这段程序将系统(system)加载到0x10000(64k)处,
! 注意不要跨越64kb边界。我们试图以最快的速度
! 来加载,只要可能就整个磁道一起读入。
!
! 输入(in): es - 开始地址段(通常是0x1000)
!
sread: .word 0 ! 当前磁道已读的扇区数;
head: .word 0 ! 当前磁头;
track: .word 0 ! 当前磁道;
read_it:
mov al,setup_sects
inc al
mov sread,al !! 当前sread=5;
mov ax,es !! es=0x1000;
test ax,#0x0fff !! (ax AND 0x0fff, if ax=0x1000 then zero-flag=1 );
die: jne die ! es 必须在64kB的边界;
xor bx,bx ! bx 是段内的开始地址;
rp_read:
#ifdef __BIG_KERNEL__
#define CALL_HIGHLOAD_KLUDGE .word 0x1eff, 0x220 ! 调用 far * bootsect_kludge
! 注意: as86不能汇编这;
CALL_HIGHLOAD_KLUDGE ! 这是在setup.S中的程序;
#else
mov ax,es
sub ax,#SYSSEG ! 当前es段值减system加载时的启始段值(0x1000);
#endif
cmp ax,syssize ! 我们是否已经都加载了?(ax=0x7f00 ?);
jbe ok1_read !! if ax <= syssize then 继续读;
ret !! 全都加载完了,返回!
ok1_read:
mov ax,sectors !! sectors=每磁道扇区数;
sub ax,sread !! 减去当前磁道已读扇区数,al=当前磁道未读的扇区数(ah=0);
mov cx,ax
shl cx,#9 !! 乘512,cx = 当前磁道未读的字节数;
add cx,bx !! 加上段内偏移值,es:bx为当前读入的数据缓冲区地址;
jnc ok2_read !! 如果没有超过64K则继续读;
je ok2_read !! 如果正好64K也继续读;
xor ax,ax
sub ax,bx
shr ax,#9
ok2_read:
call read_track !! es:bx ->;缓冲区,al=要读的扇区数,也即当前磁道未读的扇区数;
mov cx,ax !! ax仍为调用read_track之前的值,即为读入的扇区数;
add ax,sread !! ax = 当前磁道已读的扇区数;
cmp ax,sectors !! 已经读完当前磁道上的扇区了吗?
jne ok3_read !! 没有,则跳转;
mov ax,#1
sub ax,head !! 当前是磁头1吗?
jne ok4_read !! 不是(是磁头0)则跳转(此时ax=1);
inc track !! 当前是磁头1,则读下一磁道(当前磁道加1);
ok4_read:
mov head,ax !! 保存当前磁头号;
xor ax,ax !! 本磁道已读扇区数清零;
ok3_read:
mov sread,ax !! 存本磁道已读扇区数;
shl cx,#9 !! 刚才一次读操作读入的扇区数 * 512;
add bx,cx !! 调整数据缓冲区的起始指针;
jnc rp_read !! 如果该指针没有超过64K的段内最大偏移量,则跳转继续读操作;
mov ax,es !! 如果超过了,则将段地址加0x1000(下一个64K段);
add ah,#0x10
mov es,ax
xor bx,bx !! 缓冲区地址段内偏移量置零;
jmp rp_read !! 继续读操作;
read_track:
pusha !! 将寄存器ax,cx,dx,bx,sp,bp,si,di压入堆栈;
pusha
mov ax,#0xe2e ! loading... message 2e = . !! 显示一个.
mov bx,#7
int 0x10
popa
mov dx,track !! track = 当前磁道;
mov cx,sread
inc cx !! cl = 扇区号,要读的起始扇区;
mov ch,dl !! ch = 磁道号的低8位;
mov dx,head !!
mov dh,dl !! dh = 当前磁头号;
and dx,#0x0100 !! dl = 驱动器号(0);
mov ah,#2 !! 功能2(读),es:bx指向读数据缓冲区;
push dx ! 为出错转储保存寄存器的值到堆栈上;
push cx
push bx
push ax
int 0x13
jc bad_rt !! 如果出错,则跳转;
add sp, #8 !! 清(放弃)堆栈上刚推入的4个寄存器值;
popa
ret
bad_rt: push ax ! 保存出错码;
call print_all ! ah = error, al = read;
xor ah,ah
xor dl,dl
int 0x13
add sp,#10
popa
jmp read_track
/*
* print_all是用于调试的。
* 它将打印出所有寄存器的值。所作的假设是
* 从一个子程序中调用的,并有如下所示的堆栈帧结构
* dx
* cx
* bx
* ax
* error
* ret <- sp
*
*/
print_all:
mov cx,#5 ! 出错码 + 4个寄存器
mov bp,sp
print_loop:
push cx ! 保存剩余的计数值
call print_nl ! 为了增强阅读性,打印换行
cmp cl, #5
jae no_reg ! 看看是否需要寄存器的名称
mov ax,#0xe05 + A - l
sub al,cl
int 0x10
mov al,#X
int 0x10
mov al,#:
int 0x10
no_reg:
add bp,#2 ! 下一个寄存器
call print_hex ! 打印值
pop cx
loop print_loop
ret
print_nl: !! 打印回车换行。
mov ax,#0xe0d ! CR
int 0x10
mov al,#0xa ! LF
int 0x10
ret
/*
* print_hex是用于调试目的的,打印出
* ss:bp所指向的十六进制数。
* !! 例如,十六进制数是0x4321时,则al分别等于4,3,2,1调用中断打印出来 4321
*/
print_hex:
mov cx, #4 ! 4个十六进制数字
mov dx, (bp) ! 将(bp)所指的值放入dx中
print_digit:
rol dx, #4 ! 循环以使低4比特用上 !! 取dx的高4比特移到低4比特处。
mov ax, #0xe0f ! ah = 请求的功能值,al = 半字节(4个比特)掩码。
and al, dl !! 取dl的低4比特值。
add al, #0x90 ! 将al转换为ASCII十六进制码(4个指令)
daa !! 十进制调整
adc al, #0x40 !! (adc dest, src ==>; dest := dest + src + c )
daa
int 0x10
loop print_digit
ret
/*
* 这个过程(子程序)关闭软驱的马达,这样
* 我们进入内核后它的状态就是已知的,以后也就
* 不用担心它了。
*/
kill_motor:
push dx
mov dx,#0x3f2
xor al,al
outb
pop dx
ret
!! 数据区
sectors:
.word 0 !! 当前每磁道扇区数。(36||18||15||9)
disksizes: !! 每磁道扇区数表
.byte 36, 18, 15, 9
msg1:
.byte 13, 10
.ascii "Loading"
.org 497 !! 从boot程序的二进制文件的497字节开始
setup_sects:
.byte SETUPSECS
root_flags:
.word CONFIG_ROOT_RDONLY
syssize:
.word SYSSIZE
swap_dev:
.word SWAP_DEV
ram_size:
.word RAMDISK
vid_mode:
.word SVGA_MODE
root_dev:
.word ROOT_DEV
boot_flag: !! 分区启动标志
.word 0xAA55
[目录]
--------------------------------------------------------------------------------
setup.S
1、按规定得有个头,所以一开始是惯用的JMP;
2、头里边内容很丰富,具体用法走着瞧;
3、自我检测,不知道有什么用,防伪造?防篡改?
4、如果装载程序不对,只好死掉!以下终于走入正题;
5、获取内存容量(使用了三种办法,其中的E820和E801看不明白,int 15倒是老朋友了--应该是上个世纪80年代末认识的了,真佩服十年过去了,情意依旧,不过遇上一些不守规矩的BIOS,不知道还行不行);
6、将键盘重复键的重复率设为最大,灵敏一点?
7、检测硬盘,不懂,放这里干什么?
8、检测MCA总线(不要问我这是什么);
9、检测PS/2鼠标,用int 11,只是不知道为何放这里;
10、检测电源管理BIOS;唉,书到用时方恨少,不懂的太多了,真不好意思;不过也没有关系, 不懂的就别去动它就行了;以下要进入内核了;
11、 在进入保护模式之前,可以调用一个你提供的试模式下的过程,让你最后在看她一眼,当然你要是不提供,那就有个默认的,无非是塞住耳朵闭上眼睛禁止任何中断,包括著名的NMI ;
12、设置保护模式起动后的例程地址, 你可以写自己的例程,但不是代替而是把它加在setup提供的例程的前面(显示一个小鸭子?);
13、如果内核是zImage, 将它移动到0x10000处;
14、如果自己不在0x90000处,则移动到0x90000处;
15、建立idt, gdt表;
16、启动A20;
17、屏住呼吸,屏闭所有中断;
18、启动!movw $1, %ax ; lmsw %ax; 好已经进入保护模式下,马上进行局部调整;
19、jmpi 0x100000, __KERNEL_CS,终于进入内核;
setup.S
A summary of the setup.S code 。The slight differences in the operation of setup.S due to a big kernel is documented here. When the switch to 32 bit protected mode begins the code32_start address is defined as 0x100000 (when loaded) here.
code32_start:
#ifndef __BIG_KERNEL__
.long 0x1000
#else
.long 0x100000
#endif
After setting the keyboard repeat rate to a maximum, calling video.S, storing the video parameters, checking for the hard disks, PS/2 mouse, and APM BIOS the preparation for real mode switch begins.
The interrupts are disabled. Since the loader changed the code32_start address, the code32 varable is updated. This would be used for the jmpi instruction when the setup.S finally jumps to compressed/head.S. In case of a big kernel this is loacted at 0x100000.
seg cs
mov eax, code32_start !modified above by the loader
seg cs
mov code32,eax
!code32 contains the correct address to branch to after setup.S finishes After the above code there is a slight difference in the ways the big and small kernels are dealt. In case of a small kernel the kernel is moved down to segment address 0x100, but a big kernel is not moved. Before decompression, the big kernel stays at 0x100000. The following is the code that does thischeck.test byte ptr loadflags,
#LOADED_HIGH
jz do_move0 ! a normal low loaded zImage is moved
jmp end_move ! skip move
The interrupt and global descriptors are initialized:
lidt idt_48 ! load idt wit 0,0
lgdt gdt_48 ! load gdt with whatever appropriate
After enabling A20 and reprogramming the interrupts, it is ready to set the PE bit:
mov ax,#1
lmsw ax
jmp flush_instr
flush_instr:
xor bx.bx !flag to indicate a boot
! Manual, mixing of 16-bit and 32 bit code
db 0x166,0xea !prefix jmpi-opcode
code32: dd ox1000 !this has been reset in caes of a big kernel, to 0x100000
dw __KERNEL_CS
Finally it prepares the opcode for jumping to compressed/head.S which in the big kernel is at 0x100000. The compressed kernel would start at 0x1000 in case of a small kernel.
compressed/head.S
When setup.S relinquishes control to compressed/head.S at beginning of the compressed kernmel at 0x100000. It checks to see if A20 is really enabled otherwise it loops forever.
Itinitializes eflags, and clears BSS (Block Start by Symbol) creating reserved space for uninitialized static or global variables. Finally it reserves place for the moveparams structure (defined in misc.c) and pushes the current stack pointer on the stack and calls the C function decompress_kernel which takes a struct moveparams * as an argument
subl $16,%esp
pushl %esp
call SYMBOL_NAME(decompress_kernel)
orl ??,??
jnz 3f
Te C function decompress_kernel returns the variable high_loaded which is set to 1 in the function setup_output_buffer_if_we_run_high, which is called in decompressed_kernel if a big kernel was loaded.
When decompressed_kernel returns, it jumps to 3f which moves the move routine.
movl $move_routine_start,%esi ! puts the offset of the start of the source in the source index register
mov $0x1000,?? ! the destination index now contains 0x1000, thus after move, the move routine starts at 0x1000
movl $move_routine_end,??
sub %esi,?? ! ecx register now contains the number of bytes to be moved
! (number of bytes between the labels move_routine_start and move_routine_end)
cld
rep
movsb ! moves the bytes from ds:si to es:di, in each loop it increments si and di, and decrements cx
! the movs instruction moves till ecx is zero
Thus the movsb instruction moves the bytes of the move routine between the labels move_routine_start and move_routine_end. At the end the entire move routine labeled move_routine_start is at 0x1000. The movsb instruction moves bytes from ds:si to es:si.
At the start of the head.S code es,ds,fs,gs were all intialized to __KERNEL_DS, which is defined in /usr/src/linux/include/asm/segment.h as 0x18. This is the offset from the goobal descriptor table gdtwhich was setup in setup.S. The 24th byte is the start of the data segment descriptor, which has the base address = 0. Thus the moe routine is moved and
starts at offset 0x1000 from __KERNEL_DS, the kernel data segment base (which is 0).
The salient features of what is done by the decompress_kernel is discussed in the next section but it is worth noting that the when the decompressed_kernel function is invoked, space was created at the top of the stack to contain the information about the decompressed kernel. The decompressed kernel if big may be in the high buffer and in the low buffer. After the decompressed_kernel function returns, the decompressed kernel has to be moved so that we
have a contiguous decompressed kernel starting from address 0x100000. To move the decompressed kernel, the important parameters needed are the start addresses of the high buffer and low buffer, and the number of bytes in the high and low buffers. This is at the top of the stack when decompressed_kernel returns (the top of the stack was passed as an argument : struct moveparams*, and in the function the fileds of the moveparams struture was adjusted toreflect the state of the decompression.)
/* in compressed/misc.c */
struct moveparams {
uch *low_buffer_start; ! start address of the low buffer
int count; ! number of bytes in the low buffer after decompression is doneuch *high_buffer_start; ! start address of the high buffer
int hcount; ! number of bytes in the high buffer aftre decompression is done
};
Thus when the decompressed_kernel returns, the relevant bytes are popped in the respective registers as shown below. After preparing these registers the decompressed kernel is ready to be moved and the control jumps to the moved move routine at __KERNEL_CS:0x1000. The code for setting the appropriate registers is given below:
popl %esi ! discard the address, has the return value (high_load) most probably
popl %esi ! low_buffer_start
popl ?? ! lcount
popl ?? ! high_buffer_count
popl ?? ! hcount
movl %0x100000,??
cli ! disable interrutps when the decompressed kernel is being moved
ljmp $(__KERNEL_CS), $0x1000 ! jump to the move routine which was moved to low memory, 0x1000
The move_routine_start basically has two parts, first it moves the part of the decompressed kernel in the low buffer, then it moves (if required) the high buffer contents. It should be noted that the ecx has been intialized to the number of bytes in the low end buffer, and the destination index register di has been intialized to 0x100000.
move_routine_start:
rep ! repeat, it stops repeating when ecx == 0
movsb ! the movsb instruction repeats till ecx is 0. In each loop byte is transferred from ds:esi to es:edi! In each loop the edi and the esi are incremented and ecx is decremented
! when the low end buffer has been moved the value of di is not changed and the next pasrt of the code! uses it to transfer the bytes from the high buffer
movl ??,%esi ! esi now has the offset corresponding to the start of the high buffer
movl ??,?? ! ecx is now intialized to the number of bytes in the high buffer
rep
movsb ! moves all the bytes in the high buffer, and doesn’t move at all if hcount was zero (if it was determined, in! close_output_buffer_if_we_run_high that the high buffer need not be moveddown )
xorl ??,??
mov $0x90000, %esp ! stack pointer is adjusted, most probably to be used by the kernel in the intialization
ljmp $(__KERNEL_CS), $0x100000 ! jump to __KERNEL_CS:0X100000, where the kernel code starts
move_routine_end:At the end of the this the control goes to the kernel code segment.
Linux Assembly code taken from head.S and setup.S
Comment code added by us
[目录]
--------------------------------------------------------------------------------
head.S
因为setup.S最后的为一条转跳指令,跳到内核第一条指令并开始执行。指令中指向的是内存中的绝对地址,我们无法依此判断转跳到了head.S。但是我们可以通过Makefile简单的确定head.S位于内核的前端。
在arch/i386 的 Makefile 中定义了
HEAD := arch/i386/kernel/head.o
而在linux总的Makefile中由这样的语句
include arch/$(ARCH)/Makefile
说明HEAD定义在该文件中有效
然后由如下语句:
vmlinux: $(CONFIGURATION) init/main.o init/version.o linuxsubdirs
$(LD) $(LINKFLAGS) $(HEAD) init/main.o init/version.o \
$(ARCHIVES) \
$(FILESYSTEMS) \
$(DRIVERS) \
$(LIBS) -o vmlinux
$(NM) vmlinux | grep -v ' compiled \| \.o$$ \| a ' | sort >; System.map
从这个依赖关系我们可以获得大量的信息
1>;$(HEAD)即head.o的确第一个被连接到核心中
2>;所有内核中支持的文件系统全部编译到$(FILESYSTEMS)即fs/filesystems.a中
所有内核中支持的网络协议全部编译到net.a中
所有内核中支持的SCSI驱动全部编译到scsi.a中
...................
原来内核也不过是一堆库文件和目标文件的集合罢了,有兴趣对内核减肥的同学,
可以好好比较一下看究竟是那个部分占用了空间。
3>;System.map中包含了所有的内核输出的函数,我们在编写内核模块的时候
可以调用的系统函数大概就这些了。
好了,消除了心中的疑问,我们可以仔细分析head.s了。
Head.S分析
1 首先将ds,es,fs,gs指向系统数据段KERNEL_DS
KERNEL_DS 在asm/segment.h中定义,表示全局描述符表中
中的第三项。
注意:该此时生效的全局描述符表并不是在head.s中定义的
而仍然是在setup.S中定义的。
2 数据段全部清空。
3 setup_idt为一段子程序,将中断向量表全部指向ignore_int函数
该函数打印出:unknown interrupt
当然这样的中断处理函数什么也干不了。
4 察看数据线A20是否有效,否则循环等待。
地址线A20是x86的历史遗留问题,决定是否能访问1M以上内存。
5 拷贝启动参数到0x5000页的前半页,而将setup.s取出的bios参数
放到后半页。
6 检查CPU类型
@#$#%$^*@^?(^%#$%!#!@?谁知道干了什么?
7 初始化页表,只初始化最初几页。
1>;将swapper_pg_dir(0x2000)和pg0(0x3000)清空
swapper_pg_dir作为整个系统的页目录
2>;将pg0作为第一个页表,将其地址赋到swapper_pg_dir的第一个32
位字中。
3>;同时将该页表项也赋给swapper_pg_dir的第3072个入口,表示虚拟地址
0xc0000000也指向pg0。
4>;将pg0这个页表填满指向内存前4M
5>;进入分页方式
注意:以前虽然在在保护模式但没有启用分页。
--------------------
| swapper_pg_dir | -----------
| |-------| pg0 |----------内存前4M
| | -----------
| |
--------------------
8 装入新的gdt和ldt表。
9 刷新段寄存器ds,es,fs,gs
10 使用系统堆栈,即预留的0x6000页面
11 执行start_kernel函数,这个函数是第一个C编制的
函数,内核又有了一个新的开始。
[目录]
--------------------------------------------------------------------------------
compressed/misc.c
compressed/misc.c
The differences in decompressing big and small kernels.
http://www.vuse.vanderbilt.edu/~knopfdg/documentation/hw3_part3.htm
The function decompressed_kernel is invoked from head.S and a parameter to the top of the stack is passed to store the results of the decompression namely, the start addresses of the high and the low buffers which contain the decompressed kernel and the numebr of bytes in each buffer (hcount and lcount).
int decompress_kernel(struct moveparams *mv)
{
if (SCREEN_INFO.orig_video_mode == 7) {
vidmem = (char *) 0xb0000;
vidport = 0x3b4;
} else {
vidmem = (char *) 0xb8000;
vidport = 0x3d4;
}
lines = SCREEN_INFO.orig_video_lines;
cols = SCREEN_INFO.orig_video_cols;
if (free_mem_ptr < 0x100000) setup_normal_output_buffer(); // Call if smallkernel
else setup_output_buffer_if_we_run_high(mv); // Call if big kernel
makecrc();
puts("Uncompressing Linux... ";
gunzip();
puts("Ok, booting the kernel.\n";
if (high_loaded) close_output_buffer_if_we_run_high(mv);
return high_loaded;
}
The first place where a distinction is made is when the buffers are to be setup for the decmpression routine gunzip(). Free_mem_ptr, is loaded with the value of the address of the extern variabe end. The variable end marks the end of the compressed kernel. If the free_mem-ptr is less than the 0x100000,then a high buffer has to be setup. Thus the function setup_output_buffer_if_we_run_high is called and the pointer to the top of the moveparams structure is passed so that when the buffers are setup, the start addresses fields are updated in moveparams structure. It is also checked to see if the high buffer needs to be moved down after decompression and this is reflected by the hcount which is 0 if we need not move the high buffer down.
void setup_output_buffer_if_we_run_high(struct moveparams *mv)
{
high_buffer_start = (uch *)(((ulg)&end) HEAP_SIZE);
//the high buffer start address is at the end HEAP_SIZE
#ifdef STANDARD_MEMORY_BIOS_CALL
if (EXT_MEM_K < (3*1024)) error("Less than 4MB of memory.\n";
#else
if ((ALT_MEM_K >; EXT_MEM_K ? ALT_MEM_K : EXT_MEM_K) < (3*1024)) error("Less
than 4MB of memory.\n";
#endif
mv->;low_buffer_start = output_data = (char *)LOW_BUFFER_START;
//the low buffer start address is at 0x2000 and it extends till 0x90000.
high_loaded = 1; //high_loaded is set to 1, this is returned by decompressed_kernel
free_mem_end_ptr = (long)high_buffer_start;
// free_mem_end_ptr points to the same address as te high_buffer_start
// the code below finds out if the high buffer needs to be moved after decompression
// if the size if the low buffer is >; the size of the compressed kernel and the HEAP_SIZE
// then the high_buffer_start has to be shifted up so that when the decompression starts it doesn’t
// overwrite the compressed kernel data. Thus when the high_buffer_start islow then it is shifted
// up to exactly match the end of the compressed kernel and the HEAP_SIZE. The hcount filed is
// is set to 0 as the high buffer need not be moved down. Otherwise if the high_buffer_start is too
// high then the hcount is non zero and while closing the buffers the appropriate number of bytes
// in the high buffer is asigned to the filed hcount. Since the start address of the high buffer is
// known the bytes could be moved down
if ( (0x100000 LOW_BUFFER_SIZE) >; ((ulg)high_buffer_start)) {
high_buffer_start = (uch *)(0x100000 LOW_BUFFER_SIZE);
mv->;hcount = 0; /* say: we need not to move high_buffer */
}
else mv->;hcount = -1;
mv->;high_buffer_start = high_buffer_start;
// finally the high_buffer_start field is set to the varaible high_buffer_start
}
After the buffers are set gunzip() is invoked which decompresses the kernel Upon return, bytes_out has the number of bytes in the decompressed kernel.Finally close_output_buffer_if_we_run_high is invoked if high_loaded is non zero:
void close_output_buffer_if_we_run_high(struct moveparams *mv)
{
mv->;lcount = bytes_out;
// if the all of decompressed kernel is in low buffer, lcount = bytes_out
if (bytes_out >; LOW_BUFFER_SIZE) {
// if there is a part of the decompressed kernel in the high buffer, the lcount filed is set to
// the size of the low buffer and the hcount field contains the rest of the bytes
mv->;lcount = LOW_BUFFER_SIZE;
if (mv->;hcount) mv->;hcount = bytes_out - LOW_BUFFER_SIZE;
// if the hcount field is non zero (made in setup_output_buffer_if_we_run_high)
// then the high buffer has to be moved doen and the number of bytes in the high buffer is
// in hcount
}
else mv->;hcount = 0; // all the data is in the high buffer
}
Thus at the end of the the decompressed_kernel function the top of the stack has the addresses of the buffers and their sizes which is popped and the appropriate registers set for the move routine to move the entire kernel. After the move by the move_routine the kernel resides at 0x100000. If a small kernel is being decompressed then the setup_normal_output_buffer() is invoked from decompressed_kernel, which just initializes output_data to 0x100000 where the decompressed kernel would lie. The variable high_load is still 0 as setup_output_buffer_if_we_run_high() is not invoked. Decompression is done starting at address 0x100000. As high_load is 0, when decompressed_kernel returns in head.S, a zero is there in the eax. Thus the control jumps directly to 0x100000. Since the decompressed kernel lies there directly and the move routine need not be called.
Linux code taken from misc.c
Comment code added by us
[目录]
--------------------------------------------------------------------------------
内核解压
概述
----
1) Linux的初始内核映象以gzip压缩文件的格式存放在zImage或bzImage之中, 内核的自举代码将它解压到1M内存开始处. 在内核初始化时, 如果加载了压缩的initrd映象, 内核会将它解压到内存盘中, 这两处解压过程都使用了lib/inflate.c文件.
2) inflate.c是从gzip源程序中分离出来的, 包含了一些对全局数据的直接引用, 在使用时需要直接嵌入到代码中. gzip压缩文件时总是在前32K字节的范围内寻找重复的字符串进行编码, 在解压时需要一个至少为32K字节的解压缓冲区, 它定义为window[WSIZE].inflate.c使用get_byte()读取输入文件, 它被定义成宏来提高效率. 输入缓冲区指针必须定义为inptr, inflate.c中对之有减量操作. inflate.c调用flush_window()来输出window缓冲区中的解压出的字节串, 每次输出长度用outcnt变量表示. 在flush_window()中, 还必须对输出字节串计算CRC并且刷新crc变量. 在调用gunzip()开始解压之前, 调用makecrc()初始化CRC计算表. 最后gunzip()返回0表示解压成功.
3) zImage或bzImage由16位引导代码和32位内核自解压映象两个部分组成. 对于zImage, 内核自解压映象被加载到物理地址0x1000, 内核被解压到1M的部位. 对于bzImage, 内核自解压映象被加载到1M开始的地方, 内核被解压为两个片段, 一个起始于物理地址0x2000-0x90000,另一个起始于高端解压映象之后, 离1M开始处不小于低端片段最大长度的区域. 解压完成后,这两个片段被合并到1M的起始位置.
解压根内存盘映象文件的代码
--------------------------
; drivers/block/rd.c
#ifdef BUILD_CRAMDISK
/*
* gzip declarations
*/
#define OF(args) args ; 用于函数原型声明的宏
#ifndef memzero
#define memzero(s, n) memset ((s), 0, (n))
#endif
typedef unsigned char uch; 定义inflate.c所使用的3种数据类型
typedef unsigned short ush;
typedef unsigned long ulg;
#define INBUFSIZ 4096 用户输入缓冲区尺寸
#define WSIZE 0x8000 /* window size--must be a power of two, and */
/* at least 32K for zip's deflate method */
static uch *inbuf; 用户输入缓冲区,与inflate.c无关
static uch *window; 解压窗口
static unsigned insize; /* valid bytes in inbuf */
static unsigned inptr; /* index of next byte to be processed in inbuf */
static unsigned outcnt; /* bytes in output buffer */
static int exit_code;
static long bytes_out; 总解压输出长度,与inflate.c无关
static struct file *crd_infp, *crd_outfp;
#define get_byte() (inptr
/* Diagnostic functions (stubbed out) */ 一些调试宏
#define Assert(cond,msg)
#define Trace(x)
#define Tracev(x)
#define Tracevv(x)
#define Tracec(c,x)
#define Tracecv(c,x)
#define STATIC static
static int fill_inbuf(void);
static void flush_window(void);
static void *malloc(int size);
static void free(void *where);
static void error(char *m);
static void gzip_mark(void **);
static void gzip_release(void **);
#include "../../lib/inflate.c"
static void __init *malloc(int size)
{
return kmalloc(size, GFP_KERNEL);
}
static void __init free(void *where)
{
kfree(where);
}
static void __init gzip_mark(void **ptr)
{
; 读取用户一个标记
}
static void __init gzip_release(void **ptr)
{
; 归还用户标记
}
/* ===========================================================================
* Fill the input buffer. This is called only when the buffer is empty
* and at least one byte is really needed.
*/
static int __init fill_inbuf(void) 填充输入缓冲区
{
if (exit_code) return -1;
insize = crd_infp->;f_op->;read(crd_infp, inbuf, INBUFSIZ,
if (insize == 0) return -1;
inptr = 1;
return inbuf[0];
}
/* ===========================================================================
* Write the output window window[0..outcnt-1] and update crc and bytes_out.
* (Used for the decompressed data only.)
*/
static void __init flush_window(void) 输出window缓冲区中outcnt个字节串
{
ulg c = crc; /* temporary variable */
unsigned n;
uch *in, ch;
crd_outfp->;f_op->;write(crd_outfp, window, outcnt,
in = window;
for (n = 0; n ch = *in++;
c = crc_32_tab[((int)c ^ ch) 0xff] ^ (c >;>; ; 计算输出串的CRC
}
crc = c;
bytes_out += (ulg)outcnt; 刷新总字节数
outcnt = 0;
}
static void __init error(char *x) 解压出错调用的函数
{
printk(KERN_ERR "%s", x);
exit_code = 1;
}
static int __init
crd_load(struct file * fp, struct file *outfp)
{
int result;
insize = 0; /* valid bytes in inbuf */
inptr = 0; /* index of next byte to be processed in inbuf */
outcnt = 0; /* bytes in output buffer */
exit_code = 0;
bytes_out = 0;
crc = (ulg)0xffffffffL; /* shift register contents */
crd_infp = fp;
crd_outfp = outfp;
inbuf = kmalloc(INBUFSIZ, GFP_KERNEL);
if (inbuf == 0) {
printk(KERN_ERR "RAMDISK: Couldn't allocate gzip buffer\n";
return -1;
}
window = kmalloc(WSIZE, GFP_KERNEL);
if (window == 0) {
printk(KERN_ERR "RAMDISK: Couldn't allocate gzip window\n";
kfree(inbuf);
return -1;
}
makecrc();
result = gunzip();
kfree(inbuf);
kfree(window);
return result;
}
#endif /* BUILD_CRAMDISK */
32位内核自解压代码
------------------
; arch/i386/boot/compressed/head.S
.text
#include ·
#include
.globl startup_32 对于zImage该入口地址为0x1000; 对于bzImage为0x101000
startup_32:
cld
cli
movl $(__KERNEL_DS),%eax
movl %eax,%ds
movl %eax,%es
movl %eax,%fs
movl %eax,%gs
lss SYMBOL_NAME(stack_start),%esp # 自解压代码的堆栈为misc.c中定义的16K字节的数组
xorl %eax,%eax
1: incl %eax # check that A20 really IS enabled
movl %eax,0x000000 # loop forever if it isn't
cmpl %eax,0x100000
je 1b
/*
* Initialize eflags. Some BIOS's leave bits like NT set. This would
* confuse the debugger if this code is traced.
* XXX - best to initialize before switching to protected mode.
*/
pushl $0
popfl
/*
* Clear BSS 清除解压程序的BSS段
*/
xorl %eax,%eax
movl $ SYMBOL_NAME(_edata),%edi
movl $ SYMBOL_NAME(_end),%ecx
subl %edi,%ecx
cld
rep
stosb
/*
* Do the decompression, and jump to the new kernel..
*/
subl $16,%esp # place for structure on the stack
movl %esp,%eax
pushl %esi # real mode pointer as second arg
pushl %eax # address of structure as first arg
call SYMBOL_NAME(decompress_kernel)
orl %eax,%eax # 如果返回非零,则表示为内核解压为低端和高端的两个片断
jnz 3f
popl %esi # discard address
popl %esi # real mode pointer
xorl %ebx,%ebx
ljmp $(__KERNEL_CS), $0x100000 # 运行start_kernel
/*
* We come here, if we were loaded high.
* We need to move the move-in-place routine down to 0x1000
* and then start it with the buffer addresses in registers,
* which we got from the stack.
*/
3:
movl $move_routine_start,%esi
movl $0x1000,%edi
movl $move_routine_end,%ecx
subl %esi,%ecx
addl $3,%ecx
shrl $2,%ecx # 按字取整
cld
rep
movsl # 将内核片断合并代码复制到0x1000区域, 内核的片段起始为0x2000
popl %esi # discard the address
popl %ebx # real mode pointer
popl %esi # low_buffer_start 内核低端片段的起始地址
popl %ecx # lcount 内核低端片段的字节数量
popl %edx # high_buffer_start 内核高端片段的起始地址
popl %eax # hcount 内核高端片段的字节数量
movl $0x100000,%edi 内核合并的起始地址
cli # make sure we don't get interrupted
ljmp $(__KERNEL_CS), $0x1000 # and jump to the move routine
/*
* Routine (template) for moving the decompressed kernel in place,
* if we were high loaded. This _must_ PIC-code !
*/
move_routine_start:
movl %ecx,%ebp
shrl $2,%ecx
rep
movsl # 按字拷贝第1个片段
movl %ebp,%ecx
andl $3,%ecx
rep
movsb # 传送不完全字
movl %edx,%esi
movl %eax,%ecx # NOTE: rep movsb won't move if %ecx == 0
addl $3,%ecx
shrl $2,%ecx # 按字对齐
rep
movsl # 按字拷贝第2个片段
movl %ebx,%esi # Restore setup pointer
xorl %ebx,%ebx
ljmp $(__KERNEL_CS), $0x100000 # 运行start_kernel
move_routine_end:
; arch/i386/boot/compressed/misc.c
/*
* gzip declarations
*/
#define OF(args) args
#define STATIC static
#undef memset
#undef memcpy
#define memzero(s, n) memset ((s), 0, (n))
ypedef unsigned char uch;
typedef unsigned short ush;
typedef unsigned long ulg;
#define WSIZE 0x8000 /* Window size must be at least 32k, */
/* and a power of two */
static uch *inbuf; /* input buffer */
static uch window[WSIZE]; /* Sliding window buffer */
static unsigned insize = 0; /* valid bytes in inbuf */
static unsigned inptr = 0; /* index of next byte to be processed in inbuf */
static unsigned outcnt = 0; /* bytes in output buffer */
/* gzip flag byte */
#define ASCII_FLAG 0x01 /* bit 0 set: file probably ASCII text */
#define CONTINUATION 0x02 /* bit 1 set: continuation of multi-part gzip file */
#define EXTRA_FIELD 0x04 /* bit 2 set: extra field present */
#define ORIG_NAME 0x08 /* bit 3 set: original file name present */
#define COMMENT 0x10 /* bit 4 set: file comment present */
#define ENCRYPTED 0x20 /* bit 5 set: file is encrypted */
#define RESERVED 0xC0 /* bit 6,7: reserved */
#define get_byte() (inptr
/* Diagnostic functions */
#ifdef DEBUG
# define Assert(cond,msg) {if(!(cond)) error(msg);}
# define Trace(x) fprintf x
# define Tracev(x) {if (verbose) fprintf x ;}
# define Tracevv(x) {if (verbose>;1) fprintf x ;}
# define Tracec(c,x) {if (verbose (c)) fprintf x ;}
# define Tracecv(c,x) {if (verbose>;1 (c)) fprintf x ;}
#else
# define Assert(cond,msg)
# define Trace(x)
# define Tracev(x)
# define Tracevv(x)
# define Tracec(c,x)
# define Tracecv(c,x)
#endif
static int fill_inbuf(void);
static void flush_window(void);
static void error(char *m);
static void gzip_mark(void **);
static void gzip_release(void **);
/*
* This is set up by the setup-routine at boot-time
*/
static unsigned char *real_mode; /* Pointer to real-mode data */
#define EXT_MEM_K (*(unsigned short *)(real_mode + 0x2))
#ifndef STANDARD_MEMORY_BIOS_CALL
#define ALT_MEM_K (*(unsigned long *)(real_mode + 0x1e0))
#endif
#define SCREEN_INFO (*(struct screen_info *)(real_mode+0))
extern char input_data[];
extern int input_len;
static long bytes_out = 0;
static uch *output_data;
static unsigned long output_ptr = 0;
static void *malloc(int size);
static void free(void *where);
static void error(char *m);
static void gzip_mark(void **);
static void gzip_release(void **);
static void puts(const char *);
extern int end;
static long free_mem_ptr = (long)
static long free_mem_end_ptr;
#define INPLACE_MOVE_ROUTINE 0x1000 内核片段合并代码的运行地址
#define LOW_BUFFER_START 0x2000 内核低端解压片段的起始地址
#define LOW_BUFFER_MAX 0x90000 内核低端解压片段的终止地址
#define HEAP_SIZE 0x3000 为解压低码保留的堆的尺寸,堆起始于BSS的结束
static unsigned int low_buffer_end, low_buffer_size;
static int high_loaded =0;
static uch *high_buffer_start /* = (uch *)(((ulg) + HEAP_SIZE)*/;
static char *vidmem = (char *)0xb8000;
static int vidport;
static int lines, cols;
#include "../../../../lib/inflate.c"
static void *malloc(int size)
{
void *p;
if (size if (free_mem_ptr
free_mem_ptr = (free_mem_ptr + 3) ~3; /* Align */
p = (void *)free_mem_ptr;
free_mem_ptr += size;
if (free_mem_ptr >;= free_mem_end_ptr)
error("\nOut of memory\n";
return p;
}
static void free(void *where)
{ /* Don't care */
}
static void gzip_mark(void **ptr)
{
*ptr = (void *) free_mem_ptr;
}
static void gzip_release(void **ptr)
{
free_mem_ptr = (long) *ptr;
}
static void scroll(void)
{
int i;
memcpy ( vidmem, vidmem + cols * 2, ( lines - 1 ) * cols * 2 );
for ( i = ( lines - 1 ) * cols * 2; i vidmem[ i ] = ' ';
}
static void puts(const char *s)
{
int x,y,pos;
char c;
x = SCREEN_INFO.orig_x;
y = SCREEN_INFO.orig_y;
while ( ( c = *s++ ) != '\0' ) {
if ( c == '\n' ) {
x = 0;
if ( ++y >;= lines ) {
scroll();
y--;
}
} else {
vidmem [ ( x + cols * y ) * 2 ] = c;
if ( ++x >;= cols ) {
x = 0;
if ( ++y >;= lines ) {
scroll();
y--;
}
}
}
}
SCREEN_INFO.orig_x = x;
SCREEN_INFO.orig_y = y;
pos = (x + cols * y) * 2; /* Update cursor position */
outb_p(14, vidport);
outb_p(0xff (pos >;>; 9), vidport+1);
outb_p(15, vidport);
outb_p(0xff (pos >;>; 1), vidport+1);
}
void* memset(void* s, int c, size_t n)
{
int i;
char *ss = (char*)s;
for (i=0;i return s;
}
void* memcpy(void* __dest, __const void* __src,
size_t __n)
{
int i;
char *d = (char *)__dest, *s = (char *)__src;
for (i=0;i return __dest;
}
/* ===========================================================================
* Fill the input buffer. This is called only when the buffer is empty
* and at least one byte is really needed.
*/
static int fill_inbuf(void)
{
if (insize != 0) {
error("ran out of input data\n";
}
inbuf = input_data;
insize = input_len;
inptr = 1;
return inbuf[0];
}
/* ===========================================================================
* Write the output window window[0..outcnt-1] and update crc and bytes_out.
* (Used for the decompressed data only.)
*/
static void flush_window_low(void)
{
ulg c = crc; /* temporary variable */
unsigned n;
uch *in, *out, ch;
in = window;
out =
for (n = 0; n ch = *out++ = *in++;
c = crc_32_tab[((int)c ^ ch) 0xff] ^ (c >;>; ;
}
crc = c;
bytes_out += (ulg)outcnt;
output_ptr += (ulg)outcnt;
outcnt = 0;
}
static void flush_window_high(void)
{
ulg c = crc; /* temporary variable */
unsigned n;
uch *in, ch;
in = window;
for (n = 0; n ch = *output_data++ = *in++;
if ((ulg)output_data == low_buffer_end) output_data=high_buffer_start;
c = crc_32_tab[((int)c ^ ch) 0xff] ^ (c >;>; ;
}
crc = c;
bytes_out += (ulg)outcnt;
outcnt = 0;
}
static void flush_window(void)
{
if (high_loaded) flush_window_high();
else flush_window_low();
}
static void error(char *x)
{
puts("\n\n";
puts(x);
puts("\n\n -- System halted");
while(1); /* Halt */
}
#define STACK_SIZE (4096)
long user_stack [STACK_SIZE];
struct {
long * a;
short b;
} stack_start = { user_stack [STACK_SIZE] , __KERNEL_DS };
void setup_normal_output_buffer(void) 对于zImage, 直接解压到1M
{
#ifdef STANDARD_MEMORY_BIOS_CALL
if (EXT_MEM_K #else
if ((ALT_MEM_K >; EXT_MEM_K ? ALT_MEM_K : EXT_MEM_K) #endif
output_data = (char *)0x100000; /* Points to 1M */
free_mem_end_ptr = (long)real_mode;
}
struct moveparams {
uch *low_buffer_start; int lcount;
uch *high_buffer_start; int hcount;
};
void setup_output_buffer_if_we_run_high(struct moveparams *mv)
{
high_buffer_start = (uch *)(((ulg) + HEAP_SIZE); 内核高端片段的最小起始地址
#ifdef STANDARD_MEMORY_BIOS_CALL
if (EXT_MEM_K #else
if ((ALT_MEM_K >; EXT_MEM_K ? ALT_MEM_K : EXT_MEM_K) #endif
mv->;low_buffer_start = output_data = (char *)LOW_BUFFER_START;
low_buffer_end = ((unsigned int)real_mode >; LOW_BUFFER_MAX
? LOW_BUFFER_MAX : (unsigned int)real_mode) ~0xfff;
low_buffer_size = low_buffer_end - LOW_BUFFER_START;
high_loaded = 1;
free_mem_end_ptr = (long)high_buffer_start;
if ( (0x100000 + low_buffer_size) >; ((ulg)high_buffer_start)) {
; 如果高端片段的最小起始地址小于它实际应加载的地址,则将它置为实际地址,
; 这样高端片段就无需再次移动了,否则它要向前移动
high_buffer_start = (uch *)(0x100000 + low_buffer_size);
mv->;hcount = 0; /* say: we need not to move high_buffer */
}
else mv->;hcount = -1; 待定
mv->;high_buffer_start = high_buffer_start;
}
void close_output_buffer_if_we_run_high(struct moveparams *mv)
{
if (bytes_out >; low_buffer_size) {
mv->;lcount = low_buffer_size;
if (mv->;hcount)
mv->;hcount = bytes_out - low_buffer_size; 求出高端片段的字节数
} else { 如果解压后内核只有低端的一个片段
mv->;lcount = bytes_out;
mv->;hcount = 0;
}
}
int decompress_kernel(struct moveparams *mv, void *rmode)
{
real_mode = rmode;
if (SCREEN_INFO.orig_video_mode == 7) {
vidmem = (char *) 0xb0000;
vidport = 0x3b4;
} else {
vidmem = (char *) 0xb8000;
vidport = 0x3d4;
}
lines = SCREEN_INFO.orig_video_lines;
cols = SCREEN_INFO.orig_video_cols;
if (free_mem_ptr else setup_output_buffer_if_we_run_high(mv);
makecrc();
puts("Uncompressing Linux... ");
gunzip();
puts("Ok, booting the kernel.\n");
if (high_loaded) close_output_buffer_if_we_run_high(mv);
return high_loaded;
}
Edited by lucian_yao on 04/28/01 01:36 PM.
[目录]
--------------------------------------------------------------------------------
用网卡从并口启动(I386)
标题 用网络卡从并口上启动Linux(I386) [re: raoxianhong]
作者 raoxianhong (journeyman)
时间 10/07/01 12:31 PM
“十一”假期,哪儿也不去,做个程序博各位一笑。
=============================================
1、到底想干什么
了解Linux的启动过程,制作一个自己的Linux启动程序,可以增加对Linux的了解,还能学习PC机的启动机制,增进对计算机结构的了解,增强对Linux内核学习的信心。也可以在某些专用产品中使用(比如专用的服务器)。为此,我尝试在原来代码的基础上修改制作了一个用网络卡从并口上启动Linux的程序,以博一笑,其中有许多问题值得研究。
2、Linux对启动程序的要求
Linux(bzImage Kernel)对启动程序的要求比较简单,你只要能够建立一个启动头(setup.S),给出一些信息,然后将kernel/usr/src/linux/arch/i386/boot/compressed/bvmlinux.out)调到绝对地址0x100000(1M地址处),如果有initrd,则将它调到内存高端(离0x100000越远越好,比如如果initrd小于4M,就可以将它调到地址0xB00000,即12M处,相信现在已经很少有少于16M内存的机器了),然后执行一些初始化操作,跳到内核处就行了。
当然,说起来容易做起来还有点麻烦,以下分几个问题解释。
3、PC机开机流程--启动程序放在何处
PC机加电后,进入实模式,先进行自检,然后初始化各个总线扩展设备(ISA, EISA,PCI,AGP),
全部初始化做完后,从当前启动设备中读一个块(512字节)到07C0:0000处,将控制转到该处。
了解这个过程,我们可以决定将启动程序放在何处:
1)放在启动设备的MBR(主启动记录中),比如磁盘的启动扇区。这是一般的启动方式。
2)放在总线扩展设备的扩展rom中,比如网卡的boot rom就行,这里制作的启动程序就是放在网卡中,可以支持16K字节。
3)哪位高手能够修改ROMBIOS,让BIOS在做完初始化后不要马上从启动设备读数据,而是调用一段外面加入的程序(2K字节就够了,当然也必须与修改后的BIOS一起烧在BIOS ROM中),就可以从BIOS启动!
4)先启动一个操作系统,再在此操作系统中写启动程序(比如lodlin16就是从DOS中启动Linux,好象中软提供了一个从Windows下启动Linux的启动程序)。
4、操作系统放在何处
操作系统(一般内核在500K-1M之间,加上应用程序可以控制在2M以内,当然都经过压缩了)的数据选择余地就大了,可以从软盘、硬盘、CDROM、网络、磁带机、并口(软件狗上烧个内核和应用程序?)、串口(外接你的设备)、USB设备(?)、PCI扩展卡、IC卡等等上面来读;各位还有什么意见,提醒提醒。有位老兄说实在不行可以用键盘启动,每次启动时把内核敲进去,还有int 16h支持呢,做起来也不难,应该是最节省的方案了。
反正一个原则是,在启动程序中能够从该设备上读就行了,这里最简单的就是并口了,简单的端口操作,不需要任何驱动程序支持,不需要BIOS支持,比磁盘还简单(磁盘一般使用int 13h,主要是计算柱面啊、磁头啊、磁道啊、扇区啊好麻烦,幸好有现成的源代码,可以学习学习)。
好了,我们挑个简单的方案,将启动代码(bootsect.S+setup.S)放到网络卡的boot rom中,内核数据和应用数据放到另外一台计算机上,用并口提供。下面谈谈几个相关的问题。
5、将数据移动到绝对地址处
第一个问题,我们得到数据,因为是在实模式下,所以一般是放在1M地址空间内,怎样将它移动到指定的地方去,在setup.S 的源代码中,使用了int 15h(87h号功能)。这里将该段代码稍加改动,做了些假设,列到下面,流程是:
if (%cs:move_es==0)/*由于使用前move_es初始化为0,因此这是第一次调用,此时es:bx是要移动的数据
存放处bx=0,es低四为位为零表示es:bx在64K边界上,fs的低8位指定目的地地址,
也以64K字节为单位,用不着那么精确,以简化操作*/
{
将es右移四位,得到64K单位的8位地址(这样一来,最多只能将数据移动到16M以下了),作为源数据
描述符中24位地址的高8位,低16位为零。
将fs的低8位作为目的地的描述符中24位地址的高8位,同样,它的低16位为零。
将es存放在move_es中,es自然不会是零,因此以后再调用该例程时就进行正常的移动操作了。
ax清零返回。
}
else
{
if (bx==0)/*bx为零,表示数据已经满64K了,应该进行实际的移动*/
{
调用int15h 87h号功能,进行实际的数据移动(64K, 0x8000个16字节块)。
目的地址(24位)高8位增一,往后走64K
ax = 1
return;
}
else
{
ax = 0;
return;
}
}
# we will move %cx bytes from es:bx to %fs(64Kbytes per unit)
# when we first call movetohigh(%cs:move_es is zero),
# the es:bx and %edx is valid
# we configure the param first
# follow calls will move data actually
# %ax return 0 if no data really moved, and return 1 if there is data
# really to be moved
#
movetohigh:
cmpw $0, %cs:move_es
jnz move_second
# at this point , es:bx(bx = 0) is the source address
# %edx is the destination address
movb $0x20, %cs:type_of_loader
movw %es, %ax
shrw $4, %ax
movb %ah, %cs:move_src_base+2
movw %fs, %ax
movb %al, %cs:move_dst_base+2
movw %es, %ax
movw %ax, %cs:move_es
xorw %ax, %ax
ret # nothing else to do for now
move_second:
xorw %ax, %ax
testw %bx, %bx
jne move_ex
pushw %ds
pushw %cx
pushw %si
pushw %bx
movw $0x8000, %cx # full 64K, INT15 moves words
pushw %cs
popw %es
leaw %cs:move_gdt, %si
movw $0x8700, %ax
int $0x15
jc move_panic # this, if INT15 fails
movw %cs:move_es, %es # we reset %es to always point
incb %cs:move_dst_base+2 # to 0x10000
popw %bx
popw %si
popw %cx
popw %ds
movw $1, %ax
move_ex:
ret
move_gdt:
.word 0, 0, 0, 0
.word 0, 0, 0, 0
move_src:
.word 0xffff
move_src_base:
.byte 0x00, 0x00, 0x01 # base = 0x010000
.byte 0x93 # typbyte
.word 0 # limit16,base24 =0
move_dst:
.word 0xffff
move_dst_base:
.byte 0x00, 0x00, 0x10 # base = 0x100000
.byte 0x93 # typbyte
.word 0 # limit16,base24 =0
.word 0, 0, 0, 0 # BIOS CS
.word 0, 0, 0, 0 # BIOS DS
move_es:
.word 0
move_panic:
pushw %cs
popw %ds
cld
leaw move_panic_mess, %si
call prtstr
move_panic_loop:
jmp move_panic_loop
move_panic_mess:
.string "INT15 refuses to access high mem, giving up."
6、用并口传输数据
用并口传输数据,可以从/usr/src/linux/driver/net/plip.c中抄一段,我们采用半字节协议,并口线连接参考该文件。字节收发过程如下:
#define PORT_BASE 0x378
#define data_write(b) outportb(PORT_BASE, b)
#define data_read() inportb(PORT_BASE+1)
#define OK 0
#define TIMEOUT 1
#define FAIL 2
int sendbyte(unsigned char data)
{
unsigned char c0;
unsigned long cx;
data_write((data & 0x0f));
data_write((0x10 | (data & 0x0f)));
cx = 32767l * 1024l;
while (1) {
c0 = data_read();
if ((c0 & 0x80) == 0)
break;
if (--cx == 0)
return TIMEOUT;
}
data_write(0x10 | (data >;>; 4));
data_write((data >;>; 4));
cx = 32767l * 1024l;
while (1) {
c0 = data_read();
if (c0 & 0x80)
break;
中断
Linux系统中有很多不同的硬件设备。你可以同步使用这些设备,也就是说你可以发出一个请求,然后等待一直到设备完成操作以后再进行其他的工作。但这种方法的效率却非常的低,因为操作系统要花费很多的等待时间。一个更为有效的方法是发出请求以后,操作系统继续其他的工作,等设备完成操作以后,给操作系统发送一个中断,操作系统再继续处理和此设备有关的操作。
在将多个设备的中断信号送往CPU的中断插脚之前,系统经常使用中断控制器来综合多个设备的中断。这样即可以节约CPU的中断插脚,也可以提高系统设计的灵活性。中断控制器用来控制系统的中断,它包括屏蔽和状态寄存器。设置屏蔽寄存器的各个位可以允许或屏蔽某一个中断,状态寄存器则用来返回系统中正在使用的中断。
大多数处理器处理中断的过程都相同。当一个设备发出中段请求时,CPU停止正在执行的指令,转而跳到包括中断处理代码或者包括指向中断处理代码的转移指令所在的内存区域。这些代码一般在CPU的中断方式下运行。在此方式下,将不会再有中断发生。但有些CPU的中断有自己的优先权,这样,更高优先权的中断则可以发生。这意味着第一级的中断处理程序必须拥有自己的堆栈,以便在处理更高级别的中断前保存CPU的执行状态。当中断处理完毕以后,CPU将恢复到以前的状态,继续执行中断处理前正在执行的指令。
中断处理程序十分简单有效,这样,操作系统就不会花太长的时间屏蔽其他的中断。
[设置Softirq]
cpu_raise_softirq是一个轮训,唤醒ksoftirqd_CPU0内核线程, 进行管理
cpu_raise_softirq
|__cpu_raise_softirq
|wakeup_softirqd
|wake_up_process
·cpu_raise_softirq [kernel/softirq.c]
·__cpu_raise_softirq [include/linux/interrupt.h]
·wakeup_softirq [kernel/softirq.c]
·wake_up_process [kernel/sched.c]
[执行Softirq]
当内核线程ksoftirqd_CPU0被唤醒, 它会执行队列里的工作。当然ksoftirqd_CPU0也是一个死循环:
for (; {
if (!softirq_pending(cpu))
schedule();
__set_current_state(TASK_RUNNING);
while (softirq_pending(cpu)) {
do_softirq();
if (current->;need_resched)
schedule
}
__set_current_state(TASK_INTERRUPTIBLE)
}
·ksoftirqd [kernel/softirq.c]
[目录]
--------------------------------------------------------------------------------
软中断
发信人: fist (星仔迷), 信区: SysInternals WWW-POST
标 题: 软中断
发信站: 武汉白云黄鹤站 (Thu Mar 22 14:12:46 2001) , 转信
软中断「一」
一、 引言
软中断是linux系统原“底半处理”的升级,在原有的基础上发展的新的处理方式,以适应多cpu 、多线程的软中断处理。要了解软中断,我们必须要先了原来底半处理的处理机制。
二、底半处理机制(基于2.0.3版本)
某些特殊时刻我们并不愿意在核心中执行一些操作。例如中断处理过程中。当中断发生时处理器将停止当前的工作, 操作系统将中断发送到相应的设备驱动上去。由于此时系统中其他程序都不能运行, 所以设备驱动中的中断处理过程不宜过长。有些任务最好稍后执行。Linux底层部分处理机制可以让设备驱动和Linux核心其他部分将这些工作进行排序以延迟执行。
系统中最多可以有32个不同的底层处理过程;bh_base是指向这些过程入口的指针数组。而bh_active和 bh_mask用来表示那些处理过程已经安装以及那些处于活动状态。如果bh_mask的第N位置位则表示bh_base的 第N个元素包含底层部分处理例程。如果bh_active的第N位置位则表示第N个底层处理过程例程可在调度器认 为合适的时刻调用。这些索引被定义成静态的;定时器底层部分处理例程具有最高优先级(索引值为0), 控制台底层部分处理例程其次(索引值为1)。典型的底层部分处理例程包含与之相连的任务链表。例如 immediate底层部分处理例程通过那些需要被立刻执行的任务的立即任务队列(tq_immediate)来执行。
--引自David A Rusling的《linux核心》。
三、对2.4.1 软中断处理机制
下面,我们进入软中断处理部份(softirq.c):
由softirq.c的代码阅读中,我们可以知道,在系统的初始化过程中(softirq_init()),它使用了两个数组:bh_task_vec[32],softirq_vec[32]。其中,bh_task_vec[32]填入了32个bh_action()的入口地址,但soft_vec[32]中,只有softirq_vec[0],和softirq_vec[3]分别填入了tasklet_action()和tasklet_hi_action()的地址。其余的保留它用。
当发生软中断时,系统并不急于处理,只是将相应的cpu的中断状态结构中的active 的相应的位置位,并将相应的处理函数挂到相应的队列,然后等待调度时机来临(如:schedule(),
系统调用返回异常时,硬中断处理结束时等),系统调用do_softirq()来测试active位,再调用被激活的进程在这处过程中,软中断的处理与底半处理有了差别,active 和mask不再对应bh_base[nr], 而是对应softirq_vec[32]。在softirq.c中,我们只涉及了softirq_vec[0]、softirq_vec[3]。这两者分别调用了tasklet_action()和tasklet_hi_action()来进行后续处理。这两个过程比较相似,大致如下:
1 锁cpu的tasklet_vec[cpu]链表,取出链表,将原链表清空,解锁,还给系统。
2 对链表进行逐个处理。
3 有无法处理的,(task_trylock(t)失败,可能有别的进程锁定),插回系统链表。至此,系统完成了一次软中断的处理。
接下来有两个问题:
1 bh_base[]依然存在,但应在何处调用?
2 tasklet_vec[cpu]队列是何时挂上的?
四、再探讨
再次考查softirq.c 的bh_action()部份,发现有两个判断:
A:if(!spin_trylock(&global_bh_lock))goto:rescue 指明如果global_bh_lock 不能被锁上(已被其它进程锁上),则转而执行rescue,将bh_base[nr]挂至tasklet_hi_vec[cpu]队列中。等候中断调度。
B:if(!hardirq_trylock(cpu)) goto tescue unlock 此时有硬中断发生,放入队列推迟执行。若为空闲,现在执行。
由此可见,这部分正是对应底半处理的程序,bh_base[]的延时处理正是底半处理的特点,可以推测,如果没有其它函数往tasklet_hi_vec[cpu]队列挂入,那tasklet_hi_vec[cpu]正完全对应着bh_base[]底半处理
在bh_action()中,把bh_ation()挂入tasklet_hi_vec[cpu]的正是mark_bh(),在整个源码树中查找,发现调用mark_bh()的函数很多,可以理解,软中断产生之时,相关的函数会调用mark_bh(),将bh_action挂上tasklet_hi_vec队列,而bh_action()的作用不过是在发现bh_base[nr]暂时无法处理时重返队列的方法。
由此可推测tasklet_vec队列的挂接应与此相似,查看interrupt.h,找到tasklet_schedule()函数:
157 static inline void tasklet_schedule(struct tasklet_struct *t)
158 {
159 if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->;state)) {
160 int cpu = smp_processor_id();
161 unsigned long flags;
162
163 local_irq_save(flags);
164 t->;next = tasklet_vec[cpu].list;
165 tasklet_vec[cpu].list = t; /*插入队列。
166 __cpu_raise_softirq(cpu, TASKLET_SOFTIRQ);
167 local_irq_restore(flags);
168 }
169 }
正是它为tasklet_vec[cpu]队列的建立立下了汗马功劳,在源码树中,它亦被多个模块调用,来完成它的使命。
至此,我们可以描绘一幅完整的软中断处理图了。
现在,再来考查do_softirq()的softirq_vec[32],在interrupt.h中有如下定义:
56 enum
57 {
58 HI_SOFTIRQ=0,
59 NET_TX_SOFTIRQ,
60 NET_RX_SOFTIRQ,
61 TASKLET_SOFTIRQ
62 };
这四个变量应都是为softirq_vec[]的下标,那么,do_softirq()也将会处理NET_TX_SOFTIRQ和NET_RX_SOFTIRQ,是否还处理其它中断,这有待探讨。也许,这个do_softirq()有着极大的拓展性,等着我们去开发呢。
主要通过__cpu_raise_softirq来设置
在hi_tasklet(也就是一般用于bh的)的处理里面,在处理完当前的队列后,会将补充的队列重新挂上,然后标记(不管是否补充队列里面有tasklet):
local_irq_disable();
t->;next = tasklet_hi_vec[cpu].list;
tasklet_hi_vec[cpu].list = t;
__cpu_raise_softirq(cpu, HI_SOFTIRQ);
local_irq_enable();
因此,对mark_bh根本不用设置这个active位。对于一般的tasklet也一样:
local_irq_disable();
t->;next = tasklet_vec[cpu].list;
tasklet_vec[cpu].list = t;
__cpu_raise_softirq(cpu, TASKLET_SOFTIRQ);
local_irq_enable();
其它的设置,可以检索上面的__cpu_raise_softirq
bottom half, softirq, tasklet, tqueue
[bottom half]
bh_base[32]
|
\/
bh_action();
|
\/
bh_task_vec[32];
| mark_bh(), tasklet_hi_schedule()
\/
task_hi_action
bh_base对应的是32个函数,这些函数在bh_action()中调用
static void bh_action(unsigned long nr)
{
int cpu = smp_processor_id();
if (!spin_trylock(&global_bh_lock))
goto resched;
if (!hardirq_trylock(cpu))
goto resched_unlock;
if (bh_base[nr])
bh_base[nr]();
hardirq_endlock(cpu);
spin_unlock(&global_bh_lock);
return;
resched_unlock:
spin_unlock(&global_bh_lock);
resched:
mark_bh(nr);
}
在软中断初始化时,将bh_action()放到bh_task_vec[32]中,bh_task_vec[32]中元素的类型是tasklet_struct,系统使用mark_bh()或task_hi_schedule()函数将它挂到task_hi_vec[]的对列中,在系统调用do_softirq()时执行。
static inline void mark_bh(int nr)
{
tasklet_hi_schedule(bh_task_vec+nr);
}
static inline void tasklet_hi_schedule(struct tasklet_struct *t)
{
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->;state)) {
int cpu = smp_processor_id();
unsigned long flags;
local_irq_save(flags);
t->;next = tasklet_hi_vec[cpu].list;
tasklet_hi_vec[cpu].list = t;
__cpu_raise_softirq(cpu, HI_SOFTIRQ);
local_irq_restore(flags);
}
}
[softirq]
softirq_vec[32];
struct softirq_action
{
void (*action)(struct softirq_action *);
void *data;
};
软中断对应一个softirq_action的结构,在do_softirq()中调用相应的action()做处理。
软中断初始化时只设置了0,3两项,对应的action是task_hi_action和task_action.
1: task_hi_action
/\
|
tasklet_hi_vec[NR_CPU]
struct tasklet_head tasklet_hi_vec[NR_CPUS] __cacheline_aligned;
struct tasklet_head
{
struct tasklet_struct *list;
} __attribute__ ((__aligned__(SMP_CACHE_BYTES)));
task_hi_action处理的对象是一个tasklet的队列,每个cpu都有一个对应的tasklet队列,
它在tasklet_hi_schedule中动态添加。
3: task_action
/\
|
tasklet_vec[NR_CPU]
[tasklet]
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};
从上面的分析来看tasklet只是一个调用实体,在do_softirq()中被调用。softirq的组织和结构才是最重要的。
[目录]
--------------------------------------------------------------------------------
硬中断
标题 Linux设备驱动程序的中断
作者 coly (journeyman)
时间 07/02/01 11:24 AM
Linux设备驱动程序的中断 Coly V0.1
指定参考书:《Linux设备驱动程序》(第一版)
这里总结一下Linux设备驱动程序中涉及的中断机制。
一、前言
Linux的中断宏观分为两种:软中断和硬中断。声明一下,这里的软和硬的意思是指和软件相关以及和硬件相关,而不是软件实现的中断或硬件实现的中断。软中断就是“信号机制”。软中断不是软件中断。Linux通过信号来产生对进程的各种中断操作,我们现在知道的信号共有31个,其具体内容这里略过,感兴趣读者可参看相关参考文献[1]。
一般来说,软中断是由内核机制的触发事件引起的(例如进程运行超时),但是不可忽视有大量的软中断也是由于和硬件有关的中断引起的,例如当打印机端口产生一个硬件中断时,会通知和硬件相关的硬中断,硬中断就会产生一个软中断并送到操作系统内核里,这样内核就会根据这个软中断唤醒睡眠在打印机任务队列中的处理进程。
硬中断就是通常意义上的“中断处理程序”,它是直接处理由硬件发过来的中断信号的。当硬中断收到它应当处理的中断信号以后,就回去自己驱动的设备上去看看设备的状态寄存器以了解发生了什么事情,并进行相应的操作。
对于软中断,我们不做讨论,那是进程调度里要考虑的事情。由于我们讨论的是设备驱动程序的中断问题,所以焦点集中在硬中断里。我们这里讨论的是硬中断,即和硬件相关的中断。
二、中断产生
要中断,是因为外设需要通知操作系统她那里发生了一些事情,但是中断的功能仅仅是一个设备报警灯,当灯亮的时候中断处理程序只知道有事情发生了,但发生了什么事情还要亲自到设备那里去看才行。也就是说,当中断处理程序得知设备发生了一个中断的时候,它并不知道设备发生了什么事情,只有当它访问了设备上的一些状态寄存器以后,才能知道具体发生了什么,要怎么去处理。
设备通过中断线向中断控制器发送高电平告诉操作系统它产生了一个中断,而操作系统会从中断控制器的状态位知道是哪条中断线上产生了中断。PC机上使用的中断控制器是8259,这种控制器每一个可以管理8条中断线,当两个8259级联的时候共可以控制15条中断线。这里的中断线是实实在在的电路,他们通过硬件接口连接到CPU外的设备控制器上。
三、IRQ
并不是每个设备都可以向中断线上发中断信号的,只有对某一条确定的中断线勇有了控制权,才可以向这条中断线上发送信号。由于计算机的外部设备越来越多,所以15条中断线已经不够用了,中断线是非常宝贵的资源。要使用中断线,就得进行中断线的申请,就是IRQ(Interrupt Requirement),我们也常把申请一条中断线成为申请一个IRQ或者是申请一个中断号。
IRQ是非常宝贵的,所以我们建议只有当设备需要中断的时候才申请占用一个IRQ,或者是在申请IRQ时采用共享中断的方式,这样可以让更多的设备使用中断。无论对IRQ的使用方式是独占还是共享,申请IRQ的过程都是一样的,分为3步:
1.将所有的中断线探测一遍,看看哪些中断还没有被占用。从这些还没有被占用的中断中选一个作为该设备的IRQ。
2.通过中断申请函数申请选定的IRQ,这是要指定申请的方式是独占还是共享。
3.根据中断申请函数的返回值决定怎么做:如果成功了万事大吉,如果没成功则或者重新申请或者放弃申请并返回错误。
申请IRQ的过程,在参考书的配的源代码里有详细的描述,读者可以通过仔细阅读源代码中的short一例对中断号申请由深刻的理解。
四、中断处理程序
Linux中的中断处理程序很有特色,它的一个中断处理程序分为两个部分:上半部(top half)和下半部(bottom half)。之所以会有上半部和下半部之分,完全是考虑到中断处理的效率。
上半部的功能是“登记中断”。当一个中断发生时,他就把设备驱动程序中中断例程的下半部挂到该设备的下半部执行队列中去,然后就没事情了--等待新的中断的到来。这样一来,上半部执行的速度就会很快,他就可以接受更多她负责的设备产生的中断了。上半部之所以要快,是因为它是完全屏蔽中断的,如果她不执行完,其它的中断就不能被及时的处理,只能等到这个中断处理程序执行完毕以后。所以,要尽可能多得对设备产生的中断进行服务和处理,中断处理程序就一定要快。
但是,有些中断事件的处理是比较复杂的,所以中断处理程序必须多花一点时间才能够把事情做完。可怎么样化解在短时间内完成复杂处理的矛盾呢,这时候Linux引入了下半部的概念。下半部和上半部最大的不同是下半部是可中断的,而上半部是不可中断的。下半部几乎做了中断处理程序所有的事情,因为上半部只是将下半部排到了他们所负责的设备的中断处理队列中去,然后就什么都不管了。下半部一般所负责的工作是察看设备以获得产生中断的事件信息,并根据这些信息(一般通过读设备上的寄存器得来)进行相应的处理。如果有些时间下半部不知道怎么去做,他就使用著名的鸵鸟算法来解决问题--说白了就是忽略这个事件。
由于下半部是可中断的,所以在它运行期间,如果其它的设备产生了中断,这个下半部可以暂时的中断掉,等到那个设备的上半部运行完了,再回头来运行它。但是有一点一定要注意,那就是如果一个设备中断处理程序正在运行,无论她是运行上半部还是运行下半部,只要中断处理程序还没有处理完毕,在这期间设备产生的新的中断都将被忽略掉。因为中断处理程序是不可重入的,同一个中断处理程序是不能并行的。
在Linux Kernel 2.0以前,中断分为快中断和慢中断(伪中断我们这里不谈),其中快中断的下半部也是不可中断的,这样可以保证它执行的快一点。但是由于现在硬件水平不断上升,快中断和慢中断的运行速度已经没有什么差别了,所以为了提高中断例程事务处理的效率,从Linux kernel 2.0以后,中断处理程序全部都是慢中断的形式了--他们的下半部是可以被中断的。
但是,在下半部中,你也可以进行中断屏蔽--如果某一段代码不能被中断的话。你可以使用cti、sti或者是save_flag、restore_flag来实现你的想法。至于他们的用法和区别,请参看本文指定参考书中断处理部分。
进一步的细节请读者参看本文指定参考书,这里就不再所说了,详细介绍细节不是我的目的,我的目的是整理概念。
五、置中断标志位
在处理中断的时候,中断控制器会屏蔽掉原先发送中断的那个设备,直到她发送的上一个中断被处理完了为止。因此如果发送中断的那个设备载中断处理期间又发送了一个中断,那么这个中断就被永远的丢失了。
之所以发生这种事情,是因为中断控制器并不能缓冲中断信息,所以当前一个中断没有处理完以前又有新的中断到达,他肯定会丢掉新的中断的。但是这种缺陷可以通过设置主处理器(CPU)上的“置中断标志位”(sti)来解决,因为主处理器具有缓冲中断的功能。如果使用了“置中断标志位”,那么在处理完中断以后使用sti函数就可以使先前被屏蔽的中断得到服务。
六、中断处理程序的不可重入性
上一节中我们提到有时候需要屏蔽中断,可是为什么要将这个中断屏蔽掉呢?这并不是因为技术上实现不了同一中断例程的并行,而是出于管理上的考虑。之所以在中断处理的过程中要屏蔽同一IRQ来的新中断,是因为中断处理程序是不可重入的,所以不能并行执行同一个中断处理程序。在这里我们举一个例子,从这里子例中可以看出如果一个中断处理程序是可以并行的话,那么很有可能会发生驱动程序锁死的情况。当驱动程序锁死的时候,你的操作系统并不一定会崩溃,但是锁死的驱动程序所支持的那个设备是不能再使用了--设备驱动程序死了,设备也就死了。
A是一段代码,B是操作设备寄存器R1的代码,C是操作设备寄存器R2的代码。其中激发PS1的事件会使A1产生一个中断,然后B1去读R1中已有的数据,然后代码C1向R2中写数据。而激发PS2的事件会使A2产生一个中断,然后B2删除R1中的数据,然后C2读去R2中的数据。
如果PS1先产生,且当他执行到A1和B1之间的时候,如果PS2产生了,这是A2会产生一个中断,将PS2中断掉(挂到任务队列的尾部),然后删除了R1的内容。当PS2运行到C2时,由于C1还没有向R2中写数据,所以C2将会在这里被挂起,PS2就睡眠在代码C2上,直到有数据可读的时候被信号唤醒。这是由于PS1中的B2原先要读的R1中的数据被PS2中的B2删除了,所以PS1页会睡眠在B1上,直到有数据可读的时候被信号唤醒。这样一来,唤醒PS1和PS2的事件就永远不会发生了,因此PS1和PS2之间就锁死了。
由于设备驱动程序要和设备的寄存器打交道,所以很难写出可以重入的代码来,因为设备寄存器就是全局变量。因此,最简洁的办法就是禁止同一设备的中断处理程序并行,即设备的中断处理程序是不可重入的。
有一点一定要清楚:在2.0版本以后的Linux kernel中,所有的上半部都是不可中断的(上半部的操作是原子性的);不同设备的下半部可以互相中断,但一个特定的下半部不能被它自己所中断(即同一个下半部不能并)。
由于中断处理程序要求不可重入,所以程序员也不必为编写可重入的代码而头痛了。以我的经验,编写可重入的设备驱动程序是可以的,编写可重入的中断处理程序是非常难得,几乎不可能。
七、避免竞争条件的出现
我们都知道,一旦竞争条件出现了,就有可能会发生死锁的情况,严重时可能会将整个系统锁死。所以一定要避免竞争条件的出现。这里我不多说,大家只要注意一点:绝大多数由于中断产生的竞争条件,都是在带有中断的
内核进程被睡眠造成的。所以在实现中断的时候,一定要相信谨慎的让进程睡眠,必要的时候可以使用cli、sti或者save_flag、restore_flag。具体细节请参看本文指定参考书。
八、实现
如何实现驱动程序的中断例程,是各位读者的事情了。只要你们仔细的阅读short例程的源代码,搞清楚编写驱动程序中断例程的规则,就可以编写自己的中断例程了。只要概念正确,
在正确的规则下编写你的代码,那就是符合道理的东西。我始终强调,概念是第一位的,能编多少代码是很其次的,我们一定要概念正确,才能进行正确的思考。
九、小结
本文介绍了Linux驱动程序中的中断,如果读者已经新痒了的话,那么打开机器开始动手吧!
Time for you to leave!
参考文献:
1.Linux网络编程
2.编程之道
3.Linux设备驱动程序
4.Mouse drivers
5.Linux Kernel Hacking Guide
6.Unreliable Guide To Hacking The Linux Kernel
[目录]
--------------------------------------------------------------------------------
定时器代码分析
时钟和定时器中断
IRQ 0 [Timer]
|
\|/
|IRQ0x00_interrupt // wrapper IRQ handler
|SAVE_ALL ---
|do_IRQ | wrapper routines
|handle_IRQ_event ---
|handler() ->; timer_interrupt // registered IRQ 0 handler
|do_timer_interrupt
|do_timer
|jiffies++;
|update_process_times
|if (--counter <= 0) { // if time slice ended then
|counter = 0; // reset counter
|need_resched = 1; // prepare to reschedule
|}
|do_softirq
|while (need_resched) { // if necessary
|schedule // reschedule
|handle_softirq
|}
|RESTORE_ALL
·IRQ0x00_interrupt, SAVE_ALL [include/asm/hw_irq.h]
·do_IRQ, handle_IRQ_event [arch/i386/kernel/irq.c]
·timer_interrupt, do_timer_interrupt [arch/i386/kernel/time.c]
·do_timer, update_process_times [kernel/timer.c]
·do_softirq [kernel/soft_irq.c]
·RESTORE_ALL, while loop [arch/i386/kernel/entry.S]
系统启动核心时,调用start_kernal()继续各方面的初始化,在这之前,各种中断都被禁止,只有在完成必要的初始化后,直到执行完Kmalloc_init()后,才允许中断(init\main.c)。与时钟中断有关的部分初始化如下:
调用trap_init()设置各种trap入口,如system_call、GDT entry、LDT entry、call gate等。其中0~17为各种错误入口,18~47保留。
调用init_IRQ()函数设置核心系统的时钟周期为10ms,即100HZ,它是以后按照轮转法进行CPU调度时所依照的基准时钟周期。每10ms产生的时钟中断信号直接输入到第一块8259A的INT 0(即irq0)。初始化中断矢量表中从0x20起的17个中断矢量,用bad_IRQ#_interrupt函数的地址(#为中断号)填写。
调用sched_init()函数,设置启动第一个进程init_task。设置用于管理bottom_half机制的数据结构bh_base[],规定三类事件的中断处理函数,即时钟TIMER_BH、设备TQUEUE_BH和IMMEDIATE_BH。
调用time_init()函数,首先读取当时的CMOS时间,最后调用setup_x86_irq(0,&irq0)函数,把irq0挂到irq_action[0]队列的后面,并把中断矢量表中第0x20项,即timer中断对应的中断矢量改为IRQ0_interrupt函数的地址,在irq0中,指定时间中断服务程序是timer_interrupt,
static struct irqaction irq0 = { timer_interrupt, 0, 0, "timer", NULL, NULL}
结构irqaction的定义如下:
struct irqaction {
void (*handler)(int, void *, struct pt_regs *); /* 中断服务函数入口 */
unsigned long flags; /* 服务允中与否标记 */
unsigned long mask;
const char *name;
void *dev_id;
struct irqaction *next;
};
其中,若flag==SA_INTERRUPT,则中断矢量改为fast_IRQ#_interrupt,在执行中断服务的过程中不允许出现中断,若为其它标记,则中断矢量为IRQ#_interrupt,在执行中断服务的过程中,允许出现中断。
Irq_action的定义与初始化如下:
static void (*interrupt[17])(void) = {IRQ#_interrupt};
static void (*fast_interrupt[16])(void) = {fast_IRQ#_interrupt};
static void (*bad_interrupt[16])(void) = {bad_IRQ#_interrupt};(以上#为中断号)
static struct irqaction *irq_action[16] = {
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL
};
irq_action是一个全局数组,每个元素指向一个irq队列,共16个irq队列,时钟中断请求队列在第一个队列,即irq_action[0]。当每个中断请求到来时,都调用setup_x86_irq把该请求挂到相应的队列的后面。
以后,系统每10ms产生一次时钟中断信号,该信号直接输入到第一块8259A的INT 0(即irq0)。CPU根据中断矢量表和中断源,找到中断矢量函数入口IRQ0_interrupt(程序运行过程中允许中断)或者fast_IRQ0_interrupt(程序运行过程中不允许中断)或者bad_IRQ0_interrupt(不执行任何动作,直接返回),这些函数由宏BUILD_TIMER_IRQ(chip, nr, mask)展开定义。
宏BUILD_TIMER_IRQ(chip, nr, mask)的定义如下:
#define BUILD_TIMER_IRQ(chip,nr,mask) \
asmlinkage void IRQ_NAME(nr); \
asmlinkage void FAST_IRQ_NAME(nr); \
asmlinkage void BAD_IRQ_NAME(nr); \
__asm__( \
"\n"__ALIGN_STR"\n" \
SYMBOL_NAME_STR(fast_IRQ) #nr "_interrupt:\n\t" \
SYMBOL_NAME_STR(bad_IRQ) #nr "_interrupt:\n\t" \
SYMBOL_NAME_STR(IRQ) #nr "_interrupt:\n\t" \
"pushl $-"#nr"-2\n\t" \
SAVE_ALL \
ENTER_KERNEL \
ACK_##chip(mask,(nr&7)) \
"incl "SYMBOL_NAME_STR(intr_count)"\n\t"\ /* intr_count为进入临界区的同步信号量 */
"movl %esp,%ebx\n\t" \
"pushl %ebx\n\t" \
"pushl $" #nr "\n\t" \ /* 把do_irq函数参数压进堆栈 */
"call "SYMBOL_NAME_STR(do_IRQ)"\n\t" \
"addl $8,%esp\n\t" \
"cli\n\t" \
UNBLK_##chip(mask) \
"decl "SYMBOL_NAME_STR(intr_count)"\n\t" \
"incl "SYMBOL_NAME_STR(syscall_count)"\n\t" \
"jmp ret_from_sys_call\n";
其中nr为中断请求类型,取值0~15。在irq.c中通过语句BUILD_TIMER_IRQ(first, 0, 0x01)调用该宏,在执行宏的过程中处理时钟中断响应程序do_irq()。
函数do_irq()的第一个参数是中断请求队列序号,时钟中断请求传进来的该参数是0。于是程序根据参数0找到请求队列irq_action[0],逐个处理该队列上handler所指的时钟中断请求的服务函数。由于已经指定时钟中断请求的服务函数是timer_interrupt,在函数timer_interrupt中,立即调用do_timer()函数。
函数do_timer()把jiffies和lost_ticks加1,接着就执行mark_bh(TIMER_BH)函数,把bottom_half中时钟队列对应的位置位,表示该队列处于激活状态。在做完这些动作后,程序从函数do_irq()中返回,继续执行以后的汇编代码。于是,程序在执行语句jmp ret_from_sys_call后,跳到指定的位置处继续执行。
代码段jmp ret_from_sys_call及其相关的代码段如下:
ALIGN
.globl ret_from_sys_call
ret_from_sys_call:
cmpl $0,SYMBOL_NAME(intr_count)
jne 2f
9: movl SYMBOL_NAME(bh_mask),%eax
andl SYMBOL_NAME(bh_active),%eax
jne handle_bottom_half
#ifdef __SMP__
cmpb $(NO_PROC_ID), SYMBOL_NAME(saved_active_kernel_processor)
jne 2f
#endif
movl EFLAGS(%esp),%eax # check VM86 flag: CS/SS are
testl $(VM_MASK),%eax # different then
jne 1f
cmpw $(KERNEL_CS),CS(%esp) # was old code segment supervisor ?
je 2f
1: sti
orl $(IF_MASK),%eax # these just try to make sure
andl $~NT_MASK,%eax # the program doesn't do anything
movl %eax,EFLAGS(%esp) # stupid
cmpl $0,SYMBOL_NAME(need_resched)
jne reschedule
#ifdef __SMP__
GET_PROCESSOR_OFFSET(%eax)
movl SYMBOL_NAME(current_set)(,%eax), %eax
#else
movl SYMBOL_NAME(current_set),%eax
#endif
cmpl SYMBOL_NAME(task),%eax # task[0] cannot have signals
je 2f
movl blocked(%eax),%ecx
movl %ecx,%ebx # save blocked in %ebx for signal handling
notl %ecx
andl signal(%eax),%ecx
jne signal_return
2: RESTORE_ALL
ALIGN
signal_return:
movl %esp,%ecx
pushl %ecx
testl $(VM_MASK),EFLAGS(%ecx)
jne v86_signal_return
pushl %ebx
call SYMBOL_NAME(do_signal)
popl %ebx
popl %ebx
RESTORE_ALL
ALIGN
v86_signal_return:
call SYMBOL_NAME(save_v86_state)
movl %eax,%esp
pushl %eax
pushl %ebx
call SYMBOL_NAME(do_signal)
popl %ebx
popl %ebx
RESTORE_ALL
handle_bottom_half:
incl SYMBOL_NAME(intr_count)
call SYMBOL_NAME(do_bottom_half)
decl SYMBOL_NAME(intr_count)
jmp 9f
ALIGN
reschedule:
pushl $ret_from_sys_call
jmp SYMBOL_NAME(schedule) # test
另外,一些与时钟中断及bottom half机制有关的数据结构介绍如下:
#define HZ 100
unsigned long volatile jiffies=0;
系统每隔10ms自动把它加1,它是核心系统计时的单位。
enum {
TIMER_BH = 0,
CONSOLE_BH,
TQUEUE_BH,
DIGI_BH,
SERIAL_BH,
RISCOM8_BH,
SPECIALIX_BH,
BAYCOM_BH,
NET_BH,
IMMEDIATE_BH,
KEYBOARD_BH,
CYCLADES_BH,
CM206_BH
};
现在只定义了13个bottom half队列,将来可扩充到32个队列。
unsigned long intr_count = 0;
相当于信号量的作用。只有其等于0,才可以do_bottom_half。
int bh_mask_count[32];
用来计算bottom half队列被屏蔽的次数。只有某队列的bh_mask_count数为0,才能enable该队列。
unsigned long bh_active = 0;
bh_active是32位长整数,每一位表示一个bottom half队列,该位置1,表示该队列处于激活状态,随时准备在CPU认为合适的时候执行该队列的服务,置0则相反。
unsigned long bh_mask = 0;
bh_mask也是32位长整数,每一位对应一个bottom half队列,该位置1,表示该队列可用,并把处理函数的入口地址赋给bh_base,置0则相反。
void (*bh_base[32])(void);
bottom half服务函数入口地址数组。定时器处理函数拥有最高的优先级,它的地址存放在bh_base[0],总是最先执行它所指向的函数。
我们注意到,在IRQ#_interrupt和fast_IRQ#_interrupt中断函数处理返回前,都通过语句jmp ret_from_sys_call,跳到系统调用的返回处(见irq.h),如果bottom half队列不为空,则在那里做类似:
if (bh_active & bh_mask) {
intr_count = 1;
do_bottom_half();
intr_count = 0;
}(该判断的汇编代码见Entry.S)
的判断,调用do_bottom_half()函数。
在CPU调度时,通过schedule函数执行上述的判断,再调用do_bottom_half()函数。
总而言之,在下列三种时机:
CPU调度时
系统调用返回前
中断处理返回前
都会作判断调用do_bottom_half函数。Do_bottom_half函数依次扫描32个队列,找出需要服务的队列,执行服务后把对应该队列的bh_active的相应位置0。由于bh_active标志中TIMER_BH对应的bit为1,因而系统根据服务函数入口地址数组bh_base找到函数timer_bh()的入口地址,并马上执行该函数,在函数timer_bh中,调用函数run_timer_list()和函数run_old_timers()函数,定时执行服务。
TVECS结构及其实现
有关TVECS结构的一些数据结构定义如下:
#define TVN_BITS 6
#define TVR_BITS 8
#define TVN_SIZE (1 << TVN_BITS)
#define TVR_SIZE (1 << TVR_BITS)
#define TVN_MASK (TVN_SIZE - 1)
#define TVR_MASK (TVR_SIZE - 1)
#define SLOW_BUT_DEBUGGING_TIMERS 0
struct timer_vec {
int index;
struct timer_list *vec[TVN_SIZE];
};
struct timer_vec_root {
int index;
struct timer_list *vec[TVR_SIZE];
};
static struct timer_vec tv5 = { 0 };
static struct timer_vec tv4 = { 0 };
static struct timer_vec tv3 = { 0 };
static struct timer_vec tv2 = { 0 };
static struct timer_vec_root tv1 = { 0 };
static struct timer_vec * const tvecs[] = {
(struct timer_vec *)&tv1, &tv2, &tv3, &tv4, &tv5
};
#define NOOF_TVECS (sizeof(tvecs) / sizeof(tvecs[0]))
static unsigned long timer_jiffies = 0;
TVECS结构是一个元素个数为5的数组,分别指向tv1,tv2,tv3,tv4,tv5的地址。其中,tv1是结构timer_vec_root的变量,它有一个index域和有256个元素的指针数组,该数组的每个元素都是一条类型为timer_list的链表。其余四个元素都是结构timer_vec的变量,它们各有一个index域和64个元素的指针数组,这些数组的每个元素也都是一条链表。
函数internal_add_timer(struct timer_list *timer)
函数代码如下:
static inline void internal_add_timer(struct timer_list *timer)
{
/*
* must be cli-ed when calling this
*/
unsigned long expires = timer->;expires;
unsigned long idx = expires - timer_jiffies;
if (idx < TVR_SIZE) {
int i = expires & TVR_MASK;
insert_timer(timer, tv1.vec, i);
} else if (idx < 1 << (TVR_BITS + TVN_BITS)) {
int i = (expires >;>; TVR_BITS) & TVN_MASK;
insert_timer(timer, tv2.vec, i);
} else if (idx < 1 << (TVR_BITS + 2 * TVN_BITS)) {
int i = (expires >;>; (TVR_BITS + TVN_BITS)) & TVN_MASK;
insert_timer(timer, tv3.vec, i);
} else if (idx < 1 << (TVR_BITS + 3 * TVN_BITS)) {
int i = (expires >;>; (TVR_BITS + 2 * TVN_BITS)) & TVN_MASK;
insert_timer(timer, tv4.vec, i);
} else if (expires < timer_jiffies) {
/* can happen if you add a timer with expires == jiffies,
* or you set a timer to go off in the past
*/
insert_timer(timer, tv1.vec, tv1.index);
} else if (idx < 0xffffffffUL) {
int i = (expires >;>; (TVR_BITS + 3 * TVN_BITS)) & TVN_MASK;
insert_timer(timer, tv5.vec, i);
} else {
/* Can only get here on architectures with 64-bit jiffies */
timer->;next = timer->;prev = timer;
}
}
expires
在调用该函数之前,必须关中。对该函数的说明如下:
取出要加进TVECS的timer的激发时间(expires),算出expires与timer_jiffies的差值idx,用来决定该插到哪个队列中去。
若idx小于2^8,则取expires的第0位到第7位的值I,把timer加到tv1.vec中第I个链表的第一个表项之前。
若idx小于2^14,则取expires的第8位到第13位的值I,把timer加到tv2.vec中第I个链表的第一个表项之前。
若idx小于2^20,则取expires的第14位到第19位的值I,把timer加到tv3.vec中第I个链表的第一个表项之前。
若idx小于2^26,则取expires的第20位到第25位的值I,把timer加到tv4.vec中第I个链表的第一个表项之前。
若expires小于timer_jiffies,即idx小于0,则表明该timer到期,应该把timer放入tv1.vec中tv1.index指定的链表的第一个表项之前。
若idx小于2^32,则取expires的第26位到第32位的值I,把timer加到tv5.vec中第I个链表的第一个表项之前。
若idx大等于2^32,该情况只有在64位的机器上才有可能发生,在这种情况下,不把timer加入TVECS结构。
函数cascade_timers(struct timer_vec *tv)
该函数只是把tv->;index指定的那条链表上的所有timer调用internal_add_timer()函数进行重新调整,这些timer将放入TVECS结构中比原来位置往前移一级,比如说,tv4上的timer将放到tv3上去,tv2上的timer将放到tv1上。这种前移是由run_timer_list函数里调用cascade_timers函数的时机来保证的。然后把该条链表置空,tv->;index加1,若tv->;index等于64,则重新置为0。
函数run_timer_list()
函数代码如下:
static inline void run_timer_list(void)
{
cli();
while ((long)(jiffies - timer_jiffies) >;= 0) {
struct timer_list *timer;
if (!tv1.index) {
int n = 1;
do {
cascade_timers(tvecs[n]);
} while (tvecs[n]->;index == 1 && ++n < NOOF_TVECS);
}
while ((timer = tv1.vec[tv1.index])) {
void (*fn)(unsigned long) = timer->;function;
unsigned long data = timer->;data;
detach_timer(timer);
timer->;next = timer->;prev = NULL;
sti();
fn(data);
cli();
}
++timer_jiffies;
tv1.index = (tv1.index + 1) & TVR_MASK;
}
sti();
}
对run_timer_list函数的说明如下:
关中。
判断jiffies是否大等于timer_jiffies,若不是,goto 8。
判断tv1.index是否为0(即此时系统已经扫描过整个tv1的256个timer_list链表,又回到的第一个链表处,此时需重整TVECS结构),若是,置n为1;若不是,goto 6。
调用cascade_timers()函数把TVECS[n]中由其index指定的那条链表上的timer放到TVECS[n-1]中来。注意:调用cascade_timers()函数后,index已经加1。
判断TVECS[n]->;index是否为1,即原来为0。如果是(表明TVECS[n]上所有都已经扫描一遍,此时需对其后一级的TVECS[++n]调用cascade_timers()进行重整),把n加1,goto 4。
执行tv1.vec上由tv1->;index指定的那条链表上的所有timer的服务函数,并把该timer从链表中移走。在执行服务函数的过程中,允许中断。
timer_jiffies加1,tv1->;index加1,若tv1->;index等于256,则重新置为0,goto 2。
开中,返回。
Linux提供了两种定时器服务。一种早期的由timer_struct等结构描述,由run_old_times函数处理。另一种“新”的服务由timer_list等结构描述,由add_timer、del_timer、cascade_time和run_timer_list等函数处理。
早期的定时器服务利用如下数据结构:
struct timer_struct {
unsigned long expires; /*本定时器被唤醒的时刻 */
void (*fn)(void); /* 定时器唤醒后的处理函数 */
}
struct timer_struct timer_table[32]; /*最多可同时启用32个定时器 */
unsigned long timer_active; /* 每位对应一定时器,置1表示启用 */
新的定时器服务依靠链表结构突破了32个的限制,利用如下的数据结构:
struct timer_list {
struct timer_list *next;
struct timer_list *prev;
unsigned long expires;
unsigned long data; /* 用来存放当前进程的PCB块的指针,可作为参数传
void (*function)(unsigned long); 给function */
}
表示上述数据结构的图示如下:
在这里,顺便简单介绍一下旧的timer机制的运作情况。
系统在每次调用函数do_bottom_half时,都会调用一次函数run_old_timers()。
函数run_old_timers()
该函数处理的很简单,只不过依次扫描timer_table中的32个定时器,若扫描到的定时器已经到期,并且已经被激活,则执行该timer的服务函数。
间隔定时器itimer
系统为每个进程提供了三个间隔定时器。当其中任意一个定时器到期时,就会发出一个信号给进程,同时,定时器重新开始运作。三种定时器描述如下:
ITIMER_REAL 真实时钟,到期时送出SIGALRM信号。
ITIMER_VIRTUAL 仅在进程运行时的计时,到期时送出SIGVTALRM信号。
ITIMER_PROF 不仅在进程运行时计时,在系统为进程运作而运行时它也计时,与ITIMER_VIRTUAL对比,该定时器通常为那些在用户态和核心态空间运行的应用所花去的时间计时,到期时送出SIGPROF信号。
与itimer有关的数据结构定义如下:
struct timespec {
long tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};
struct timeval {
int tv_sec; /* seconds */
int tv_usec; /* microseconds */
};
struct itimerspec {
struct timespec it_interval; /* timer period */
struct timespec it_value; /* timer expiration */
};
struct itimerval {
struct timeval it_interval; /* timer interval */
struct timeval it_value; /* current value */
};
这三种定时器在task_struct中定义:
struct task_struct {
……
unsigned long timeout;
unsigned long it_real_value,it_prof_value,it_virt_value;
unsigned long it_real_incr,it_prof_incr,it_virt_incr;
struct timer_list real_timer;
……
}
在进程创建时,系统把it_real_fn函数的入口地址赋给real_timer.function。(见sched.h)
我们小组分析了三个系统调用:sys_getitimer,sys_setitimer,sys_alarm。
在这三个系统调用中,需用到以下一些函数:
函数static int _getitimer(int which, struct itimerval *value)
该函数的运行过程大致如下:
根据传进的参数which按三种itimer分别处理:
若是ITIMER_REAL,则设置interval为current进程的it_real_incr,val设置为0;判断current进程的real_timer有否设置并挂入TVECS结构中,若有,设置val为current进程real_timer的expires,并把real_timer重新挂到TVECS结构中,接着把val与当前jiffies作比较,若小等于当前jiffies,则说明该real_timer已经到期,于是重新设置val为当前jiffies的值加1。最后把val减去当前jiffies的值,goto 2。
若是ITIMER_VIRTUAL,则分别设置interval,val的值为current进程的it_virt_incr、it_virt_value,goto 2。
若是ITIMER_PROF,则分别设置interval,val的值为current进程的it_prof_incr、it_prof_value,goto 2。
(2)调用函数jiffiestotv把val,interval的jiffies值转换为timeval,返回0。
函数 int _setitimer(int which, struct itimerval *value, struct itimerval *ovalue)
该函数的运行过程大致如下:
调用函数tvtojiffies把value中的interval和value转换为jiffies i 和 j。
判断指针ovalue是否为空,若空,goto ;若不空,则把由which指定类型的itimer存入ovalue中,若存放不成功,goto 4;
根据which指定的itimer按三种类型分别处理:
若是ITIMER_REAL,则从TVECS结构中取出current进程的real_timer,并重新设置current进程的it_real_value和it_real_incr为j和i。若j等于0,goto 4;若不等于0,则把当前jiffies的值加上定时器剩余时间j,得到触发时间。若i小于j,则表明I已经溢出,应该重新设为ULONG_MAX。最后把current进程的real_timer的expires设为i,把设置过的real_timer重新加入TVECS结构,goto 4。
若是ITIMER_VIRTUAL,则设置current进程的it-_virt_value和it_virt_incr为j和i。
若是ITIMER_PROF,则设置current进程的it-_prof_value和it_prof_incr为j和i。
(4)返回0。
函数verify_area(int type, const void *addr, unsigned long size)
该函数的主要功能是对以addr为始址的,长度为size的一块存储区是否有type类型的操作权利。
函数memcpy_tofs(to, from, n)
该函数的主要功能是从以from为始址的存储区中取出长度为n的一块数据放入以to为始址的存储区。
函数memcpy_fromfs(from, to, n)
该函数的主要功能是从以from为始址的存储区中取出长度为n的一块数据放入以to为始址的存储区。
函数memset((char*)&set_buffer, 0, sizeof(set_buffer))
该函数的主要功能是把set_buffer中的内容置为0,在这里,即把it_value和it_interval置为0。
现在,我简单介绍一下这三个系统调用:
系统调用sys_getitimer(int which, struct itimerval *value)
首先,若value为NULL,则返回-EFAULT,说明这是一个bad address。
其次,把which类型的itimer取出放入get_buffer。
再次,若存放成功,再确认对value的写权利。
最后,则把get_buffer中的itimer取出,拷入value。
系统调用sys_setitimer(int which, struct itimerval *value,struct itimerval *ovalue)
首先,判断value是否为NULL,若不是,则确认对value是否有读的权利,并把set_buffer中的数据拷入value;若value为NULL,则把set_buffer中的内容置为0,即把it_value和it_interval置为0。
其次,判断ovalue是否为NULL,若不是,则确认对ovalue是否有写的权利。
再次,调用函数_setitimer设置由which指定类型的itimer。
最后,调用函数memcpy_tofs把get_buffer中的数据拷入ovalue,返回。
系统调用sys_alarm(unsigned int seconds)
该系统调用重新设置进程的real_itimer,若seconds为0,则把原先的alarm定时器删掉。并且设interval为0,故只触发一次,并把旧的real_timer存入oldalarm,并返回oldalarm。
[目录]
--------------------------------------------------------------------------------
from aka
[目录]
--------------------------------------------------------------------------------
硬件中断
硬件中断
硬件中断概述
中断可以用下面的流程来表示:
中断产生源 -->; 中断向量表 (idt) -->; 中断入口 ( 一般简单处理后调用相应的函数) --->;do_IRQ-->; 后续处理(软中断等工作)
具体地说,处理过程如下:
中断信号由外部设备发送到中断芯片(模块)的引脚
中断芯片将引脚的信号转换成数字信号传给CPU,例如8259主芯片引脚0发送的是0x20
CPU接收中断后,到中断向量表IDT中找中断向量
根据存在中断向量中的数值找到向量入口
由向量入口跳转到一个统一的处理函数do_IRQ
在do_IRQ中可能会标注一些软中断,在执行完do_IRQ后执行这些软中断。
下面一一介绍。
8259芯片
本文主要参考周明德《微型计算机系统原理及应用》和billpan的相关帖子
1.中断产生过程
(1)如果IR引脚上有信号,会使中断请求寄存器(Interrupt Request Register,IRR)相应的位置位,比如图中, IR3, IR4, IR5上有信号,那么IRR的3,4,5为1
(2)如果这些IRR中有一个是允许的,也就是没有被屏蔽,那么就会通过INT向CPU发出中断请求信号。屏蔽是由中断屏蔽寄存器(Interrupt Mask Register,IMR)来控制的,比如图中位3被置1,也就是IRR位3的信号被屏蔽了。在图中,还有4,5的信号没有被屏蔽,所以,会向CPU发出请求信号。
(3)如果CPU处于开中断状态,那么在执行指令的最后一个周期,在INTA上做出回应,并且关中断.
(4)8259A收到回应后,将中断服务寄存器(In-Service Register)置位,而将相应的IRR复位:
8259芯片会比较IRR中的中断的优先级,如上图中,由于IMR中位3处于屏蔽状态,所以实际上只是比较IR4,I5,缺省情况下,IR0最高,依次往下,IR7最低(这种优先级可以被设置),所以上图中,ISR被设置为4.
(5)在CPU发出下一个INTA信号时,8259将中断号送到数据线上,从而能被CPU接收到,这里有个问题:比如在上图中,8259获得的是数4,但是CPU需要的是中断号(并不为4),从而可以到idt找相应的向量。所以有一个从ISR的信号到中断号的转换。在Linux的设置中,4对应的中断号是0x24.
(6)如果8259处于自动结束中断(Automatic End of Interrupt AEOI)状态,那么在刚才那个INTA信号结束前,8259的ISR复位(也就是清0),如果不处于这个状态,那么直到CPU发出EOI指令,它才会使得ISR复位。
2.一些相关专题
(1)从8259
在x86单CPU的机器上采用两个8259芯片,主芯片如上图所示,x86模式规定,从8259将它的INT脚与主8259的IR2相连,这样,如果从8259芯片的引脚IR8-IR15上有中断,那么会在INT上产生信号,主8259在IR2上产生了一个硬件信号,当它如上面的步骤处理后将IR2的中断传送给CPU,收到应答后,会通过CAS通知从8259芯片,从8259芯片将IRQ中断号送到数据线上,从而被CPU接收。
由此,我猜测它产生的所有中断在主8259上优先级为2,不知道对不对。
(2)关于屏蔽
从上面可以看出,屏蔽有两种方法,一种作用于CPU, 通过清除IF标记,使得CPU不去响应8259在INT上的请求。也就是所谓关中断。
另一种方法是,作用于8259,通过给它指令设置IMR,使得相应的IRR不参与ISR(见上面的(4)),被称为禁止(disable),反之,被称为允许(enable).
每次设置IMR只需要对端口0x21(主)或0xA1(从)输出一个字节即可,字节每位对应于IMR每位,例如:
outb(cached_21,0x21);
为了统一处理16个中断,Linux用一个16位cached_irq_mask变量来记录这16个中断的屏蔽情况:
static unsigned int cached_irq_mask = 0xffff;
为了分别对应于主从芯片的8位IMR,将这16位cached_irq_mask分成两个8位的变量:
#define __byte(x,y) (((unsigned char *)&(y))[x])
#define cached_21 (__byte(0,cached_irq_mask))
#define cached_A1 (__byte(1,cached_irq_mask))
在禁用某个irq的时候,调用下面的函数:
void disable_8259A_irq(unsigned int irq){
unsigned int mask = 1 << irq;
unsigned long flags;
spin_lock_irqsave(&i8259A_lock, flags);
cached_irq_mask |= mask; /*-- 对这16位变量设置 */
if (irq & /*-- 看是对主8259设置还是对从芯片设置 */
outb(cached_A1,0xA1); /*-- 对从8259芯片设置 */
else
outb(cached_21,0x21); /*-- 对主8259芯片设置 */
spin_unlock_irqrestore(&i8259A_lock, flags);
}
(3)关于中断号的输出
8259在ISR里保存的只是irq的ID,但是它告诉CPU的是中断向量ID,比如ISR保存时钟中断的ID 0,但是在通知CPU却是中断号0x20.因此需要建立一个映射。在8259芯片产生的IRQ号必须是连续的,也就是如果irq0对应的是中断向量0x20,那么irq1对应的就是0x21,...
在i8259.c/init_8259A()中,进行设置:
outb_p(0x11, 0x20); /* ICW1: select 8259A-1 init */
outb_p(0x20 + 0, 0x21); /* ICW2: 8259A-1 IR0-7 mapped to 0x20-0x27 */
outb_p(0x04, 0x21); /* 8259A-1 (the master) has a slave on IR2 */
if (auto_eoi)
outb_p(0x03, 0x21); /* master does Auto EOI */
else
outb_p(0x01, 0x21); /* master expects normal EOI */
outb_p(0x11, 0xA0); /* ICW1: select 8259A-2 init */
outb_p(0x20 + 8, 0xA1); /* ICW2: 8259A-2 IR0-7 mapped to 0x28-0x2f */
outb_p(0x02, 0xA1); /* 8259A-2 is a slave on master's IR2 */
outb_p(0x01, 0xA1); /* (slave's support for AEOI in flat mode is to be investigated) */
这样,在IDT的向量0x20-0x2f可以分别填入相应的中断处理函数的地址了。
i386中断门描述符
段选择符和偏移量决定了中断处理函数的入口地址
在这里段选择符指向内核中唯一的一个代码段描述符的地址__KERNEL_CS(=0x10),而这个描述符定义的段为0到4G:
---------------------------------------------------------------------------------
ENTRY(gdt_table) .quad 0x0000000000000000 /* NULL descriptor */
.quad 0x0000000000000000 /* not used */
.quad 0x00cf9a000000ffff /* 0x10 kernel 4GB code at 0x00000000 */
... ...
---------------------------------------------------------------------------------
而偏移量就成了绝对的偏移量了,在IDT的描述符中被拆成了两部分,分别放在头和尾。
P标志着这个代码段是否在内存中,本来是i386提供的类似缺页的机制,在Linux中这个已经不用了,都设成1(当然内核代码是永驻内存的,但即使不在内存,推测linux也只会用缺页的标志)。
DPL在这里是0级(特权级)
0D110中,D为1,表明是32位程序(这个细节见i386开发手册).110是中断门的标识,其它101是任务门的标识, 111是陷阱(trap)门标识。
Linux对中断门的设置
于是在Linux中对硬件中断的中断门的设置为:
init_IRQ(void)
---------------------------------------------------------
for (i = 0; i < NR_IRQS; i++) {
int vector = FIRST_EXTERNAL_VECTOR + i;
if (vector != SYSCALL_VECTOR)
set_intr_gate(vector, interrupt[ i]);
}
----------------------------------------------------------
其中,FIRST_EXTERNAL_VECTOR=0x20,恰好为8259芯片的IR0的中断门(见8259部分),也就是时钟中断的中断门),interrupt[ i]为相应处理函数的入口地址
NR_IRQS=224, =256(IDT的向量总数)-32(CPU保留的中断的个数),在这里设置了所有可设置的向量。
SYSCALL_VECTOR=0x80,在这里意思是避开系统调用这个向量。
而set_intr_gate的定义是这样的:
----------------------------------------------------
void set_intr_gate(unsigned int n, void *addr){
_set_gate(idt_table+n,14,0,addr);
}
----------------------------------------------------
其中,需要解释的是:14是标识指明这个是中断门,注意上面的0D110=01110=14;另外,0指明的是DPL.
中断入口
以8259的16个中断为例:
通过宏BUILD_16_IRQS(0x0), BI(x,y),以及
#define BUILD_IRQ(nr) \
asmlinkage void IRQ_NAME(nr); \
__asm__( \
"\n"__ALIGN_STR"\n" \
SYMBOL_NAME_STR(IRQ) #nr "_interrupt:\n\t" \
"pushl $"#nr"-256\n\t" \
"jmp common_interrupt";
得到的16个中断处理函数为:
IRQ0x00_interrupt:
push $0x00 - 256
jump common_interrupt
IRQ0x00_interrupt:
push $0x01 - 256
jump common_interrupt
... ...
IRQ0x0f_interrupt:
push $0x0f - 256
jump common_interrupt
这些处理函数简单的把中断号-256(为什么-256,也许是避免和内部中断的中断号有冲突)压到栈中,然后跳到common_interrupt
其中common_interrupt是由宏BUILD_COMMON_IRQ()展开:
#define BUILD_COMMON_IRQ() \
asmlinkage void call_do_IRQ(void); \
__asm__( \
"\n" __ALIGN_STR"\n" \
"common_interrupt:\n\t" \
SAVE_ALL \
"pushl $ret_from_intr\n\t" \
SYMBOL_NAME_STR(call_do_IRQ)":\n\t" \
"jmp "SYMBOL_NAME_STR(do_IRQ));
.align 4,0x90common_interrupt:
SAVE_ALL展开的保护现场部分
push $ret_from_intrcall
do_IRQ:
jump do_IRQ;
从上面可以看出,这16个的中断处理函数不过是把中断号-256压入栈中,然后保护现场,最后调用do_IRQ .在common_interrupt中,为了使do_IRQ返回到entry.S的ret_from_intr标号,所以采用的是压入返回点ret_from_intr,用jump来模拟一个从ret_from_intr上面对do_IRQ的一个调用。
和IDT的衔接
为了便于IDT的设置,在数组interrupt中填入所有中断处理函数的地址:
void (*interrupt[NR_IRQS])(void) = {
IRQ0x00_interrupt,
IRQ0x01_interrupt,
... ...
}
在中断门的设置中,可以看到是如何利用这个数组的。
硬件中断处理函数do_IRQ
do_IRQ的相关对象
在do_IRQ中,一个中断主要由三个对象来完成
其中, irq_desc_t对象构成的irq_desc[]数组元素分别对应了224个硬件中断(idt一共256项,cpu自己前保留了32项,256-32=224,当然这里面有些项是不用的,比如x80是系统调用).
当发生中断时,函数do_IRQ就会在irq_desc[]相应的项中提取各种信息来完成对中断的处理。
irq_desc有一个字段handler指向发出这个中断的设备的处理对象hw_irq_controller,比如在单CPU,这个对象一般就是处理芯片8259的对象。为什么要指向这个对象呢?因为当发生中断的时候,内核需要对相应的中断进行一些处理,比如屏蔽这个中断等。这个时候需要对中断设备(比如8259芯片)进行操作,于是可以通过这个指针指向的对象进行操作。
irq_desc还有一个字段action指向对象irqaction,后者是产生中断的设备的处理对象,其中的handler就是处理函数。由于一个中断可以由多个设备发出,Linux内核采用轮询的方式,将所有产生这个中断的设备的处理对象连成一个链表,一个一个执行。
例如,硬盘1,硬盘2都产生中断IRQx,在do_IRQ中首先找到irq_desc[x],通过字段handler对产生中断IRQx的设备进行处理(对8259而言,就是屏蔽以后的中断IRQx),然后通过action先后运行硬盘1和硬盘2的处理函数。
hw_irq_controller
hw_irq_controller有多种:
1.在一般单cpu的机器上,通常采用两个8259芯片,因此hw_irq_controller指的就是i8259A_irq_type
2.在多CPU的机器上,采用APIC子系统来处理芯片,APIC有3个部分组成,一个是I/O APIC模块,其作用可比做8259芯片,但是它发出的中断信号会通过 APIC总线送到其中一个(或几个)CPU中的Local APIC模块,因此,它还起一个路由的作用;它可以接收16个中断。
中断可以采取两种方式,电平触发和边沿触发,相应的,I/O APIC模块的hw_irq_controller就有两种:
ioapic_level_irq_type
ioapic_edge_irq_type
(这里指的是intel的APIC,还有其它公司研制的APIC,我没有研究过)
3. Local APIC自己也能单独处理一些直接对CPU产生的中断,例如时钟中断(这和没有使用Local APIC模块的CPU不同,它们接收的时钟中断来自外围的时钟芯片),因此,它也有自己的 hw_irq_controller:
lapic_irq_type
struct hw_interrupt_type {
const char * typename;
unsigned int (*startup)(unsigned int irq);
void (*shutdown)(unsigned int irq);
void (*enable)(unsigned int irq);
void (*disable)(unsigned int irq);
void (*ack)(unsigned int irq);
void (*end)(unsigned int irq);
void (*set_affinity)(unsigned int irq, unsigned long mask);
};
typedef struct hw_interrupt_type hw_irq_controller;
startup 是启动中断芯片(模块),使得它开始接收中断,一般情况下,就是将 所有被屏蔽的引脚取消屏蔽
shutdown 反之,使得芯片不再接收中断
enable 设某个引脚可以接收中断,也就是取消屏蔽
disable 屏蔽某个引脚,例如,如果屏蔽0那么时钟中断就不再发生
ack 当CPU收到来自中断芯片的中断信号,给相应的引脚的处理,这个各种情况下 (8259, APIC电平,边沿)的处理都不相同
end 在CPU处理完某个引脚产生的中断后,对中断芯片(模块)的操作。
irqaction
将一个硬件处理函数挂到相应的处理队列上去(当然首先要生成一个irqaction结构):
-----------------------------------------------------
int request_irq(unsigned int irq,
void (*handler)(int, void *, struct pt_regs *),
unsigned long irqflags,
const char * devname,
void *dev_id)
-----------------------------------------------------
参数说明在源文件里说得非常清楚。
handler是硬件处理函数,在下面的代码中可以看得很清楚:
---------------------------------------------
do {
status |= action->;flags;
action->;handler(irq, action->;dev_id, regs);
action = action->;next;
} while (action);
---------------------------------------------
第二个参数就是action的dev_id,这个参数非常灵活,可以派各种用处。而且要保证的是,这个dev_id在这个处理链中是唯一的,否则删除会遇到麻烦。
第三个参数是在entry.S中压入的各个积存器的值。
它的大致流程是:
1.在slab中分配一个irqaction,填上必需的数据
以下在函数setup_irq中。
2.找到它的irq对应的结构irq_desc
3.看它是否想对随机数做贡献
4.看这个结构上是否已经挂了其它处理函数了,如果有,则必须确保它本身和这个队列上所有的处理函数都是可共享的(由于传递性,只需判断一个就可以了)
5.挂到队列最后
6.如果这个irq_desc只有它一个irqaction,那么还要进行一些初始化工作
7在proc/下面登记 register_irq_proc(irq)(这个我不太明白)
将一个处理函数取下:
void free_irq(unsigned int irq, void *dev_id)
首先在队列里找到这个处理函数(严格的说是irqaction),主要靠dev_id来匹配,这时dev_id的唯一性就比较重要了。
将它从队列里剔除。
如果这个中断号没有处理函数了,那么禁止这个中断号上再产生中断:
if (!desc->;action) {
desc->;status |= IRQ_DISABLED;
desc->;handler->;shutdown(irq);
}
如果其它CPU在运行这个处理函数,要等到它运行完了,才释放它:
#ifdef CONFIG_SMP
/* Wait to make sure it's not being used on another CPU */
while (desc->;status & IRQ_INPROGRESS)
barrier();
#endif
kfree(action);
do_IRQ
asmlinkage unsigned int do_IRQ(struct pt_regs regs)
1.首先取中断号,并且获取对应的irq_desc:
int irq = regs.orig_eax & 0xff; /* high bits used in ret_from_ code */
int cpu = smp_processor_id();
irq_desc_t *desc = irq_desc + irq;
2.对中断芯片(模块)应答:
desc->;handler->;ack(irq);
3.修改它的状态(注:这些状态我觉得只有在SMP下才有意义):
status = desc->;status & ~(IRQ_REPLAY | IRQ_WAITING);
status |= IRQ_PENDING; /* we _want_ to handle it */
IRQ_REPLAY是指如果被禁止的中断号上又产生了中断,这个中断是不会被处理的,当这个中断号被允许产生中断时,会将这个未被处理的中断转为IRQ_REPLAY。
IRQ_WAITING 探测用,探测时,会将所有没有挂处理函数的中断号上设置IRQ_WAITING,如果这个中断号上有中断产生,就把这个状态去掉,因此,我们就可以知道哪些中断引脚上产生过中断了。
IRQ_PENDING , IRQ_INPROGRESS是为了确保:
同一个中断号的处理程序不能重入
不能丢失这个中断号的下一个处理程序
具体的说,当内核在运行某个中断号对应的处理程序(链)时,状态会设置成IRQ_INPROGRESS。如果在这期间,同一个中断号上又产生了中断,并且传给CPU,那么当内核打算再次运行这个中断号对应的处理程序(链)时,发现已经有一个实例在运行了,就将这下一个中断标注为IRQ_PENDING, 然后返回。这个已在运行的实例结束的时候,会查看是否期间有同一中断发生了,是则再次执行一遍。
这些状态的操作不是在什么情况下都必须的,事实上,一个CPU,用8259芯片,无论即使是开中断,也不会发生中断重入的情况,因为在这期间,内核把同一中断屏蔽掉了。
多个CPU比较复杂,因为CPU由Local APIC,每个都有自己的中断,但是它们可能调用同一个函数,比如时钟中断,每个CPU都可能产生,它们都会调用时钟中断处理函数。
从I/O APIC传过来的中断,如果是电平触发,也不会,因为在结束发出EOI前,这个引脚上是不接收中断信号。如果是边沿触发,要么是开中断,要么I/O APIC选择不同的CPU,在这两种情况下,会有重入的可能。
/*
* If the IRQ is disabled for whatever reason, we cannot
* use the action we have.
*/
action = NULL;
if (!(status & (IRQ_DISABLED | IRQ_INPROGRESS))) {
action = desc->;action;
status &= ~IRQ_PENDING; /* we commit to handling */
status |= IRQ_INPROGRESS; /* we are handling it *//*进入执行状态*/
}
desc->;status = status;
/*
* If there is no IRQ handler or it was disabled, exit early.
Since we set PENDING, if another processor is handling
a different instance of this same irq, the other processor
will take care of it.
*/
if (!action)
goto out;/*要么该中断没有处理函数;要么被禁止运行(IRQ_DISABLE);要么有一个实例已经在运行了*/
/*
* Edge triggered interrupts need to remember
* pending events.
* This applies to any hw interrupts that allow a second
* instance of the same irq to arrive while we are in do_IRQ
* or in the handler. But the code here only handles the _second_
* instance of the irq, not the third or fourth. So it is mostly
* useful for irq hardware that does not mask cleanly in an
* SMP environment.
*/
for (; {
spin_unlock(&desc->;lock);
handle_IRQ_event(irq, ®s, action);/*执行函数链*/
spin_lock(&desc->;lock);
if (!(desc->;status & IRQ_PENDING))/*发现期间有中断,就再次执行*/
break;
desc->;status &= ~IRQ_PENDING;
}
desc->;status &= ~IRQ_INPROGRESS;/*退出执行状态*/
out:
/*
* The ->;end() handler has to deal with interrupts which got
* disabled while the handler was running.
*/
desc->;handler->;end(irq);/*给中断芯片一个结束的操作,一般是允许再次接收中断*/
spin_unlock(&desc->;lock);
if (softirq_active(cpu) & softirq_mask(cpu))
do_softirq();/*执行软中断*/
return 1;
}
[目录]
--------------------------------------------------------------------------------
软中断
软中断softirq
softirq简介
提出softirq的机制的目的和老版本的底半部分的目的是一致的,都是将某个中断处理的一部分任务延迟到后面去执行。
Linux内核中一共可以有32个softirq,每个softirq实际上就是指向一个函数。当内核执行softirq(do_softirq),就对这32个softirq进行轮询:
(1)是否该softirq被定义了,并且允许被执行?
(2)是否激活了(也就是以前有中断要求它执行)?
如果得到肯定的答复,那么就执行这个softirq指向的函数。
值得一提的是,无论有多少个CPU,内核一共只有32个公共的softirq,但是每个CPU可以执行不同的softirq,可以禁止/起用不同的softirq,可以激活不同的softirq,因此,可以说,所有CPU有相同的例程,但是
每个CPU却有自己完全独立的实例。
对(1)的判断是通过考察irq_stat[ cpu ].mask相应的位得到的。这里面的cpu指的是当前指令所在的cpu.在一开始,softirq被定义时,所有的cpu的掩码mask都是一样的。但是在实际运行中,每个cpu上运行的程序可以根据自己的需要调整。
对(2)的判断是通过考察irq_stat[ cpu ].active相应的位得到的.
虽然原则上可以任意定义每个softirq的函数,Linux内核为了进一步加强延迟中断功能,提出了tasklet的机制。tasklet实际上也就是一个函数。在第0个softirq的处理函数tasklet_hi_action中,我们可以看到,当执行这个函数的时候,会依次执行一个链表上所有的tasklet.
我们大致上可以把softirq的机制概括成:
内核依次对32个softirq轮询,如果遇到一个可以执行并且需要的softirq,就执行对应的函数,这些函数有可能又会执行一个函数队列。当执行完这个函数队列后,才会继续询问下一个softirq对应的函数。
挂上一个软中断
void open_softirq(int nr, void (*action)(struct softirq_action*), void *data)
{
unsigned long flags;
int i;
spin_lock_irqsave(&softirq_mask_lock, flags);
softirq_vec[nr].data = data;
softirq_vec[nr].action = action;
for (i=0; i<NR_CPUS; i++)
softirq_mask(i) |= (1<<nr);
spin_unlock_irqrestore(&softirq_mask_lock, flags);
}
其中对每个CPU的softirq_mask都标注一下,表明这个softirq被定义了。
tasklet
在这个32个softirq中,有的softirq的函数会依次执行一个队列中的tasklet
tasklet其实就是一个函数。它的结构如下:
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};
next 用于将tasklet串成一个队列
state 表示一些状态,后面详细讨论
count 用来禁用(count = 1 )或者启用( count = 0 )这个tasklet.因为一旦一个tasklet被挂到队列里,如果没有这个机制,它就一定会被执行。 这个count算是一个事后补救措施,万一挂上了不想执行,就可以把它置1。
func 即为所要执行的函数。
data 由于可能多个tasklet调用公用函数,因此用data可以区分不同tasklet.
如何将一个tasklet挂上
首先要初始化一个tasklet,填上相应的参数
void tasklet_init(struct tasklet_struct *t,
void (*func)(unsigned long), unsigned long data)
{
t->;func = func;
t->;data = data;
t->;state = 0;
atomic_set(&t->;count, 0);
}
然后调用schedule函数,注意,下面的函数仅仅是将这个tasklet挂到 TASKLET_SOFTIRQ对应的软中断所执行的tasklet队列上去, 事实上,还有其它的软中断,比如HI_SOFTIRQ,会执行其它的tasklet队列,如果要挂上,那么就要调用tasklet_hi_schedule(). 如果你自己写的softirq执行一个tasklet队列,那么你需要自己写类似下面的函数。
static inline void tasklet_schedule(struct tasklet_struct *t)
{
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->;state)) {
int cpu = smp_processor_id();
unsigned long flags;
local_irq_save(flags);
/**/ t->;next = tasklet_vec[cpu].list;
/**/ tasklet_vec[cpu].list = t;
__cpu_raise_softirq(cpu, TASKLET_SOFTIRQ);
local_irq_restore(flags);
}
}
这个函数中/**/标注的句子用来挂接上tasklet,
__cpu_raise_softirq用来激活TASKLET_SOFTIRQ,这样,下次执行do_softirq就会执行这个TASKLET_SOFTIRQ软中断了
__cpu_raise_softirq定义如下:
static inline void __cpu_raise_softirq(int cpu, int nr)
{
softirq_active(cpu) |= (1<<nr);
}
tasklet的运行方式
我们以tasklet_action为例,来说明tasklet运行机制。事实上,还有一个函数tasklet_hi_action同样也运行tasklet队列。
首先值得注意的是,我们前面提到过,所有的cpu共用32个softirq,但是同一个softirq在不同的cpu上执行的数据是独立的,基于这个原则,tasklet_vec对每个cpu都有一个,每个cpu都运行自己的tasklet队列。
当执行一个tasklet队列时,内核将这个队列摘下来,以list为队列头,然后从list的下一个开始依次执行。这样做达到什么效果呢?在执行这个队列时,这个队列的结构是静止的,如果在运行期间,有中断产生,并且往这个队列里添加tasklet的话,将填加到tasklet_vec[cpu].list中, 注意这个时候,这个队列里的任何tasklet都不会被执行,被执行的是list接管的队列。
见/*1*//*2/之间的代码。事实上,在一个队列上同时添加和运行也是可行的,没这个简洁。
-----------------------------------------------------------------
static void tasklet_action(struct softirq_action *a)
{
int cpu = smp_processor_id();
struct tasklet_struct *list;
/*1*/ local_irq_disable();
list = tasklet_vec[cpu].list;
tasklet_vec[cpu].list = NULL;
/*2*/ local_irq_enable();
while (list != NULL) {
struct tasklet_struct *t = list;
list = list->;next;
/*3*/ if (tasklet_trylock(t)) {
if (atomic_read(&t->;count) == 0) {
clear_bit(TASKLET_STATE_SCHED, &t->;state);
t->;func(t->;data);
/*
* talklet_trylock() uses test_and_set_bit that imply
* an mb when it returns zero, thus we need the explicit
* mb only here: while closing the critical section.
*/
#ifdef CONFIG_SMP
/*?*/ smp_mb__before_clear_bit();
#endif
tasklet_unlock(t);
continue;
}
tasklet_unlock(t);
}
/*4*/ local_irq_disable();
t->;next = tasklet_vec[cpu].list;
tasklet_vec[cpu].list = t;
__cpu_raise_softirq(cpu, TASKLET_SOFTIRQ);
/*5*/ local_irq_enable();
}
}
-------------------------------------------------------------
/*3*/看其它cpu是否还有同一个tasklet在执行,如果有的话,就首先将这个tasklet重新放到tasklet_vec[cpu].list指向的预备队列(见/*4*/~/*5*/),而后跳过这个tasklet.
这也就说明了tasklet是不可重入的,以防止两个相同的tasket访问同样的变量而产生竞争条件(race condition)
tasklet的状态
在tasklet_struct中有一个属性state,用来表示tasklet的状态:
tasklet的状态有3个:
1.当tasklet被挂到队列上,还没有执行的时候,是 TASKLET_STATE_SCHED
2.当tasklet开始要被执行的时候,是 TASKLET_STATE_RUN
其它时候,则没有这两个位的设置
其实还有另一对状态,禁止或允许,tasklet_struct中用count表示,用下面的函数操作
-----------------------------------------------------
static inline void tasklet_disable_nosync(struct tasklet_struct *t)
{
atomic_inc(&t->;count);
}
static inline void tasklet_disable(struct tasklet_struct *t)
{
tasklet_disable_nosync(t);
tasklet_unlock_wait(t);
}
static inline void tasklet_enable(struct tasklet_struct *t)
{
atomic_dec(&t->;count);
}
-------------------------------------------------------
下面来验证1,2这两个状态:
当被挂上队列时:
首先要测试它是否已经被别的cpu挂上了,如果已经在别的cpu挂上了,则不再将它挂上,否则设置状态为TASKLET_STATE_SCHED
static inline void tasklet_schedule(struct tasklet_struct *t)
{
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->;state)) {
... ...
}
为什么要这样做?试想,如果一个tasklet已经挂在一队列上,内核将沿着这个队列一个个执行,现在如果又被挂到另一个队列上,那么这个tasklet的指针指向另一个队列,内核就会沿着它走到错误的队列中去了。
tasklet开始执行时:
在tasklet_action中:
------------------------------------------------------------
while (list != NULL) {
struct tasklet_struct *t = list;
/*0*/ list = list->;next;
/*1*/ if (tasklet_trylock(t)) {
/*2*/ if (atomic_read(&t->;count) == 0) {
/*3*/ clear_bit(TASKLET_STATE_SCHED, &t->;state);
t->;func(t->;data);
/*
* talklet_trylock() uses test_and_set_bit that imply
* an mb when it returns zero, thus we need the explicit
* mb only here: while closing the critical section.
*/
#ifdef CONFIG_SMP
smp_mb__before_clear_bit();
#endif
/*4*/ tasklet_unlock(t);
continue;
}
---------------------------------------------------------------
1 看是否是别的cpu上这个tasklet已经是 TASKLET_STATE_RUN了,如果是就跳过这个tasklet
2 看这个tasklet是否被允许运行?
3 清除TASKLET_STATE_SCHED,为什么现在清除,它不是还没有从队列上摘下来吗?事实上,它的指针已经不再需要的,它的下一个tasklet已经被list记录了(/*0*/)。这样,如果其它cpu把它挂到其它的队列上去一点影响都没有。
4 清除TASKLET_STATE_RUN标志
1和4确保了在所有cpu上,不可能运行同一个tasklet,这样在一定程度上确保了tasklet对数据操作是安全的,但是不要忘了,多个tasklet可能指向同一个函数,所以仍然会发生竞争条件。
可能会有疑问:假设cpu 1上已经有tasklet 1挂在队列上了,cpu2应该根本挂不上同一个tasklet 1,怎么会有tasklet 1和它发生重入的情况呢?
答案就在/*3*/上,当cpu 1的tasklet 1已经不是TASKLET_STATE_SCHED,而它还在运行,这时cpu2完全有可能挂上同一个tasklet 1,而且使得它试图运行,这时/*1*/的判断就起作用了。
软中断的重入
一般情况下,在硬件中断处理程序后都会试图调用do_softirq执行软中断,但是如果发现现在已经有中断在运行,或者已经有软中断在运行,则
不再运行自己调用的中断。也就是说,软中断是不能进入硬件中断部分的,并且软中断在一个cpu上是不可重入的,或者说是串行化的(serialize)
其目的是避免访问同样的变量导致竞争条件的出现。在开中断的中断处理程序中不允许调用软中断可能是希望这个中断处理程序尽快结束。
这是由do_softirq中的
if (in_interrupt())
return;
保证的.
其中,
#define in_interrupt() ({ int __cpu = smp_processor_id(); \
(local_irq_count(__cpu) + local_bh_count(__cpu) != 0); })
前者local_irq_count(_cpu):
当进入硬件中断处理程序时,handle_IRQ_event中的irq_enter(cpu, irq)会将它加1,表明又进入一个硬件中断
退出则调用irq_exit(cpu, irq)
后者local_bh_count(__cpu) :
当进入软中断处理程序时,do_softirq中的local_bh_disable()会将它加1,表明处于软中断中
local_bh_disable();
一个例子:
当内核正在执行处理定时器的软中断时,这期间可能会发生多个时钟中断,这些时钟中断的处理程序都试图再次运行处理定时器的软中断,但是由于 已经有个软中断在运行了,于是就放弃返回。
软中断调用时机
最直接的调用:
当硬中断执行完后,迅速调用do_softirq来执行软中断(见下面的代码),这样,被硬中断标注的软中断能得以迅速执行。当然,不是每次调用都成功的,见前面关于重入的帖子。
内存
内存管理系统是操作系统中最为重要的部分,因为系统的物理内存总是少于系统所需要的内存数量。虚拟内存就是为了克服这个矛盾而采用的策略。系统的虚拟内存通过在各个进程之间共享内存而使系统看起来有多于实际内存的内存容量。
虚拟内存可以提供以下的功能:
*广阔的地址空间。
系统的虚拟内存可以比系统的实际内存大很多倍。
*进程的保护。
系统中的每一个进程都有自己的虚拟地址空间。这些虚拟地址空间是完全分开的,这样一个进程的运行不会影响其他进程。并且,硬件上的虚拟内存机制是被保护的,内存不能被写入,这样可以防止迷失的应用程序覆盖代码的数据。
*内存映射。
内存映射用来把文件映射到进程的地址空间。在内存映射中,文件的内容直接连接到进程的虚拟地址空间。
*公平的物理内存分配。
内存管理系统允许系统中每一个运行的进程都可以公平地得到系统的物理内存。
*共享虚拟内存。
虽然虚拟内存允许进程拥有自己单独的虚拟地址空间,但有时可能会希望进程共享内存。
linux仅仅使用四个段
两个代表 (code 和 data/stack)是内核空间从[0xC000 0000] (3 GB)到[0xFFFF FFFF] (4 GB)
两个代表 (code 和 data/stack)是用户空间从[0] (0 GB) 到 [0xBFFF FFFF] (3 GB)
__
4 GB--->;| | |
| Kernel | | 内核空间 (Code + Data/Stack)
| | __|
3 GB--->;|----------------| __
| | |
| | |
2 GB--->;| | |
| Tasks | | 用户空间 (Code + Data/Stack)
| | |
1 GB--->;| | |
| | |
|________________| __|
0x00000000
内核/用户 线性地址
linux可以使用3层页表映射,例如在高级的I64服务器上,但是i386体系结构下只有2层有实际意义:
------------------------------------------------------------------
线性地址
------------------------------------------------------------------
\___/ \___/ \_____/
PD 偏移 PF 偏移 Frame 偏移
[10 bits] [10 bits] [12 bits]
| | |
| | ----------- |
| | | Value |----------|---------
| | | | |---------| /|\ | |
| | | | | | | | |
| | | | | | | Frame 偏移 |
| | | | | | \|/ |
| | | | |---------|<------ |
| | | | | | | |
| | | | | | | x 4096 |
| | | PF 偏移 |_________|------- |
| | | /|\ | | |
PD 偏移 |_________|----- | | | _________|
/|\ | | | | | | |
| | | | \|/ | | \|/
_____ | | | ------>;|_________| 物理地址
| | \|/ | | x 4096 | |
| CR3 |-------->;| | | |
|_____| | ....... | | ....... |
| | | |
页目录表 页表
Linux i386 分页
注意内核(仅仅内核)线性空间就等于内核物理空间,所以如下:
________________ _____
| 其他内核数据 |___ | | |
|----------------| | |__| |
| 内核 |\ |____| 实际的其他 |
3 GB --->;|----------------| \ | 内核数据 |
| |\ \ | |
| __|_\_\____|__ Real |
| Tasks | \ \ | Tasks |
| __|___\_\__|__ Space |
| | \ \ | |
| | \ \|----------------|
| | \ | 实际内核空间 |
|________________| \|________________|
逻辑地址 物理地址
[内存实时分配]
|copy_mm
|allocate_mm = kmem_cache_alloc
|__kmem_cache_alloc
|kmem_cache_alloc_one
|alloc_new_slab
|kmem_cache_grow
|kmem_getpages
|__get_free_pages
|alloc_pages
|alloc_pages_pgdat
|__alloc_pages
|rmqueue
|reclaim_pages
·copy_mm [kernel/fork.c]
·allocate_mm [kernel/fork.c]
·kmem_cache_alloc [mm/slab.c]
·__kmem_cache_alloc
·kmem_cache_alloc_one
·alloc_new_slab
·kmem_cache_grow
·kmem_getpages
·__get_free_pages [mm/page_alloc.c]
·alloc_pages [mm/numa.c]
·alloc_pages_pgdat
·__alloc_pages [mm/page_alloc.c]
·rm_queue
·reclaim_pages [mm/vmscan.c]
[内存交换线程kswapd]
|kswapd
|// initialization routines
|for (; { // Main loop
|do_try_to_free_pages
|recalculate_vm_stats
|refill_inactive_scan
|run_task_queue
|interruptible_sleep_on_timeout // we sleep for a new swap request
|}
·kswapd [mm/vmscan.c]
·do_try_to_free_pages
·recalculate_vm_stats [mm/swap.c]
·refill_inactive_scan [mm/vmswap.c]
·run_task_queue [kernel/softirq.c]
·interruptible_sleep_on_timeout [kernel/sched.c]
[内存交换机制:出现内存不足的Exception]
| Page Fault Exception
| cause by all these conditions:
| a-) User page
| b-) Read or write access
| c-) Page not present
|
|
----------->; |do_page_fault
|handle_mm_fault
|pte_alloc
|pte_alloc_one
|__get_free_page = __get_free_pages
|alloc_pages
|alloc_pages_pgdat
|__alloc_pages
|wakeup_kswapd // We wake up kernel thread kswapd
·do_page_fault [arch/i386/mm/fault.c]
·handle_mm_fault [mm/memory.c]
·pte_alloc
·pte_alloc_one [include/asm/pgalloc.h]
·__get_free_page [include/linux/mm.h]
·__get_free_pages [mm/page_alloc.c]
·alloc_pages [mm/numa.c]
·alloc_pages_pgdat
·__alloc_pages
·wakeup_kswapd [mm/vmscan.c]
[目录]
--------------------------------------------------------------------------------
内存管理子系统导读from aka
我的目标是‘导读’,提供linux内存管理子系统的整体概念,同时给出进一步深入研究某个部分时的辅助信息(包括代码组织,文件和主要函数的意义和一些参考文档)。之所以采取这种方式,是因为我本人在阅读代码的过程中,深感“读懂一段代码容易,把握整体思想却极不容易”。而且,在我写一些内核代码时,也觉得很多情况下,不一定非得很具体地理解所有内核代码,往往了解它的接口和整体工作原理就够了。当然,我个人的能力有限,时间也很不够,很多东西也是近期迫于讲座压力临时学的:),内容难免偏颇甚至错误,欢迎大家指正。
存储层次结构和x86存储管理硬件(MMU)
这里假定大家对虚拟存储,段页机制有一定的了解。主要强调一些很重要的或者容易误解的概念。
存储层次
高速缓存(cache) --〉 主存(main memory) ---〉 磁盘(disk)
理解存储层次结构的根源:CPU速度和存储器速度的差距。
层次结构可行的原因:局部性原理。
LINUX的任务:
减小footprint,提高cache命中率,充分利用局部性。
实现虚拟存储以满足进程的需求,有效地管理内存分配,力求最合理地利用有限的资源。
参考文档:
《too little,too small》by Rik Van Riel, Nov. 27,2000.
以及所有的体系结构教材:)
MMU的作用
辅助操作系统进行内存管理,提供虚实地址转换等硬件支持。
x86的地址
逻辑地址: 出现在机器指令中,用来制定操作数的地址。段:偏移
线性地址:逻辑地址经过分段单元处理后得到线性地址,这是一个32位的无符号整数,可用于定位4G个存储单元。
物理地址:线性地址经过页表查找后得出物理地址,这个地址将被送到地址总线上指示所要访问的物理内存单元。
LINUX: 尽量避免使用段功能以提高可移植性。如通过使用基址为0的段,使逻辑地址==线性地址。
x86的段
保护模式下的段:选择子+描述符。不仅仅是一个基地址的原因是为了提供更多的信息:保护、长度限制、类型等。描述符存放在一张表中(GDT或LDT),选择子可以认为是表的索引。段寄存器中存放的是选择子,在段寄存器装入的同时,描述符中的数据被装入一个不可见的寄存器以便cpu快速访问。(图)P40
专用寄存器:GDTR(包含全局描述附表的首地址),LDTR(当前进程的段描述附表首地址),TSR(指向当前进程的任务状态段)
LINUX使用的段:
__KERNEL_CS: 内核代码段。范围 0-4G。可读、执行。DPL=0。
__KERNEL_DS:内核代码段。范围 0-4G。可读、写。DPL=0。
__USER_CS:内核代码段。范围 0-4G。可读、执行。DPL=3。
__USER_DS:内核代码段。范围 0-4G。可读、写。DPL=3。
TSS(任务状态段):存储进程的硬件上下文,进程切换时使用。(因为x86硬件对TSS有一定支持,所有有这个特殊的段和相应的专用寄存器。)
default_ldt:理论上每个进程都可以同时使用很多段,这些段可以存储在自己的ldt段中,但实际linux极少利用x86的这些功能,多数情况下所有进程共享这个段,它只包含一个空描述符。
还有一些特殊的段用在电源管理等代码中。
(在2.2以前,每个进程的ldt和TSS段都存在GDT中,而GDT最多只能有8192项,因此整个系统的进程总数被限制在4090左右。2。4里不再把它们存在GDT中,从而取消了这个限制。)
__USER_CS和__USER_DS段都是被所有在用户态下的进程共享的。注意不要把这个共享和进程空间的共享混淆:虽然大家使用同一个段,但通过使用不同的页表由分页机制保证了进程空间仍然是独立的。
x86的分页机制
x86硬件支持两级页表,奔腾pro以上的型号还支持Physical address Extension Mode和三级页表。所谓的硬件支持包括一些特殊寄存器(cr0-cr4)、以及CPU能够识别页表项中的一些标志位并根据访问情况做出反应等等。如读写Present位为0的页或者写Read/Write位为0的页将引起CPU发出page fault异常,访问完页面后自动设置accessed位等。
linux采用的是一个体系结构无关的三级页表模型(如图),使用一系列的宏来掩盖各种平台的细节。例如,通过把PMD看作只有一项的表并存储在pgd表项中(通常pgd表项中存放的应该是pmd表的首地址),页表的中间目录(pmd)被巧妙地‘折叠’到页表的全局目录(pgd),从而适应了二级页表硬件。
TLB
TLB全称是Translation Look-aside Buffer,用来加速页表查找。这里关键的一点是:如果操作系统更改了页表内容,它必须相应的刷新TLB以使CPU不误用过时的表项。
Cache
Cache 基本上是对程序员透明的,但是不同的使用方法可以导致大不相同的性能。linux有许多关键的地方对代码做了精心优化,其中很多就是为了减少对cache不必要的污染。如把只有出错情况下用到的代码放到.fixup section,把频繁同时使用的数据集中到一个cache行(如struct task_struct),减少一些函数的footprint,在slab分配器里头的slab coloring等。
另外,我们也必须知道什么时候cache要无效:新map/remap一页到某个地址、页面换出、页保护改变、进程切换等,也即当cache对应的那个地址的内容或含义有所变化时。当然,很多情况下不需要无效整个cache,只需要无效某个地址或地址范围即可。实际上,
intel在这方面做得非常好用,cache的一致性完全由硬件维护。
关于x86处理器更多信息,请参照其手册:Volume 3: Architecture and Programming Manual
8. Linux 相关实现
这一部分的代码和体系结构紧密相关,因此大多位于arch子目录下,而且大量以宏定义和inline函数形式存在于头文件中。以i386平台为例,主要的文件包括:
page.h
页大小、页掩码定义。PAGE_SIZE,PAGE_SHIFT和PAGE_MASK。
对页的操作,如清除页内容clear_page、拷贝页copy_page、页对齐page_align
还有内核虚地址的起始点:著名的PAGE_OFFSET和相关的内核中虚实地址转换的宏__pa和__va.。
virt_to_page从一个内核虚地址得到该页的描述结构struct page *.我们知道,所有物理内存都由一个memmap数组来描述。这个宏就是计算给定地址的物理页在这个数组中的位置。另外这个文件也定义了一个简单的宏检查一个页是不是合法:VALID_PAGE(page)。如果page离memmap数组的开始太远以至于超过了最大物理页面应有的距离则是不合法的。
比较奇怪的是页表项的定义也放在这里。pgd_t,pmd_t,pte_t和存取它们值的宏xxx_val
pgtable.h pgtable-2level.h pgtable-3level.h
顾名思义,这些文件就是处理页表的,它们提供了一系列的宏来操作页表。pgtable-2level.h和pgtable-2level.h则分别对应x86二级、三级页表的需求。首先当然是表示每级页表有多少项的定义不同了。而且在PAE模式下,地址超过32位,页表项pte_t用64位来表示(pmd_t,pgd_t不需要变),一些对整个页表项的操作也就不同。共有如下几类:
·[pte/pmd/pgd]_ERROR 出措时要打印项的取值,64位和32位当然不一样。
·set_[pte/pmd/pgd] 设置表项值
·pte_same 比较 pte_page 从pte得出所在的memmap位置
·pte_none 是否为空。
·__mk_pte 构造pte
pgtable.h的宏太多,不再一一解释。实际上也比较直观,通常从名字就可以看出宏的意义来了。pte_xxx宏的参数是pte_t,而ptep_xxx的参数是pte_t *。2.4 kernel在代码的clean up方面还是作了一些努力,不少地方含糊的名字变明确了,有些函数的可读性页变好了。
pgtable.h里除了页表操作的宏外,还有cache和tlb刷新操作,这也比较合理,因为他们常常是在页表操作时使用。这里的tlb操作是以__开始的,也就是说,内部使用的,真正对外接口在pgalloc.h中(这样分开可能是因为在SMP版本中,tlb的刷新函数和单机版本区别较大,有些不再是内嵌函数和宏了)。
pgalloc.h
包括页表项的分配和释放宏/函数,值得注意的是表项高速缓存的使用:
pgd/pmd/pte_quicklist
内核中有许多地方使用类似的技巧来减少对内存分配函数的调用,加速频繁使用的分配。如buffer cache中buffer_head和buffer,vm区域中最近使用的区域。
还有上面提到的tlb刷新的接口
segment.h
定义 __KERNEL_CS[DS] __USER_CS[DS]
参考:
《Understanding the Linux Kernel》的第二章给了一个对linux 的相关实现的简要描述,
物理内存的管理。
2.4中内存管理有很大的变化。在物理页面管理上实现了基于区的伙伴系统(zone based buddy system)。区(zone)的是根据内存的不同使用类型划分的。对不同区的内存使用单独的伙伴系统(buddy system)管理,而且独立地监控空闲页等。
(实际上更高一层还有numa支持。Numa(None Uniformed Memory Access)是一种体系结构,其中对系统里的每个处理器来说,不同的内存区域可能有不同的存取时间(一般是由内存和处理器的距离决定)。而一般的机器中内存叫做DRAM,即动态随机存取存储器,对每个单元,CPU用起来是一样快的。NUMA中访问速度相同的一个内存区域称为一个Node,支持这种结构的主要任务就是要尽量减少Node之间的通信,使得每个处理器要用到的数据尽可能放在对它来说最快的Node中。2.4内核中node�相应的数据结构是pg_data_t,每个node拥有自己的memmap数组,把自己的内存分成几个zone,每个zone再用独立的伙伴系统管理物理页面。Numa要对付的问题还有很多,也远没有完善,就不多说了)
基于区的伙伴系统的设计�物理页面的管理
内存分配的两大问题是:分配效率、碎片问题。一个好的分配器应该能够快速的满足各种大小的分配要求,同时不能产生大量的碎片浪费空间。伙伴系统是一个常用的比较好的算法。(解释:TODO)
引入区的概念是为了区分内存的不同使用类型(方法?),以便更有效地利用它们。
2.4有三个区:DMA, Normal, HighMem。前两个在2.2实际上也是由独立的buddy system管理的,但2.2中还没有明确的zone的概念。DMA区在x86体系结构中通常是小于16兆的物理内存区,因为DMA控制器只能使用这一段的内存。而HighMem是物理地址超过某个值(通常是约900M)的高端内存。其他的是Normal区内存。由于linux实现的原因,高地址的内存不能直接被内核使用,如果选择了CONFIG_HIGHMEM选项,内核会使用一种特殊的办法来使用它们。(解释:TODO)。HighMem只用于page cache和用户进程。这样分开之后,我们将可以更有针对性地使用内存,而不至于出现把DMA可用的内存大量给无关的用户进程使用导致驱动程序没法得到足够的DMA内存等情况。此外,每个区都独立地监控本区内存的使用情况,分配时系统会判断从哪个区分配比较合算,综合考虑用户的要求和系统现状。2.4里分配页面时可能会和高层的VM代码交互(分配时根据空闲页面的情况,内核可能从伙伴系统里分配页面,也可能直接把已经分配的页收回�reclaim等),代码比2.2复杂了不少,要全面地理解它得熟悉整个VM工作的机理。
整个分配器的主要接口是如下函数(mm.h page_alloc.c):
struct page * alloc_pages(int gfp_mask, unsigned long order) 根据gftp_mask的要求,从适当的区分配2^order个页面,返回第一个页的描述符。
#define alloc_page(gfp_mask) alloc_pages(gfp_mask,0)
unsigned long __get_free_pages((int gfp_mask, unsigned long order) 工作同alloc_pages,但返回首地址。
#define __get_free_page(gfp_mask) __get_free_pages(gfp_mask,0)
get_free_page 分配一个已清零的页面。
__free_page(s) 和free_page(s)释放页面(一个/多个)前者以页面描述符为参数,后者以页面地址为参数。
关于Buddy算法,许多教科书上有详细的描述,第六章对linux的实现有一个很好的介绍。关于zone base buddy更多的信息,可以参见Rik Van Riel 写的" design for a zone based memory allocator"。这个人是目前linuxmm的维护者,权威啦。这篇文章有一点过时了,98年写的,当时还没有HighMem,但思想还是有效的。还有,下面这篇文章分析2.4的实现代码:
http://home.earthlink.net/~jknapka/linux-mm/zonealloc.html。
Slab--连续物理区域管理
单单分配页面的分配器肯定是不能满足要求的。内核中大量使用各种数据结构,大小从几个字节到几十上百k不等,都取整到2的幂次个页面那是完全不现实的。2.0的内核的解决方法是提供大小为2,4,8,16,...,131056字节的内存区域。需要新的内存区域时,内核从伙伴系统申请页面,把它们划分成一个个区域,取一个来满足需求;如果某个页面中的内存区域都释放了,页面就交回到伙伴系统。这样做的效率不高。有许多地方可以改进:
不同的数据类型用不同的方法分配内存可能提高效率。比如需要初始化的数据结构,释放后可以暂存着,再分配时就不必初始化了。
内核的函数常常重复地使用同一类型的内存区,缓存最近释放的对象可以加速分配和释放。
对内存的请求可以按照请求频率来分类,频繁使用的类型使用专门的缓存,很少使用的可以使用类似2.0中的取整到2的幂次的通用缓存。
使用2的幂次大小的内存区域时高速缓存冲突的概率较大,有可能通过仔细安排内存区域的起始地址来减少高速缓存冲突。
缓存一定数量的对象可以减少对buddy系统的调用,从而节省时间并减少由此引起的高速缓存污染。
2.2实现的slab分配器体现了这些改进思想。
主要数据结构
接口:
kmem_cache_create/kmem_cache_destory
kmem_cache_grow/kmem_cache_reap 增长/缩减某类缓存的大小
kmem_cache_alloc/kmem_cache_free 从某类缓存分配/释放一个对象
kmalloc/kfree 通用缓存的分配、释放函数。
相关代码(slab.c)。
相关参考:
http://www.lisoleg.net/lisoleg/memory/slab.pdf :Slab发明者的论文,必读经典。
第六章,具体实现的详细清晰的描述。
AKA2000年的讲座也有一些大虾讲过这个主题,请访问aka主页:www.aka.org.cn
vmalloc/vfree �物理地址不连续,虚地址连续的内存管理
使用kernel页表。文件vmalloc.c,相对简单。
2.4内核的VM(完善中。。。)
进程地址空间管理
创建,销毁。
mm_struct, vm_area_struct, mmap/mprotect/munmap
page fault处理,demand page, copy on write
相关文件:
include/linux/mm.h:struct page结构的定义,page的标志位定义以及存取操作宏定义。struct vm_area_struct定义。mm子系统的函数原型说明。
include/linux/mman.h:和vm_area_struct的操作mmap/mprotect/munmap相关的常量宏定义。
memory.c:page fault处理,包括COW和demand page等。
对一个区域的页表相关操作:
zeromap_page_range: 把一个范围内的页全部映射到zero_page
remap_page_range:给定范围的页重新映射到另一块地址空间。
zap_page_range:把给定范围内的用户页释放掉,页表清零。
mlock.c: mlock/munlock系统调用。mlock把页面锁定在物理内存中。
mmap.c::mmap/munmap/brk系统调用。
mprotect.c: mprotect系统调用。
前面三个文件都大量涉及vm_area_struct的操作,有很多相似的xxx_fixup的代码,它们的任务是修补受到影响的区域,保证vm_area_struct 链表正确。
交换
目的:
使得进程可以使用更大的地址空间。同时容纳更多的进程。
任务:
选择要换出的页
决定怎样在交换区中存储页面
决定什么时候换出
kswapd内核线程:每10秒激活一次
任务:当空闲页面低于一定值时,从进程的地址空间、各类cache回收页面
为什么不能等到内存分配失败再用try_to_free_pages回收页面?原因:
有些内存分配时在中断或异常处理调用,他们不能阻塞
有时候分配发生在某个关键路径已经获得了一些关键资源的时候,因此它不能启动IO。如果不巧这时所有的路径上的内存分配都是这样,内存就无法释放。
kreclaimd 从inactive_clean_list回收页面,由__alloc_pages唤醒。
相关文件:
mm/swap.c kswapd使用的各种参数以及操作页面年龄的函数。
mm/swap_file.c 交换分区/文件的操作。
mm/page_io.c 读或写一个交换页。
mm/swap_state.c swap cache相关操作,加入/删除/查找一个swap cache等。
mm/vmscan.c 扫描进程的vm_area,试图换出一些页面(kswapd)。
reclaim_page:从inactive_clean_list回收一个页面,放到free_list
kclaimd被唤醒后重复调用reclaim_page直到每个区的
zone->;free_pages>;= zone->;pages_low
page_lauder:由__alloc_pages和try_to_free_pages等调用。通常是由于freepages + inactive_clean_list的页太少了。功能:把inactive_dirty_list的页面转移到inactive_clean_list,首先把已经被写回文件或者交换区的页面(by bdflush)放到inactive_clean_list,如果freepages确实短缺,唤醒bdflush,再循环一遍把一定数量的dirty页写回。
关于这几个队列(active_list,inactive_dirty_list,inactive_clean_list)的逻辑,请参照:文档:RFC: design for new VM,可以从lisoleg的文档精华获得。
page cache、buffer cache和swap cache
page cache:读写文件时文件内容的cache,大小为一个页。不一定在磁盘上连续。
buffer cache:读写磁盘块的时候磁盘块内容的cache,buffer cache的内容对应磁盘上一个连续的区域,一个buffer cache大小可能从512(扇区大小)到一个页。
swap cache: 是page cache的子集。用于多个进程共享的页面被换出到交换区的情况。
page cache 和 buffer cache的关系
本质上是很不同的,buffer cache缓冲磁盘块内容,page cache缓冲文件的一页内容。page cache写回时会使用临时的buffer cache来写磁盘。
bdflush: 把dirty的buffer cache写回磁盘。通常只当dirty的buffer太多或者需要更多的buffer而内存开始不足时运行。page_lauder也可能唤醒它。
kupdate: 定时运行,把写回期限已经到了的dirty buffer写回磁盘。
2.4的改进:page cache和buffer cache耦合得更好了。在2.2里,磁盘文件的读使用page cache,而写绕过page cache,直接使用buffer cache,因此带来了同步的问题:写完之后必须使用update_vm_cache()更新可能有的page cache。2.4中page cache做了比较大的改进,文件可以通过page cache直接写了,page cache优先使用high memory。而且,2.4引入了新的对象:file address space,它包含用来读写一整页数据的方法。这些方法考虑到了inode的更新、page cache处理和临时buffer的使用。page cache和buffer cache的同步问题就消除了。原来使用inode+offset查找page cache变成通过file address space+offset;原来struct page 中的inode成员被address_space类型的mapping成员取代。这个改进还使得匿名内存的共享成为可能(这个在2.2很难实现,许多讨论过)。
虚存系统则从freeBSD借鉴了很多经验,针对2.2的问题作了巨大的调整。
文档:RFC: design for new VM不可不读。
由于时间仓促,新vm的很多细微之处我也还没来得及搞清楚。先大致罗列一下,以后我将进一步完善本文,争取把问题说清楚。另外,等这学期考试过后,我希望能为大家提供一些详细注释过的源代码。
[目录]
--------------------------------------------------------------------------------
用户态
用户空间存取内核空间,具体的实现方法要从两个方面考虑,先是用户进程,需要调用mmapp来将自己的一段虚拟空间映射到内核态分配的物理内存;然后内核空间需要重新设置用户进程的这段虚拟内存的页表,使它的物理地址指向对应的物理内存。针对linux内核的几种不同的内存分配方式(kmalloc、vmalloc和ioremap),需要进行不同的处理。
一、Linux内存管理概述
这里说一下我的理解,主要从数据结构说。
1、物理内存都是按顺序分成一页一页的,每页用一个page结构来描述。系统所有的物理页 面的page结
构描述就组成了一个数组mem_map。
2、进程的虚拟地址空间用task_struct的域mm来描述,它是一个mm_struct结构,这个结构包包含了指向?
程页目录的指针(pgd_t * pgd)和指向进程虚拟内存区域的指针(struct vm_area_structt * mmap)
3、进程虚拟内存区域具有相同属性的段用结构vm_area_struct描述(简称为VMA)。进程所所有的VMA?
树组织。
4、每个VMA就是一个对象,定义了一组操作,可以通过这组操作来对不同类型的VMA进行不屯 的处理。
例如对vmalloc分配的内存的映射就是通过其中的nopage操作实现的。
二、mmap处理过程
当用户调用mmap的时候,内核进行如下的处理:
1、先在进程的虚拟空间查找一块VMA;
2、将这块VMA去映射
3、如果设备驱动程序或者文件系统的file_operations定义了mmap操作,则调用它
4、将这个VMA插入到进程的VMA链中
file_operations的中定义的mmap方法原型如下:
int (*mmap) (struct file *, struct vm_area_struct *);
其中file是虚拟空间映射到的文件结构,vm_area_struct就是步骤1中找到的VMA。
三、缺页故障处理过程
当访问一个无效的虚拟地址(可能是保护故障,也可能缺页故障等)的时候,就会产生一个个页故障,?
统的处理过程如下:
1、找到这个虚拟地址所在的VMA;
2、如果必要,分配中间页目录表和页表
3、如果页表项对应的物理页面不存在,则调用这个VMA的nopage方法,它返回物理页面的paage描述结构
(当然这只是其中的一种情况)
4、针对上面的情况,将物理页面的地址填充到页表中
当页故障处理完后,系统将重新启动引起故障的指令,然后就可以正常访问了
下面是VMA的方法:
struct vm_operations_struct {
void (*open)(struct vm_area_struct * area);
void (*close)(struct vm_area_struct * area);
struct page * (*nopage)(struct vm_area_struct * area, unsigned long address, innt
write_access);
};
其中缺页函数nopage的address是引起缺页故障的虚拟地址,area是它所在的VMA,write_acccess是存取
属性。
三、具体实现
3.1、对kmalloc分配的内存的映射
对kmalloc分配的内存,因为是一段连续的物理内存,所以它可以简单的在mmap例程中设置汉 页表的物
理地址,方法是使用函数remap_page_range。它的原型如下:
int remap_page_range(unsigned long from, unsigned long phys_addr, unsigned long size,
pgprot_t prot)
其中from是映射开始的虚拟地址。这个函数为虚拟地址空间from和from+size之间的范围构栽 页表;
phys_addr是虚拟地址应该映射到的物理地址;size是被映射区域的大小;prot是保护标志?
remap_page_range的处理过程是对from到form+size之间的每一个页面,查找它所在的页目侣己 页表(
必要时建立页表),清除页表项旧的内容,重新填写它的物理地址与保护域。
remap_page_range可以对多个连续的物理页面进行处理。<<Linux设备驱动程序>;>;指出,
remap_page_range只能给予对保留的页和物理内存之上的物理地址的访问,当对非保留的页页使?
remap_page_range时,缺省的nopage处理控制映射被访问的虚地址处的零页。所以在分配内内存后,就?
对所分配的内存置保留位,它是通过函数mem_map_reserve实现的,它就是对相应物理页面?
PG_reserved标志位。(关于这一点,参见前面的主题为“关于remap_page_range的疑问”档奶致郏?
因为remap_page_range有上面的限制,所以可以用另外一种方式,就是采用和vmalloc分配档哪 存同样
的方法,对缺页故障进行处理。
3.2、对vmalloc分配的内存的映射
3.2.1、vmalloc分配内存的过程
(1)、进行预处理和合法性检查,例如将分配长度进行页面对齐,检查分配长度是否过大?
(2)、以GFP_KERNEL为优先级调用kmalloc分配(GFP_KERNEL用在进程上下文中,所以这里里就限制了?
中断处理程序中调用vmalloc)描述vmalloc分配的内存的vm_struct结构。
(3)、将size加一个页面的长度,使中间形成4K的隔离带,然后在VMALLOC_START和VMALLOOC_END之间
编历vmlist链表,寻找一段自由内存区间,将其地址填入vm_struct结构中
(4)、返回这个地址
vmalloc分配的物理内存并不连续
3.2.2、页目录与页表的定义
typedef struct { unsigned long pte_low; } pte_t;
typedef struct { unsigned long pmd; } pmd_t;
typedef struct { unsigned long pgd; } pgd_t;
#define pte_val(x) ((x).pte_low)
3.2.3、常见例程:
(1)、virt_to_phys():内核虚拟地址转化为物理地址
#define __pa(x) ((unsigned long)(x)-PAGE_OFFSET)
extern inline unsigned long virt_to_phys(volatile void * address)
{
return __pa(address);
}
上面转换过程是将虚拟地址减去3G(PAGE_OFFSET=0XC000000),因为内核空间从3G到3G+实实际内存一?
映射到物理地址的0到实际内存
(2)、phys_to_virt():内核物理地址转化为虚拟地址
#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))
extern inline void * phys_to_virt(unsigned long address)
{
return __va(address);
}
virt_to_phys()和phys_to_virt()都定义在include\asm-i386\io.h中
(3)、#define virt_to_page(kaddr) (mem_map + (__pa(kaddr) >;>; PAGE_SHIFT))(内核核2.4?
#define VALID_PAGE(page) ((page - mem_map) < max_mapnr)(内核2.4)
第一个宏根据虚拟地址,将其转换为相应的物理页面的page描述结构,第二个宏判断页面是是不是在有?
的物理页面内。(这两个宏处理的虚拟地址必须是内核虚拟地址,例如kmalloc返回的地址#杂?
vmalloc返回的地址并不能这样,因为vmalloc分配的并不是连续的物理内存,中间可能有空空洞?
3.2.4、vmalloc分配的内存的mmap的实现:
对vmalloc分配的内存需要通过设置相应VMA的nopage方法来实现,当产生缺页故障的时候,,会调用VM
的nopage方法,我们的目的就是在nopage方法中返回一个page结构的指针,为此,需要通过过如下步骤?
(1) pgd_offset_k或者 pgd_offset:查找虚拟地址所在的页目录表,前者对应内核空间档男 拟地址
,后者对应用户空间的虚拟地址
#define pgd_offset(mm, address) ((mm)->;pgd+pgd_index(address))
#define pgd_offset_k(address) pgd_offset(&init_mm, address)
对于后者,init_mm是进程0(idle process)的虚拟内存mm_struct结构,所有进程的内核 页表都一样
。在vmalloc分配内存的时候,要刷新内核页目录表,2.4中为了节省开销,只更改了进程0档哪 核页目
录,而对其它进程则通过访问时产生页面异常来进行更新各自的内核页目录
(2)pmd_offset:找到虚拟地址所在的中间页目录项。在查找之前应该使用pgd_none判断适 否存在相
应的页目录项,这些函数如下:
extern inline int pgd_none(pgd_t pgd) { return 0; }
extern inline pmd_t * pmd_offset(pgd_t * dir, unsigned long address)
{
return (pmd_t *) dir;
}
(3)pte_offset:找到虚拟地址对应的页表项。同样应该使用pmd_none判断是否存在相应档 中间页目
录:
#define pmd_val(x) ((x).pmd)
#define pmd_none(x) (!pmd_val(x))
#define __pte_offset(address) \
((address >;>; PAGE_SHIFT) & (PTRS_PER_PTE - 1))
#define pmd_page(pmd) \
((unsigned long) __va(pmd_val(pmd) & PAGE_MASK))
#define pte_offset(dir, address) ((pte_t *) pmd_page(*(dir)) + \
__pte_offset(address))
(4)pte_present和pte_page:前者判断页表对应的物理地址是否有效,后者取出页表中物物理地址对?
的page描述结构
#define pte_present(x) ((x).pte_low & (_PAGE_PRESENT | _PAGE_PROTNONE))
#define pte_page(x) (mem_map+((unsigned long)(((x).pte_low >;>; PAGE_SHIFT))))
#define page_address(page) ((page)->;virtual)
下面的一个DEMO与上面的关系不大,它是做这样一件事情,就是在启动的时候保留一段内存存,然后使?
ioremap将它映射到内核虚拟空间,同时又用remap_page_range映射到用户虚拟空间,这样亮 边都能访
问,通过内核虚拟地址将这段内存初始化串"abcd",然后使用用户虚拟地址读出来。
/************mmap_ioremap.c**************/
#include <linux/module.h>;
#include <linux/kernel.h>;
#include <linux/errno.h>;
#include <linux/mm.h>;
#include <linux/wrapper.h>; /* for mem_map_(un)reserve */
#include <asm/io.h>; /* for virt_to_phys */
#include <linux/slab.h>; /* for kmalloc and kfree */
MODULE_PARM(mem_start,"i";
MODULE_PARM(mem_size,"i";
static int mem_start=101,mem_size=10;
static char * reserve_virt_addr;
static int major;
int mmapdrv_open(struct inode *inode, struct file *file);
int mmapdrv_release(struct inode *inode, struct file *file);
int mmapdrv_mmap(struct file *file, struct vm_area_struct *vma);
static struct file_operations mmapdrv_fops =
{
owner: THIS_MODULE,
mmap: mmapdrv_mmap,
open: mmapdrv_open,
release: mmapdrv_release,
};
int init_module(void)
{
if ( ( major = register_chrdev(0, "mmapdrv", &mmapdrv_fops) ) < 0 )
{
printk("mmapdrv: unable to register character device\n";
return (-EIO);
}
printk("mmap device major = %d\n",major );
printk( "high memory physical address 0x%ldM\n",
virt_to_phys(high_memory)/1024/1024 );
reserve_virt_addr = ioremap( mem_start*1024*1024,mem_size*1024*1024);
printk( "reserve_virt_addr = 0x%lx\n", (unsigned long)reserve_virt_addr );
if ( reserve_virt_addr )
{
int i;
for ( i=0;i<mem_size*1024*1024;i+=4)
{
reserve_virt_addr = 'a';
reserve_virt_addr[i+1] = 'b';
reserve_virt_addr[i+2] = 'c';
reserve_virt_addr[i+3] = 'd';
}
}
else
{
unregister_chrdev( major, "mmapdrv" );
return -ENODEV;
}
return 0;
}
/* remove the module */
void cleanup_module(void)
{
if ( reserve_virt_addr )
iounmap( reserve_virt_addr );
unregister_chrdev( major, "mmapdrv" );
return;
}
int mmapdrv_open(struct inode *inode, struct file *file)
{
MOD_INC_USE_COUNT;
return(0);
}
int mmapdrv_release(struct inode *inode, struct file *file)
{
MOD_DEC_USE_COUNT;
return(0);
}
int mmapdrv_mmap(struct file *file, struct vm_area_struct *vma)
{
unsigned long offset = vma->;vm_pgoff<<AGE_SHIFT;
unsigned long size = vma->;vm_end - vma->;vm_start;
if ( size >; mem_size*1024*1024 )
{
printk("size too big\n";
return(-ENXIO);
}
offset = offset + mem_start*1024*1024;
/* we do not want to have this area swapped out, lock it */
vma->;vm_flags |= VM_LOCKED;
if ( remap_page_range(vma->;vm_start,offset,size,PAGE_SHARED))
{
printk("remap page range failed\n";
return -ENXIO;
}
return(0);
}
使用LDD2源码里面自带的工具mapper测试结果如下:
[root@localhost modprg]# insmod mmap_ioremap.mod
mmap device major = 254
high memory physical address 0x100M
reserve_virt_addr = 0xc7038000
[root@localhost modprg]# mknod mmapdrv c 254 0
[root@localhost modprg]# ./mapper mmapdrv 0 1024 | od -Ax -t x1
mapped "mmapdrv" from 0 to 1024
000000 61 62 63 64 61 62 63 64 61 62 63 64 61 62 63 64
*
000400
[root@localhost modprg]#
[目录]
--------------------------------------------------------------------------------
内核页目录的初始化
内核页目录的初始化
内核页目录的初始化
/* swapper_pg_dir is the main page directory, address 0x00101000*/
>;>;>; 内核页目录,第0,1项和第768、767项均为映射到物理内存0-8M的页目录项
>;>;>; 其页表的物理地址是0x00102000和0x00103000,即下面的pg0和pg1所在的位置
>;>;>; (在启动的时候,将内核映像移到0x0010000处)。
>;>;>; 之所以第0,1项与第768和767相同,是因为在开启分页前的线性地址0-8M和开启
>;>;>; 分页之后的3G-3G+8M均映射到相同的物理地址0-8M
/*
* This is initialized to create an identity-mapping at 0-8M (for bootup
* purposes) and another mapping of the 0-8M area at virtual address
* PAGE_OFFSET.
*/
.org 0x1000
ENTRY(swapper_pg_dir)
.long 0x00102007
.long 0x00103007
.fill BOOT_USER_PGD_PTRS-2,4,0
/* default: 766 entries */
.long 0x00102007
.long 0x00103007
/* default: 254 entries */
.fill BOOT_KERNEL_PGD_PTRS-2,4,0
/*
* The page tables are initialized to only 8MB here - the final page
* tables are set up later depending on memory size.
*/
>;>;>; 下面为物理地址0-8M的页表项
>;>;>; 从0x4000到0x2000共2k个页表项,映射0-8M的物理内存
.org 0x2000
ENTRY(pg0)
.org 0x3000
ENTRY(pg1)
/*
* empty_zero_page must immediately follow the page tables ! (The
* initialization loop counts until empty_zero_page)
*/
.org 0x4000
ENTRY(empty_zero_page)
>;>;>; 进程0的页目录指向swapper_pg_dir
#define INIT_MM(name) \
{ \
mmap: &init_mmap, \
mmap_avl: NULL, \
mmap_cache: NULL, \
pgd: swapper_pg_dir, \
mm_users: ATOMIC_INIT(2), \
mm_count: ATOMIC_INIT(1), \
map_count: 1, \
mmap_sem: __RWSEM_INITIALIZER(name.mmap_sem), \
page_table_lock: SPIN_LOCK_UNLOCKED, \
mmlist: LIST_HEAD_INIT(name.mmlist), \
}
/*
* paging_init() sets up the page tables - note that the first 8MB are
* already mapped by head.S.
*
* This routines also unmaps the page at virtual kernel address 0, so
* that we can trap those pesky NULL-reference errors in the kernel.
*/
void __init paging_init(void)
{
pagetable_init();
__asm__( "movl %%ecx,%%cr3\n" ::"c"(__pa(swapper_pg_dir)));
。。。。。。。。。。。
}
static void __init pagetable_init (void)
{
unsigned long vaddr, end;
pgd_t *pgd, *pgd_base;
int i, j, k;
pmd_t *pmd;
pte_t *pte, *pte_base;
>;>;>; end虚拟空间的最大值(最大物理内存+3G)
/*
* This can be zero as well - no problem, in that case we exit
* the loops anyway due to the PTRS_PER_* conditions.
*/
end = (unsigned long)__va(max_low_pfn*PAGE_SIZE);
pgd_base = swapper_pg_dir;
#if CONFIG_X86_PAE
for (i = 0; i < PTRS_PER_PGD; i++)
set_pgd(pgd_base + i, __pgd(1 + __pa(empty_zero_page)));
#endif
>;>;>; 内核起始虚拟空间在内核页目录表中的索引
i = __pgd_offset(PAGE_OFFSET);
pgd = pgd_base + i;
>;>;>; #define PTRS_PER_PGD 1024
>;>;>; 对页目录的从768项开始的每一项
for (; i < PTRS_PER_PGD; pgd++, i++) {
>;>;>; vaddr为第i项页目录项所映射的内核空间的起始虚拟地址,PGDIR_SIZE=4M
vaddr = i*PGDIR_SIZE;
if (end && (vaddr >;= end))
break;
#if CONFIG_X86_PAE
pmd = (pmd_t *) alloc_bootmem_low_pages(PAGE_SIZE);
set_pgd(pgd, __pgd(__pa(pmd) + 0x1));
#else
>;>;>; 对两级映射机制,pmd实际上是pgd
pmd = (pmd_t *)pgd;
#endif
if (pmd != pmd_offset(pgd, 0))
BUG();
for (j = 0; j < PTRS_PER_PMD; pmd++, j++) {
vaddr = i*PGDIR_SIZE + j*PMD_SIZE;
if (end && (vaddr >;= end))
break;
>;>;>; 假如内核不支持 Page Size Extensions
if (cpu_has_pse) {
。。。。。。。。。。
}
>;>;>; 分配内核页表
pte_base = pte = (pte_t *) alloc_bootmem_low_pages(PAGE_SIZE);
>;>;>; 对每一项页表项
for (k = 0; k < PTRS_PER_PTE; pte++, k++) {
vaddr = i*PGDIR_SIZE + j*PMD_SIZE + k*PAGE_SIZE;
if (end && (vaddr >;= end))
break;
>;>;>; 将页面的物理地址填入页表项中
*pte = mk_pte_phys(__pa(vaddr), PAGE_KERNEL);
}
>;>;>; 将页表的物理地址填入到页目录项中
set_pmd(pmd, __pmd(_KERNPG_TABLE + __pa(pte_base)));
if (pte_base != pte_offset(pmd, 0))
BUG();
}
}
/*
* Fixed mappings, only the page table structure has to be
* created - mappings will be set by set_fixmap():
*/
vaddr = __fix_to_virt(__end_of_fixed_addresses - 1) & PMD_MASK;
fixrange_init(vaddr, 0, pgd_base);
#if CONFIG_HIGHMEM
。。。。。。。。。。。。
#endif
#if CONFIG_X86_PAE
。。。。。。。。。。。。
#endif
}
[目录]
--------------------------------------------------------------------------------
内核线程页目录的借用
创建内核线程的时候,由于内核线程没有用户空间,而所有进程的内核页目录都是一样的((某些情况下可能有不同步的情况出现,主要是为了减轻同步所有进程内核页目录的开销,而只是在各个进程要访问内核空间,如果有不同步的情况,然后才进行同步处理),所以创建的内核线程的内核页目录总是借用进程0的内核页目录。
>;>;>; kernel_thread以标志CLONE_VM调用clone系统调用
/*
* Create a kernel thread
*/
int kernel_thread(int (*fn)(void *), void * arg, unsigned long flags)
{
long retval, d0;
__asm__ __volatile__(
"movl %%esp,%%esi\n\t"
"int $0x80\n\t" /* Linux/i386 system call */
"cmpl %%esp,%%esi\n\t" /* child or parent? */
/* Load the argument into eax, and push it. That way, it does
* not matter whether the called function is compiled with
* -mregparm or not. */
"movl %4,%%eax\n\t"
"pushl %%eax\n\t"
"call *%5\n\t" /* call fn */
"movl %3,%0\n\t" /* exit */
"int $0x80\n"
"1:\t"
:"=&a" (retval), "=&S" (d0)
:"0" (__NR_clone), "i" (__NR_exit),
"r" (arg), "r" (fn),
"b" (flags | CLONE_VM)
: "memory";
return retval;
}
>;>;>; sys_clone->;do_fork->;copy_mm:
static int copy_mm(unsigned long clone_flags, struct task_struct * tsk)
{
struct mm_struct * mm, *oldmm;
int retval;
。。。。。。。。
tsk->;mm = NULL;
tsk->;active_mm = NULL;
/*
* Are we cloning a kernel thread?
*
* We need to steal a active VM for that..
*/
>;>;>; 如果是内核线程的子线程(mm=NULL),则直接退出,此时内核线程mm和active_mm均为为NULL
oldmm = current->;mm;
if (!oldmm)
return 0;
>;>;>; 内核线程,只是增加当前进程的虚拟空间的引用计数
if (clone_flags & CLONE_VM) {
atomic_inc(&oldmm->;mm_users);
mm = oldmm;
goto good_mm;
}
。。。。。。。。。。
good_mm:
>;>;>; 内核线程的mm和active_mm指向当前进程的mm_struct结构
tsk->;mm = mm;
tsk->;active_mm = mm;
return 0;
。。。。。。。
}
然后内核线程一般调用daemonize来释放对用户空间的引用:
>;>;>; daemonize->;exit_mm->;_exit_mm:
/*
* Turn us into a lazy TLB process if we
* aren't already..
*/
static inline void __exit_mm(struct task_struct * tsk)
{
struct mm_struct * mm = tsk->;mm;
mm_release();
if (mm) {
atomic_inc(&mm->;mm_count);
if (mm != tsk->;active_mm) BUG();
/* more a memory barrier than a real lock */
task_lock(tsk);
>;>;>; 释放用户虚拟空间的数据结构
tsk->;mm = NULL;
task_unlock(tsk);
enter_lazy_tlb(mm, current, smp_processor_id());
>;>;>; 递减mm的引用计数并是否为0,是则释放mm所代表的映射
mmput(mm);
}
}
asmlinkage void schedule(void)
{
。。。。。。。。。
if (!current->;active_mm) BUG();
。。。。。。。。。
prepare_to_switch();
{
struct mm_struct *mm = next->;mm;
struct mm_struct *oldmm = prev->;active_mm;
>;>;>; mm = NULL,选中的为内核线程
if (!mm) {
>;>;>; 对内核线程,active_mm = NULL,否则一定是出错了
if (next->;active_mm) BUG();
>;>;>; 选中的内核线程active_mm借用老进程的active_mm
next->;active_mm = oldmm;
atomic_inc(&oldmm->;mm_count);
enter_lazy_tlb(oldmm, next, this_cpu);
} else {
>;>;>; mm != NULL 选中的为用户进程,active_mm必须与mm相等,否则一定是出错了
if (next->;active_mm != mm) BUG();
switch_mm(oldmm, mm, next, this_cpu);
}
>;>;>; prev = NULL ,切换出去的是内核线程
if (!prev->;mm) {
>;>;>; 设置其 active_mm = NULL 。
prev->;active_mm = NULL;
mmdrop(oldmm);
}
}
}
对内核线程的虚拟空间总结一下:
1、创建的时候:
父进程是用户进程,则mm和active_mm均共享父进程的,然后内核线程一般调用daemonize适头舖m
父进程是内核线程,则mm和active_mm均为NULL
总之,内核线程的mm = NULL;进程调度的时候以此为依据判断是用户进程还是内核线程。
2、进程调度的时候
如果切换进来的是内核线程,则置active_mm为切换出去的进程的active_mm;
如果切换出去的是内核线程,则置active_mm为NULL。
[目录]
--------------------------------------------------------------------------------
用户进程内核页目录的建立
用户进程内核页目录的建立
在fork一个进程的时候,必须建立进程自己的内核页目录项(内核页目录项要
与用户空间的的页目录放在同一个物理地址连续的页面上,所以不能共享,但
所有进程的内核页表与进程0共享?
3G用户,页目录中一项映射4M的空间(一项页目录1024项页表,每项页表对应1个页面4K)# 即:
#define PGDIR_SHIFT 22
#define PGDIR_SIZE (1UL << PGDIR_SHIFT)
>;>;>; sys_fork->;do_fork->;copy_mm->;mm_init->;pgd_alloc->;get_pgd_slow
#if CONFIG_X86_PAE
。。。。。。。。。。。。。
#else
extern __inline__ pgd_t *get_pgd_slow(void)
{
>;>;>; 分配页目录表(包含1024项页目录),即为一个进程分配的页目录可以映射的空间为10024*4M=4G
pgd_t *pgd = (pgd_t *)__get_free_page(GFP_KERNEL);
if (pgd) {
>;>;>; #define USER_PTRS_PER_PGD (TASK_SIZE/PGDIR_SIZE)
>;>;>; TASK_SIZE为3G大小,USER_PTRS_PER_PGD为用户空间对应的页目录项数目(3G/4M=768?
>;>;>; 将用户空间的页目录项清空
memset(pgd, 0, USER_PTRS_PER_PGD * sizeof(pgd_t));
>;>;>; 将内核页目录表(swapper_pg_dir)的第768项到1023项拷贝到进程的页目录表的第7688项到1023项中
memcpy(pgd + USER_PTRS_PER_PGD, swapper_pg_dir + USER_PTRS_PER_PGD, (PTRS_PER__PGD - USER_PTRS_PER_PGD) * sizeof(pgd_t));
}
return pgd;
}
#endif
[目录]
--------------------------------------------------------------------------------
内核页目录的同步
内核页目录的同步
当一个进程在内核空间发生缺页故障的时候,在其处理程序中,就要通过0号进程的页目录览 同步本进程的内核页目录,实际上就是拷贝0号进程的内核页目录到本进程中(内核页表与进程0共享,故不需要复制)。如下:
asmlinkage void do_page_fault(struct pt_regs *regs, unsigned long error_code)
{
。。。。。。。。
>;>;>; 缺页故障产生的地址
/* get the address */
__asm__("movl %%cr2,%0":"=r" (address));
tsk = current;
/*
* We fault-in kernel-space virtual memory on-demand. The
* 'reference' page table is init_mm.pgd.
*/
>;>;>; 如果缺页故障在内核空间
if (address >;= TASK_SIZE)
goto vmalloc_fault;
。。。。。。。。。
vmalloc_fault:
{
/*
* Synchronize this task's top level page-table
* with the 'reference' page table.
*/
int offset = __pgd_offset(address);
pgd_t *pgd, *pgd_k;
pmd_t *pmd, *pmd_k;
pgd = tsk->;active_mm->;pgd + offset;
pgd_k = init_mm.pgd + offset;
>;>;>; /*
>;>;>; * (pmds are folded into pgds so this doesnt get actually called,
>;>;>; * but the define is needed for a generic inline function.)
>;>;>; */
>;>;>; #define set_pmd(pmdptr, pmdval) (*(pmdptr) = pmdval)
>;>;>; #define set_pgd(pgdptr, pgdval) (*(pgdptr) = pgdval)
>;>;>; 如果本进程的该地址的内核页目录不存在
if (!pgd_present(*pgd)) {
>;>;>; 如果进程0的该地址处的内核页目录也不存在,则出错
if (!pgd_present(*pgd_k))
goto bad_area_nosemaphore;
>;>;>; 复制进程0的该地址的内核页目录到本进程的相应页目录中
set_pgd(pgd, *pgd_k);
return;
}
>;>;>; extern inline pmd_t * pmd_offset(pgd_t * dir, unsigned long address)
>;>;>; {
>;>;>; return (pmd_t *) dir;
>;>;>; }
pmd = pmd_offset(pgd, address);
pmd_k = pmd_offset(pgd_k, address);
>;>;>; 对中间页目录,如果是两级页表,下面的几步操作与上面的重复
if (pmd_present(*pmd) || !pmd_present(*pmd_k))
goto bad_area_nosemaphore;
set_pmd(pmd, *pmd_k);
return;
}
/*
* Switch to real mode and then execute the code
* specified by the code and length parameters.
* We assume that length will aways be less that 100!
*/
void machine_real_restart(unsigned char *code, int length)
{
。。。。。。。。。。。。。
/* Remap the kernel at virtual address zero, as well as offset zero
from the kernel segment. This assumes the kernel segment starts at
virtual address PAGE_OFFSET. */
memcpy (swapper_pg_dir, swapper_pg_dir + USER_PGD_PTRS,
sizeof (swapper_pg_dir [0]) * KERNEL_PGD_PTRS);
/* Make sure the first page is mapped to the start of physical memory.
It is normally not mapped, to trap kernel NULL pointer dereferences. */
pg0[0] = _PAGE_RW | _PAGE_PRESENT;
/*
* Use `swapper_pg_dir' as our page directory.
*/
asm volatile("movl %0,%%cr3": :"r" (__pa(swapper_pg_dir)));
[目录]
--------------------------------------------------------------------------------
mlock代码分析
系统调用mlock的作用是屏蔽内存中某些用户进程所要求的页。
mlock调用的语法为:
int sys_mlock(unsigned long start, size_t len);
初始化为:
len=(len+(start &~PAGE_MASK)+ ~PAGE_MASK)&AGE_MASK;
start &=PAGE_MASK;
其中mlock又调用do_mlock(),语法为:
int do_mlock(unsigned long start, size_t len,int on);
初始化为:
len=(len+~PAGE_MASK)&AGE_MASK;
由mlock的参数可看出,mlock对由start所在页的起始地址开始,长度为len(注:len=(len+(start&~PAGE_MASK)+ ~PAGE_MASK)&AGE_MASK)的内存区域的页进行加锁。
sys_mlock如果调用成功返回,这其中所有的包含具体内存区域的页必须是常驻内存的,或者说在调用munlock 或 munlockall之前这部分被锁住的页面必须保留在内存。当然,如果调用mlock的进程终止或者调用exec执行其他程序,则这部分被锁住的页面被释放。通过fork()调用所创建的子进程不能够继承由父进程调用mlock锁住的页面。
内存屏蔽主要有两个方面的应用:实时算法和高度机密数据的处理。实时应用要求严格的分时,比如调度,调度页面是程序执行延时的一个主要因素。保密安全软件经常处理关键字节,比如密码或者密钥等数据结构。页面调度的结果是有可能将这些重要字节写到外存(如硬盘)中去。这样一些黑客就有可能在这些安全软件删除这些在内存中的数据后还能访问部分在硬盘中的数据。 而对内存进行加锁完全可以解决上述难题。
内存加锁不使用压栈技术,即那些通过调用mlock或者mlockall被锁住多次的页面可以通过调用一次munlock或者munlockall释放相应的页面
mlock的返回值分析:若调用mlock成功,则返回0;若不成功,则返回-1,并且errno被置位,进程的地址空间保持原来的状态。返回错误代码分析如下:
ENOMEM:部分具体地址区域没有相应的进程地址空间与之对应或者超出了进程所允许的最大可锁页面。
EPERM:调用mlock的进程没有正确的优先权。只有root进程才允许锁住要求的页面。
EINVAL:输入参数len不是个合法的正数。
mlock所用到的主要数据结构和重要常量
1.mm_struct
struct mm_struct {
int count;
pgd_t * pgd; /* 进程页目录的起始地址,如图2-3所示 */
unsigned long context;
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack, start_mmap;
unsigned long arg_start, arg_end, env_start, env_end;
unsigned long rss, total_vm, locked_vm;
unsigned long def_flags;
struct vm_area_struct * mmap; /* 指向vma双向链表的指针 */
struct vm_area_struct * mmap_avl; /* 指向vma AVL树的指针 */
struct semaphore mmap_sem;
}
start_code、end_code:进程代码段的起始地址和结束地址。
start_data、end_data:进程数据段的起始地址和结束地址。
arg_start、arg_end:调用参数区的起始地址和结束地址。
env_start、env_end:进程环境区的起始地址和结束地址。
rss:进程内容驻留在物理内存的页面总数。
2. 虚存段(vma)数据结构:vm_area_atruct
虚存段vma由数据结构vm_area_atruct(include/linux/mm.h)描述:
struct vm_area_struct {
struct mm_struct * vm_mm; /* VM area parameters */
unsigned long vm_start;
unsigned long vm_end;
pgprot_t vm_page_prot;
unsigned short vm_flags;
/* AVL tree of VM areas per task, sorted by address */
short vm_avl_height;
struct vm_area_struct * vm_avl_left;
struct vm_area_struct * vm_avl_right;
/* linked list of VM areas per task, sorted by address */
struct vm_area_struct * vm_next;
/* for areas with inode, the circular list inode->;i_mmap */
/* for shm areas, the circular list of attaches */
/* otherwise unused */
struct vm_area_struct * vm_next_share;
struct vm_area_struct * vm_prev_share;
/* more */
struct vm_operations_struct * vm_ops;
unsigned long vm_offset;
struct inode * vm_inode;
unsigned long vm_pte; /* shared mem */
};
vm_start;//所对应内存区域的开始地址
vm_end; //所对应内存区域的结束地址
vm_flags; //进程对所对应内存区域的访问权限
vm_avl_height;//avl树的高度
vm_avl_left; //avl树的左儿子
vm_avl_right; //avl树的右儿子
vm_next;// 进程所使用的按地址排序的vm_area链表指针
vm_ops;//一组对内存的操作
这些对内存的操作是当对虚存进行操作的时候Linux系统必须使用的一组方法。比如说,当进程准备访问某一虚存区域但是发现此区域在物理内存不存在时(缺页中断),就激发某种对内存的操作执行正确的行为。这种操作是空页(nopage)操作。当Linux系统按需调度可执行的页面映象进入内存时就使用这种空页(nopage)操作。
当一个可执行的页面映象映射到进程的虚存地址时,一组vm_area_struct结构的数据结构(vma)就会生成。每一个vm_area_struct的数据结构(vma)代表可执行的页面映象的一部分:可执行代码,初始化数据(变量),非初始化数据等等。Linux系统可以支持大量的标准虚存操作,当vm_area_struct数据结构(vma)一被创建,它就对应于一组正确的虚存操作。
属于同一进程的vma段通过vm_next指针连接,组成链表。如图2-3所示,struct mm_struct结构的成员struct vm_area_struct * mmap 表示进程的vma链表的表头。
为了提高对vma段 查询、插入、删除操作的速度,LINUX同时维护了一个AVL(Adelson-Velskii and Landis)树。在树中,所有的vm_area_struct虚存段均有左指针vm_avl_left指向相邻的低地址虚存段,右指针vm_avl_right指向相邻的高地址虚存段,如图2-5。struct mm_struct结构的成员struct vm_area_struct * mmap_avl表示进程的AVL树的根,vm_avl_height表示AVL树的高度。
对平衡树mmap_avl的任何操作必须满足平衡树的一些规则:
Consistency and balancing rulesJ(一致性和平衡规则):
tree->;vm_avl_height==1+max(heightof(tree->;vm_avl_left),heightof(
tree->;vm_avl_right))
abs( heightof(tree->;vm_avl_left) - heightof(tree->;vm_avl_right) ) <= 1
foreach node in tree->;vm_avl_left: node->;vm_avl_key <= tree->;vm_avl_key, foreach node in tree->;vm_avl_right: node->;vm_avl_key >;= tree->;vm_avl_key.
注:其中node->;vm_avl_key= node->;vm_end
对vma可以进行加锁、加保护、共享和动态扩展等操作。
3.重要常量
mlock系统调用所用到的重要常量有:PAGE_MASK、PAGE_SIZE、PAGE_SHIFT、RLIMIT_MEMLOCK、VM_LOCKED、 PF_SUPERPRIV等。它们的值分别如下:
PAGE_SHIFT 12 // PAGE_SHIFT determines the page size
PAGE_SIZE 0x1000 //1UL<<AGE_SHIFT
PAGE_MASK ~(PAGE_SIZE-1) //a very useful constant variable
RLIMIT_MEMLOCK 8 //max locked-in-memory address space
VM_LOCKED 0x2000 //8*1024=8192, vm_flags的标志之一。
PF_SUPERPRIV 0x00000100 //512,
mlock系统调用代码函数功能分析
下面对各个函数的功能作详细的分析((1)和(2)在前面简介mlock时已介绍过,并在后面有详细的程序流程):
suser():如果用户有效(即current->;euid == 0 ),则设置进程标志为root优先权(current->;flags |= PF_SUPERPRIV),并返回1;否则返回0。
find_vma(struct mm_struct * mm, unsigned long addr):输入参数为当前进程的mm、需要加锁的开始内存地址addr。find_vma的功能是在mm的mmap_avl树中寻找第一个满足mm->;mmap_avl->;vm_start<=addr< mm->;mmap_avl->;vm_end的vma,如果成功则返回此vma;否则返回空null。
mlock_fixup(struct vm_area_struct * vma, unsigned long start, unsigned long end, unsigned int newflags):输入参数为vm_mmap链中的某个vma、需要加锁内存区域起始地址和结束地址、需要修改的标志(0:加锁,1:释放锁)。
merge_segments(struct mm_struct * mm, unsigned long start_addr, unsigned long end_addr):输入参数为当前进程的mm、需要加锁的开始内存地址start_addr和结束地址end_addr。merge_segments的功能的是尽最大可能归并相邻(即内存地址偏移量连续)并有相同属性(包括vm_inode,vm_pte,vm_ops,vm_flags)的内存段,在这过程中冗余的vm_area_structs被释放,这就要求vm_mmap链按地址大小排序(我们不需要遍历整个表,而只需要遍历那些交叉或者相隔一定连续区域的邻接vm_area_structs)。当然在缺省的情况下,merge_segments是对vm_mmap_avl树进行循环处理,有多少可以合并的段就合并多少。
mlock_fixup_all(struct vm_area_struct * vma, int newflags):输入参数为vm_mmap链中的某个vma、需要修改的标志(0:加锁,1:释放锁)。mlock_fixup_all的功能是根据输入参数newflags修改此vma的vm_flags。
mlock_fixup_start(struct vm_area_struct * vma,unsigned long end, int newflags):输入参数为vm_mmap链中的某个vma、需要加锁内存区域结束地址、需要修改的标志(0:加锁,1:释放锁)。mlock_fixup_start的功能是根据输入参数end,在内存中分配一个新的new_vma,把原来的vma分成两个部分: new_vma和vma,其中new_vma的vm_flags被设置成输入参数newflags;并且按地址(new_vma->;start和new_vma->;end)大小序列把新生成的new->;vma插入到当前进程mm的mmap链或mmap_avl树中(缺省情况下是插入到mmap_avl树中)。
注:vma->;vm_offset+= vma->;vm_start-new_vma->;vm_start;
mlock_fixup_end(struct vm_area_struct * vma,unsigned long start, int newflags):输入参数为vm_mmap链中的某个vma、需要加锁内存区域起始地址、需要修改的标志(0:加锁,1:释放锁)。mlock_fixup_end的功能是根据输入参数start,在内存中分配一个新的new_vma,把原来的vma分成两个部分:vma和new_vma,其中new_vma的vm_flags被设置成输入参数newflags;并且按地址大小序列把new->;vma插入到当前进程mm的mmap链或mmap_avl树中。
注:new_vma->;vm_offset= vma->;vm_offset+(new_vma->;vm_start-vma->;vm_start);
mlock_fixup_middle(struct vm_area_struct * vma,unsigned long start, unsigned long end, int newflags):输入参数为vm_mmap链中的某个vma、需要加锁内存区域起始地址和结束地址、需要修改的标志(0:加锁,1:释放锁)。mlock_fixup_middle的功能是根据输入参数start、end,在内存中分配两个新vma,把原来的vma分成三个部分:left_vma、vma和right_vma,其中vma的vm_flags被设置成输入参数newflags;并且按地址大小序列把left->;vma和right->;vma插入到当前进程mm的mmap链或mmap_avl树中。
注:vma->;vm_offset += vma->;vm_start-left_vma->;vm_start;
right_vma->;vm_offset += right_vma->;vm_start-left_vma->;vm_start;
kmalloc():将在后面3.3中有详细讨论。
insert_vm_struct(struct mm_struct *mm, struct vm_area_struct *vmp):输入参数为当前进程的mm、需要插入的vmp。insert_vm_struct的功能是按地址大小序列把vmp插入到当前进程mm的mmap链或mmap_avl树中,并且把vmp插入到vmp->;inode的i_mmap环(循环共享链)中。
avl_insert_neighbours(struct vm_area_struct * new_node,** ptree,** to_the_left,** to_the_right):输入参数为当前需要插入的新vma结点new_node、目标mmap_avl树ptree、新结点插入ptree后它左边的结点以及它右边的结点(左右边结点按mmap_avl中各vma->;vma_end大小排序)。avl_insert_neighbours的功能是插入新vma结点new_node到目标mmap_avl树ptree中,并且调用avl_rebalance以保持ptree的平衡树特性,最后返回new_node左边的结点以及它右边的结点。
avl_rebalance(struct vm_area_struct *** nodeplaces_ptr, int count):输入参数为指向vm_area_struct指针结构的指针数据nodeplaces_ptr[](每个元素表示需要平衡的mmap_avl子树)、数据元素个数count。avl_rebalance的功能是从nodeplaces_ptr[--count]开始直到nodeplaces_ptr[0]循环平衡各个mmap_avl子树,最终使整个mmap_avl树平衡。
down(struct semaphore * sem):输入参数为同步(进入临界区)信号量sem。down的功能根据当前信号量的设置情况加锁(阻止别的进程进入临界区)并继续执行或进入等待状态(等待别的进程执行完成退出临界区并释放锁)。
down定义在/include/linux/sched.h中:
extern inline void down(struct semaphore * sem)
{
if (sem->;count <= 0)
__down(sem);
sem->;count--;
}
up(struct semaphore * sem)输入参数为同步(进入临界区)信号量sem。up的功能根据当前信号量的设置情况(当信号量的值为负数:表示有某个进程在等待使用此临界区 )释放锁。
up定义在/include/linux/sched.h中:
extern inline void up(struct semaphore * sem)
{
sem->;count++;
wake_up(&sem->;wait);
}
kfree_s(a,b):kfree_s定义在/include/linux/malloc.h中:#define kfree_s(a,b) kfree(a)。而kfree()将在后面3.3中详细讨论。
avl_neighbours(struct vm_area_struct * node,* tree,** to_the_left,** to_the_right):输入参数为作为查找条件的vma结点node、目标mmap_avl树tree、node左边的结点以及它右边的结点(左右边结点按mmap_avl中各vma->;vma_end大小排序)。avl_ neighbours的功能是根据查找条件node在目标mmap_avl树ptree中找到node左边的结点以及它右边的结点,并返回。
avl_remove(struct vm_area_struct * node_to_delete, ** ptree):输入参数为需要删除的结点node_to_delete和目标mmap_avl树ptree。avl_remove的功能是在目标mmap_avl树ptree中找到结点node_to_delete并把它从平衡树中删除,并且调用avl_rebalance以保持ptree的平衡树特性。
remove_shared_vm_struct(struct vm_area_struct *mpnt):输入参数为需要从inode->;immap环中删除的vma结点mpnt。remove_shared_vm_struct的功能是从拥有vma结点mpnt 的inode->;immap环中删除的该结点。
[目录]
--------------------------------------------------------------------------------
memory.c
Memory.c中,Linux提供了对虚拟内存操作的若干函数,其中包括对虚拟页的复制、新建页表、清除页表、处理缺页中断等等。
[目录]
--------------------------------------------------------------------------------
copy_page
1.static inline void copy_page(unsigned long from, unsigned long to)
为了节约内存的使用,在系统中,各进程通常采用共享内存,即不同的进程可以共享同一段代码段或数据段。当某一进程发生对共享的内存发生写操作时,为了不影响其它进程的正常运行,系统将把该内存块复制一份,供需要写操作的进程使用,这就是所谓的copy-on-write机制。copy_page就是提供复制内存功能的函数,它调用C语言中标准的内存操作函数,将首地址为from的一块虚拟内存页复制到首地址为to的空间中。
[目录]
--------------------------------------------------------------------------------
clear_page_tables
2、void clear_page_tables(struct task_struct * tsk)
clear_page_table的功能是将传入的结构tsk中的pgd页表中的所有项都清零,同时将二级页表所占的空间都释放掉。传入clear_page_tables的是当前进程的tsk结构,取得该进程的一级页目录指针pgd后,采用循环的方式,调用free_one_pgd清除pgd表。表共1024项。在free_one_pgd中,实际执行的功能只调用一次free_one_pmd(在80x86中,由于硬件的限制,只有两级地址映射,故将pmd与pgd合并在一起)。在free_one_pmd中,函数调用pte_free将对应于pmd的二级页表所占的物理空间释放掉(进程代码、数据所用的物理内存在do_munmap释放掉了)并将pmd赋值为零。
clear_page_table在系统启动一个可执行文件的映象或载入一个动态链接库时被调用。在fs/exec.c中的do_load_elf_binary()或do_load_aout_binary()调用flash_old_exec,后者调用exec_mmap,而exec_mmap调用clear_page_table。其主要功能是当启动一个新的应用程序的时候,将复制的mm_struct中的页表清除干净,并释放掉原有的所有二级页表空间。
[目录]
--------------------------------------------------------------------------------
oom
3、void oom(struct task_struct * task)
返回出错信息。
[目录]
--------------------------------------------------------------------------------
free_page_tables
4、void free_page_tables(struct mm_struct * mm)
在free_page_table中,大部分的代码与clear_page_table中的函数一致。所不同的是,该函数在最后调用了pgd_free(page_dir),即不光释放掉二级页表所占的空间,同时还释放一级页目录所占的空间。这是因为free_page_tables被__exit_mm调用,__exit_mm又被do_exit (kernel/kernel.c)调用。当进程中止、系统退出或系统重起时都需要用do_exit(属于进程管理)将所有的进程结束掉。在结束进程过程中 ,将调用free_page_table将进程的空间全部释放掉,当然包括释放进程一级页目录所占的空间。
[目录]
--------------------------------------------------------------------------------
new_page_tables
5、int new_page_tables(struct task_struct * tsk)
该函数的主要功能是建立新的页目录表,它的主要流程如如下:
·调用pgd_alloc()为新的页目录表申请一片4K空间 。
·将初始化进程的内存结构中从768项开始到1023项的内容复制给新的页表(所有的进程都共用虚拟空间中 3G~4G的内存,即在核心态时可以访问所有相同的存储空间)。
·调用宏SET_PAGE_DIR(include/asm/pgtable.h)将进程控制块tsk->;ts->;CR3的值改为新的页目录表的首地址,同时将CPU中的CR3寄存器的值改为新的页目录表的首地址,从而使新进程进入自己的运行空间。
·将tsk->;mm->;pgd改为新的页目录表的首地址。
·new_page_tables被copy_mm调用,而copy_mm被copy_mm_do_fork调用,这两个函数都在kernel/fork.c中。同时,new_page_tables也可以在exec_mmap(fs/exec.c)中调用。即新的进程的产生可以通过两种途径,一种是fork,在程序中动态地生成新的进程,这样新进程的页表原始信息利用copy_mm从其父进程中继承而得,另一种是运行一个可执行文件映象,通过文件系统中的exec.c,将映象复制到tsk结构中。两种方法都需要调用new_page_tables为新进程分配页目录表。
[目录]
--------------------------------------------------------------------------------
copy_one_pte
6、static inline void copy_one_pte(pte_t * old_pte, pte_t * new_pte, int cow)
将原pte页表项复制到new_pte上,其流程如下:
·检测old_pte是否在内存中,如不在物理内存中,调用swap_duplicate按old_pte在swap file中的入口地址,将old_pte复制到内存中,同时把old_pte的入口地址赋给new_pte并返回。反之转向3。
获取old_pte对应的物理地址的页号。
·根据页号判断old_pte是否为系统保留的,如果为系统保留的,这些页为所有的进程在核心态下使用,用户进程没有写的权利,则只需将old_pte指针直接转赋给new_pte后返回。反之则该pte属于普通内存的,则转向4。
·根据传入的C-O-W标志,为old_pte置写保护标志,如果该页是从swap_cache中得来的,将old_pte页置上“dirty”标志。将old_pte赋值给new_pte。
·将mem_map结构中关于物理内存使用进程的个数的数值count加1。
[目录]
--------------------------------------------------------------------------------
copy_pte_range
7、static inline int copy_pte_range(pmd_t *dst_pmd, pmd_t *src_pmd,
unsigned long address, unsigned long size, int cow)
通过循环调用copy_one_pte将从源src_pmd中以地址address开始的长度为size的空间复制给dst_pmd中。如dst_pmd中还未分配地址为address的页表项,则先给三级页表pte表分配4K空间。(每调用一次copy_one_pte复制4K空间。在一次copy_pte_range中最多可复制4M空间)。
[目录]
--------------------------------------------------------------------------------
copy_pmd_range
8、static inline int copy_pmd_range(pgd_t *dst_pgd, pgd_t *src_pgd,
unsigned long address, unsigned long size, int cow)
通过循环调用copy_pte_range将从源src_pgd中以地址address开始的长度为size的空间复制给dst_pgd中。如dst_pgd中还未分配地址为address的页表项,则在一级(同时也是二级)页表中给对应的pmd分配目录项。
[目录]
--------------------------------------------------------------------------------
copy_page_range
9、int copy_page_range(struct mm_struct *dst, struct mm_struct *src,
struct vm_area_struct *vma)
该函数的主要功能是将某个任务或进程的vma块复制给另一个任务或进程。其工作机制是循环调用copy_pmd_range,将vma块中的所有虚拟空间复制到对应的虚拟空间中。在做复制之前,必须确保新任务对应的被复制的虚拟空间中必须都为零。copy_page_range按dup_mmap()->;copy_mm()->;do_fork()的顺序被调用(以上三个函数均在kernel/fork.c中)。当进程被创建的时候,需要从父进程处复制所有的虚拟空间,copy_page_range完成的就是这个任务。
[目录]
--------------------------------------------------------------------------------
free_pte
9、static inline void free_pte(pte_t page)
虚存页page如在内存中,且不为系统的保留内存,调用free_page将其释放掉(如在系统保留区中,则为全系统共享,故不能删除)。
如page在swap file中,调用swap_free()将其释放。
[目录]
--------------------------------------------------------------------------------
forget_pte
10、static inline void forget_pte(pte_t page)
如page不为空,调用free_pte将其释放。
[目录]
--------------------------------------------------------------------------------
zap_pte_range
11、static inline void zap_pte_range(pmd_t * pmd, unsigned long address,
unsigned long size)
zap为zero all pages的缩写。该函数的作用是将在pmd中从虚拟地址address开始,长度为size的内存块通过循环调用pte_clear将其页表项清零,调用free_pte将所含空间中的物理内存或交换空间中的虚存页释放掉。在释放之前,必须检查从address开始长度为size的内存块有无越过PMD_SIZE.(溢出则可使指针逃出0~1023的区间)。
进程
一 进程调度
进程的状态([include/linux.h]):
TASK_RUNNING, it means that it is in the "Ready List"
TASK_INTERRUPTIBLE, task waiting for a signal or a resource (sleeping)
TASK_UNINTERRUPTIBLE, task waiting for a resource (sleeping), it is in same "Wait Queue"
TASK_ZOMBIE, task child without father
TASK_STOPPED, task being debugged
______________ CPU Available ______________
| | ---------------->; | |
| TASK_RUNNING | | Real Running |
|______________| <---------------- |______________|
CPU Busy
| /|\
Waiting for | | Resource
Resource | | Available
\|/ |
______________________
| |
| TASK_INTERRUPTIBLE / |
| TASK-UNINTERRUPTIBLE |
|______________________|
Main Multitasking Flow
从系统内核的角度看来,一个进程仅仅是进程控制表(process table)中的一项。进程控制表中的每一项都是一个task_struct 结构,而task_struct 结构本身是在include/linux/sched.h中定义的。在task_struct结构中存储各种低级和高级的信息,包括从一些硬件设备的寄存器拷贝到进程的工作目录的链接点。
进程控制表既是一个数组,又是一个双向链表,同时又是一个树。其物理实现是一个包括多个指针的静态数组。此数组的长度保存在include/linux/tasks.h 定义的常量NR_TASKS中,其缺省值为128,数组中的结构则保存在系统预留的内存页中。链表是由next_task 和prev_task两个指针实现的,而树的实现则比较复杂。
系统启动后,内核通常作为某一个进程的代表。一个指向task_struct的全局指针变量current用来记录正在运行的进程。变量current只能由kernel/sched.c中的进程调度改变。当系统需要查看所有的进程时,则调用for_each_task,这将比系统搜索数组的速度要快得多。
二、用户进程和内核线程
某一个进程只能运行在用户方式(user mode)或内核方式(kernel mode)下。用户程序运行在用户方式下,而系统调用运行在内核方式下。在这两种方式下所用的堆栈不一样:用户方式下用的是一般的堆栈,而内核方式下用的是固定大小的堆栈(一般为一个内存页的大小)
尽管linux是一个宏内核系统,内核线程依然存在,以便并行地处理一些内核的“家务室”。这些任务不占用USER memory(用户空间),而仅仅使用KERNEL memory。和其他内核模块一样,它们也在高级权限(i386系统中的RING 0)下工作作。内核线程是被kernel_thread [arch/i386/kernel/process]创建的,它又通过调用著名的clone系统调用[arch/i386/kernel/process.c] (类似fork系统调用的所有功能都是由它最终实现):
int kernel_thread(int (*fn)(void *), void * arg, unsigned long flags)
{
long retval, d0;
__asm__ __volatile__(
"movl %%esp,%%esi\n\t"
"int $0x80\n\t" /* Linux/i386 system call */
"cmpl %%esp,%%esi\n\t" /* child or parent? */
"je 1f\n\t" /* parent - jump */
/* Load the argument into eax, and push it. That way, it does
* not matter whether the called function is compiled with
* -mregparm or not. */
"movl %4,%%eax\n\t"
"pushl %%eax\n\t"
"call *%5\n\t" /* call fn */
"movl %3,%0\n\t" /* exit */
"int $0x80\n"
"1:\t"
:"=&a" (retval), "=&S" (d0)
:"0" (__NR_clone), "i" (__NR_exit),
"r" (arg), "r" (fn),
"b" (flags | CLONE_VM)
: "memory";
return retval;
}
一旦调用,我们就有了一个新的任务(Task) (一般PID都很小, 例如2,3,等) 等待一个响应很慢的资源,例如swap或者usb事件,以便同步。下面是一些最常用的内核线程(你可以用ps x命令):
PID COMMAND
1 init
2 keventd
3 kswapd
4 kreclaimd
5 bdflush
6 kupdated
7 kacpid
67 khubd
init内核线程也是启动以后最初的进程。 它会调用其它用户模式的任务,(/etc/inittab)例如控制台守护进程(daemons), tty守护进程以及网络守护进程(rc脚本)。
下面是一个典型的内核线程kswapd [mm/vmscan.c].
kswapd是被clone()建立的 [arch/i386/kernel/process.c]''
|do_initcalls
|kswapd_init
|kernel_thread
|syscall fork (in assembler)
·do_initcalls [init/main.c]
·kswapd_init [mm/vmscan.c]
·kernel_thread [arch/i386/kernel/process.c]
三 进程创建,运行和消失
Linux系统使用系统调用fork( )来创建一个进程,使用exit( )来结束进程。fork( )和exit( )的源程序保存在kernel/fork.c and kernel/exit.c中。fork( )的主要任务是初始化要创建进程的数据结构,其主要的步骤有:
1)申请一个空闲的页面来保存task_struct。
2)查找一个空的进程槽(find_empty_process( ))。
3)为kernel_stack_page申请另一个空闲的内存页作为堆栈。
4)将父进程的LDT表拷贝给子进程。
5)复制父进程的内存映射信息。
6)管理文件描述符和链接点。
|sys_fork
|do_fork
|alloc_task_struct
|__get_free_pages
|p->;state = TASK_UNINTERRUPTIBLE
|copy_flags
|p->;pid = get_pid
|copy_files
|copy_fs
|copy_sighand
|copy_mm // should manage CopyOnWrite (I part)
|allocate_mm
|mm_init
|pgd_alloc ->; get_pgd_fast
|get_pgd_slow
|dup_mmap
|copy_page_range
|ptep_set_wrprotect
|clear_bit // set page to read-only
|copy_segments // For LDT
|copy_thread
|childregs->;eax = 0
|p->;thread.esp = childregs // child fork returns 0
|p->;thread.eip = ret_from_fork // child starts from fork exit
|retval = p->;pid // parent fork returns child pid
|SET_LINKS // insertion of task into the list pointers
|nr_threads++ // Global variable
|wake_up_process(p) // Now we can wake up just created child
|return retval
·sys_fork [arch/i386/kernel/process.c]
·do_fork [kernel/fork.c]
·alloc_task_struct [include/asm/processor.c]
·__get_free_pages [mm/page_alloc.c]
·get_pid [kernel/fork.c]
·copy_files
·copy_fs
·copy_sighand
·copy_mm
·allocate_mm
·mm_init
·pgd_alloc ->; get_pgd_fast [include/asm/pgalloc.h]
·get_pgd_slow
·dup_mmap [kernel/fork.c]
·copy_page_range [mm/memory.c]
·ptep_set_wrprotect [include/asm/pgtable.h]
·clear_bit [include/asm/bitops.h]
·copy_segments [arch/i386/kernel/process.c]
·copy_thread
·SET_LINKS [include/linux/sched.h]
·wake_up_process [kernel/sched.c]
撤消一个进程可能稍微复杂些,因为撤消子进程必须通知父进程。另外,使用kill( )也可以结束一个进程。sys_kill( )、sys_wait( )和sys_exit( )都保存在文件exit.c中。
使用fork ( )创建一个进程后,程序的两个拷贝都在运行。通常一个拷贝使用exec ( )调用另一个拷贝。系统调用exec ( )负责定位可执行文件的二进制代码,并负责装入和运行。Linux系统中的exec ( )通过使用linux_binfmt结构支持多种二进制格式。每种二进制格式都代表可执行代码和链接库。linux _binfmt结构种包含两个指针,一个指向装入可执行代码的函数,另一个指向装入链接库的函数。
Unix系统提供给程序员6种调用exec( ) 的方法。其中的5种是作为库函数实现,而sys_execve( )是由系统内核实现的。它执行一个十分简单的任务:装入可执行文件的文件头,并试图执行它。如果文件的头两个字节是#! ,那么它就调用在文件第一行中所指定的解释器,否则,它将逐个尝试注册的二进制格式。
[目录]
--------------------------------------------------------------------------------
信号
struct semaphore {
atomic_t count; 进程抓取semaphore时减1
int sleepers; 抓取semaphore失败时增1
wait_queue_head_t wait; semaphore的等待队列
};
down(&sem) 编绎成:
movl $sem,% ecx 通过寄存器ecx向__down函数传递sem指针
decl sem
js 2f 如果为负值,表示semaphore已被占用,执行__down_failed过程
1:
由于出现semaphore竞争的可能性比较小,将分支代码转移到.text.lock段,以缩短正常的指令路径.
.section .text.lock,"ax"
2: call __down_failed
jmp 1b
.previous
...
up(&sem) 编绎成:
movl $sem,% ecx
incl sem
jle 2f 如果小于或等于0,表示该semaphore有进程在等待,就去调用__up_wakeup
1:
.section .text.lock,"ax"
2: call __up_wakeup
jmp 1b
.previous
...
__down_failed:
pushl % eax
pushl % edx
pushl % ecx ; eax,edx,ecx是3个可用于函数参数的寄存器
call __down
popl % ecx
popl % edx
popl % eax
ret
__up_wakeup:
pushl % eax
pushl % edx
pushl % ecx
call __up
popl % ecx
popl % edx
popl % eax
ret
; semaphore.c
void __down(struct semaphore * sem)
{
struct task_struct *tsk = current;
DECLARE_WAITQUEUE(wait, tsk);
tsk->;state = TASK_UNINTERRUPTIBLE;
add_wait_queue_exclusive(&sem->;wait, &wait);
// 将当前进程加入到该semaphore的等待队列中
spin_lock_irq(&semaphore_lock);
sem->;sleepers++;
for (; {
int sleepers = sem->;sleepers;
/*
* Add "everybody else" into it. They aren't
* playing, because we own the spinlock.
*/
// atomic_add_negative(int i,atomic_t *v)将i + v->;counter相加,
// 结果为负返回1,否则返回0
if (!atomic_add_negative(sleepers - 1, &sem->;count)) {
// 如果(sleepers - 1 + sem->;count.counter)非负,则说明
// semaphore已经被释放,可以返回
sem->;sleepers = 0;
break;
}
sem->;sleepers = 1; /* us - see -1 above */
spin_unlock_irq(&semaphore_lock);
// 当semaphore被up()唤醒时,schedule()返回
schedule();
// 虽然已线程被up恢复,但为防止碰巧又有一个线程获得了semaphore,
// 因此将它们放在循环体中
tsk->;state = TASK_UNINTERRUPTIBLE;
spin_lock_irq(&semaphore_lock);
}
spin_unlock_irq(&semaphore_lock);
// 该进程获得了semaphore,将它从等待队列中删除
remove_wait_queue(&sem->;wait, &wait);
tsk->;state = TASK_RUNNING;
// 为什么这里要调用wake_up,是因为调用它没有副作用从而防止潜在的死锁吗?
wake_up(&sem->;wait);
}
void __up(struct semaphore *sem)
{
扩展为
__wake_up_common(&sem->;wait,TASK_UNINTERRUPTIBLE|TASK_INTERRUPTIBLE,1,0);
唤醒队列中第1个进程,即将第1个进程放入运行队列
wake_up(&sem->;wait);
}
; sched.c
static inline void __wake_up_common (wait_queue_head_t *q, unsigned int
mode,
int nr_exclusive, const int sync)
{
struct list_head *tmp, *head;
struct task_struct *p;
unsigned long flags;
if (!q)
goto out;
wq_write_lock_irqsave(&q->;lock, flags);
#if WAITQUEUE_DEBUG
CHECK_MAGIC_WQHEAD(q);
#endif
head = &q->;task_list;
#if WAITQUEUE_DEBUG
if (!head->;next || !head->;prev)
WQ_BUG();
#endif
tmp = head->;next;
while (tmp != head) {
unsigned int state;
wait_queue_t *curr = list_entry(tmp, wait_queue_t,
task_list);
tmp = tmp->;next;
#if WAITQUEUE_DEBUG
CHECK_MAGIC(curr->;__magic);
#endif
p = curr->;task;
state = p->;state;
if (state & mode) {
#if WAITQUEUE_DEBUG
curr->;__waker = (long)__builtin_return_address(0);
#endif
if (sync)
wake_up_process_synchronous(p);
else
wake_up_process(p);
if ((curr->;flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
break;
}
}
wq_write_unlock_irqrestore(&q->;lock, flags);
out:
return;
}
; sched.c
inline void wake_up_process(struct task_struct * p)
{
unsigned long flags;
/*
* We want the common case fall through straight, thus the goto.
*/
spin_lock_irqsave(&runqueue_lock, flags);
p->;state = TASK_RUNNING;
if (task_on_runqueue(p))
goto out;
add_to_runqueue(p);
reschedule_idle(p);
out:
spin_unlock_irqrestore(&runqueue_lock, flags);
}
; sched.c
static inline void wake_up_process_synchronous(struct task_struct * p)
{
unsigned long flags;
/*
* We want the common case fall through straight, thus the goto.
*/
spin_lock_irqsave(&runqueue_lock, flags);
p->;state = TASK_RUNNING;
if (task_on_runqueue(p))
goto out;
add_to_runqueue(p);
out:
spin_unlock_irqrestore(&runqueue_lock, flags);
}
; sched.h
static inline int task_on_runqueue(struct task_struct *p)
{
return (p->;run_list.next != NULL);
}
; sched.c
static inline void add_to_runqueue(struct task_struct * p)
{
list_add(&p->;run_list, &runqueue_head);
nr_running++;
}
static LIST_HEAD(runqueue_head);
; fork.c
void add_wait_queue_exclusive(wait_queue_head_t *q, wait_queue_t *
wait)
{
unsigned long flags;
wq_write_lock_irqsave(&q->;lock, flags);
wait->;flags = WQ_FLAG_EXCLUSIVE;
__add_wait_queue_tail(q, wait);
wq_write_unlock_irqrestore(&q->;lock, flags);
}
; wait.h
static inline void __add_wait_queue_tail(wait_queue_head_t *head,
wait_queue_t *new)
{
#if WAITQUEUE_DEBUG
if (!head || !new)
WQ_BUG();
CHECK_MAGIC_WQHEAD(head);
CHECK_MAGIC(new->;__magic);
if (!head->;task_list.next || !head->;task_list.prev)
WQ_BUG();
#endif
list_add_tail(&new->;task_list, &head->;task_list);
}
正执行调度的函数是schedule(void),它选择一个最合适的进程执行,并且真正进行上下文切换,
使得选中的进程得以执行。而reschedule_idle(struct task_struct *p)的作用是为进程选择
一个合适的CPU来执行,如果它选中了某个CPU,则将该CPU上当前运行进程的need_resched标志
置为1,然后向它发出一个重新调度的处理机间中断,使得选中的CPU能够在中断处理返回时执行
schedule函数,真正调度进程p在CPU上执行。在schedule()和reschedule_idle()中调用了goodness()
函数。goodness()函数用来衡量一个处于可运行状态的进程值得运行的程度。此外,在schedule()
函数中还调用了schedule_tail()函数;在reschedule_idle()函数中还调用了reschedule_idle_slow()。
[目录]
--------------------------------------------------------------------------------
sched.c
|schedule
|do_softirq // manages post-IRQ work
|for each task
|calculate counter
|prepare_to__switch // does anything
|switch_mm // change Memory context (change CR3 value)
|switch_to (assembler)
|SAVE ESP
|RESTORE future_ESP
|SAVE EIP
|push future_EIP *** push parameter as we did a call
|jmp __switch_to (it does some TSS work)
|__switch_to()
..
|ret *** ret from call using future_EIP in place of call address
new_task
/*
* 'sched.c' is the main kernel file. It contains scheduling primitives
* (sleep_on, wakeup, schedule etc) as well as a number of simple system
* call functions (type getpid(), which just extracts a field from
* current-task
*/
#include
#include
#include
#include
#include
#include
#include
#define LATCH (1193180/HZ)
extern void mem_use(void);
extern int timer_interrupt(void);
extern int system_call(void);
union task_union {
struct task_struct task;
char stack[PAGE_SIZE];
};
static union task_union init_task = {INIT_TASK,};
long volatile jiffies=0;
long startup_time=0;
struct task_struct *current = &(init_task.task), *last_task_used_math =
NULL;
struct task_struct * task[NR_TASKS] = {&(init_task.task), };
long user_stack [ PAGE_SIZE>;>;2 ] ;
struct {
long * a;
short b;
} stack_start = { & user_stack [PAGE_SIZE>;>;2] , 0x10 };
/*
* 'math_state_restore()' saves the current math information in the
* old math state array, and gets the new ones from the current task
*/
void math_state_restore() @@协处理器状态保存
{
if (last_task_used_math)
__asm__("fnsave %0"::"m" (last_task_used_math->;tss.i387));
if (current->;used_math)
__asm__("frstor %0"::"m" (current->;tss.i387));
else {
__asm__("fninit":;
current->;used_math=1;
}
last_task_used_math=current;
}
/*
* 'schedule()' is the scheduler function. This is GOOD CODE! There
* probably won't be any reason to change this, as it should work well
* in all circumstances (ie gives IO-bound processes good response etc).
* The one thing you might take a look at is the signal-handler code
here.
*
* NOTE!! Task 0 is the 'idle' task, which gets called when no other
* tasks can run. It can not be killed, and it cannot sleep. The 'state'
* information in task[0] is never used.
*/
void schedule(void)
{
int i,next,c;
struct task_struct ** p;
/* check alarm, wake up any interruptible tasks that have got a signal
*/
for(p = &LAST_TASK ; p >; &FIRST_TASK ; --p)
if (*p) {
if ((*p)->;alarm && (*p)->;alarm < jiffies) {
@@??
(*p)->;signal |= (1<<(SIGALRM-1));@@14-1
(*p)->;alarm = 0;
}
if ((*p)->;signal && (*p)->;state==TASK_INTERRUPTIBLE)
(*p)->;state=TASK_RUNNING;
}
@@ task 1 如何变为TASK_RUNNING??signal 如何得到,alarm如何变非0且 /* this is the
scheduler proper: */
@@操作系统最重要的函数,调度算法
@@这个循环要找到一个可运行的任务才能退出,会死在这吗?即如没有一个可运行
while (1) {
c = -1;
next = 0;
i = NR_TASKS;
p = &task[NR_TASKS];
while (--i) {
if (!*--p)
continue;
if ((*p)->;state == TASK_RUNNING && (*p)->;counter >; c)
c = (*p)->;counter, next = i;
}
if (c) break; @@记数大于零
for(p = &LAST_TASK ; p >; &FIRST_TASK ; --p)
if (*p)
(*p)->;counter = ((*p)->;counter >;>; 1) +
(*p)->;priority;
}
switch_to(next);
}
int sys_pause(void)
{
current->;state = TASK_INTERRUPTIBLE; @@任务可中断
schedule();
return 0;
}
void sleep_on(struct task_struct **p)
{
struct task_struct *tmp;
if (!p)
return;
if (current == &(init_task.task))
panic("task[0] trying to sleep";
tmp = *p;
*p = current;
current->;state = TASK_UNINTERRUPTIBLE;
schedule();
if (tmp) @@激活p,什么时候回来?唤醒上次睡眠的进程
tmp->;state=0;
}
void interruptible_sleep_on(struct task_struct **p)
{
struct task_struct *tmp;
if (!p)
return;
if (current == &(init_task.task))
panic("task[0] trying to sleep";
tmp=*p;
*p=current;
repeat: current->;state = TASK_INTERRUPTIBLE;
schedule();
if (*p && *p != current) {
(**p).state=0;
goto repeat;
}
@@好象下不来
*p=NULL;
if (tmp)
tmp->;state=0;
}
void wake_up(struct task_struct **p)
{
if (p && *p) {
(**p).state=0; @@唤醒该进程running
*p=NULL; @@睡眠栈为0
}
}
void do_timer(long cpl) @@定时调度
{
if (cpl)
current->;utime++; @@用户态时间加一
else
current->;stime++; @@系统态时间加一
if ((--current->;counter)>;0) return; @@当前记数减一
current->;counter=0;
if (!cpl) return;
schedule();
}
int sys_alarm(long seconds)
{
current->;alarm = (seconds>;0)?(jiffies+HZ*seconds):0;
return seconds;
}
int sys_getpid(void)
{
return current->;pid;
}
int sys_getppid(void)
{
return current->;father;
}
int sys_getuid(void)
{
return current->;uid;
}
int sys_geteuid(void)
{
return current->;euid;
}
int sys_getgid(void)
{
return current->;gid;
}
int sys_getegid(void)
{
return current->;egid;
}
int sys_nice(long increment)
{
if (current->;priority-increment>;0)
current->;priority -= increment;
return 0;
}
int sys_signal(long signal,long addr,long restorer)
{
long i;
switch (signal) {
case SIGHUP: case SIGINT: case SIGQUIT: case SIGILL:
case SIGTRAP: case SIGABRT: case SIGFPE: case SIGUSR1:
case SIGSEGV: case SIGUSR2: case SIGPIPE: case SIGALRM:
case SIGCHLD:
i=(long) current->;sig_fn[signal-1];
current->;sig_fn[signal-1] = (fn_ptr) addr;
current->;sig_restorer = (fn_ptr) restorer;
return i;
default: return -1;
}
}
void sched_init(void)
{
int i;
struct desc_struct * p;
set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));@@init task tss
set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));@@init ldt
p = gdt+2+FIRST_TSS_ENTRY;
for(i=1;i task = NULL;
p->;a=p->;b=0;
p++;
p->;a=p->;b=0;
p++;
}
ltr(0); @@调入task 0的tss
lldt(0); @@调入task 0的ldt
outb_p(0x36,0x43); /* binary, mode 3, LSB/MSB, ch 0 */
outb_p(LATCH & 0xff , 0x40); /* LSB */
outb(LATCH >;>; 8 , 0x40); /* MSB */
set_intr_gate(0x20,&timer_interrupt); @@irq 0 时钟中断
outb(inb_p(0x21)&~0x01,0x21);
set_system_gate(0x80,&system_call);
}
[目录]
--------------------------------------------------------------------------------
进程信号队列
每个进程具有一个sigpending结构所描述的信号队列,它有3个成员,head指向第一个sigqueue成员,tail指向最末的sigqueue成员的next指针,signal描述了此队列中的信号集.
static int
send_signal(int sig, struct siginfo *info, struct sigpending *signals);
将信号sig和对应的消息结构info添加到信号队列signal中.
static int
collect_signal(int sig, struct sigpending *list, siginfo_t *info);
返回信号sig在队列list中的信息info.
struct task_struct {
...
struct sigpending pending;
...
};
struct sigpending {
struct sigqueue *head, **tail;
sigset_t signal;
};
struct sigqueue {
struct sigqueue *next;
siginfo_t info;
};
// kernel/signal.c
static int
send_signal(int sig, struct siginfo *info, struct sigpending *signals)
{
struct sigqueue * q = NULL;
/* Real-time signals must be queued if sent by sigqueue, or
some other real-time mechanism. It is implementation
defined whether kill() does so. We attempt to do so, on
the principle of least surprise, but since kill is not
allowed to fail with EAGAIN when low on memory we just
make sure at least one signal gets delivered and don't
pass on the info struct. */
if (atomic_read(&nr_queued_signals) < max_queued_signals) {
q = kmem_cache_alloc(sigqueue_cachep, GFP_ATOMIC);
}
// nr_queued_signals和max_queued_signals用来限制全局sigqueue成员的数目
if (q) {
atomic_inc(&nr_queued_signals);
q->;next = NULL;
*signals->;tail = q;
signals->;tail = &q->;next; tail总是指向最末的信号成员的next指针 switch ((unsign
ed long) info)
{
case 0:
// info参数如果为0,表示信号来源于当前用户进程 q->;info.si_signo =
sig;
q->;info.si_errno = 0;
q->;info.si_code = SI_USER;
q->;info.si_pid = current->;pid;
q->;info.si_uid = current->;uid;
break;
case 1:
// info参数如果为1,表示信号来源于内核本身 q->;info.si_signo = sig;
q->;info.si_errno = 0;
q->;info.si_code = SI_KERNEL;
q->;info.si_pid = 0;
q->;info.si_uid = 0;
break;
default:
// 否则从info指针中拷贝信号
copy_siginfo(&q->;info, info);
break;
}
}
else if (sig >;= SIGRTMIN && info && (unsigned long)info != 1 && info->;
si_code != SI_USER)
{
; 如果该信号是内核发出的实时信号,就返回错误码
/*
* Queue overflow, abort. We may abort if the signal was rt
* and sent by user using something other than kill().
*/
return -EAGAIN;
}
sigaddset(&signals->;signal, sig); 将sig号标记在队列的信号集上
return 0;
}
static int
collect_signal(int sig, struct sigpending *list, siginfo_t *info)
{
if (sigismember(&list->;signal, sig)) {
/* Collect the siginfo appropriate to this signal. */ struct sigqueue *q, **
pp;
pp = &list->;head; pp指向第一个信号成员的next指针
while ((q = *pp) != NULL) {
if (q->;info.si_signo == sig) goto found_it;
pp = &q->;next;
}
/* Ok, it wasn't in the queue. We must have
been out of queue space. So zero out the
info.
*/
sigdelset(&list->;signal, sig);
info->;si_signo = sig;
info->;si_errno = 0;
info->;si_code = 0;
info->;si_pid = 0;
info->;si_uid = 0;
return 1;
found_it:
// 将找到信号成员从信号队列中删除
if ((*pp = q->;next) == NULL)
list->;tail = pp;
/* Copy the sigqueue information and free the queue entry */
copy_siginfo(info, &q->;info);
kmem_cache_free(sigqueue_cachep,q);
atomic_dec(&nr_queued_signals);
/* Non-RT signals can exist multiple times.. */
if (sig >;= SIGRTMIN) {
while ((q = *pp) != NULL) {
if (q->;info.si_signo == sig) goto found_another;
pp = &q->;next;
}
}
sigdelset(&list->;signal, sig);
found_another:
return 1;
}
return 0;
}
[目录]
--------------------------------------------------------------------------------
SMP
多处理机系统正在变得越来越普通。尽管大多数用户空间代码仍将完美地运行,而且有些情况下不需要增加额外的代码就能利用SMP特性的优势,但是内核空间代码必须编写成具备“SMP意识”且是“SMP安全的”。以下几段文字解释如何去做。
问题
当有多个CPU时,同样的代码可能同时在两个或多个CPU上执行。这在如下所示用于初始化某个图像设备的例程中可能会出问题。
void init_hardware(void)
{
outb(0x1, hardware_base + 0x30);
outb(0x2, hardware_base + 0x30);
outb(0x3, hardware_base + 0x30);
outb(0x4, hardware_base + 0x30);
}
假设该硬件依赖于寄存器0x30按顺序依次被设为0、1、2、3来初始化,那么要是有另一个CPU来参乎的话,事情就会搞糟。想象有两个CPU的情形,它们都在执行这个例程,不过2号CPU进入得稍慢点:
CPU 1 CPU 2
0x30 = 1
0x30 = 2 0x30 = 1
0x30 = 3 0x30 = 2
0x30 = 4 0x30 = 3
0x30 = 4
这会发生什么情况呢?从我们设想的硬件设备看来,它在寄存器0x30上收到的字节按顺序为:1、2、1、3、2、4、3、4。
啊!原本好好的事第二个CPU一来就搞得一团糟了也。所幸的是,我们有防止这类事情发生的办法。
自旋锁小历史
2.0.x版本的Linux内核通过给整个内核引入一个全局变量来防止多于一个CPU会造成的问题。这意味着任何时刻只有一个CPU能够执行来自内核空间的代码。这样尽管能工作,但是当系统开始以多于2个的CPU出现时,扩展性能就不怎么好。
2.1.x版本的内核系列加入了粒度更细的SMP支持。这意味着不再依赖于以前作为全局变量出现的“大锁”,而是每个没有SMP意识的例程现在都需要各自的自旋锁。文件asm/spinlock.h中定义了若干类型的自旋锁。
有了局部化的自旋锁后,不止一个CPU同时执行内核空间代码就变得可能了。
简单的自旋锁
理解自旋锁的最简单方法是把它作为一个变量看待,该变量把一个例程或者标记为“我当前在另一个CPU上运行,请稍等一会”,或者标记为“我当前不在运行”。如果1号CPU首先进入该例程,它就获取该自旋锁。当2号CPU试图进入同一个例程时,该自旋锁告诉它自己已为1号CPU所持有,需等到1号CPU释放自己后才能进入。
spinlock_t my_spinlock = SPIN_LOCK_UNLOCKED;
unsigned long flags;
spin_lock (&my_spinlock);
...
critical section
...
spin_unlock (&my_spinlock);
中断
设想我们的硬件的驱动程序还有一个中断处理程序。该处理程序需要修改某些由我们的驱动程序定义的全局变量。这会造成混乱。我们如何解决呢?
保护某个数据结构,使它免遭中断之修改的最初方法是全局地禁止中断。在已知只有自己的中断才会修改自己的驱动程序变量时,这么做效率很低。所幸的是,我们现在有更好的办法了。我们只是在使用共享变量期间禁止中断,此后重新使能。
实现这种办法的函数有三个:
disable_irq()
enable_irq()
disable_irq_nosync()
这三个函数都取一个中断号作为参数。注意,禁止一个中断的时间太长会导致难以追踪程序缺陷,丢失数据,甚至更坏。
disable_irq函数的非同步版本允许所指定的IRQ处理程序继续运行,前提是它已经在运行,普通的disable_irq则所指定的IRQ处理程序不在如何CPU上运行。
如果需要在中断处理程序中修改自旋锁,那就不能使用普通的spin_lock()和spin_unlock(),而应该保存中断状态。这可通过给这两个函数添加_irqsave后缀很容易地做到:
spinlock_t my_spinlock = SPIN_LOCK_UNLOCKED;
unsigned long flags;
spin_lock_irqsave(&my_spinlock, flags);
...
critical section
...
spin_unlock_irqrestore (&my_spinlock, flags);
[目录]
--------------------------------------------------------------------------------
内核线程页目录的借用
创建内核线程的时候,由于内核线程没有用户空间,而所有进程的内核页目录都是一样的((某些情况下可能有不同步的情况出现,主要是为了减轻同步所有进程内核页目录的开销,而只是在各个进程要访问内核空间,如果有不同步的情况,然后才进行同步处理),所以创建的内核线程的内核页目录总是借用进程0的内核页目录。
>;>;>; kernel_thread以标志CLONE_VM调用clone系统调用
/*
* Create a kernel thread
*/
int kernel_thread(int (*fn)(void *), void * arg, unsigned long flags)
{
long retval, d0;
__asm__ __volatile__(
"movl %%esp,%%esi\n\t"
"int $0x80\n\t" /* Linux/i386 system call */
"cmpl %%esp,%%esi\n\t" /* child or parent? */
/* Load the argument into eax, and push it. That way, it does
* not matter whether the called function is compiled with
* -mregparm or not. */
"movl %4,%%eax\n\t"
"pushl %%eax\n\t"
"call *%5\n\t" /* call fn */
"movl %3,%0\n\t" /* exit */
"int $0x80\n"
"1:\t"
:"=&a" (retval), "=&S" (d0)
:"0" (__NR_clone), "i" (__NR_exit),
"r" (arg), "r" (fn),
"b" (flags | CLONE_VM)
: "memory";
return retval;
}
>;>;>; sys_clone->;do_fork->;copy_mm:
static int copy_mm(unsigned long clone_flags, struct task_struct * tsk)
{
struct mm_struct * mm, *oldmm;
int retval;
。。。。。。。。
tsk->;mm = NULL;
tsk->;active_mm = NULL;
/*
* Are we cloning a kernel thread?
*
* We need to steal a active VM for that..
*/
>;>;>; 如果是内核线程的子线程(mm=NULL),则直接退出,此时内核线程mm和active_mm均为为NULL
oldmm = current->;mm;
if (!oldmm)
return 0;
>;>;>; 内核线程,只是增加当前进程的虚拟空间的引用计数
if (clone_flags & CLONE_VM) {
atomic_inc(&oldmm->;mm_users);
mm = oldmm;
goto good_mm;
}
。。。。。。。。。。
good_mm:
>;>;>; 内核线程的mm和active_mm指向当前进程的mm_struct结构
tsk->;mm = mm;
tsk->;active_mm = mm;
return 0;
。。。。。。。
}
然后内核线程一般调用daemonize来释放对用户空间的引用:
>;>;>; daemonize->;exit_mm->;_exit_mm:
/*
* Turn us into a lazy TLB process if we
* aren't already..
*/
static inline void __exit_mm(struct task_struct * tsk)
{
struct mm_struct * mm = tsk->;mm;
mm_release();
if (mm) {
atomic_inc(&mm->;mm_count);
if (mm != tsk->;active_mm) BUG();
/* more a memory barrier than a real lock */
task_lock(tsk);
>;>;>; 释放用户虚拟空间的数据结构
tsk->;mm = NULL;
task_unlock(tsk);
enter_lazy_tlb(mm, current, smp_processor_id());
>;>;>; 递减mm的引用计数并是否为0,是则释放mm所代表的映射
mmput(mm);
}
}
asmlinkage void schedule(void)
{
。。。。。。。。。
if (!current->;active_mm) BUG();
。。。。。。。。。
prepare_to_switch();
{
struct mm_struct *mm = next->;mm;
struct mm_struct *oldmm = prev->;active_mm;
>;>;>; mm = NULL,选中的为内核线程
if (!mm) {
>;>;>; 对内核线程,active_mm = NULL,否则一定是出错了
if (next->;active_mm) BUG();
>;>;>; 选中的内核线程active_mm借用老进程的active_mm
next->;active_mm = oldmm;
atomic_inc(&oldmm->;mm_count);
enter_lazy_tlb(oldmm, next, this_cpu);
} else {
>;>;>; mm != NULL 选中的为用户进程,active_mm必须与mm相等,否则一定是出错了
if (next->;active_mm != mm) BUG();
switch_mm(oldmm, mm, next, this_cpu);
}
>;>;>; prev = NULL ,切换出去的是内核线程
if (!prev->;mm) {
>;>;>; 设置其 active_mm = NULL 。
prev->;active_mm = NULL;
mmdrop(oldmm);
}
}
}
对内核线程的虚拟空间总结一下:
1、创建的时候:
父进程是用户进程,则mm和active_mm均共享父进程的,然后内核线程一般调用daemonize适头舖m
父进程是内核线程,则mm和active_mm均为NULL
总之,内核线程的mm = NULL;进程调度的时候以此为依据判断是用户进程还是内核线程。
2、进程调度的时候
如果切换进来的是内核线程,则置active_mm为切换出去的进程的active_mm;
如果切换出去的是内核线程,则置active_mm为NULL。
[目录]
--------------------------------------------------------------------------------
代码分析
LINUX系统是分时多用户系统, 它有多进程系统的特点,CPU按时间片分配给各个用户使用, 而在实质上应该说CPU按时间片分配给各个进程使用, 每个进程都有自己的运行环境以使得在CPU做进程切换时保存该进程已计算了一半的状态。
进程的切换包括三个层次:
·用户数据的保存: 包括正文段(TEXT), 数据段(DATA,BSS), 栈段(STACK), 共享内存段(SHARED MEMORY)的保存。
·寄存器数据的保存: 包括PC(program counter,指向下一条要执行的指令的地址), PSW(processor status word,处理机状态字), SP(stack pointer,栈指针), PCBP(pointer of process control block,进程控制块指针), FP(frame pointer,指向栈中一个函数的local 变量的首地址), AP(augument pointer,指向栈中函数调用的实参位置), ISP(interrupt stack pointer,中断栈指针), 以及其他的通用寄存器等。
·系统层次的保存: 包括proc,u,虚拟存储空间管理表格,中断处理栈。以便于该进程再一次得到CPU时间片时能正常运行下去。
多进程系统的一些突出的特点:
并行化
一件复杂的事件是可以分解成若干个简单事件来解决的, 这在程序员的大脑中早就形成了这种概念, 首先将问题分解成一个个小问题, 将小问题再细分, 最后在一个合适的规模上做成一个函数。 在软件工程中也是这么说的。如果我们以图的方式来思考, 一些小问题的计算是可以互不干扰的, 可以同时处理, 而在关键点则需要统一在一个地方来处理, 这样程序的运行就是并行的, 至少从人的时间观念上来说是这样的。 而每个小问题的计算又是较简单的。
简单有序
这样的程序对程序员来说不亚于管理一班人, 程序员为每个进程设计好相应的功能, 并通过一定的通讯机制将它们有机地结合在一起, 对每个进程的设计是简单的, 只在总控部分小心应付(其实也是蛮简单的), 就可完成整个程序的施工。
互不干扰
这个特点是操作系统的特点, 各个进程是独立的, 不会串位。
事务化
比如在一个数据电话查询系统中, 将程序设计成一个进程只处理一次查询即可, 即完成一个事务。当电话查询开始时, 产生这样一个进程对付这次查询; 另一个电话进来时, 主控程序又产生一个这样的进程对付, 每个进程完成查询任务后消失. 这样的编程多简单, 只要做一次查询的程序就可以了。
Linux是一个多进程的操作系统,进程是分离的任务,拥有各自的权利和责任。如果一个进程崩溃,它不应该让系统的另一个进程崩溃。每一个独立的进程运行在自己的虚拟地址空间,除了通过安全的核心管理的机制之外无法影响其他的进程。
在一个进程的生命周期中,进程会使用许多系统资源。比如利用系统的CPU执行它的指令,用系统的物理内存来存储它和它的数据。它会打开和使用文件系统中的文件,会直接或者间接使用系统的物理设备。如果一个进程独占了系统的大部分物理内存和CPU,对于其他进程就是不公平的。所以Linux必须跟踪进程本身和它使用的系统资源以便公平地管理系统中的进程。
系统最宝贵的资源就是CPU。通常系统只有一个CPU。Linux作为一个多进程的操作系统,它的目标就是让进程在系统的CPU上运行,充分利用CPU。如果进程数多于CPU(一般情况都是这样),其他的进程就必须等到CPU被释放才能运行。多进程的思想就是:一个进程一直运行,直到它必须等待,通常是等待一些系统资源,等拥有了资源,它才可以继续运行。在一个单进程的系统中,比如DOS,CPU被简单地设为空闲,这样等待资源的时间就会被浪费。而在一个多进程的系统中,同一时刻许多进程在内存中,当一个进程必须等待时,操作系统将CPU从这个进程切换到另一个更需要的进程。
我们组分析的是Linux进程的状态转换以及标志位的作用,它没有具体对应某个系统调用,而是分布在各个系统调用中。所以我们详细而广泛地分析了大量的原码,对进程状态转换的原因、方式和结果进行了分析,大致总结了整个Linux系统对进程状态管理的实现机制。
Linux中,每个进程用一个task_struct的数据结构来表示,用来管理系统中的进程。Task向量表是指向系统中每一个task_struct数据结构的指针的数组。这意味着系统中的最大进程数受到Task向量表的限制,缺省是512。这个表让Linux可以查到系统中的所有的进程。操作系统初始化后,建立了第一个task_struct数据结构INIT_TASK。当新的进程创建时,从系统内存中分配一个新的task_struct,并增加到Task向量表中。为了更容易查找,用current指针指向当前运行的进程。
task_struct结构中有关于进程调度的两个重要的数据项:
struct task_struct {
………….
volatile long state; /* -1 unrunnable , 0 runnable , >;0 stopped */
unsigned long flags; /* per process flags, defined below */
………….
};
每个在Task向量表中登记的进程都有相应的进程状态和进程标志,是进行进程调度的重要依据。进程在执行了相应的进程调度操作后,会由于某些原因改变自身的状态和标志,也就是改变state和flags这两个数据项。进程的状态不同、标志位不同对应了进程可以执行不同操作。在Linux2.2.8版本的sched.h中定义了六种状态,十三种标志。
//进程状态
#define TASK_RUNNING 0
#define TASK_INTERRUPTIBLE 1
#define TASK_UNINTERRUPTIBLE 2
#define TASK_ZOMBIE 4
#define TASK_STOPPED 8
#define TASK_SWAPPING 16
它们的含义分别是:
TASK_RUNNING:正在运行的进程(是系统的当前进程)或准备运行的进程(在Running队列中,等待被安排到系统的CPU)。处于该状态的进程实际参与了进程调度。
TASK_INTERRUPTIBLE:处于等待队列中的进程,待资源有效时唤醒,也可由其它进程被信号中断、唤醒后进入就绪状态。
TASK_UNINTERRUPTIBLE:处于等待队列中的进程,直接等待硬件条件,待资源有效时唤醒,不可由其它进程通过信号中断、唤醒。
TASK_ZOMBIE:终止的进程,是进程结束运行前的一个过度状态(僵死状态)。虽然此时已经释放了内存、文件等资源,但是在Task向量表中仍有一个task_struct数据结构项。它不进行任何调度或状态转换,等待父进程将它彻底释放。
TASK_STOPPED:进程被暂停,通过其它进程的信号才能唤醒。正在调试的进程可以在该停止状态。
TASK_SWAPPING:进程页面被兑换出内存的进程。这个状态基本上没有用到,只有在sched.c的count_active_tasks()函数中判断处于该种状态的进程也属于active的进程,但没有对该状态的赋值。
//进程标志位:
#define PF_ALIGNWARN 0x00000001
#define PF_STARTING 0x00000002
#define PF_EXITING 0x00000004
#define PF_PTRACED 0x00000010
#define PF_TRACESYS 0x00000020
#define PF_FORKNOEXEC 0x00000040
#define PF_SUPERPRIV 0x00000100
#define PF_DUMPCORE 0x00000200
#define PF_SIGNALED 0x00000400
#define PF_MEMALLOC 0x00000800
#define PF_VFORK 0x00001000
#define PF_USEDFPU 0x00100000
#define PF_DTRACE 0x00200000
其中PF_STARTING没有用到。
PF_MEMEALLOC和PF_VFORK这两个标志位是新版本中才有的。
各个标志位的代表着不同含义,对应着不同调用:
PF_ALIGNWARN 标志打印“对齐”警告信息,只有在486机器上实现
PF_STARTING 进程正被创建
PF_EXITING 标志进程开始关闭。
在do_exit()时置位。
current->;flags |= PF_EXITING
用于判断是否有效进程。
在nlmclnt_proc()(在fs\lockd\clntproc.c),如果current_flag为PF_EXITING,则进程由于正在退出清除所有的锁,将执行异步RPC 调用。
PF_PTRACED 进程被跟踪标志,
在do_fork()时清位。
p->;flags &= ~PF_PTRACED
当ptrace(0)被调用时置位,在进程释放前要清掉。
current->;flags |= PF_PTRACED
在sys_trace()中判断
如果request为PTRACE_TRACEME,如是则将current_flag置为PF_PTRACED;
如果request为PTRACE_ATTACH,则将child_flag置为PF_PTRACED,给child发一个SIGSTOP信号;
如果request为PTRACE_DETACH ,则将child清除PF_PTRACED。
在syscall_trace()中判断current_flag如果为PF_TRACED和PF_TRACESYS,则current强行退出时的出错代码置为SIGTRAP并将状态置为STOPPED。
PF_TRACESYS 正在跟踪系统调用。
do_fork()时清位,在进程释放前要清掉。
在sys_trace()中判断request如果为PTRACE_SYSCALL,则将child->;flags 置为 PF_TRACESYS;如为PTRACE_SYSCALL,则将child->;flags 清除 PF_TRACESYS;然后唤醒child。如果request为PTRACE_SINGLESTEP(即单步跟踪),则将child_flag清除PF_TRACESYS,唤醒child。
PF_FORKNOEXEC 进程刚创建,但还没执行。
在do_fork()时置位。
p->;flags |= PF_FORKNOEXEC
在调入格式文件时清位。
p->;flags &= ~ PF_FORKNOEXEC
PF_SUPERPRIV 超级用户特权标志。
如果是超级用户进程则置位,用户特权设为超级用户,如是超级用户,在统计时置统计标志(accounting flag)为ASU。
PF_DUMPCORE 标志进程是否清空core文件。
Core文件由gdb进行管理,给用户提供有用信息,例如查看浮点寄存器的内容比较困难,事实上我们可以从内核文件里的用户结构中得到
Core文件格式如下图:
UPAGE
DATA
STACK
Core 文件结构
UPAGE是包含用户结构的一个页面,告诉gdb文件中现有内容所有寄存器也在 UPAGE中,通常只有一页。DATA存放数据区。STACK堆栈区
最小Core文件长度为三页(12288字节)
在task_struct中定义一个dumpable变量,当dumpable==1时表示进程可以清空core文件(即将core文件放入回收站),等于0时表示该进程不能清空core文件(即core文件以放在回收站中,不可再放到回收站中),此变量初值为1。
例如在调用do_aout_core_dump()时判断current->;dumpable是否等于1(即判断该进程是否能将core文件放入回收站),如果等于1则将该变量置为0,在当前目录下建立一个core dump image ,在清空用户结构前,由gdb算出数据段和堆栈段的位置和使用的虚地址,用户数据区和堆栈区在清空前将相应内容写入core dump,将PF_DUMPCORE置位,清空数据区和堆栈区。
只有在aout_core_dump()内调用do_aout_core_dump(),而没有地方调用aout_core_dump()。对其它文件格式也是类似。
9、 PF_SIGNALED 标志进程被信号杀出。
在do_signal()中判断信号,如果current收到信号为SIGHUP, SIGINT, SIGIOT, SIGKILL, SIGPIPE, SIGTERM, SIGALRM, SIGSTKFLT, SIGURG, SIGXCPU, SIGXFSZ, SIGVTALRM, SIGPROF, SIGIO, SIGPOLL, SIGLOST, SIGPWR,则执行lock_kernel(),将信号加入current的信号队列,将current->;flag置为PF_SIGNALED,然后执行do_exit()
PF_USEDFPU 标志该进程使用FPU,此标志只在SMP时使用。
在task_struct中有一变量used_math,进程是否使用FPU。
在CPU从prev切换到next时,如果prev使用FPU则prev的flag清除PF_USEDFPU。
prev->;flags&=~PF_USEDFPU
在flush_thread()(arch\i386\kernel\process.c)、restore_i387_hard()、save_i387_hard()(arch\i386\kernel\signal.c)中,如果是SMP方式,且使用FPU则stts(),否则清除PF_USEDFPU。
current->;flags &= ~PF_USEDFPU
在sys_trace()中如果request为PTRACE_SETFPREGS,则将child的used_math置为1,将child_flag清除PF_USEDFPU。
child->;flags &= ~PF_USEDFPU
在SMP方式下进行跟踪时,判断是否使用FPU。
在跟踪时出现数学错误时清位。
current->;flags &= ~PF_USEDFPU
PF_DTRACE 进程延期跟踪标志,只在m68k下使用。
跟踪一个trapping指令时置位。
current->;flags |= PF_DTRACE
PF_ONSIGSTK 标志进程是否工作在信号栈,只在m68k方式下使用。
liunx 2.1.19版本中使用此标志位,而2.2.8版本中不使用。
在处理信号建立frame时如果sigaction标志为ONSTACK,则将current->;flag置为PF_ONSIGSTK。
PF_MEMALLOC 进程分配内存标志。
linux 2.2.8版本中使用此标志位。
在kpiod()和kwpad()中置位。
tsk->;flags |= PF_MEMALLOC
PF_VFORK linux 2.2.8版本中使用此标志位。
在copy_flags(unsigned long clone_flags, struct task_struct *p),如果clone_flags为CLONE_VFORK,则将p的flags置为PF_VFORK。
在mm_release()中将current ->;flags清除PF_VFORK。
tsk->;flags &= ~PF_VFORK
具体的分析由我组的另外同学进行。
Linux的各进程之间的状态转换的系统调用
我将参与Linux的各进程之间的状态转换的系统调用总结成一张流程图:
进程的创建:TASK_RUNNING
第一个进程在系统启动时创建,当系统启动的时候它运行在核心态,这时,只有一个进程:初始化进程。象所有其他进程一样,初始进程有一组用堆栈、寄存器等等表示的机器状态。当系统中的其他进程创建和运行的时候这些信息存在初始进程的task_struct数据结构中。在系统初始化结束的时候,初始进程启动一个核心进程(叫做init)然后执行空闲循环,什么也不做。当没有什么可以做的时候,调度程序会运行这个空闲的进程。这个空闲进程的task_struct是唯一一个不是动态分配而是在核心连接的时候静态定义的,为了不至于混淆,叫做init_task。
系统调用sys_fork 和sys_clone都调用函数do_fork()(在kernel/fork.中定义)。
进程由do_fork()函数创建,先申请空间,申请核心堆栈;然后在Task向量表中找到空闲位置;在进行正式初始化以前,将新创建的进程的状态都置为TASK_UNINTERRUPTIBLE,以免初始化过程被打断;开始初始化工作,如初始化进程时钟、信号、时间等数据;继承父进程的资源,如文件、信号量、内存等;完成进程初始化后,由父进程调用wake_up_process()函数将其唤醒,状态变为TASK_RUNNING,挂到就绪队列run queue,返回子进程的pid。
// C:\SRCLNX\KERNEL\FORK.C
int do_fork(unsigned long clone_flags, unsigned long usp, struct pt_regs *regs)
{
为新进程申请PCB空间;
if (申请不到)
返回错误,退出;
为新进程申请核心堆栈;
if (核心堆栈申请不到)
返回错误,退出;
为新进程在Task向量表中找到空闲位置;
/*复制父进程current PCB中的信息,继承current的资源*/;
p = current;
在进行正式初始化以前,将新创建的进程的状态都置为TASK_UNINTERRUPTIBLE,以免初始化过程被打断,并置一些标志位.
/*为防止信号、定时中断误唤醒未创建完毕的进 程,将子进程的状态设成不可中断的*/
p->;state = TASK_UNINTERRUPTIBLE;
/*跟踪状态和超级用户特权是没有继承性的,因为在root用户为普通用户创建进程时,出于安全考虑这个普通用户的进程不允许拥有超级用户特权。*/
p->;flags &= ~(PF_PTRACED|PF_TRACESYS|PF_SUPERPRIV);
/*将进程标志设成初建,在进程第一次获得CPU时,内核将根据此标志进行一定操作*/
p->;flags |= PF_FORKNOEXEC;
开始Task_struct的初始化工作,如初始化进程时钟、信号、时间等数据;
继承父进程所有资源:
拷贝父进程当前打开的文件;
拷贝父进程在VFS的位置;
拷贝父进程的信号量;
拷贝父进程运行的内存;
拷贝父进程的线程;
初始化工作结束,父进程将其将其唤醒,挂入running队列中,返回子进程的pid;
}
进程的调度(schedule()):
处于TASK_RUNNING状态的进程移到run queue,会由schedule()按CPU调度算法在合适的时候选中,分配给CPU。
新创建的进程都是处于TASK_RUNNING状态,而且被挂到run queue的队首。进程调度采用变形的轮转法(round robin)。当时间片到时(10ms的整数倍),由时钟中断引起新一轮调度,把当前进程挂到run queue队尾。
所有的进程部分运行与用户态,部分运行于系统态。底层的硬件如何支持这些状态各不相同但是通常有一个安全机制从用户态转入系统态并转回来。用户态比系统态的权限低了很多。每一次进程执行一个系统调用,它都从用户态切换到系统态并继续执行。这时让核心执行这个进程。Linux中,进程不是互相争夺成为当前运行的进程,它们无法停止正在运行的其它进程然后执行自身。每一个进程在它必须等待一些系统事件的时候会放弃CPU。例如,一个进程可能不得不等待从一个文件中读取一个字符。这个等待发生在系统态的系统调用中。进程使用了库函数打开并读文件,库函数又执行系统调用从打开的文件中读入字节。这时,等候的进程会被挂起,另一个更加值得的进程将会被选择执行。进程经常调用系统调用,所以经常需要等待。即使进程执行到需要等待也有可能会用去不均衡的CPU事件,所以Linux使用抢先式的调度。用这种方案,每一个进程允许运行少量一段时间,200毫秒,当这个时间过去,选择另一个进程运行,原来的进程等待一段时间直到它又重新运行。这个时间段叫做时间片。
需要调度程序选择系统中所有可以运行的进程中最值得的进程。一个可以运行的进程是一个只等待CPU的进程。Linux使用合理而简单的基于优先级的调度算法在系统当前的进程中进行选择。当它选择了准备运行的新进程,它就保存当前进程的状态、和处理器相关的寄存器和其他需要保存的上下文信息到进程的task_struct数据结构中。然后恢复要运行的新的进程的状态(又和处理器相关),把系统的控制交给这个进程。为了公平地在系统中所有可以运行(runnable)的进程之间分配CPU时间,调度程序在每一个进程的task_struct结构中保存了信息。
policy 进程的调度策略:Linux有两种类型的进程:普通和实时。实时进程比所有其它进程的优先级高。如果有一个实时的进程准备运行,那么它总是先被运行。实时进程有两种策略:环或先进先出(round robin and first in first out)。在环的调度策略下,每一个实时进程依次运行,而在先进先出的策略下,每一个可以运行的进程按照它在调度队列中的顺序运行,这个顺序不会改变。
Priority 进程的调度优先级。也是它允许运行的时候可以使用的时间量(jiffies)。你可以通过系统调用或者renice命令来改变一个进程的优先级。
Rt_priority Linux支持实时进程。这些进程比系统中其他非实时的进程拥有更高的优先级。这个域允许调度程序赋予每一个实时进程一个相对的优先级。实时进程的优先级可以用系统调用来修改Coutner 这时进程可以运行的时间量(jiffies)。进程启动的时候等于优先级(priority),每一次时钟周期递减。
调度程序schedule()从核心的多个地方运行。它可以在把当前进程放到等待队列之后运行,也可以在系统调用之后进程从系统态返回进程态之前运行。需要运行调度程序的另一个原因是系统时钟刚好把当前进程的计数器(counter)置成了0。每一次调度程序运行它做以下工作:
(1)kernel work 调度程序运行bottom half handler并处理系统的调度任务队列。
(2)Current pocess 在选择另一个进程之前必须处理当前进程。
(3)如果当前进程的调度策略是环则它放到运行队列的最后。
(4)如果任务状态是TASK_INTERRUPTIBLE的而且它上次调度的时候收到过一个信号,它的状态变为TASK_RUNNING;
如果当前进程超时,它的状态成为RUNNING;
如果当前进程的状态为RUNNING则保持此状态;
不是RUNNING或者INTERRUPTIBLE的进程被从运行队列中删除。这意味着当调度程序查找最值得运行的进程时不会考虑这样的进程。
(5)Process Selection 调度程序查看运行队列中的进程,查找最值得运行的进程。如果有实时的进程(具有实时调度策略),就会比普通进程更重一些。普通进程的重量是它的counter,但是对于实时进程则是counter 加1000。这意味着如果系统中存在可运行的实时进程,就总是在任何普通可运行的进程之前运行。当前的进程,因为用掉了一些时间片(它的counter减少了),所以如果系统中由其他同等优先级的进程,就会处于不利的位置:这也是应该的。如果几个进程又同样的优先级,最接近运行队列前段的那个就被选中。当前进程被放到运行队列的后面。如果一个平衡的系统,拥有大量相同优先级的进程,那么回按照顺序执行这些进程。这叫做环型调度策略。不过,因为进程需要等待资源,它们的运行顺序可能会变化。
(6)Swap Processes 如果最值得运行的进程不是当前进程,当前进程必须被挂起,运行新的进程。当一个进程运行的时候它使用了CPU和系统的寄存器和物理内存。每一次它调用例程都通过寄存器或者堆栈传递参数、保存数值比如调用例程的返回地址等。因此,当调度程序运行的时候它在当前进程的上下文运行。它可能是特权模式:核心态,但是它仍旧是当前运行的进程。当这个进程要挂起时,它的所有机器状态,包括程序计数器(PC)和所有的处理器寄存器,必须存到进程的task_struct数据结构中。然后,必须加载新进程的所有机器状态。这种操作依赖于系统,不同的CPU不会完全相同地实现,不过经常都是通过一些硬件的帮助。
(7)交换出去进程的上下文发生在调度的最后。前一个进程存储的上下文,就是当这个进程在调度结束的时候系统的硬件上下文的快照。相同的,当加载新的进程的上下文时,仍旧是调度结束时的快照,包括进程的程序计数器和寄存器的内容。
(如果前一个进程或者新的当前进程使用虚拟内存,则系统的页表需要更新。同样,这个动作适合体系结构相关。Alpha AXP处理器,使用TLT(Translation Look-aside Table)或者缓存的页表条目,必须清除属于前一个进程的缓存的页表条目。
下面我就来总结一下进程创建以后到被杀死的整个进程生命周期中,状态可能在TASK_RUNNING、TASK_INTERRUPTIBLE、TASK_UNINTERRUPTIBLE 、TASK_STOPPED以及TASK_ZOMBLE之间转换的原因。
进程在TASK_RUNNING以及TASK_UNINTERRUPTIBLE、TASK_INTERRUPTIBLE之间转换:
获得CPU而正在运行的进程会由于某些原因,比如:申请不到某个资源,其状态会从TASK_RUNNING变为TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE的等待状态。同样在经历了某些情况,处于等待状态的进程会被重新唤醒,等待分配给CPU。状态为TASK_INTERRUPTIBLE的睡眠进程会被唤醒,回到TASK_RUNNING状态,重新等待schedule()分配给它CPU,继续运行,比如:当申请资源有效时,也可以由signal或定时中断唤醒。而状态为TASK_INTERRUPTIBLE的睡眠进程只有当申请资源有效时被唤醒,不能被signal、定时中断唤醒。
1.通过sleep_on()、interruptible_sleep_on()、sleep_on_timeout()、interruptible_sleep_on_timeout()以及wake_up()、wake_up_process()、wake_up_interruptible()函数对进行的转换:
sleep_on():TASK_RUNNING->;TASK_UNINTERRUPTIBLE
当拥有CPU的进程申请资源无效时,会通过sleep_on(),将进程从TASK_RUNNING切换到TASK_UNINTERRUPTIBLE状态。sleep_on()函数的作用就是将current进程的状态置成TASK_UNINTERRUPTIBLE,并加到等待队列中。
一般来说引起状态变成TASK_UNINTERRUPTIBLE的资源申请都是对一些硬件资源的申请,如果得不到这些资源,进程将不能执行下去,不能由signal信号或时钟中断唤醒,而回到TASK_RUNNING状态。
我们总结了这种类型的转换原因有:
(1)对某些资源的操作只能由一个进程进行,所以系统对该项资源采用上锁机制。在申请该项资源时,必须先申请资源的锁,如果已经被别的进程占用,则必须睡眠在对该锁的等待队列上。而且这种睡眠不能被中断,必须等到得到了资源才能继续进行下去。
如:
对网络连接表锁(Netlink table lock)的申请, sleep_on(&nl_table_wait);
对交换页进行I/O操作的锁的申请, sleep_on(&lock_queue);
对Hash表操作的锁的申请, sleep_on(&hash_wait);
在UMSDOS文件系统创建文件或目录时,必须等待其他同样的创建工作结束,sleep_on (&dir->;u.umsdos_i.u.dir_info.p);
(2)某些进程在大部分时间处于睡眠状态,仅在需要时被唤醒去执行相应的操作,当执行完后,该进程又强制去睡眠。
如:
wakeup_bdflush()是对dirty buffer进行动态的响应,一旦该进程被激活,就将一定数量的dirty buffer写回磁盘,然后调用sleep_on(&bdflush_done),又去睡眠。
interruptible_sleep_on():TASK_RUNNING->;TASK_INTERRUPTIBLE
与sleep_on()函数非常地相象,当拥有CPU的进程申请资源无效时,会通过interruptible_sleep_on(),将进程从TASK_RUNNING切换到TASK_INTERRUPTIBLE状态。interruptible_sleep_on()函数的作用就是将current进程的状态置成TASK_INTERRUPTIBLE,并加到等待队列中。
处于TASK_INTERRUPTIBLE状态的进程可以在资源有效时被wake_up()、wake_up_interruptible()或wake_up_process()唤醒,或收到signal信号以及时间中断后被唤醒。
进行这种转换的原因基本上与sleep_on()相同,申请资源无效时进程切换到等待状态。与之不同的是处于interruptible_sleep_on()等待状态的进程是可以接受信号或中断而重新变为running状态。所以可以认为对这些资源的申请没有象在sleep_on()中资源的要求那么严格,必须得到该资源进程才能继续其运行下去。
sleep_on_timeout():TASK_RUNNING->;TASK_UNINTERRUPTIBLE
sleep_on_timeout(&block.b_wait, 30*HZ);
interruptible_sleep_on_timeout():TASK_RUNNING->;TASK_INTERRUPTIBLE
虽然在申请资源或运行中出现了某种错误,但是系统仍然给进程一次重新运行的机会。调用该函数将进程从TASK_RUNNING切换到TASK_INTERRUTIBLE状态,并等待规定的时间片长度,再重新试一次。
如:在smb_request_ok 中产生了连接失败的错误,会在sem_retry()中给一次重新连接的机会。//interruptible_sleep_on_timeout(&server->;wait, 5*HZ);
wake_up():TASK_UNINTERRUPTIBLE->; TASK_RUNNING;
TASK_INTERRUPTIBLE->; TASK_RUNNING
处于TASK_UNINTERRUPTIBLE状态的进程不能由signal信号或时钟中断唤醒,只能由wake_up()或wake_up_process()唤醒。wake_up()函数的作用是将wait_queue中的所有状态为TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE的进程状态都置为TASK_RUNNING,并将它们都放到running队列中去,即唤醒了所有等待在该队列上的进程。
void wake_up(struct wait_queue **q)
{
struct wait_queue *next;
struct wait_queue *head;
if (!q || !(next = *q))
return;
head = WAIT_QUEUE_HEAD(q);
while (next != head) {
struct task_struct *p = next->;task;
next = next->;next;
if (p != NULL) {
if ((p->;state == TASK_UNINTERRUPTIBLE) ||
(p->;state == TASK_INTERRUPTIBLE))
wake_up_process(p);
}
if (!next)
goto bad;
}
return;
bad:
printk("wait_queue is bad (eip = %p)\n",
__builtin_return_address(0));
printk(" q = %p\n",q);
printk(" *q = %p\n",*q);
}
wake_up()在下列情况下被调用:
这个函数通常在资源有效时调用,资源锁已经被释放,等待该资源的所有进程都被置为TASK_RUNNING状态,移到run queue,重新参与调度,对这一资源再次竞争。这时又会有某个进程竞争到了该项资源,而其他的进程在申请失败后,又回到TASK_UNINTERRUPTIBLE或TASK_INTERRUPTIBLE状态。
如:
网络连接表锁(Netlink table lock)释放后,唤醒等待该锁的所有睡眠进程 wake_up(&nl_table_wait);
对交换页进行I/O操作的锁释放后,唤醒等待该锁的所有睡眠进程, wake_up(&lock_queue);
对Hash表操作的锁释放后,唤醒等待该锁的所有睡眠进程,wake_up(&hash_wait);
在UMSDOS文件系统创建文件或目录工作结束后,唤醒其他由于等待它创建结束而睡眠的进程, wake_up (&dir->;u.umsdos_i.u.dir_info.p);
唤醒睡眠进程执行某些操作:
如:
bd_flush()函数要将一些dirty buffer写回磁盘,就调用wake_up(&bdflush_done),唤醒正在睡眠的wakeup_bdflush()进程去处理写回。
wake_up_process():TASK_UNINTERRUPTIBLE->; TASK_RUNNING;
TASK_INTERRUPTIBLE->; TASK_RUNNING
wake_up_process()函数的作用是将参数所指的那个进程状态从TASK_INTERRUPTIBLE,TASK_UNINTERRUPTIBLE变为TASK_RUNNING,并将它放到running队列中去。
void wake_up_process(struct task_struct * p)
{
unsigned long flags;
/*
* We want the common case fall through straight, thus the goto.
*/
spin_lock_irqsave(&runqueue_lock, flags);
p->;state = TASK_RUNNING;
if (p->;next_run)
goto out;
add_to_runqueue(p);
spin_unlock_irqrestore(&runqueue_lock, flags);
reschedule_idle(p);
return;
out:
spin_unlock_irqrestore(&runqueue_lock, flags);
}
这个函数的实现机制与wake_up()的不同在于,它只能唤醒某一个特定的睡眠进程,而wake_up()是唤醒整个等待队列的睡眠进程。所以,它的唤醒的原因与wake_up()也有一定的区别,除了由于wake_up()对它的调用之外,它唤醒进程并不是由于资源有效造成的,唤醒的进程也不是因等待资源有效而睡眠的进程。有以下几种情况:
父进程对子进程的唤醒:
如:
在sys_ptrace()中当收到的跟踪请求为:PTRACE_CONT(在处理完信号后继续);PTRACE_KILL(将子进程杀出);PTRACE_SINGLESTEP(对子进程进行单步跟踪);PTRACE_DETACH的时候,都会在处理结束时,唤醒子进程,给子进程一个运行的机会。
在do_fork()中,新建进程初始化完毕,会由父进程唤醒它,将该进程移到run queue中,置状态为TASK_RUNNING。
当需要的时候唤醒某个睡眠的系统调用,进行处理:
如:
kswapd_process页面交换进程,通常是处于睡眠状态的,当某个进程需要更多的内存,而调用try_to_free_pages()时,就会唤醒kswapd_process页面交换进程,调入更多的内存页面。
收到信号所进行的相应处理:
如:
某一进程的时间片到了,process_timeout()会调用wake_up_process()唤醒该进程;
收到某些signal信号:处于STOPPED状态的进程收到SIGKILL或SIGCONT会被唤醒(注:处于STOPPED状态的进程不能被wake_up()唤醒);以及收到某些非实时信号,不需加到signal队列中去,处于TASK_INTERRUPTIBLE的进程有机会被唤醒。
资源有效时,wake_up()对整个等待队列的唤醒是通过对每个等待队列上的进程调用wake_up_process()实现的。
wake_up_interruptible():TASK_INTERRUPTIBLE->; TASK_RUNNING
将wait_queue中的所有状态为 TASK_INTERRUPTIBLE的进程状态都置为TASK_RUNNING,并将它们都放到running queue中去。
这个函数通常在send_sig(发出信号)后调用,以使信号发出后能及时得到响应,或者当空闲下来时,希望检查一下是否有收到有效信号的能运行的进程时,也可以调用这个函数,如:
在进程退出前调用notify_parent(),给父进程send_sig()后,将调用wake_up_interruptible (),使信号能够得到及时的响应。
usr\src\linux\KERNEL\EXIT.C 中定义了
void notify_parent(struct task_struct * tsk, int signal)
{
send_sig(signal, tsk->;p_pptr, 1);
wake_up_interruptible(&tsk->;p_pptr->;wait_chldexit);
}
当某一进程要结束时,它可以通过调用notify_parent(current, current->;exit_signal)通知父进程以唤醒睡眠在wait_chldexit上的父进程
2. Semaphores(信号灯)
信号量用于生成锁机制,避免发生数据不一致。
信号量最简单的形式就是内存中一个位置,它的取值可以由多个进程检验和设置。检验和设置的操作,至少对于关联的每一个进程来讲,是不可中断或者说有原子性:只要启动就不能中止。检验和设置操作的结果是信号灯当前值和设置值的和,可以是正或者负。根据测试和设置操作的结果,一个进程可能必须睡眠直到信号灯的值被另一个进程改变。信号灯可以用于实现临界区域(critical regions),就是重要的代码区,同一时刻只能有一个进程运行。
对信号灯的操作是通过以下两组基本函数实现的:
1.void __up(struct semaphore *sem) :TASK_UNINTERRUPTIBLE->; TASK_RUNNING;
TASK_INTERRUPTIBLE->; TASK_RUNNING
int __do_down(struct semaphore * sem, int task_state)由以下两个函数调用,分别转换到不同的等待状态:
(1)int __down_interruptible (struct semaphore * sem):
TASK_RUNNING ->;TASK_INTERRUPTIBLE;
(2)void __down(struct semaphore * sem):
TASK_RUNNING ->;TASK_UNINTERRUPTIBLE;
2. extern inline void up(struct semaphore * sem)
extern inline void down(struct semaphore * sem);
extern inline int down_interruptible(struct semaphore * sem);
Linux信号量是通过两路counter变量实现的:当进程由于申请不到临界区资源而睡眠时,会将semaphore结构中的”count”变量值原子地递减1,进程睡眠等待临界区资源的释放;而当up()函数唤醒睡眠等待进程时,如果”count”变量值小于0,会将semaphore结构中的” waking”变量值原子地递增1,唤醒睡眠进程。虽然所有等待进程都被唤醒。但只有首先得到” waking”的进程才能得到信号量,继续运行下去,其他进程仍然回到最初的等待状态。
Linux定义信号灯结构是:
struct semaphore {
atomic_t count;
int waking;
struct wait_queue * wait;
};
信号灯的值初始化为一个宏定义的结构MUTEX的值{count=1,waking=0,wait=NULL}。
void __up(struct semaphore *sem):
占有临界区资源的进程,调用__up()释放资源。在__up()函数中,调用wake_one_more ()函数,原子地读sem->;count, 如果sem->;count <=0,则sem->;waking ++,并唤醒所有等待在该sem-->;wait上的进程。
void __up(struct semaphore *sem)
{
wake_one_more(sem);
wake_up(&sem->;wait);
}
int __do_down(struct semaphore * sem, int task_state):
申请临界区资源的进程会通过调用__do_down()来竞争资源。在__do_down()函数中,调用waking_non_zero(struct semaphore *sem)或waking_non_zero_interruptible(struct semaphore *sem)抢占临界区资源,如果抢占到,则将当前进程置为TASK_RUNNING,否则将当前进程的状态置为task_state,并处于循环等待状态。
进程通过waking_non_zero()来竞争临界区资源,在该函数中判断sem-->;waking的值,如果sem-->;waking 大于0,sem->;waking -- 并返回1,否则返回0。
int __do_down(struct semaphore * sem, int task_state)
{
struct task_struct *tsk = current;
struct wait_queue wait = { tsk, NULL };
int ret = 0 ;
tsk->;state = task_state;
add_wait_queue(&sem->;wait, &wait); /*将进程加入到等待队列*/
for (;
{
if (waking_non_zero(sem)) /* 是否已经被唤醒 */
break ; /* 是的,跳出循环 */
if ( task_state == TASK_INTERRUPTIBLE
&& (tsk->;signal & ~tsk->;blocked)
/* 如果进程状态为TASK_INTERRUPTIBLE,且收到信号量,并未被屏蔽*/
)
{
ret = -EINTR ; /* 中断 */
atomic_inc(&sem->;count) ; /* 放弃down操作,原子递增信号量的count值 */
break ;
}
schedule(); /* 重新调度 */
tsk->;state = task_state; /*未能竞争到信号量的进程重新置成执行down操
作前的状态*/
}
tsk->;state = TASK_RUNNING; /*竞争到信号量的进程置为TASK_RUNNING状态*/
remove_wait_queue(&sem->;wait, &wait);/*将进程从等待队列中删除*/
return(ret) ;
} /* __do_down */
其中_do__down()又分别由__down()和__do_down()调用,进程转换到不同状态。
void __down(struct semaphore * sem): TASK_RUNNING ->;TASK_UNINTERRUPTIBLE;
void __down(struct semaphore * sem)
{
__do_down(sem,TASK_UNINTERRUPTIBLE) ;
}
int __down_interruptible (struct semaphore * sem): TASK_RUNNING ->;TASK_INTERRUPTIBLE;
int __down_interruptible(struct semaphore * sem)
{
return(__do_down(sem,TASK_INTERRUPTIBLE)) ;
}
在Linux中定义了两种不同的信号灯:
(1)定义在某个数据结构上:
在linux系统中有很多数据结构中定义了这样的信号灯,来控制对这个数据结构的资源访问,比如不允许对某个内存单元进行多进程访问,就通过定义在该内存单元上的某个信号灯mmap_sem进行__up()、_down()、up()、down()操作。
如:
struct mm_struct中有mmap_sem信号灯;
struct inode中有i_sem、i_atomic_write信号灯;
struct nlm_file中有f_sema信号灯;
struct nlm_host中有h_sema信号灯;
struct superblock中有s_vfs_rename_sem信号灯;
struct vfsmount中有mnt_dquot.semaphore信号灯;
struct task_struct中有vfork_sem信号灯;//注:这个信号灯在2.0.36版本是没有的,新版本2.2.8中才有的,用于vfork()。
struct unix_opt中有readsem信号灯;
struct smb_sb_info中有sem信号灯;
申请这些数据结构中的临界区资源,就要进行相应的信号灯操作。
(2)定义在全局的单独信号灯数据:
还有一些单独的全局信号灯,它们并不属于某一个数据结构,而是系统定义的全局静态的信号灯,可能有多个进程对这种不属于某个特定数据结构的全局临界资源的申请,则系统通过这些全局信号灯来分配资源。
如:
nlm_file_sema;
nlmsvc_sema;
lockd_start;
read_sem;
nlm_host_sema;
read_semaphore;
uts_sem
mount_sem;
cache_chain_sem;
rpciod_sema;
rpciod_running;
mfw_sema;
firewall_sem;
我们来分析一个例子说明信号灯的操作。例如对文件的写操作,我们假设有许多协作的进程对一个单一的数据文件进行写操作。我们希望对文件的访问必须严格地协调。因此这里就利用了inode结构上定义的信号灯inode->;i_sem。
在 /usr/src/linux/mm/filemap.c中:
static int filemap_write_page(struct vm_area_struct * vma,
unsigned long offset,
unsigned long page)
{
int result;
struct file file;
struct inode * inode;
struct buffer_head * bh;
……………
down(&inode->;i_sem);
result = do_write_page(inode, &file, (const char *) page, offset);
up(&inode->;i_sem);
return result;
}
在该文件写操作的代码中,加入两个信号灯操作,第一个down(&inode->;i_sem)检查并把信号灯的值减小,第二个up(&inode->;i_sem)检查并增加它。访问文件的第一个进
网络
BSD是UNIX系统中通用的网络接口,它不仅支持各种不同的网络类型,而且也是一种内部进程之间的通信机制。两个通信进程都用一个套接口来描述通信链路的两端。套接口可以认为是一种特殊的管道,但和管道不同的是,套接口对于可以容纳的数据的大小没有限制。
Linux支持多种类型的套接口,也叫做套接口寻址族,这是因为每种类型的套接口都有自己的寻址方法。Linux支持以下的套接口类型:
UNIX UNIX域套接口
INET Internet地址族TCP/IP协议支持通信。
AX25 Amateur radio X25
IPX Novell IPX
APPLETALK Appletalk DDP
X25 X25
这些类型的套接口代表各种不同的连接服务。
Linux的BSD 套接口支持下面的几种套接口类型:
1. 流式(stream)
提供了可靠的双向顺序数据流连接。可以保证数据传输中的完整性、正确性和单一性。INET寻址族中TCP协议支持这种类型。
2. 数据报(Datagram)
这种类型的套接口也可以像流式套接口一样提供双向的数据传输,但它们不能保证传输的数据一定能够到达目的节点。即使数据能够到达,也无法保证数据以正确的顺序到达以及数据的单一性、正确性。UDP协议支持这种类型的套接口。
3. 原始(Raw)
这种类型的套接口允许进程直接存取下层的协议。
4. 可靠递送消息(Reliable Delivered Messages)
这种套接口和数据报套接口一样,只能保证数据的到达。
5. 顺序数据包(Sequenced Packets)
这种套接口和流式套接口相同,除了数据包的大小是固定的。
6. 数据包(Packet)
这不是标准的BSD 套接口类型,而是Linux 中的一种扩展。它允许进程直接存取设备层的数据包。
INET套接口层包括支持TCP/IP协议的Internet地址族。正如上面提到的,这些协议是分层的,每一个协议都使用另一个协议的服务。Linux系统中的TCP/IP代码和数据结构也反映了这种分层的思想。它和BSD 套接口层的接口是通过一系列与Internet地址族有关的套接口操作来实现的,而这些套接口操作是在网络初始化的过程中由INET 套接口层在BSD 套接口层中注册的。这些操作和其他地址族的操作一样保存在pops向量中。
BSD 套接口层通过INET的proto_ops数据结构来调用与INET 层有关的套接口子程序来实现有关INET层的服务。例如,当BSD 套接口创建一个发送给INET地址族的请求时将会使用INET的套接口创建功能。BSD 套接口层将会把套接口数据结构传递给每一个操作中的INET层。INET 套接口层在它自己的数据结构sock中而不是在BSD 套接口的数据结构中插入有关TCP/IP的信息,但sock数据结构是和B S D套接口的数据结构有关的。它使用BSD 套接口中的数据指针来连接sock数据结构和BSD 套接口数据结构,这意味着以后的INET 套接口调用可以十分方便地得到sock数据结构。数据结构sock中的协议操作指针也会在创建时设置好,并且此指针是和所需要的协议有关的。如果需要的是TCP协议,那么数据结构sock中的协议操
下面是Linux系统的TCP包,从netif_rx开始 [net/core/dev.c]
中断管理管理: "netif_rx"
|netif_rx
|__skb_queue_tail
|qlen++
|* simple pointer insertion *
|cpu_raise_softirq
|softirq_active(cpu) |= (1 << NET_RX_SOFTIRQ) // set bit NET_RX_SOFTIRQ in the BH vector
·__skb_queue_tail [include/linux/skbuff.h]
·cpu_raise_softirq [kernel/softirq.c]
中断的后半部分: "net_rx_action"
IRQ的基本处理以后,还需要另外的“底半”处理,(参考软中断)这里的是NET_RX_SOFTIRQ完成的。
net_rx_action [net/core/dev.c]
net_dev_init [net/core/dev.c]
|net_rx_action
|skb = __skb_dequeue (the exact opposite of __skb_queue_tail)
|for (ptype = first_protocol; ptype < max_protocol; ptype++) // Determine
|if (skb->;protocol == ptype) // what is the network protocol
|ptype->;func ->; ip_rcv // according to ''struct ip_packet_type [net/ipv4/ip_output.c]''
**** NOW WE KNOW THAT PACKET IS IP ****
|ip_rcv
|NF_HOOK (ip_rcv_finish)
|ip_route_input // search from routing table to determine function to call
|skb->;dst->;input ->; ip_local_deliver // according to previous routing table check, destination is local machine
|ip_defrag // reassembles IP fragments
|NF_HOOK (ip_local_deliver_finish)
|ipprot->;handler ->; tcp_v4_rcv // according to ''tcp_protocol [include/net/protocol.c]''
**** NOW WE KNOW THAT PACKET IS TCP ****
|tcp_v4_rcv
|sk = __tcp_v4_lookup
|tcp_v4_do_rcv
|switch(sk->;state)
*** Packet can be sent to the task which uses relative socket ***
|case TCP_ESTABLISHED:
|tcp_rcv_established
|__skb_queue_tail // enqueue packet to socket
|sk->;data_ready ->; sock_def_readable
|wake_up_interruptible
*** Packet has still to be handshaked by 3-way TCP handshake ***
|case TCP_LISTEN:
|tcp_v4_hnd_req
|tcp_v4_search_req
|tcp_check_req
|syn_recv_sock ->; tcp_v4_syn_recv_sock
|__tcp_v4_lookup_established
|tcp_rcv_state_process
*** 3-Way TCP Handshake ***
|switch(sk->;state)
|case TCP_LISTEN: // We received SYN
|conn_request ->; tcp_v4_conn_request
|tcp_v4_send_synack // Send SYN + ACK
|tcp_v4_synq_add // set SYN state
|case TCP_SYN_SENT: // we received SYN + ACK
|tcp_rcv_synsent_state_process
tcp_set_state(TCP_ESTABLISHED)
|tcp_send_ack
|tcp_transmit_skb
|queue_xmit ->; ip_queue_xmit
|ip_queue_xmit2
|skb->;dst->;output
|case TCP_SYN_RECV: // We received ACK
|if (ACK)
|tcp_set_state(TCP_ESTABLISHED)
·net_rx_action [net/core/dev.c]
·__skb_dequeue [include/linux/skbuff.h]
·ip_rcv [net/ipv4/ip_input.c]
·NF_HOOK ->; nf_hook_slow [net/core/netfilter.c]
·ip_rcv_finish [net/ipv4/ip_input.c]
·ip_route_input [net/ipv4/route.c]
·ip_local_deliver [net/ipv4/ip_input.c]
·ip_defrag [net/ipv4/ip_fragment.c]
·ip_local_deliver_finish [net/ipv4/ip_input.c]
·tcp_v4_rcv [net/ipv4/tcp_ipv4.c]
·__tcp_v4_lookup
·tcp_v4_do_rcv
·tcp_rcv_established [net/ipv4/tcp_input.c]
·__skb_queue_tail [include/linux/skbuff.h]
·sock_def_readable [net/core/sock.c]
·wake_up_interruptible [include/linux/sched.h]
·tcp_v4_hnd_req [net/ipv4/tcp_ipv4.c]
·tcp_v4_search_req
·tcp_check_req
·tcp_v4_syn_recv_sock
·__tcp_v4_lookup_established
·tcp_rcv_state_process [net/ipv4/tcp_input.c]
·tcp_v4_conn_request [net/ipv4/tcp_ipv4.c]
·tcp_v4_send_synack
·tcp_v4_synq_add
·tcp_rcv_synsent_state_process [net/ipv4/tcp_input.c]
·tcp_set_state [include/net/tcp.h]
·tcp_send_ack [net/ipv4/tcp_output.c]
[目录]
--------------------------------------------------------------------------------
网络接口源码导读
[目录]
--------------------------------------------------------------------------------
网络接口源码的结构(一)
Linux最新稳定内核2.4.x的网络接口源码的结构(一)
李元佳
一.前言
Linux的源码里,网络接口的实现部份是非常值得一读的,通过读源码,不仅对网络协议会有更深的了解,也有助于在网络编程的时候,对应用函数有更精确的了解和把握。本文把重点放在网络接口程序的总体结构上,希望能作为读源码时一些指导性的文字。
本文以Linux2.4.16内核作为讲解的对象,内核源码可以在http://www.kernel.org上下载。我读源码时参考的是http://lxr.linux.no/这个交差参考的网站,我个人认为是一个很好的工具,如果有条件最好上这个网站。国内http://211.71.69.201/joyfire/有类似
二.网络接口程序的结构
Linux的网络接口分为四部份:网络设备接口部份,网络接口核心部份,网络协议族部份,以及网络接口socket层。
网络设备接口部份主要负责从物理介质接收和发送数据。实现的文件在linu/driver/net目录下面。
网络接口核心部份是整个网络接口的关键部位,它为网络协议提供统一的发送接口,屏蔽各种各样的物理介质,同时有负责把来自下层的包向合适的协议配送。它是网络接口的中枢部份。它的主要实现文件在linux/net/core目录下,其中linux/net/core/dev.c为主要管理文件。
网络协议族部份是各种具体协议实现的部份。Linux支持TCP/IP,IPX,X.25,AppleTalk等的协议,各种具体协议实现的源码在linux/net/目录下相应的名称。在这里主要讨论TCP/IP(IPv4)协议,实现的源码在linux/net/ipv4,其中linux/net/ipv4/af_inet.c是主要的管理文件。
网络接口Socket层为用户提供的网络服务的编程接口。主要的源码在linux/net/socket.c
三.网络设备接口部份
物理层上有许多不同类型的网络接口设备, 在文件include/linux/if_arp.h的28行里定义了ARP能处理的各种的物理设备的标志符。网络设备接口要负责具体物理介质的控制,从物理介质接收以及发送数据,并对物理介质进行诸如最大数据包之类的各种设置。这里我们以比较简单的3Com3c501 太网卡的驱动程序为例,大概讲一下这层的工作原理。源码在Linux/drivers/net/3c501.c。
我们从直觉上来考虑,一个网卡当然最主要的是完成数据的接收和发送,在这里我们来看看接收和发送的过程是怎么样的。
发送相对来说比较简单,在Linux/drivers/net/3c501.c的行475 开始的el_start_xmit()这个函数就是实际向3Com3c501以太网卡发送数据的函数,具体的发送工作不外乎是对一些寄存器的读写,源码的注释很清楚,大家可以看看。
接收的工作相对来说比较复杂。通常来说,一个新的包到了,或者一个包发送完成了,都会产生一个中断。Linux/drivers/net/3c501.c的572开始el_interrupt()的函数里面,前半部份处理的是包发送完以后的汇报,后半部份处理的是一个新的包来的,就是说接收到了新的数据。el_interrupt()函数并没有对新的包进行太多的处理,就交给了接收处理函数el_receive()。el_receive()首先检查接收的包是否正确,如果是一个“好”包就会为包分配一个缓冲结构(dev_alloc_skb()),这样驱动程序对包的接收工作就完成了,通过调用上层的函数netif_rx()(net/core/dev.c1214行) ,把包交给上层。
现在驱动程序有了发送和接收数据的功能了,驱动程序怎么样和上层建立联系呢?就是说接收到包以后怎么送给上层,以及上层怎么能调用驱动程序的发送函数呢?
由下往上的关系,是通过驱动程序调用上层的netif_rx()(net/core/dev.c 1214行)函数实现的,驱动程序通过这个函数把接到的数据交给上层,请注意所有的网卡驱动程序都需要调用这个函数的,这是网络接口核心层和网络接口设备联系的桥梁。
由上往下的关系就复杂点。网络接口核心层需要知道有多少网络设备可以用,每个设备的函数的入口地址等都要知道。网络接口核心层会大声喊,“嘿,有多少设备可以帮我发送数据包?能发送的请给我排成一队!”。这一队就由dev_base开始,指针structnet_device *dev_base (Linux/include/linux/netdevice.h 436行)就是保存了网络接口核心层所知道的所有设备。对于网络接口核心层来说,所有的设备都是一个net_device结构,它在include/linux/netdevice.h,line 233里被定义,这是从网络接口核心层的角度看到的一个抽象的设备,我们来看看网络接口核心层的角度看到的网络设备具有的功能:
struct net_device {
………
open()
stop()
hard_start_xmit()
hard_header()
rebuild_header()
set_mac_address()
do_ioctl()
set_config()
hard_header_cache()
header_cache_update()
change_mtu()
tx_timeout()
hard_header_parse()
neigh_setup()
accept_fastpath()
………
}
如果网络接口核心层需要由下层发送数据的时候,在dev_base找到设备以后,就直接调dev->;hard_start_xmit()的这个函数来让下层发数据包。
驱动程序要让网络接口核心层知道自己的存在,当然要加入dev_base所指向的指针链,然后把自己的函数以及各种参数和net_device里的相应的域对应起来。加入dev_base所指向的指针链是通过函数register_netdev(&dev_3c50)(linux/drivers/net/net_init.c, line 532)
建立的。而把自己的函数以和net_device里的相应的域及各种参数关系的建立是在el1_probe1()(Linux/drivers/net/3c501.c)里进行的:
el1_probe1(){
………
dev->;open = &el_open;
dev->;hard_start_xmit = &el_start_xmit;
dev->;tx_timeout = &el_timeout;
dev->;watchdog_timeo = HZ;
dev->;stop = &el1_close;
dev->;get_stats = &el1_get_stats;
dev->;set_multicast_list = &set_multicast_list;
………
ether_setup(dev);
………
}
进一步的对应工作在ether_setup(dev) (drivers/net/net_init.c, line 405 )里进行。我们注意到dev->;hard_start_xmit =&el_start_xmit,这样发送函数的关系就建立了,上层只知道调用dev->;hard_start_xmit这个来发送数据,上面的语句就把驱动程序实际的发送函数告诉了上层。
四.网络接口核心部分
刚才谈论了驱动程序怎么和网络接口核心层衔接的。网络接口核心层知道驱动程序以及驱动程序的函数的入口是通过*dev_base指向的设备链的,而下层是通过调用这一层的函数netif_rx()(net/core/dev.c 1214行) 把数据传递个这一层的。
网络接口核心层的上层是具体的网络协议,下层是驱动程序,我们以及解决了下层的关系,但和上层的关系没有解决。先来讨论一下网络接口核心层和网络协议族部份的关系,这种关系不外乎也是接收和发送的关系。
网络协议,例如IP,ARP等的协议要发送数据包的时候会把数据包传递给这层,那么这种传递是通过什么函数来发生的呢?网络接口核心层通过dev_queue_xmit()(net/core/dev.c,line975)这个函数向上层提供统一的发送接口,也就是说无论是IP,还是ARP协议,通过这个函数把要发送的数据传递给这一层,想发送数据的时候就调用这个函数就可以了。dev_queue_xmit()做的工作最后会落实到dev->;hard_start_xmit(),而dev->;h
ard_start_xmit()会调用实际的驱动程序来完成发送的任务。例如上面的例子中,调用dev->;hard_start_xmit()实际就是调用了el_start_xmit()。
现在讨论接收的情况。网络接口核心层通过的函数netif_rx()(net/core/dev.c 1214行)接收了上层发送来的数据,这时候当然要把数据包往上层派送。所有的协议族的下层协议都需要接收数据,TCP/IP的IP协议和ARP协议,SPX/IPX的IPX协议,AppleTalk的DDP和AARP协议等都需要直接从网络接口核心层接收数据,网络接口核心层接收数据是如何把包发给这些协议的呢?这时的情形和于下层的关系很相似,网络接口核心层的下面
可能有许多的网卡的驱动程序,为了知道怎么向这些驱动程序发数据,前面以及讲过时,是通过*dev_base这个指针指向的链解决的,现在解决和上层的关系是通过static struct packet_ptype_base[16]( net/core/dev.c line 164)这个数组解决的。这个数组包含了需要接收数据包的协议,以及它们的接收函数的入口。
从上面可以看到,IP协议接收数据是通过ip_rcv()函数的,而ARP协议是通过arp_rcv()的,网络接口核心层只要通过这个数组就可以把数据交给上层函数了。
如果有协议想把自己添加到这个数组,是通过dev_add_pack()(net/core/dev.c, line233)函数,从数组删除是通过dev_remove_pack()函数的。Ip层的注册是在初始化函数进行的
void __init ip_init(void) (net/ipv4/ip_output.c, line 1003)
{
………
dev_add_pack(&ip_packet_type);
………
}
重新到回我们关于接收的讨论,网络接口核心层通过的函数netif_rx()(net/core/dev.c 1214行)接收了上层发送来的数据,看看这个函数做了些什么。
由于现在还是在中断的服务里面,所有并不能够处理太多的东西,剩下的东西就通过cpu_raise_softirq(this_cpu, NET_RX_SOFTIRQ)
交给软中断处理, 从open_softirq(NET_RX_SOFTIRQ, net_rx_action, NULL)可以知道NET_RX_SOFTIRQ软中断的处理函数是net_rx_action()(net/core/dev.c, line 1419),net_rx_action()根据数据包的协议类型在数组ptype_base[16]里找到相应的协议,并从中知道了接收的处理函数,然后把数据包交给处理函数,这样就交给了上层处理,实际调用处理函数是通过net_rx_action()里的pt_prev->;func()这一句。例如如果数据
包是IP协议的话,ptype_base[ETH_P_IP]->;func()(ip_rcv()),这样就把数据包交给了IP协议。
五.网络协议部分
协议层是真正实现是在这一层。在linux/include/linux/socket.h里面,Linux的BSD Socket定义了多至32支持的协议族,其中PF_INET就是我们最熟悉的TCP/IP协议族(IPv4, 以下没有特别声明都指IPv4)。以这个协议族为例,看看这层是怎么工作的。实现TCP/IP协议族的主要文件在inux/net/ipv4/目录下面,Linux/net/ipv4/af_inet.c为主要的管理文件。
在Linux2.4.16里面,实现了TCP/IP协议族里面的的IGMP,TCP,UDP,ICMP,ARP,IP。我们先讨论一下这些协议之间的关系。IP和ARP协议是需要直接和网络设备接口打交道的协议,也就是需要从网络核心模块(core) 接收数据和发送数据的。而其它协议TCP,UDP,IGMP,ICMP是需要直接利用IP协议的,需要从IP协议接收数据,以及利用IP协议发送数据,同时还要向上层Socket层提供直接的调用接口。可以看到IP层是一个核心的协议,向
下需要和下层打交道,又要向上层提供所以的传输和接收的服务。
先来看看IP协议层。网络核心模块(core) 如果接收到IP层的数据,通过ptype_base[ETH_P_IP] 数组的IP层的项指向的IP协议的ip_packet_type->;ip_rcv()函数把数据包传递给IP层,也就是说IP层通过这个函数ip_rcv()(linux/net/ipv4/ip_input.c)接收数据的。ip_rcv()这个函数只对IP数据保做了一些checksum的检查工作,如果包是正确的就把包交给了下一个处理函数ip_rcv_finish()(注意调用是通过NF_HOOK这个宏实现的)。现在,ip_rcv_finish()这个函数真正要完成一些IP层的工作了。IP层要做的主要工作就是路由,要决定把数据包往那里送。路由的工作是通过函数ip_route_input()(/linux/net/ipv4/route.c,line 1622)实现的。对于进来的包可能的路由有这些:
属于本地的数据(即是需要传递给TCP,UDP,IGMP这些上层协议的) ;
需要要转发的数据包(网关或者NAT服务器之类的);
不可能路由的数据包(地址信息有误);
我们现在关心的是如果数据是本地数据的时候怎么处理。ip_route_input()调用ip_route_input_slow()(net/ipv4/route.c, line 1312),在ip_route_input_slow()里面的1559行rth->;u.dst.input= ip_local_deliver,这就是判断到IP包是本地的数据包,并把本地数据包处理函数的地址返回。好了,路由工作完成了,返回到ip_rcv_finish()。ip_rcv_finish()最后调用拉skb->;dst->;input(skb),从上面可以看到,这其实就是调用了ip_local_deliver()函数,而ip_local_deliver(),接着就调用了ip_local_deliver_finish()。现在真正到了往上层传递数据包的时候了。
现在的情形和网络核心模块层(core) 往上层传递数据包的情形非常相似,怎么从多个协议选择合适的协议,并且往这个协议传递数据呢?网络网络核心模块层(core) 通过一个数组ptype_base[16]保存了注册了的所有可以接收数据的协议,同样网络协议层也定义了这样一个数组struct net_protocol*inet_protos[MAX_INET_PROTOS](/linux/net/ipv4/protocol.c#L102),它保存了所有需要从IP协议层接收数据的上层协议(IGMP,TCP,UDP,ICMP)的接收处理函数的地址。我们来看看TCP协议的数据结构是怎么样的:
linux/net/ipv4/protocol.c line67
static struct inet_protocol tcp_protocol = {
handler: tcp_v4_rcv,// 接收数据的函数
err_handler: tcp_v4_err,// 出错处理的函数
next: IPPROTO_PREVIOUS,
protocol: IPPROTO_TCP,
name: "TCP"
};
第一项就是我们最关心的了,IP层可以通过这个函数把数据包往TCP层传的。在linux/net/ipv4/protocol.c的上部,我们可以看到其它协议层的处理函数是igmp_rcv(),udp_rcv(), icmp_rcv()。同样在linux/net/ipv4/protocol.c,往数组inet_protos[MAX_INET_PROTOS] 里面添加协议是通过函数inet_add_protocol()实现的,删除协议是通过 inet_del_protocol()实现的。inet_protos[MAX_INET_PROTOS]初始化的过程在linux/net/ipv4/af_inet.c inet_init()初始化函数里面。
inet_init(){
……
printk(KERN_INFO "IP Protocols: ";
for (p = inet_protocol_base; p != NULL {
struct inet_protocol *tmp = (struct inet_protocol *) p->;next;
inet_add_protocol(p);// 添加协议
printk("%s%s",p->;name,tmp?", ":"n";
p = tmp;
………
}
[目录]
--------------------------------------------------------------------------------
网络接口源码的结构(二)
Linux最新稳定内核2.4.x的网络接口源码的结构(二)
李元佳
如果你在Linux启动的时候有留意启动的信息, 或者在linux下打命令dmesg就可以看到这一段程序输出的信息:
IP Protocols: ICMP,UDP,TCP,IGMP也就是说现在数组inet_protos[]里面有了ICMP,UDP,TCP,IGMP四个协议的inet_protocol数据结构,数据结构包含了它们接收数据的处理函数。
Linux 2.4.16在linux/include/linux/socket.h里定义了32种支持的BSDsocket协议,常见的有TCP/IP,IPX/SPX,X.25等,而每种协议还提供不同的服务,例如TCP/IP协议通过TCP协议支持连接服务,而通过UDP协议支持无连接服务,面对这么多的协议,向用户提供统一的接口是必要的,这种统一是通过socket来进行的。
在BSD socket网络编程的模式下,利用一系列的统一的函数来利用通信的服务。例如一个典型的利用TCP协议通信程序是这样:
sock_descriptor = socket(AF_INET,SOCK_STREAM,0);
connect(sock_descriptor, 地址,) ;
send(sock_descriptor,”hello world”);
recv(sock_descriptor,buffer,1024,0);
第一个函数指定了协议Inet协议,即TCP/IP协议,同时是利用面向连接的服务,这样就对应到TCP协议,以后的操作就是利用socket的标准函数进行的。
从上面我们可以看到两个问题,首先socket层需要根据用户指定的协议族(上面是AF_INET) 从下面32种协议中选择一种协议来完成用户的要求,当协议族确定以后,还要把特定的服务映射到协议族下的具体协议,例如当用户指定的是面向连接的服务时,Inet协议族会映射到TCP协议。
从多个协议中选择用户指定的协议,并把具体的出理交给选中的协议,这和一起网络核心层向上和向下衔接的问题本质上是一样的,所以解决的方法也是一样的,同样还是通过数组。在Linux/net/socket.c定义了这个数组staticstruct net_proto_family*net_families[NPROTO] 。数组的元素已经确定了,net_families[2]是TCP/IP协议,net_families[3]是X.25协议,具体那一项对应什么协议,在include/linux/socket.h有定义。但是每一项的数据结构net_proto_family的ops是空的,也就是具体协议处理函数的地址是不知道的。协议的处理函数和ops建立联系是通过sock_register()(Linux/net/socket.c)这个函数建立的,例如TCP/IP协议的是这样建立关系的:
int __init inet_init(void) (net/ipv4/af_inet.c)
{
(void) sock_register(&inet_family_ops);
}
只要给出AF_INET(在宏里定义是2),就可以找到net_failies[2] 里面的处理函数了。
协议的映射完成了,现在要进行服务的映射了。上层当然不可能知道下层的什么协议能对应特定的服务,所以这种映射自然由协议族自己完成。在TCP/IP协议族里,这种映射是通过struct list_head inetsw[SOCK_MAX]( net/ipv4/af_inet.c)
这个数组进行映射的,在谈论这个数组之前我们来看另外一个数组inetsw_array[](net/ipv4/af_inet.c)
static struct inet_protosw inetsw_array[] =
{
{
type: SOCK_STREAM,
protocol: IPPROTO_TCP,
prot: &tcp_prot,
ops: &inet_stream_ops,
capability: -1,
no_check: 0,
flags: INET_PROTOSW_PERMANENT,
},
{
type: SOCK_DGRAM,
protocol: IPPROTO_UDP,
prot: &udp_prot,
ops: &inet_dgram_ops,
capability: -1,
no_check: UDP_CSUM_DEFAULT,
flags: INET_PROTOSW_PERMANENT,
},
{
type: SOCK_RAW,
protocol: IPPROTO_IP, /* wild card */
prot: &raw_prot,
ops: &inet_dgram_ops,
capability: CAP_NET_RAW,
no_check: UDP_CSUM_DEFAULT,
flags: INET_PROTOSW_REUSE,
}
};
我们看到,SOCK_STREAM映射到了TCP协议,SOCK_DGRAM映射到了UDP协议,SOCK_RAW映射到了IP协议。现在只要把inetsw_array里的三项添加到数组inetsw[SOCK_MAX]就可以了,添加是通过函数inet_register_protosw()实现的。在inet_init()(net/ipv4/af_inet.c) 里完成了这些工作。
还有一个需要映射的就是socket其它诸如accept,send(), connect(),release(),bind()等的操作函数是怎么映射的呢?我们来看一下上面的数组的TCP的项
{
type: SOCK_STREAM,
protocol: IPPROTO_TCP,
prot: &tcp_prot,
ops: &inet_stream_ops,
capability: -1,
no_check: 0,
flags: INET_PROTOSW_PERMANENT,
},
我们看到这种映射是通过ops,和prot来映射的,我们再来看看 tcp_prot这一项:
struct proto tcp_prot = {
name: "TCP",
close: tcp_close,
connect: tcp_v4_connect,
disconnect: tcp_disconnect,
accept: tcp_accept,
ioctl: tcp_ioctl,
init: tcp_v4_init_sock,
destroy: tcp_v4_destroy_sock,
shutdown: tcp_shutdown,
setsockopt: tcp_setsockopt,
getsockopt: tcp_getsockopt,
sendmsg: tcp_sendmsg,
recvmsg: tcp_recvmsg,
backlog_rcv: tcp_v4_do_rcv,
hash: tcp_v4_hash,
unhash: tcp_unhash,
get_port: tcp_v4_get_port,
};
所以的映射都已经完成了,用户调用connect()函数,其实就是调用了tcp_v4_connect()函数,按照这幅图,读起源码来就简单了很多了。
六 Socket层
上一节把socket层大多数要讨论的东西都谈论了,现在只讲讲socket 层和用户的衔接。
系统调用socket(),bind(),connect(),accept,send(),release()等是在Linux/net/socket.c里面的实现的,系统调用实现的函数是相应的函数名加上sys_的前缀。
现在看看当用户调用socket()这个函数,到底下面发生了什么。
Socket(AF_INET,SOCK_STREAM,0)调用了sys_socket(),sys_socket()接着调用socket_creat(),socket_creat()就要根据用户提供的协议族参数在net_families[]里寻找合适的协议族,如果协议族没有被安装就要请求安装该协议族的模块,然后就调用该协议族的create()函数的处理句柄。根据参数AF_INET,inet_creat()就被调用了,在inet_creat()根据服务类型在inetsw[SOCK_MAX]选择合适的协议,并把协议的操作集赋给socket就是了,根据SOCK_STREAM,TCP协议被选中,
inet_creat(){
answer=inetsw [用户要求服务服务] ;
sock->;ops = answer->;ops;
sk->;prot = answer->;prot
}
到此为止,上下都打通了,该是大家读源码的时候了。
[目录]
--------------------------------------------------------------------------------
防火墙
[目录]
--------------------------------------------------------------------------------
netfilter
标题: A new place to LKM:netfilter
作者: yawl <yawl@nsfocus.com>;
时间: 2000-10
目录:
-.前言
二.分析
三.例子代码
四.附录:与2.2在应用方面的区别简介
五.后记
-.前言
在linux2.2内核中的防火墙ipchains已经被用户广泛认可,它提供了完整的防火墙功能(包过滤,地址伪装,透明代理),又避免了商业防火墙那高的惊人的价格。如果你用的是某款国产防火墙,那么十有八九你实际在受到ipchains(有些甚至是2.0系列中ipfwadm)的保护.在未来的2.4内核中,被称为netfilter(http://netfilter.kernelnotes.org/)的防火墙以更好的结构重新构造,并实现了许多新功能,如完整的动态NAT(2.2内核实际是多对一的"地址伪装",基于MAC及用户的过滤,真正的基于状态的过滤(不再是简单的查看tcp的标志位等),包速率限制等。
在原有的网络部分的LKM中,如果对网络部分进行处理,一般是先生成struct packet_type结构,在用dev_add_pack将其插入网络层(注意此时的packet_type实际相当于一个的三层的协议,如ip_packet_type,ipx_8023_packet_type等),具体的例子可参见phrack 55期<Building into the linux network layer>;和本月小四写的月刊文章<利用LLKM处理网络通信----对抗IDS、Firewall>;。
而netfilter本身在IP层内提供了另外的5个插入点(其文档中称为HOOK):NF_IP_PRE_ROUTING,NF_IP_LOCAL_IN,NF_IP_FORWARD,NF_IP_LOCAL_OUT,NF_IP_POST_ROUTING,分别对应IP层的五个不同位置,这样理论上在写lkm时便可以选择更适合的切入点,再辅以netfilter内置的新功能(如connect tracking),应该会帮助写出功能更强的lkm。
本来准备写出一个完整的例子(限制IP连接数),但计划总赶不上变化,只好先贴出个简单的例子来,权且自我安慰成抛砖引玉了。
本文的参考配置是linux2.4.0-test4和iptable-1.1.1,好,开始抛砖,闪人喽!
二.分析
通俗的说,netfilter的架构就是在整个网络流程的若干位置放置了一些检测点(HOOK),而在每个检测点上上登记了一些处理函数进行处理(如包过滤,NAT等,甚至可以是用户自定义的功能)。
IP层的五个HOOK点的位置如下图所示(copy from <packet filter howto>;) :
--->;[1]--->;[ROUTE]--->;[3]--->;[5]--->;
| ^
| |
| [ROUTE]
v |
[2] [4]
| ^
| |
v |
[local process]
[1]:NF_IP_PRE_ROUTING:刚刚进入网络层的数据包通过此点(刚刚进行完版本号,校验和等检测),源地址转换在此点
进行;
[2]:NF_IP_LOCAL_IN:经路由查找后,送往本机的通过此检查点,INPUT包过滤在此点进行;
[3]:NF_IP_FORWARD:要转发的包通过此检测点,FORWORD包过滤在此点进行;
[4]:NF_IP_LOCAL_OUT:本机进程发出的包通过此检测点,OUTPUT包过滤在此点进行;
[5]:NF_IP_POST_ROUTING:所有马上便要通过网络设备出去的包通过此检测点,内置的目的地址转换功能(包括地址伪
装)在此点进行。
在IP层代码中,有一些带有NF_HOOK宏的语句,如IP的转发函数中有:
<-ipforward.c ip_forward()->;
NF_HOOK(PF_INET, NF_IP_FORWARD, skb, skb->;dev, dev2,
ip_forward_finish);
其中NF_HOOK宏的定义提炼如下:
<-/include/linux/netfilter.h->;
#ifdef CONFIG_NETFILTER
#define NF_HOOK(pf, hook, skb, indev, outdev, okfn) \
(list_empty(&nf_hooks[(pf)][(hook)]) \
? (okfn)(skb) \
: nf_hook_slow((pf), (hook), (skb), (indev), (outdev), (okfn)))
#else /* !CONFIG_NETFILTER */
#define NF_HOOK(pf, hook, skb, indev, outdev, okfn) (okfn)(skb)
#endif /*CONFIG_NETFILTER*/
如果在编译内核时没有配置netfilter时,就相当于调用最后一个参数,此例中即执行ip_forward_finish函数;否则进入HOOK点,执行通过nf_register_hook()登记的功能(这句话表达的可能比较含糊,实际是进入
nf_hook_slow()函数,再由它执行登记的函数)。
NF_HOOK宏的参数分别为:
1.pf:协议族名,netfilter架构同样可以用于IP层之外,因此这个变量还可以有诸如PF_INET6,PF_DECnet等名字。
2.hook:HOOK点的名字,对于IP层,就是取上面的五个值;
3.skb:不用多解释了吧;
4.indev:进来的设备,以struct net_device结构表示;
5.outdev:出去的设备,以struct net_device结构表示;
(后面可以看到,以上五个参数将传到用nf_register_hook登记的处理函数中。)
6.okfn:是个函数指针,当所有的该HOOK点的所有登记函数调用完后,转而走此流程。
这些点是已经在内核中定义好的,除非你是这部分内核代码的维护者,否则无权增加或修改,而在此检测点进行的处理,则可由用户指定。像packet filter,NAT,connection track这些功能,也是以这种方式提供的。正如netfilter的当初的设计目标--提供一个完善灵活的框架,为扩展功能提供方便。
如果我们想加入自己的代码,便要用nf_register_hook函数,其函数原型为:
int nf_register_hook(struct nf_hook_ops *reg)
我们考察一下struct nf_hook_ops结构:
struct nf_hook_ops
{
struct list_head list;
/* User fills in from here down. */
nf_hookfn *hook;
int pf;
int hooknum;
/* Hooks are ordered in ascending priority. */
int priority;
};
我们的工作便是生成一个struct nf_hook_ops结构的实例,并用nf_register_hook将其HOOK上。其中list项我们总要初始化为{NULL,NULL};由于一般在IP层工作,pf总是PF_INET;hooknum就是我们选择的HOOK点;一个HOOK点可能挂多个处理函数,谁先谁后,便要看优先级,即priority的指定了。netfilter_ipv4.h中用一个枚举类型指定了内置的处理函数的优先级:
enum nf_ip_hook_priorities {
NF_IP_PRI_FIRST = INT_MIN,
NF_IP_PRI_CONNTRACK = -200,
NF_IP_PRI_MANGLE = -150,
NF_IP_PRI_NAT_DST = -100,
NF_IP_PRI_FILTER = 0,
NF_IP_PRI_NAT_SRC = 100,
NF_IP_PRI_LAST = INT_MAX,
};
hook是提供的处理函数,也就是我们的主要工作,其原型为:
unsigned int nf_hookfn(unsigned int hooknum,
struct sk_buff **skb,
const struct net_device *in,
const struct net_device *out,
int (*okfn)(struct sk_buff *));
它的五个参数将由NFHOOK宏传进去。
了解了这些,基本上便可以可以写一个lkm出来了。
三.例子代码
这段代码是一个例子,其功能实现了一个IDS,检测几个简单攻击(land,winnuke)和特殊扫描(nmap),当然,不会有人真把
它当严肃的IDS使用吧。可以利用类似结构干点别的。。。
<-example.c begin->;
/*
* netfilter module example: it`s a kernel IDS(be quie,donot laugh, my friend)
* yawl@nsfocus.com
* Compile:gcc -O -c -Wall sample.c ,under linux2.4 kernel,netfilter is needed.
*/
#define __KERNEL__
#define MODULE
#include <linux/module.h>;
#include <linux/skbuff.h>;
#include <linux/netdevice.h>;
#include <linux/config.h>;
#include <linux/ip.h>;
#include <linux/tcp.h>;
#include <linux/udp.h>;
#include <linux/netfilter_ipv4.h>;
#define ALERT(fmt,args...) printk("nsfocus: " fmt, ##args)
/*message will be print to screen(too many~),and logged to /var/log/message*/
static unsigned int sample(unsigned int hooknum,struct sk_buff **skb,
const struct net_device *in,
const struct net_device *out,int (*okfn)(struct sk_buff *))
{
struct iphdr *iph;
struct tcphdr *tcph;
struct udphdr *udph;
__u32 sip;
__u32 dip;
__u16 sport;
__u16 dport;
iph=(*skb)->;nh.iph;
sip=iph->;saddr;
dip=iph->;daddr;
/*play ip packet here
(note:checksum has been checked,if connection track is enabled,defrag have been done )*/
if(iph->;ihl!=5){
ALERT("IP packet with packet from %d.%d.%d.%d to %d.%d.%d.%d\n",NIPQUAD(sip),NIPQUAD(dip));
}
if(iph->;protocol==6){
tcph=(struct tcphdr*)((__u32 *)iph+iph->;ihl);
sport=tcph->;source;
dport=tcph->;dest;
/*play tcp packet here*/
if((tcph->;syn)&&(sport==dport)&&(sip==dip)){
ALERT("maybe land attack\n";
}
if(ntohs(tcph->;dest)==139&&tcph->;urg){
ALERT("maybe winnuke a from %d.%d.%d.%d to %d.%d.%d.%d\n",NIPQUAD(sip),NIPQUAD(dip));
}
if(tcph->;ece&&tcph->;cwr){
ALERT("queso from %d.%d.%d.%d to %d.%d.%d.%d\n",NIPQUAD(sip),NIPQUAD(dip));
}
if((tcph->;fin)&&(tcph->;syn)&&(!tcph->;rst)&&(!tcph->;psh)&&(!tcph->;ack)&&(!tcph->;urg)){
ALERT("SF_scan from %d.%d.%d.%d to %d.%d.%d.%d\n",NIPQUAD(sip),NIPQUAD(dip));
}
if((!tcph->;fin)&&(!tcph->;syn)&&(!tcph->;rst)&&(!tcph->;psh)&&(!tcph->;ack)&&(!tcph->;urg)){
ALERT("NULL_scan from %d.%d.%d.%d to %d.%d.%d.%d\n",NIPQUAD(sip),NIPQUAD(dip));
}
if(tcph->;fin&&tcph->;syn&&tcph->;rst&&tcph->;psh&&tcph->;ack&&tcph->;urg){
ALERT("FULL_Xmas_scan from %d.%d.%d.%d to %d.%d.%d.%d\n",NIPQUAD(sip),NIPQUAD(dip));
}
if((tcph->;fin)&&(!tcph->;syn)&&(!tcph->;rst)&&(tcph->;psh)&&(!tcph->;ack)&&(tcph->;urg)){
ALERT("XMAS_Scan(FPU)from %d.%d.%d.%d to %d.%d.%d.%d\n",NIPQUAD(sip),NIPQUAD(dip));
}
}
else if(iph->;protocol==17){
udph=(struct udphdr *)((__u32 *)iph+iph->;ihl);
sport=udph->;source;
dport=udph->;dest;
/*play udp packet here*/
}
else if(iph->;protocol==1){
/*play icmp packet here*/
}
else if(iph->;protocol==2){
ALERT("igmp packet from %d.%d.%d.%d to %d.%d.%d.%d\n",NIPQUAD(sip),NIPQUAD(dip));
/*play igmp packet here*/
}
else{
ALERT("unknown protocol%d packet from %d.%d.%d.%d to %d.%d.%d.%d\n",iph->;protocol,NIPQUAD(sip),NIPQUAD(dip));
}
return NF_ACCEPT;
/*for it is IDS,we just accept all packet,
if you really want to drop this skb,just return NF_DROP*/
}
static struct nf_hook_ops iplimitfilter
={ {NULL,NULL} ,sample,PF_INET,NF_IP_PRE_ROUTING,NF_IP_PRI_FILTER-1};
int init_module(void)
{
return nf_register_hook(&iplimitfilter);
}
void cleanup_module(void)
{
nf_unregister_hook(&iplimitfilter);
}
<-example.c end->;
四.附录:与2.2在应用方面的区别简介
本来还想详细介绍一下iptables的用法,但如果说的太详细的话,还不如索性将HOWTO翻译一下,于是干脆了却了这个念头,只顺便简介一下与以前版本的变化(而且是我认为最重要的)。如果ipchains本来便没有在你的脑子中扎根,其实便没有必要看这部分。
netfilter,又可称为iptables.开发初期准备将packet filter和NAT的配置工具完全分开,一个称为iptables,另一个称为ipnatctl,而将整个项目成为netfilter.但后来可能是还是习惯2.2内核中用ipchians一个工具干两件事的用法,又改为全部用iptables配置了。
理论上还可以用2.2系列的ipchains和2.0系列的ipfwadm作配置工具,但只是做兼容或过渡的考虑了。通过源码看到他们也是通过现有的结构HOOK上去的(主要是net/ipv4/netfilter目录下的ip_fw_compat.c,ip_fw_compat_masq.c,
ip_fw_compat_redir.c,ipchains_core.c,ipfwadm_core.c这几个文件)。
一个重要的变化是原有的INPUT,OUTPUT(原来是小写的input,ouput)链(现在应称为表?)的位置变了,原来的input,output的位置相当于现在的NF_IP_PRE_ROUTING,NF_IP_POST_ROUTING 。原有的结构确实不太合理,转发的包要经过三条链,现在INPUT专指去往本机的,OUPUT专指从本机发出的,而FOWARD仍旧是转发的包。
举两个简单的例子:
1.作地址伪装(场景:对外通过拨号连接internet)注意原来的MASQ变成好长的MASQUERATE,而伪装相当于SNAT,因此位置是在POSTROUTING:
iptables -t nat -A POSTROUTING -o ppp0 -j MASQUERATE
2.还有一个限制包速率的功能比较好玩,例如如下规则:
iptables -A FORWARD -p tcp --syn -m limit --limit 1/s -j ACCEPT
简单的说就是在转发时(-A FORWARD:因为是防火墙嘛),如果是tcp协议,且有syn标记(-p tcp --syn),可以限制为每秒一个(-m limit --limit 1/s ),行动项是ACCEPT。最后连起来意义就是每秒只允许转发一个tcp连接请求。
五.后记
netfilter还提供了许多新功能,如可以将包转发到应用层,由应用程序进行处理等,可目前我还没有分析多少,慢慢抽出点时间看吧。唉,尽管以前看过ipchains的代码,但netfilter实在变动太大了,一切都要从头看起
最后,当然要感谢Rusty Russell,netfilter项目的负责人,不仅为我们提供了这个强大好用的工具,还写了大量非常优秀的文档。
参考文献:
[1.] Linux 2.4 Packet Filtering HOWTO
Rusty Russell, mailing list netfilter@lists.samba.org
v1.0.1 Mon May 1 18:09:31 CST 2000
[2.] Linux IPCHAINS-HOWTO
Paul Russell, ipchains@rustcorp.com
v1.0.7, Fri Mar 12 13:46:20 CST 1999
[3.] Linux 2.4 NAT HOWTO
Rusty Russell, mailing list netfilter@lists.samba.org
v1.0.1 Mon May 1 18:38:22 CST 2000
[4.] Linux netfilter Hacking HOWTO
Rusty Russell, mailing list netfilter@lists.samba.org
v1.0.1 Sat Jul 1 18:24:41 EST 2000
[5.] Writing a Module for netfilter
by Paul "Rusty" Russell
Linux Magazine June 2000 http://www.linux-mag.com/2000-06/gear_01.html
[6.] Salvatore Sanfilippo<antirez@invece.org>;写的一份netfilter sample,但可惜我找不到出处了,只剩下手头一份打印稿,But anyway,thanks to Salvatore.
[目录]
--------------------------------------------------------------------------------
ip/tables
1 /*
2 * 25-Jul-1998 Major changes to allow for ip chain table
3 *
4 * 3-Jan-2000 Named tables to allow packet selection for different uses.
5 */
6
7 /*
8 * Format of an IP firewall descriptor
9 *
注意这里的说明:IP地址和掩码是按照网络字节存储(大端存储)标志字节和端口号是按照主机字节序存储(依主机硬件结构而定)
10 * src, dst, src_mask, dst_mask are always stored in network byte order.
11 * flags are stored in host byte order (of course).
12 * Port numbers are stored in HOST byte order.
13 */
14
15 #ifndef _IPTABLES_H
16 #define _IPTABLES_H
17
18 #ifdef __KERNEL__
19 #include linux/if.h
20 #include linux/types.h
21 #include linux/in.h
22 #include linux/ip.h
23 #include linux/skbuff.h
24 #endif
25 #include linux/netfilter_ipv4.h
26
27 #define IPT_FUNCTION_MAXNAMELEN 30
28 #define IPT_TABLE_MAXNAMELEN 32
29
这个结构存储与IP头部有关的防火墙规则信息。这里的注释说“这个结构无须填充零字节”,就是说这个结构的大小正好是4的倍数。这里由于IFNAMSIZ等于16,所以整个结构大小确实是4的倍数。
30 /* Yes, Virginia, you have to zero the padding. */
31 struct ipt_ip {
32 /* Source and destination IP addr */
33 struct in_addr src, dst;
34 /* Mask for src and dest IP addr */
35 struct in_addr smsk, dmsk;
36 char iniface[IFNAMSIZ], outiface[IFNAMSIZ];
37 unsigned char iniface_mask[IFNAMSIZ], outiface_mask[IFNAMSIZ];
38
39 /* Protocol, 0 = ANY */
40 u_int16_t proto;
41
42 /* Flags word */
43 u_int8_t flags;
44 /* Inverse flags */
45 u_int8_t invflags;
46 };
47这个结构存储match的信息,这里的匹配主要是指与IP无关的防火墙规则信息。由系统缺省设置的匹配主要有三个“tcp”、“udp”,“icmp”,我在分析ip_tables.c时将详细描述。
48 struct ipt_entry_match
49 {
50 union {
51 struct {
52 u_int16_t match_size;
53
54 /* Used by userspace */
55 char name[IPT_FUNCTION_MAXNAMELEN];
56 } user;
57 struct {
58 u_int16_t match_size;
59
60 /* Used inside the kernel */
61 struct ipt_match *match;
62 } kernel;
63
64 /* Total length */
65 u_int16_t match_size;
66 } u;
67
68 unsigned char data[0];
69 };
70 target结构信息,是决定一个分组命运的信息。也可以理解为action信息,其意义是指当一个分组与rule和match信息匹配后,如何处置该分组。处置方法一般有三种:一,命令常数,比如DROP ACCEPT等等;二 系统预定义的模块处理函数,比如”SNAT DNAT"等等;第三种是用户自己写模块函数。
71 struct ipt_entry_target
72 {
73 union {
74 struct {
75 u_int16_t target_size;
76
77 /* Used by userspace */
78 char name[IPT_FUNCTION_MAXNAMELEN];
79 } user;
80 struct {
81 u_int16_t target_size;
82
83 /* Used inside the kernel */
84 struct ipt_target *target;
85 } kernel;
86
87 /* Total length */
88 u_int16_t target_size;
89 } u;
90
91 unsigned char data[0];
92 };
93这个结构已经很明显给出了target的形式:命令常数、或者模块函数。
94 struct ipt_standard_target
95 {
96 struct ipt_entry_target target;
97 int verdict;
98 };
99
计数器结构,每一个rule都有一个计数器结构用来统计匹配该条规则的分组数目和字节数目。为基于统计的安全工具提供分析基础。
100 struct ipt_counters
101 {
102 u_int64_t pcnt, bcnt; /* Packet and byte counters */
103 };
104
标志字段,各个常数后面的注释已经给出了明确的解释,这里不再赘述。
105 /* Values for "flag" field in struct ipt_ip (general ip structure). */
106 #define IPT_F_FRAG 0x01 /* Set if rule is a fragment rule */
107 #define IPT_F_MASK 0x01 /* All possible flag bits mask. */
108
109 /* Values for "inv" field in struct ipt_ip. */
110 #define IPT_INV_VIA_IN 0x01 /* Invert the sense of IN IFACE. */
111 #define IPT_INV_VIA_OUT 0x02 /* Invert the sense of OUT IFACE */
112 #define IPT_INV_TOS 0x04 /* Invert the sense of TOS. */
113 #define IPT_INV_SRCIP 0x08 /* Invert the sense of SRC IP. */
114 #define IPT_INV_DSTIP 0x10 /* Invert the sense of DST OP. */
115 #define IPT_INV_FRAG 0x20 /* Invert the sense of FRAG. */
116 #define IPT_INV_PROTO 0x40 /* Invert the sense of PROTO. */
掩码标志。用法是当出现超出掩码范围的标志时,确认是非法标志。
117 #define IPT_INV_MASK 0x7F /* All possible flag bits mask. */
118
其实这个结构的构成这里的注释已经说的很清楚,但是从论坛上有人问"关于netfilter的问题“时,可以看出很多人还是不理解。与前面ipchains版本防火墙不同的是iptables的防火墙规则构成发生了变化。ipchains的构成是rule+target,而iptables的构成是ip匹配信息+match+target。同时iptables构成的每一个部分都是可变大小的,由于经常出现”char XXX[0]“就可以看出。但是我个人认为规则的组织有点不好理解,它经常是先分配一段空间,然后将规则一条一条放入。如同文件系统存放变长记录的文件时,总要在记录中放入记录长度,以便以后取出记录,这里iptables正是使用这种方法,在每个规则中都放入长度字段,这样方便提取各个组成部分和计算下一条规则的位置。
119 /* This structure defines each of the firewall rules. Consists of 3
120 parts which are 1) general IP header stuff 2) match specific
121 stuff 3) the target to perform if the rule matches */
122 struct ipt_entry
123 {
124 struct ipt_ip ip;
125
126 /* Mark with fields that we care about. */
127 unsigned int nfcache;
128下面两个字段用来计算target的位置和下一条规则的位置。
129 /* Size of ipt_entry + matches */
130 u_int16_t target_offset;
131 /* Size of ipt_entry + matches + target */
132 u_int16_t next_offset;
133
这个字段的存在,为发现规则中存在”环路“提供手段。
134 /* Back pointer */
135 unsigned int comefrom;
136
137 /* Packet and byte counters. */
138 struct ipt_counters counters;
139
140 /* The matches (if any), then the target. */
141 unsigned char elems[0];
142 };
143
144 /*
145 * New IP firewall options for [gs]etsockopt at the RAW IP level.
146 * Unlike BSD Linux inherits IP options so you don't have to use a raw
147 * socket for this. Instead we check rights in the calls. */
定义提供给set/getsockopt系统调用的命令常数的基常数。
148 #define IPT_BASE_CTL 64 /* base for firewall socket options */
149
150 #define IPT_SO_SET_REPLACE (IPT_BASE_CTL)
151 #define IPT_SO_SET_ADD_COUNTERS (IPT_BASE_CTL + 1)
152 #define IPT_SO_SET_MAX IPT_SO_SET_ADD_COUNTERS
153
154 #define IPT_SO_GET_INFO (IPT_BASE_CTL)
155 #define IPT_SO_GET_ENTRIES (IPT_BASE_CTL + 1)
156 #define IPT_SO_GET_MAX IPT_SO_GET_ENTRIES
157
158 /* CONTINUE verdict for targets */
159 #define IPT_CONTINUE 0xFFFFFFFF
160
161 /* For standard target */
162 #define IPT_RETURN (-NF_MAX_VERDICT - 1)
163
Tcp匹配规则信息。
164 /* TCP matching stuff */
165 struct ipt_tcp
166 {
167 u_int16_t spts[2]; /* Source port range. */
168 u_int16_t dpts[2]; /* Destination port range. */
169 u_int8_t option; /* TCP Option iff non-zero*/
170 u_int8_t flg_mask; /* TCP flags mask byte */
171 u_int8_t flg_cmp; /* TCP flags compare byte */
172 u_int8_t invflags; /* Inverse flags */
173 };
174
tcp的取反标志值。
175 /* Values for "inv" field in struct ipt_tcp. */
176 #define IPT_TCP_INV_SRCPT 0x01 /* Invert the sense of source ports. */
177 #define IPT_TCP_INV_DSTPT 0x02 /* Invert the sense of dest ports. */
178 #define IPT_TCP_INV_FLAGS 0x04 /* Invert the sense of TCP flags. */
179 #define IPT_TCP_INV_OPTION 0x08 /* Invert the sense of option test. */
180 #define IPT_TCP_INV_MASK 0x0F /* All possible flags. */
181
udp匹配规则信息
182 /* UDP matching stuff */
183 struct ipt_udp
184 {
185 u_int16_t spts[2]; /* Source port range. */
186 u_int16_t dpts[2]; /* Destination port range. */
187 u_int8_t invflags; /* Inverse flags */
188 };
189
190 /* Values for "invflags" field in struct ipt_udp. */
191 #define IPT_UDP_INV_SRCPT 0x01 /* Invert the sense of source ports. */
192 #define IPT_UDP_INV_DSTPT 0x02 /* Invert the sense of dest ports. */
193 #define IPT_UDP_INV_MASK 0x03 /* All possible flags. */
194
195 /* ICMP matching stuff */
ICMP匹配规则信息
196 struct ipt_icmp
197 {
198 u_int8_t type; /* type to match */
199 u_int8_t code[2]; /* range of code */
200 u_int8_t invflags; /* Inverse flags */
201 };
202
203 /* Values for "inv" field for struct ipt_icmp. */
204 #define IPT_ICMP_INV 0x01 /* Invert the sense of type/code test */
205
这个结构实质上用户通过getsockopt系统调用获取table信息时所传递参数的类型。
206 /* The argument to IPT_SO_GET_INFO */
207 struct ipt_getinfo
208 {
209 /* Which table: caller fills this in. */
210 char name[IPT_TABLE_MAXNAMELEN];
211
212 /* Kernel fills these in. */
213 /* Which hook entry points are valid: bitmask */
214 unsigned int valid_hooks;
215
216 /* Hook entry points: one per netfilter hook. */
217 unsigned int hook_entry[NF_IP_NUMHOOKS];
218
219 /* Underflow points. */
220 unsigned int underflow[NF_IP_NUMHOOKS];
221
222 /* Number of entries */
223 unsigned int num_entries;
224
225 /* Size of entries. */
226 unsigned int size;
227 };
228
这个结构是用户通过系统调用更换table是所传递的参数类型。
229 /* The argument to IPT_SO_SET_REPLACE. */
230 struct ipt_replace
231 {
232 /* Which table. */
233 char name[IPT_TABLE_MAXNAMELEN];
234
235 /* Which hook entry points are valid: bitmask. You can't
236 change this. */
237 unsigned int valid_hooks;
238
239 /* Number of entries */
240 unsigned int num_entries;
241
242 /* Total size of new entries */
243 unsigned int size;
244
245 /* Hook entry points. */
246 unsigned int hook_entry[NF_IP_NUMHOOKS];
247
248 /* Underflow points. */
249 unsigned int underflow[NF_IP_NUMHOOKS];
250
251 /* Information about old entries: */
252 /* Number of counters (must be equal to current number of entries). */
253 unsigned int num_counters;
254 /* The old entries' counters. */
255 struct ipt_counters *counters;
256
257 /* The entries (hang off end: not really an array). */
258 struct ipt_entry entries[0];
259 };
260
这个更改计数器时传递的参数类型。
261 /* The argument to IPT_SO_ADD_COUNTERS. */
262 struct ipt_counters_info
263 {
264 /* Which table. */
265 char name[IPT_TABLE_MAXNAMELEN];
266
267 unsigned int num_counters;
268
269 /* The counters (actually `number' of these). */
270 struct ipt_counters counters[0];
271 };
272
这个是想获取防火墙规则时,传递给系统调用的参数类型。
273 /* The argument to IPT_SO_GET_ENTRIES. */
274 struct ipt_get_entries
275 {
276 /* Which table: user fills this in. */
277 char name[IPT_TABLE_MAXNAMELEN];
278
279 /* User fills this in: total entry size. */
280 unsigned int size;
281
282 /* The entries. */
283 struct ipt_entry entrytable[0];
284 };
285
286 /* Standard return verdict, or do jump. */
287 #define IPT_STANDARD_TARGET ""
288 /* Error verdict. */
289 #define IPT_ERROR_TARGET "ERROR"
290
现面定义了一些使用例程
291 /* Helper functions */
获取一条防火墙规则的target位置
292 extern __inline__ struct ipt_entry_target *
293 ipt_get_target(struct ipt_entry *e)
294 {
295 return (void *)e + e->;target_offset;
296 }
297
下面的宏遍历处理一条防火墙规则的所有匹配。我已经说过每一条防火墙规则在iptables中分为三部分,而且每一部分的大小都是可变的。比如match部分,它本身可以有多个match项。
298 /* fn returns 0 to continue iteration */
299 #define IPT_MATCH_ITERATE(e, fn, args...) \
300 ({ \
301 unsigned int __i; \
302 int __ret = 0; \
303 struct ipt_entry_match *__m; \
这个for语句我来解释一下:首先__i取值为ipt_entry结构的大小,实质上就是match匹配的开始处的偏移地址,将其与e相加就得到了match匹配的地址,然后调用fn处理这个匹配。如果函数返回值为零,当前匹配的偏移地址加上当前匹配的大小,如果不超过target的偏移地址,则继续处理下一条匹配。
304 \
305 for (__i = sizeof(struct ipt_entry); \
306 __i target_offset; \
307 __i += __m->;u.match_size) { \
308 __m = (void *)(e) + __i; \
309 \
310 __ret = fn(__m , ## args); \
311 if (__ret != 0) \
312 break; \
313 } \
314 __ret; \
315 })
316
这个宏处理一个table中的所有防火墙规则。对比对上一个宏的理解,这里我就不解释了。
317 /* fn returns 0 to continue iteration */
318 #define IPT_ENTRY_ITERATE(entries, size, fn, args...) \
319 ({ \
320 unsigned int __i; \
321 int __ret = 0; \
322 struct ipt_entry *__e; \
323 \
324 for (__i = 0; __i next_offset) { \
325 __e = (void *)(entries) + __i; \
326 \
327 __ret = fn(__e , ## args); \
328 if (__ret != 0) \
329 break; \
330 } \
331 __ret; \
332 })
333
334 /*
335 * Main firewall chains definitions and global var's definitions.
336 */
337 #ifdef __KERNEL__
338
339 #include ·
340 extern void ipt_init(void) __init;
341
所有的匹配处理都注册到一个match处理链表中,链表结点的类型就是这里的结构类型。当处理匹配时都是调用这里注册的处理函数。每个结点实质上由三个函数构成,一个匹配处理函数,一个合法性检查函数,一个析构函数。最后一个是反身指针,指针的作用如注释所示。
342 struct ipt_match
343 {
344 struct list_head list;
345
346 const char name[IPT_FUNCTION_MAXNAMELEN];
347
348 /* Return true or false: return FALSE and set *hotdrop = 1 to
349 force immediate packet drop. */
350 int (*match)(const struct sk_buff *skb,
351 const struct net_device *in,
352 const struct net_device *out,
353 const void *matchinfo,
354 int offset,
355 const void *hdr,
356 u_int16_t datalen,
357 int *hotdrop);
358
359 /* Called when user tries to insert an entry of this type. */
360 /* Should return true or false. */
361 int (*checkentry)(const char *tablename,
362 const struct ipt_ip *ip,
363 void *matchinfo,
364 unsigned int matchinfosize,
365 unsigned int hook_mask);
366
367 /* Called when entry of this type deleted. */
368 void (*destroy)(void *matchinfo, unsigned int matchinfosize);
369
370 /* Set this to THIS_MODULE if you are a module, otherwise NULL */
371 struct module *me;
372 };
373
和match一样,所有的target都注册到这个结构类型的全局链表中,每个target的处理函数都是这里注册的函数。和上面的解释一样,这里也主要包含三个函数指针。
374 /* Registration hooks for targets. */
375 struct ipt_target
376 {
377 struct list_head list;
378
379 const char name[IPT_FUNCTION_MAXNAMELEN];
380
381 /* Returns verdict. */
382 unsigned int (*target)(struct sk_buff **pskb,
383 unsigned int hooknum,
384 const struct net_device *in,
385 const struct net_device *out,
386 const void *targinfo,
387 void *userdata);
388
389 /* Called when user tries to insert an entry of this type:
390 hook_mask is a bitmask of hooks from which it can be
391 called. */
392 /* Should return true or false. */
393 int (*checkentry)(const char *tablename,
394 const struct ipt_entry *e,
395 void *targinfo,
396 unsigned int targinfosize,
397 unsigned int hook_mask);
398
399 /* Called when entry of this type deleted. */
400 void (*destroy)(void *targinfo, unsigned int targinfosize);
401
402 /* Set this to THIS_MODULE if you are a module, otherwise NULL */
403 struct module *me;
404 };
405
注册函数
406 extern int ipt_register_target(struct ipt_target *target);
407 extern void ipt_unregister_target(struct ipt_target *target);
408
409 extern int ipt_register_match(struct ipt_match *match);
410 extern void ipt_unregister_match(struct ipt_match *match);
411
table结构
412 /* Furniture shopping... */
413 struct ipt_table
414 {
415 struct list_head list;
416
417 /* A unique name... */
418 char name[IPT_TABLE_MAXNAMELEN];
419
420 /* Seed table: copied in register_table */
421 struct ipt_replace *table;
422
423 /* What hooks you will enter on */
424 unsigned int valid_hooks;
425
426 /* Lock for the curtain */
427 rwlock_t lock;
428
429 /* Man behind the curtain... */
430 struct ipt_table_info *private;
431 };
432
小结iptables的防火墙组织结构:
现在我们可以给出iptables防火墙规则的组织结构了。第一级是table级,每一个防火墙可以有多个table;第二级是hook级,每一个table都有一个hook集合,每个hook都有一个防火墙规则链;第三级基本规则级,基本规则级的规则包括三部分,IP规则信息、匹配规则信息和target。而这三个组成部分的每一个都可以包括同类型的多个部分规则。
433 extern int ipt_register_table(struct ipt_table *table);
434 extern void ipt_unregister_table(struct ipt_table *table);
435 extern unsigned int ipt_do_table(struct sk_buff **pskb,
436 unsigned int hook,
437 const struct net_device *in,
438 const struct net_device *out,
439 struct ipt_table *table,
440 void *userdata);
441
有关table结构对齐的宏。
442 #define IPT_ALIGN(s) (((s) + (__alignof__(struct ipt_entry)-1)) ~(__alignof__(struct ipt_entry)-1))
443 #endif /*__KERNEL__*/
444 #endif /* _IPTABLES_H */
445
西安交通大学 王灏
[目录]
--------------------------------------------------------------------------------
防火墙技术分析讲义
防火墙技术分析讲义
yawl@docshow.net
一 基本概念
1.1 防火墙分类:
包过滤
代理(应用层网关)
1.2 代理:
两个连接(browser与proxy之间,proxy与web server之间)。
工作在应用层。
直接发往服务器的包:
GET / HTTP/1.1
Accept: */*
Accept-Language: zh-cn
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)
Host: www.lisoleg.net
Connection: Keep-Alive
往代理发出的包:
GET http://www.lisoleg.net/ HTTP/1.1
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/vnd.ms-powerpoint, application/msword, */*
Accept-Language: zh-cn
Accept-Encoding: gzip, deflate
If-Modified-Since: Thu, 14 Dec 2000 07:24:52 GMT
If-None-Match: "8026-185-3a3875c4"
User-Agent: Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)
Host: www.lisoleg.net
Proxy-Connection: Keep-Alive
增强:
cache
1.3 包过滤
单IP包检测
缺陷:无状态
1.4 增强1-状态检测(Stateful Inspection),又称动态包过滤(dynamic packet filtering)
1.4.1 规则表和动态状态表
1.4.2 ftp的例子:
A 4847->;B 21 PORT 192,168,7,60,18,241
B 21->;A 4847 PORT command successful.
B 20->;A 4849 syn
>; A classic example is transferring files using FTP. The firewall remembers the details of the
>; incoming request to get a file from an FTP server. The firewall then tracks the back-channel
>; request (the FTP Port command) by the server for transferring information back to the client.
>; As long as the information agrees (same IP addresses, no changes in port numbers, and no
>; non-FTP requests), the firewall allows the traffic. After the transfer is complete, the
>; firewall closes the ports involved.
1.4.3 两种实现方法:
1.4.3.1 checkpoint FW1,netfilter
1.4.3.2 动态添加规则(ipchains patch)
>; I believe it does exactly what I want: Installing a temporary
>; "backward"-rule to let packets in as a response to an
>; outgoing request.
1.5 增强2-地址转换:
1.5.1 静态NAT
1.5.2 动态NAT
1.5.3 地址伪装
1.6 增强3-VPN:
位置的优越性
二 Linux下防火墙的实现之一(2.2内核):
2.1 截获位置:
网络层
----------------------------------------------------------------
| ACCEPT/ lo interface |
v REDIRECT _______ |
-->; C -->; S -->; ______ -->; D -->; ~~~~~~~~ -->;|forward|---->; _______ -->;
h a |input | e {Routing } |Chain | |output |ACCEPT
e n |Chain | m {Decision} |_______| --->;|Chain |
c i |______| a ~~~~~~~~ | | ->;|_______|
k t | s | | | | |
s y | q | v | | |
u | v e v DENY/ | | v
m | DENY/ r Local Process REJECT | | DENY/
| v REJECT a | | | REJECT
| DENY d --------------------- |
v e -----------------------------
DENY
2.2 提炼出的代码:
输入检测:
/*
* Main IP Receive routine.
*/
int ip_rcv(struct sk_buff *skb, struct device *dev, struct packet_type *pt)
{
#ifdef CONFIG_FIREWALL
int fwres;
u16 rport;
#endif /* CONFIG_FIREWALL */
......
#ifdef CONFIG_FIREWALL
/*
* See if the firewall wants to dispose of the packet.
*
* We can't do ICMP reply or local delivery before routing,
* so we delay those decisions until after route. --RR
*/
fwres = call_in_firewall(PF_INET, dev, iph, &rport, &skb);
if (fwres < FW_ACCEPT && fwres != FW_REJECT)
goto drop;
iph = skb->;nh.iph;
#endif /* CONFIG_FIREWALL */
......
#ifdef CONFIG_FIREWALL
if (fwres == FW_REJECT) {
icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PORT_UNREACH, 0);
goto drop;
}
#endif /* CONFIG_FIREWALL */
return skb->;dst->;input(skb); //根据路由查找的结果决定是转发(ip_forward)还是发往上层(ip_local_deliver)
drop:
kfree_skb(skb); //如果规则匹配的结果是FW_REJECT,FW_BLOCK,丢弃此包
return(0);
}
转发检测:
int ip_forward(struct sk_buff *skb)
{
...
#ifdef CONFIG_FIREWALL
fw_res=call_fw_firewall(PF_INET, dev2, iph, NULL, &skb);
switch (fw_res) {
case FW_ACCEPT:
case FW_MASQUERADE:
break;
case FW_REJECT:
icmp_send(skb, ICMP_DEST_UNREACH, ICMP_HOST_UNREACH, 0);
/* fall thru */
default:
kfree_skb(skb);
return -1;
}
#endif
...
}
输出检测:(不同的上层协议走不同的流程,因此检测点较多)
UDP/RAW/ICMP报文:ip_build_xmit
TCP报文:ip_queue_xmit
转发的包:ip_forward
其他:ip_build_and_send_pkt
实际的匹配:
/*
* Returns one of the generic firewall policies, like FW_ACCEPT.
*
* The testing is either false for normal firewall mode or true for
* user checking mode (counters are not updated, TOS & mark not done).
*/
static int
ip_fw_check(struct iphdr *ip, //IP头位置
const char *rif, //出口网卡的名字
__u16 *redirport, //端口转发时用到
struct ip_chain *chain, //规则链的名字
struct sk_buff *skb, //要检测的数据包
unsigned int slot,
int testing) //见函数本身的注释
调用举例:
call_in_firewall实际调用ipfw_input_check,而ipfw_input_check中有:
int ipfw_input_check(struct firewall_ops *this, int pf, struct device *dev,
void *phdr, void *arg, struct sk_buff **pskb)
{
return ip_fw_check(phdr, dev->;name,
arg, IP_FW_INPUT_CHAIN, *pskb, SLOT_NUMBER(), 0);
}
实际流程:
系统调用
在系统中真正被所有进程都使用的内核通信方式是系统调用。例如当进程请求内核服务时,就使用的是系统调用。一般情况下,进程是不能够存取系统内核的。它不能存取内核使用的内存段,也不能调用内核函数,CPU的硬件结构保证了这一点。只有系统调用是一个例外。进程使用寄存器中适当的值跳转到内核中事先定义好的代码中执行,(当然,这些代码是只读的)。在Intel结构的计算机中,这是由中断0x80实现的。
进程可以跳转到的内核中的位置叫做system_call。在此位置的过程检查系统调用号,它将告诉内核进程请求的服务是什么。然后,它再查找系统调用表sys_call_table,找到希望调用的内核函数的地址,并调用此函数,最后返回。
所以,如果希望改变一个系统调用的函数,需要做的是编写一个自己的函数,然后改变sys_call_table中的指针指向该函数,最后再使用cleanup_module将系统调用表恢复到原来的状态
[目录]
--------------------------------------------------------------------------------
系统调用简述
linux里面的每个系统调用是靠一些宏,,一张系统调用表,一个系统调用入口来完成的。
[目录]
--------------------------------------------------------------------------------
宏
宏就是_syscallN(type,name,x...),N是系统调用所需的参数数目,type是返回类型,name即面向用户的系统调用函数名,x...是调用参数,个数即为N。
例如:
#define _syscall3(type,name,type1,arg1,type2,arg2,type3,arg3) \
type name(type1 arg1,type2 arg2,type3 arg3) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2)), \
"d" ((long)(arg3))); \
if (__res>;=0) \
return (type) __res; \
errno=-__res; \
return -1; \
}
(这是2.0.33版本)
这些宏定义于include\asm\Unistd.h,这就是为什么你在程序中要包含这个头文件的原因。该文件中还以__NR_name的形式定义了164个常数,这些常数就是系统调用函数name的函数指针在系统调用表中的偏移量。
[目录]
--------------------------------------------------------------------------------
系统调用表
系统调用表定义于entry.s的最后。
这个表按系统调用号(即前面提到的__NR_name)排列了所有系统调用函数的指针,以供系统调用入口函数查找。从这张表看得出,linux给它所支持的系统调用函数取名叫sys_name。
[目录]
--------------------------------------------------------------------------------
系统调用入口函数
系统调用入口函数定义于entry.s:
ENTRY(system_call)
pushl %eax # save orig_eax
SAVE_ALL
#ifdef __SMP__
ENTER_KERNEL
#endif
movl $-ENOSYS,EAX(%esp)
cmpl $(NR_syscalls),%eax
jae ret_from_sys_call
movl SYMBOL_NAME(sys_call_table)(,%eax,4),%eax
testl %eax,%eax
je ret_from_sys_call
#ifdef __SMP__
GET_PROCESSOR_OFFSET(%edx)
movl SYMBOL_NAME(current_set)(,%edx),%ebx
#else
movl SYMBOL_NAME(current_set),%ebx
#endif
andl $~CF_MASK,EFLAGS(%esp)
movl %db6,%edx
movl %edx,dbgreg6(%ebx)
testb $0x20,flags(%ebx)
jne 1f
call *%eax
movl %eax,EAX(%esp)
jmp ret_from_sys_call
这段代码现保存所有的寄存器值,然后检查调用号(__NR_name)是否合法(在系统调用表中查找),找到正确的函数指针后,就调用该函数(即你真正希望内核帮你运行的函数)。运行返回后,将调用ret_from_sys_call,这里就是著名的进程调度时机之一。
当在程序代码中用到系统调用时,编译器会将上面提到的宏展开,展开后的代码实际上是将系统调用号放入ax后移用int 0x80使处理器转向系统调用入口,然后查找系统调用表,进而由内核调用真正的功能函数。
自己添加过系统调用的人可能知道,要在程序中使用自己的系统调用,必须显示地应用宏_syscallN。
而对于linux预定义的系统调用,编译器在预处理时自动加入宏_syscall3(int,ioctl,arg1,arg2,arg3)并将其展开。所以,并不是ioctl本身是宏替换符,而是编译器自动用宏声明了ioctl这个函数。
[目录]
--------------------------------------------------------------------------------
系统调用实现过程
[目录]
--------------------------------------------------------------------------------
函数名约定
系统调用响应函数的函数名约定
函数名以“sys_”开头,后跟该系统调用的名字,由此构成164个形似sys_name()的函数名。因此,系统调用ptrace()的响应函数是sys_ptrace() (kernel/ptrace.c)。
[目录]
--------------------------------------------------------------------------------
系统调用号
系统调用号
文件include/asm/unistd.h为每个系统调用规定了唯一的编号:
#define __NR_setup 0
#define __NR_exit 1
#define __NR_fork 2
… …
#define __NR_ptrace 26
以系统调用号__NR_name作为下标,找出系统调用表sys_call_table (arch/i386/kernel/entry.S)中对应表项的内容,正好就是该系统调用的响应函数sys_name的入口地址。
[目录]
--------------------------------------------------------------------------------
系统调用表
系统调用表
系统调用表sys_call_table (arch/i386/kernel/entry.S)形如:
ENTRY(sys_call_table)
.long SYMBOL_NAME(sys_setup) /* 0 */
.long SYMBOL_NAME(sys_exit)
.long SYMBOL_NAME(sys_fork)
… …
.long SYMBOL_NAME(sys_stime) /* 25 */
.long SYMBOL_NAME(sys_ptrace)
… …
sys_call_table记录了各sys_name函数(共166项,其中2项无效)在表中的位子。有了这张表,很容易根据特定系统调用在表中的偏移量,找到对应的系统调用响应函数的入口地址。NR_syscalls(即256)表示最多可容纳的系统调用个数。这样,余下的90项就是可供用户自己添加的系统调用空间。
[目录]
--------------------------------------------------------------------------------
从ptrace系统调用命令到INT 0X80中断请求的转换
从ptrace系统调用命令到INT 0X80中断请求的转换
宏定义syscallN()(include/asm/unistd.h)用于系统调用的格式转换和参数的传递。
#define _syscall4(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4) \
type name (type1 arg1, type2 arg2, type3 arg3, type4 arg4) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2)), \
"d" ((long)(arg3)),"S" ((long)(arg4))); \
__syscall_return(type,__res); \
}
N取0与5之间任意整数。参数个数为N的系统调用由syscallN负责格式转换和参数传递。例如,ptrace()有四个参数,它对应的格式转换宏就是syscall4()。
syscallN()第一个参数说明响应函数返回值的类型,第二个参数为系统调用的名称(即name),其余的参数依次为系统调用参数的类型和名称。例如,
_syscall4(int, ptrace, long request, long pid, long addr, long data)
说明了系统调用命令
int sys_ptrace(long request, long pid, long addr, long data)
宏定义的余下部分描述了启动INT 0X80和接收、判断返回值的过程。也就是说,以系统调用号对EAX寄存器赋值,启动INT 0X80。规定返回值送EAX寄存器。函数的参数压栈,压栈顺序见下表:
参数 参数在堆栈的位置 传递参数的寄存器
arg1 00(%esp) ebx
arg2 04(%esp) ecx
arg3 08(%esp) edx
arg4 0c(%esp) esi
arg5 10(%esp) edi
若INT 0X80的返回值非负,则直接按类型type返回;否则,将INT 0X80的返回值取绝对值,保留在errno变量中,返回-1。
[目录]
--------------------------------------------------------------------------------
系统调用功能模块的初始化
系统调用功能模块的初始化
对系统调用的初始化也即对INT 0X80的初始化。系统启动时,汇编子程序setup_idt(arch/i386/kernel/head.S)准备了张256项的idt 表,由start_kernel()(init/main.c)、trap_init()(arch/i386/kernel/traps.c)调用的C语言宏定义set_system_gate(0x80, &system_call)(include/asm/system.h)设置0X80号软中断的服务程序为system_call。system_call(arch/i386/kernel/entry.S)就是所有系统调用的总入口。
[目录]
--------------------------------------------------------------------------------
内核服务
LINUX内部是如何分别为各种系统调用服务的
当进程需要进行系统调用时,必须以C语言函数的形式写一句系统调用命令。当进程执行到用户程序的系统调用命令时,实际上执行了由宏命令_syscallN()展开的函数。系统调用的参数由各通用寄存器传递。然后执行INT 0X80,以核心态进入入口地址system_call。
ENTRY(system_call)
pushl %eax # save orig_eax
SAVE_ALL
#ifdef __SMP__
ENTER_KERNEL
#endif
movl $-ENOSYS,EAX(%esp)
cmpl $(NR_syscalls),%eax
jae ret_from_sys_call
movl SYMBOL_NAME(sys_call_table)(,%eax,4),%eax
testl %eax,%eax
je ret_from_sys_call
#ifdef __SMP__
GET_PROCESSOR_OFFSET(%edx)
movl SYMBOL_NAME(current_set)(,%edx),%ebx
#else
movl SYMBOL_NAME(current_set),%ebx
#endif
andl $~CF_MASK,EFLAGS(%esp) # clear carry - assume no errors
movl %db6,%edx
movl %edx,dbgreg6(%ebx) # save current hardware debugging status
testb $0x20,flags(%ebx) # PF_TRACESYS
jne 1f
call *%eax
movl %eax,EAX(%esp) # save the return value
jmp ret_from_sys_call
从system_call入口的汇编程序的主要功能是:
·保存寄存器当前值(SAVE_ALL);
·检验是否为合法的系统调用;
·根据系统调用表_sys_call_table和EAX持有的系统调用号找出并转入系统调用响应函数;
·从该响应函数返回后,让EAX寄存器保存函数返回值,跳转至ret_from_sys_call(arch/i386/kernel/entry.S)。
·最后,在执行位于用户程序中系统调用命令后面余下的指令之前,若INT 0X80的返回值非负,则直接按类型type返回;否则,将INT 0X80的返回值取绝对值,保留在errno变量中,返回-1。
[目录]
--------------------------------------------------------------------------------
代码分析:mlock()
系统调用mlock的作用是屏蔽内存中某些用户进程所要求的页。
mlock调用的语法为:
int sys_mlock(unsigned long start, size_t len);
初始化为:
len=(len+(start &~PAGE_MASK)+ ~PAGE_MASK)&AGE_MASK;
start &=PAGE_MASK;
其中mlock又调用do_mlock(),语法为:
int do_mlock(unsigned long start, size_t len,int on);
初始化为:
len=(len+~PAGE_MASK)&AGE_MASK;
由mlock的参数可看出,mlock对由start所在页的起始地址开始,长度为len(注:len=(len+(start&~PAGE_MASK)+ ~PAGE_MASK)&AGE_MASK)的内存区域的页进行加锁。
sys_mlock如果调用成功返回,这其中所有的包含具体内存区域的页必须是常驻内存的,或者说在调用munlock 或 munlockall之前这部分被锁住的页面必须保留在内存。当然,如果调用mlock的进程终止或者调用exec执行其他程序,则这部分被锁住的页面被释放。通过fork()调用所创建的子进程不能够继承由父进程调用mlock锁住的页面。
内存屏蔽主要有两个方面的应用:实时算法和高度机密数据的处理。实时应用要求严格的分时,比如调度,调度页面是程序执行延时的一个主要因素。保密安全软件经常处理关键字节,比如密码或者密钥等数据结构。页面调度的结果是有可能将这些重要字节写到外存(如硬盘)中去。这样一些黑客就有可能在这些安全软件删除这些在内存中的数据后还能访问部分在硬盘中的数据。 而对内存进行加锁完全可以解决上述难题。
内存加锁不使用压栈技术,即那些通过调用mlock或者mlockall被锁住多次的页面可以通过调用一次munlock或者munlockall释放相应的页面
mlock的返回值分析:若调用mlock成功,则返回0;若不成功,则返回-1,并且errno被置位,进程的地址空间保持原来的状态。返回错误代码分析如下:
·ENOMEM:部分具体地址区域没有相应的进程地址空间与之对应或者超出了进程所允许的最大可锁页面。
·EPERM:调用mlock的进程没有正确的优先权。只有root进程才允许锁住要求的页面。
·EINVAL:输入参数len不是个合法的正数。
[目录]
--------------------------------------------------------------------------------
主要数据结构
1.mm_struct
struct mm_struct {
int count;
pgd_t * pgd; /* 进程页目录的起始地址*/
unsigned long context;
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack, start_mmap;
unsigned long arg_start, arg_end, env_start, env_end;
unsigned long rss, total_vm, locked_vm;
unsigned long def_flags;
struct vm_area_struct * mmap; /* 指向vma双向链表的指针 */
struct vm_area_struct * mmap_avl; /* 指向vma AVL树的指针 */
struct semaphore mmap_sem;
}
·start_code、end_code:进程代码段的起始地址和结束地址。
·start_data、end_data:进程数据段的起始地址和结束地址。
·arg_start、arg_end:调用参数区的起始地址和结束地址。
·env_start、env_end:进程环境区的起始地址和结束地址。
·rss:进程内容驻留在物理内存的页面总数。
2. 虚存段(vma)数据结构:vm_area_atruct
虚存段vma由数据结构vm_area_atruct(include/linux/mm.h)描述:
struct vm_area_struct {
struct mm_struct * vm_mm; /* VM area parameters */
unsigned long vm_start;
unsigned long vm_end;
pgprot_t vm_page_prot;
unsigned short vm_flags;
/* AVL tree of VM areas per task, sorted by address */
short vm_avl_height;
struct vm_area_struct * vm_avl_left;
struct vm_area_struct * vm_avl_right;
/* linked list of VM areas per task, sorted by address */
struct vm_area_struct * vm_next;
/* for areas with inode, the circular list inode->;i_mmap */
/* for shm areas, the circular list of attaches */
/* otherwise unused */
struct vm_area_struct * vm_next_share;
struct vm_area_struct * vm_prev_share;
/* more */
struct vm_operations_struct * vm_ops;
unsigned long vm_offset;
struct inode * vm_inode;
unsigned long vm_pte; /* shared mem */
};
vm_start;//所对应内存区域的开始地址
vm_end; //所对应内存区域的结束地址
vm_flags; //进程对所对应内存区域的访问权限
vm_avl_height;//avl树的高度
vm_avl_left; //avl树的左儿子
vm_avl_right; //avl树的右儿子
vm_next;// 进程所使用的按地址排序的vm_area链表指针
vm_ops;//一组对内存的操作
这些对内存的操作是当对虚存进行操作的时候Linux系统必须使用的一组方法。比如说,当进程准备访问某一虚存区域但是发现此区域在物理内存不存在时(缺页中断),就激发某种对内存的操作执行正确的行为。这种操作是空页(nopage)操作。当Linux系统按需调度可执行的页面映象进入内存时就使用这种空页(nopage)操作。
当一个可执行的页面映象映射到进程的虚存地址时,一组vm_area_struct结构的数据结构(vma)就会生成。每一个vm_area_struct的数据结构(vma)代表可执行的页面映象的一部分:可执行代码,初始化数据(变量),非初始化数据等等。Linux系统可以支持大量的标准虚存操作,当vm_area_struct数据结构(vma)一被创建,它就对应于一组正确的虚存操作。
属于同一进程的vma段通过vm_next指针连接,组成链表。如图2-3所示,struct mm_struct结构的成员struct vm_area_struct * mmap 表示进程的vma链表的表头。
为了提高对vma段 查询、插入、删除操作的速度,LINUX同时维护了一个AVL(Adelson-Velskii and Landis)树。在树中,所有的vm_area_struct虚存段均有左指针vm_avl_left指向相邻的低地址虚存段,右指针vm_avl_right指向相邻的高地址虚存段,如图2-5。struct mm_struct结构的成员struct vm_area_struct * mmap_avl表示进程的AVL树的根,vm_avl_height表示AVL树的高度。
对平衡树mmap_avl的任何操作必须满足平衡树的一些规则:
Consistency and balancing rulesJ(一致性和平衡规则):
tree->;vm_avl_height==1+max(heightof(tree->;vm_avl_left),heightof(
tree->;vm_avl_right))
abs( heightof(tree->;vm_avl_left) - heightof(tree->;vm_avl_right) ) <= 1
foreach node in tree->;vm_avl_left: node->;vm_avl_key <= tree->;vm_avl_key, foreach node in tree->;vm_avl_right: node->;vm_avl_key >;= tree->;vm_avl_key.
注:其中node->;vm_avl_key= node->;vm_end
对vma可以进行加锁、加保护、共享和动态扩展等操作。
[目录]
--------------------------------------------------------------------------------
重要常量
mlock系统调用所用到的重要常量有:PAGE_MASK、PAGE_SIZE、PAGE_SHIFT、RLIMIT_MEMLOCK、VM_LOCKED、 PF_SUPERPRIV等。它们的值分别如下:
PAGE_SHIFT 12 // PAGE_SHIFT determines the page size
PAGE_SIZE 0x1000 //1UL<<AGE_SHIFT
PAGE_MASK ~(PAGE_SIZE-1) //a very useful constant variable
RLIMIT_MEMLOCK 8 //max locked-in-memory address space
VM_LOCKED 0x2000 //8*1024=8192, vm_flags的标志之一。
PF_SUPERPRIV 0x00000100 //512
[目录]
--------------------------------------------------------------------------------
代码函数功能分析
mlock系统调用代码函数功能分析
下面对各个函数的功能作详细的分析((1)和(2)在前面简介mlock时已介绍过,并在后面有详细的程序流程):
suser():如果用户有效(即current->;euid == 0 ),则设置进程标志为root优先权(current->;flags |= PF_SUPERPRIV),并返回1;否则返回0。
find_vma(struct mm_struct * mm, unsigned long addr):输入参数为当前进程的mm、需要加锁的开始内存地址addr。find_vma的功能是在mm的mmap_avl树中寻找第一个满足mm->;mmap_avl->;vm_start<=addr< mm->;mmap_avl->;vm_end的vma,如果成功则返回此vma;否则返回空null。
mlock_fixup(struct vm_area_struct * vma, unsigned long start, unsigned long end, unsigned int newflags):输入参数为vm_mmap链中的某个vma、需要加锁内存区域起始地址和结束地址、需要修改的标志(0:加锁,1:释放锁)。
merge_segments(struct mm_struct * mm, unsigned long start_addr, unsigned long end_addr):输入参数为当前进程的mm、需要加锁的开始内存地址start_addr和结束地址end_addr。merge_segments的功能的是尽最大可能归并相邻(即内存地址偏移量连续)并有相同属性(包括vm_inode,vm_pte,vm_ops,vm_flags)的内存段,在这过程中冗余的vm_area_structs被释放,这就要求vm_mmap链按地址大小排序(我们不需要遍历整个表,而只需要遍历那些交叉或者相隔一定连续区域的邻接vm_area_structs)。当然在缺省的情况下,merge_segments是对vm_mmap_avl树进行循环处理,有多少可以合并的段就合并多少。
mlock_fixup_all(struct vm_area_struct * vma, int newflags):输入参数为vm_mmap链中的某个vma、需要修改的标志(0:加锁,1:释放锁)。mlock_fixup_all的功能是根据输入参数newflags修改此vma的vm_flags。
mlock_fixup_start(struct vm_area_struct * vma,unsigned long end, int newflags):输入参数为vm_mmap链中的某个vma、需要加锁内存区域结束地址、需要修改的标志(0:加锁,1:释放锁)。mlock_fixup_start的功能是根据输入参数end,在内存中分配一个新的new_vma,把原来的vma分成两个部分: new_vma和vma,其中new_vma的vm_flags被设置成输入参数newflags;并且按地址(new_vma->;start和new_vma->;end)大小序列把新生成的new->;vma插入到当前进程mm的mmap链或mmap_avl树中(缺省情况下是插入到mmap_avl树中)。
注:vma->;vm_offset+= vma->;vm_start-new_vma->;vm_start;
mlock_fixup_end(struct vm_area_struct * vma,unsigned long start, int newflags):输入参数为vm_mmap链中的某个vma、需要加锁内存区域起始地址、需要修改的标志(0:加锁,1:释放锁)。mlock_fixup_end的功能是根据输入参数start,在内存中分配一个新的new_vma,把原来的vma分成两个部分:vma和new_vma,其中new_vma的vm_flags被设置成输入参数newflags;并且按地址大小序列把new->;vma插入到当前进程mm的mmap链或mmap_avl树中。
注:new_vma->;vm_offset= vma->;vm_offset+(new_vma->;vm_start-vma->;vm_start);
mlock_fixup_middle(struct vm_area_struct * vma,unsigned long start, unsigned long end, int newflags):输入参数为vm_mmap链中的某个vma、需要加锁内存区域起始地址和结束地址、需要修改的标志(0:加锁,1:释放锁)。mlock_fixup_middle的功能是根据输入参数start、end,在内存中分配两个新vma,把原来的vma分成三个部分:left_vma、vma和right_vma,其中vma的vm_flags被设置成输入参数newflags;并且按地址大小序列把left->;vma和right->;vma插入到当前进程mm的mmap链或mmap_avl树中。
注:vma->;vm_offset += vma->;vm_start-left_vma->;vm_start;
right_vma->;vm_offset += right_vma->;vm_start-left_vma->;vm_start;
kmalloc():常用的一个内核函数
insert_vm_struct(struct mm_struct *mm, struct vm_area_struct *vmp):输入参数为当前进程的mm、需要插入的vmp。insert_vm_struct的功能是按地址大小序列把vmp插入到当前进程mm的mmap链或mmap_avl树中,并且把vmp插入到vmp->;inode的i_mmap环(循环共享链)中。
avl_insert_neighbours(struct vm_area_struct * new_node,** ptree,** to_the_left,** to_the_right):输入参数为当前需要插入的新vma结点new_node、目标mmap_avl树ptree、新结点插入ptree后它左边的结点以及它右边的结点(左右边结点按mmap_avl中各vma->;vma_end大小排序)。avl_insert_neighbours的功能是插入新vma结点new_node到目标mmap_avl树ptree中,并且调用avl_rebalance以保持ptree的平衡树特性,最后返回new_node左边的结点以及它右边的结点。
avl_rebalance(struct vm_area_struct *** nodeplaces_ptr, int count):输入参数为指向vm_area_struct指针结构的指针数据nodeplaces_ptr[](每个元素表示需要平衡的mmap_avl子树)、数据元素个数count。avl_rebalance的功能是从nodeplaces_ptr[--count]开始直到nodeplaces_ptr[0]循环平衡各个mmap_avl子树,最终使整个mmap_avl树平衡。
down(struct semaphore * sem):输入参数为同步(进入临界区)信号量sem。down的功能根据当前信号量的设置情况加锁(阻止别的进程进入临界区)并继续执行或进入等待状态(等待别的进程执行完成退出临界区并释放锁)。
down定义在/include/linux/sched.h中:
extern inline void down(struct semaphore * sem)
{
if (sem->;count <= 0)
__down(sem);
sem->;count--;
}
up(struct semaphore * sem)输入参数为同步(进入临界区)信号量sem。up的功能根据当前信号量的设置情况(当信号量的值为负数:表示有某个进程在等待使用此临界区 )释放锁。
up定义在/include/linux/sched.h中:
extern inline void up(struct semaphore * sem)
{
sem->;count++;
wake_up(&sem->;wait);
}
kfree_s(a,b):kfree_s定义在/include/linux/malloc.h中:#define kfree_s(a,b) kfree(a)。而kfree()将在后面3.3中详细讨论。
avl_neighbours(struct vm_area_struct * node,* tree,** to_the_left,** to_the_right):输入参数为作为查找条件的vma结点node、目标mmap_avl树tree、node左边的结点以及它右边的结点(左右边结点按mmap_avl中各vma->;vma_end大小排序)。avl_ neighbours的功能是根据查找条件node在目标mmap_avl树ptree中找到node左边的结点以及它右边的结点,并返回。
avl_remove(struct vm_area_struct * node_to_delete, ** ptree):输入参数为需要删除的结点node_to_delete和目标mmap_avl树ptree。avl_remove的功能是在目标mmap_avl树ptree中找到结点node_to_delete并把它从平衡树中删除,并且调用avl_rebalance以保持ptree的平衡树特性。
remove_shared_vm_struct(struct vm_area_struct *mpnt):输入参数为需要从inode->;immap环中删除的vma结点mpnt。remove_shared_vm_struct的功能是从拥有vma结点mpnt 的inode->;immap环中删除的该结点。
[目录]
--------------------------------------------------------------------------------
添加新调用
[目录]
--------------------------------------------------------------------------------
例子一
深入LINUX内核:为你的LINUX增加一条系统调用
充分利用LINUX开放源码的特性,我们可以轻易地对它进行修改,使我们能够随心所欲驾驭LINUX,完成一个真正属于自己的操作系统,这种感觉使无与伦比的,下面通过为LINUX增加一个系统调用来展示LINUX作为一个开放源码操作系统的强大魅力。
首先,让我们简单地分析一下LINUX中与系统调用的相关的部分:
LINUX的系统调用的总控程序是system_call,它是LINUX系统中所有系统调用的总入口,这个system_call是作为一个中断服务程序挂在中断0x80上,系统初始化时通过void init trap_init(void)调用一个宏set_system_ gate(SYSCALL_VERCTOR,&system_call)来对IDT表进行初始化,在0x80对应的中断描述符处填入system_call函数的地址,其中宏SYSCALL_VERCTOR就是0x80。
当发生一条系统调用时,由中断总控程序保存处理机状态,检查调用参数的合法性,然后根据系统调用向量在sys_call_table中找到相应的系统服务例程的地址,然后执行该服务例程,完成后恢复中断总控程序所保存的处理机状态,返回用户程序。
系统服务例程一般定义于kernel/sys.c中,系统调用向量定义在include/asm-386/unistd.h中,而sys_call _table表则定义在arch/i386/kernel/entry.S文件里。
现在我们知道增加一条系统调用我们首先要添加服务例程实现代码,然后在进行对应向量的申明,最后当然还要在sys_call_table表中增加一项以指明服务例程的入口地址。
OK,有了以上简单的分析,现在我们可以开始进行源码的修改,假设我们需要添加一条系统调用计算两个整数的平方和,系统调用名为add2,我们需要修改三个文件:kernel/sys.c , arch/i386/kernel/entry.S 和 include/asm-386/unistd.h。
1、修改kernel/sys.c ,增加服务例程代码:
asmlinkage int sys_add2(int a , int b)
{
int c=0;
c=a*a+b*b;
return c;
}
2、修改include/asm-386/unistd.h ,对我们刚才增加的系统调用申明向量,以使用户或系统进程能够找到这条系统调用,修改后文件如下所示:
.... .....
#define _NR_sendfile 187
#define _NR_getpmsg 188
#define _NR_putmsg 189
#define _NR_vfork 190
#define _NR_add2 191 /* 这是我们添加的部分,191即向量 */
3、修改include/asm-386/unistd.h , 将服务函数入口地址加入 sys_call_table,首先找到这么一段:
.... .....
.long SYMBOL_NAME(sys_sendfile)
.long SYMBOL_NAME(sys_ni_syscall) /* streams 1 */
.long SYMBOL_NAME(sys_ni_syscall) /* streams 2 */
.long SYMBOL_NAME(sys_vfork) /*190 */
.rept NR_syscalls-190
修改为如下:
.... .....
.long SYMBOL_NAME(sys_sendfile)
.long SYMBOL_NAME(sys_ni_syscall) /* streams 1 */
.long SYMBOL_NAME(sys_ni_syscall) /* streams 2 */
.long SYMBOL_NAME(sys_vfork) /*190 */
.long SYMBOL_NAME(sys_add2) <=我们的系统调用
.rept NR_syscalls-191 <=将190改为191
OK,大功告成,现在只需要重新编译你的LINUX内核,然后你的LINUX就有了一条新的系统调用int add2(int a, int b)。
[目录]
--------------------------------------------------------------------------------
例子二
如何在Linux中添加新的系统调用
系统调用是应用程序和操作系统内核之间的功能接口。其主要目的是使得用户可以使用操作系统提供的有关设备管理、输入/输入系统、文件系统和进程控制、通信以及存储管理等方面的功能,而不必了解系统程序的内部结构和有关硬件细节,从而起到减轻用户负担和保护系统以及提高资源利用率的作用。
1 Linux系统调用机制
在Linux系统中,系统调用是作为一种异常类型实现的。它将执行相应的机器代码指令来产生异常信号。产生中断或异常的重要效果是系统自动将用户态切换为核心态来对它进行处理。这就是说,执行系统调用异常指令时,自动地将系统切换为核心态,并安排异常处理程序的执行。
Linux用来实现系统调用异常的实际指令是:
Int $0x80
这一指令使用中断/异常向量号128(即16进制的80)将控制权转移给内核。为达到在使用系统调用时不必用机器指令编程,在标准的C语言库中为每一系统调用提供了一段短的子程序,完成机器代码的编程工作。事实上,机器代码段非常简短。它所要做的工作只是将送给系统调用的参数加载到CPU寄存器中,接着执行int $0x80指令。然后运行系统调用,系统调用的返回值将送入CPU的一个寄存器中,标准的库子程序取得这一返回值,并将它送回用户程序。
为使系统调用的执行成为一项简单的任务,Linux提供了一组预处理宏指令。它们可以用在程序中。这些宏指令取一定的参数,然后扩展为调用指定的系统调用的函数。
这些宏指令具有类似下面的名称格式:
_syscallN(parameters)
其中N是系统调用所需的参数数目,而parameters则用一组参数代替。这些参数使宏指令完成适合于特定的系统调用的扩展。例如,为了建立调用setuid()系统调用的函数,应该使用:
_syscall1( int, setuid, uid_t, uid )
syscallN( )宏指令的第1个参数int说明产生的函数的返回值的类型是整型,第2个参数setuid说明产生的函数的名称。后面是系统调用所需要的每个参数。这一宏指令后面还有两个参数uid_t和uid分别用来指定参数的类型和名称。
另外,用作系统调用的参数的数据类型有一个限制,它们的容量不能超过四个字节。这是因为执行int $0x80指令进行系统调用时,所有的参数值都存在32位的CPU寄存器中。使用CPU寄存器传递参数带来的另一个限制是可以传送给系统调用的参数的数目。这个限制是最多可以传递5个参数。所以Linux一共定义了6个不同的_syscallN()宏指令,从_syscall0()、_syscall1()直到_syscall5()。
一旦_syscallN()宏指令用特定系统调用的相应参数进行了扩展,得到的结果是一个与系统调用同名的函数,它可以在用户程序中执行这一系统调用。
2 添加新的系统调用
如果用户在Linux中添加新的系统调用,应该遵循几个步骤才能添加成功,下面几个步骤详细说明了添加系统调用的相关内容。
(1) 添加源代码
第一个任务是编写加到内核中的源程序,即将要加到一个内核文件中去的一个函数,该函数的名称应该是新的系统调用名称前面加上sys_标志。假设新加的系统调用为mycall(int number),在/usr/src/linux/kernel/sys.c文件中添加源代码,如下所示:
asmlinkage int sys_mycall(int number)
{
return number;
}
作为一个最简单的例子,我们新加的系统调用仅仅返回一个整型值。
(2) 连接新的系统调用
添加新的系统调用后,下一个任务是使Linux内核的其余部分知道该程序的存在。为了从已有的内核程序中增加到新的函数的连接,需要编辑两个文件。
在我们所用的Linux内核版本(RedHat 6.0,内核为2.2.5-15)中,第一个要修改的文件是:
/usr/src/linux/include/asm-i386/unistd.h
该文件中包含了系统调用清单,用来给每个系统调用分配一个唯一的号码。文件中每一行的格式如下:
#define __NR_name NNN
其中,name用系统调用名称代替,而NNN则是该系统调用对应的号码。应该将新的系统调用名称加到清单的最后,并给它分配号码序列中下一个可用的系统调用号。我们的系统调用如下:
#define __NR_mycall 191
系统调用号为191,之所以系统调用号是191,是因为Linux-2.2内核自身的系统调用号码已经用到190。
第二个要修改的文件是:
/usr/src/linux/arch/i386/kernel/entry.S
该文件中有类似如下的清单:
.long SYMBOL_NAME()
该清单用来对sys_call_table[]数组进行初始化。该数组包含指向内核中每个系统调用的指针。这样就在数组中增加了新的内核函数的指针。我们在清单最后添加一行:
.long SYMBOL_NAME(sys_mycall)
(3) 重建新的Linux内核
为使新的系统调用生效,需要重建Linux的内核。这需要以超级用户身份登录。
#pwd
/usr/src/linux
#
超级用户在当前工作目录(/usr/src/linux)下,才可以重建内核。
#make config
#make dep
#make clearn
#make bzImage
编译完毕后,系统生成一可用于安装的、压缩的内核映象文件:
/usr/src/linux/arch/i386/boot/bzImage
(4) 用新的内核启动系统
要使用新的系统调用,需要用重建的新内核重新引导系统。为此,需要修改/etc/lilo.conf文件,在我们的系统中,该文件内容如下:
boot=/dev/hda
map=/boot/map
install=/boot/boot.b
prompt
timeout=50
image=/boot/vmlinuz-2.2.5-15
label=linux
root=/dev/hdb1
read-only
other=/dev/hda1
label=dos
table=/dev/had
首先编辑该文件,添加新的引导内核:
image=/boot/bzImage-new
label=linux-new
root=/dev/hdb1
read-only
添加完毕,该文件内容如下所示:
boot=/dev/hda
map=/boot/map
install=/boot/boot.b
prompt
timeout=50
image=/boot/bzImage-new
label=linux-new
root=/dev/hdb1
read-only
image=/boot/vmlinuz-2.2.5-15
label=linux
root=/dev/hdb1
read-only
other=/dev/hda1
label=dos
table=/dev/hda
这样,新的内核映象bzImage-new成为缺省的引导内核。
为了使用新的lilo.conf配置文件,还应执行下面的命令:
#cp /usr/src/linux/arch/i386/boot/zImage /boot/bzImage-new
其次配置lilo:
# /sbin/lilo
现在,当重新引导系统时,在boot:提示符后面有三种选择:linux-new 、 linux、dos,新内核成为缺省的引导内核。
至此,新的Linux内核已经建立,新添加的系统调用已成为操作系统的一部分,重新启动Linux,用户就可以在应用程序中使用该系统调用了。
(5)使用新的系统调用
在应用程序中使用新添加的系统调用mycall。同样为实验目的,我们写了一个简单的例子xtdy.c。
/* xtdy.c */
#include <linux/unistd.h>;
_syscall1(int,mycall,int,ret)
main()
{
printf("%d \n",mycall(100));
}
编译该程序:
# cc -o xtdy xtdy.c
执行:
# xtdy
结果:
# 100
注意,由于使用了系统调用,编译和执行程序时,用户都应该是超级用户身份。
驱动
Linux系统支持三种类型的硬件设备:字符设备、块设备和网络设备。字符设备是直接读取的,不必使用缓冲区。例如,系统的串行口/dev/cua0和/dev/cua1。块设备每次只能读取一定大小的块的倍数,通常一块是512或者1024字节。块设备通过缓冲区读写,并且可以随机地读写。块设备可以通过它们的设备文件存取,但通常是通过文件系统存取。只有块设备支持挂接的文件系统。网络设备是通过BSD套接字界面存取的。
Linux系统支持多种设备,这些设备的驱动程序之间有一些共同的特点:
* 内核代码:设备驱动程序是系统内核的一部分,所以如果驱动程序出现错误的话,将可能严重地破坏整个系统。
* 内核接口:设备驱动程序必须为系统内核或者它们的子系统提供一个标准的接口。例如,一个终端驱动程序必须为Linux内核提供一个文件I/O接口;一个SCSI设备驱动程序应该为SCSI子系统提供一个SCSI设备接口,同时SCSI子系统也应为系统内核提供文件I/O和缓冲区。
* 内核机制和服务:设备驱动程序利用一些标准的内核服务,例如内存分配等。
* 可装入:大多数的Linux设备驱动程序都可以在需要时装入内核,在不需要时卸载。
* 可设置:Linux系统设备驱动程序可以集成为系统内核的一部分,至于哪一部分需要集成到内核中,可以在系统编译时设置。
[目录]
--------------------------------------------------------------------------------
I/O端口
关键词:设备管理、驱动程序、I/O端口、资源
申明:这份文档是按照自由软件开放源代码的精神发布的,任何人可以免费获得、使用和重新发布,但是你没有限制别人重新发布你发布内容的权利。发布本文的目的是希望它能对读者有用,但没有任何担保,甚至没有适合特定目的的隐含的担保。更详细的情况请参阅GNU通用公共许可证(GPL),以及GNU自由文档协议(GFDL)。
几乎每一种外设都是通过读写设备上的寄存器来进行的。外设寄存器也称为“I/O端口”,通常包括:控制寄存器、状态寄存器和数据寄存器三大类,而且一个外设的寄存器通常被连续地编址。CPU对外设IO端口物理地址的编址方式有两种:一种是I/O映射方式(I/O-mapped),另一种是内存映射方式(Memory-mapped)。而具体采用哪一种则取决于CPU的体系结构。
有些体系结构的CPU(如,PowerPC、m68k等)通常只实现一个物理地址空间(RAM)。在这种情况下,外设I/O端口的物理地址就被映射到CPU的单一物理地址空间中,而成为内存的一部分。此时,CPU可以象访问一个内存单元那样访问外设I/O端口,而不需要设立专门的外设I/O指令。这就是所谓的“内存映射方式”(Memory-mapped)。
而另外一些体系结构的CPU(典型地如X86)则为外设专门实现了一个单独地地址空间,称为“I/O地址空间”或者“I/O端口空间”。这是一个与CPU地RAM物理地址空间不同的地址空间,所有外设的I/O端口均在这一空间中进行编址。CPU通过设立专门的I/O指令(如X86的IN和OUT指令)来访问这一空间中的地址单元(也即I/O端口)。这就是所谓的“I/O映射方式”(I/O-mapped)。与RAM物理地址空间相比,I/O地址空间通常都比较小,如x86 CPU的I/O空间就只有64KB(0-0xffff)。这是“I/O映射方式”的一个主要缺点。
Linux将基于I/O映射方式的或内存映射方式的I/O端口通称为“I/O区域”(I/O region)。在讨论对I/O区域的管理之前,我们首先来分析一下Linux是如何实现“I/O资源”这一抽象概念的。
3.1 Linux对I/O资源的描述
Linux设计了一个通用的数据结构resource来描述各种I/O资源(如:I/O端口、外设内存、DMA和IRQ等)。该结构定义在include/linux/ioport.h头文件中:
struct resource {
const char *name;
unsigned long start, end;
unsigned long flags;
struct resource *parent, *sibling, *child;
};
各成员的含义如下:
1. name指针:指向此资源的名称。
2. start和end:表示资源的起始物理地址和终止物理地址。它们确定了资源的范围,也即是一个闭区间[start,end]。
3. flags:描述此资源属性的标志(见下面)。
4. 指针parent、sibling和child:分别为指向父亲、兄弟和子资源的指针。
属性flags是一个unsigned long类型的32位标志值,用以描述资源的属性。比如:资源的类型、是否只读、是否可缓存,以及是否已被占用等。下面是一部分常用属性标志位的定义(ioport.h):
/*
* IO resources have these defined flags.
*/
#define IORESOURCE_BITS 0x000000ff /* Bus-specific bits */
#define IORESOURCE_IO 0x00000100 /* Resource type */
#define IORESOURCE_MEM 0x00000200
#define IORESOURCE_IRQ 0x00000400
#define IORESOURCE_DMA 0x00000800
#define IORESOURCE_PREFETCH 0x00001000 /* No side effects */
#define IORESOURCE_READONLY 0x00002000
#define IORESOURCE_CACHEABLE 0x00004000
#define IORESOURCE_RANGELENGTH 0x00008000
#define IORESOURCE_SHADOWABLE 0x00010000
#define IORESOURCE_BUS_HAS_VGA 0x00080000
#define IORESOURCE_UNSET 0x20000000
#define IORESOURCE_AUTO 0x40000000
#define IORESOURCE_BUSY 0x80000000
/* Driver has marked this resource busy */
指针parent、sibling和child的设置是为了以一种树的形式来管理各种I/O资源。
3.2 Linux对I/O资源的管理
Linux是以一种倒置的树形结构来管理每一类I/O资源(如:I/O端口、外设内存、DMA和IRQ)的。每一类I/O资源都对应有一颗倒置的资源树,树中的每一个节点都是一个resource结构,而树的根结点root则描述了该类资源的整个资源空间。
基于上述这个思想,Linux在kernel/Resource.c文件中实现了对资源的申请、释放及查找等操作。
3.2.1 I/O资源的申请
假设某类资源有如下这样一颗资源树:
节点root、r1、r2和r3实际上都是一个resource结构类型。子资源r1、r2和r3通过sibling指针链接成一条单向非循环链表,其表头由root节点中的child指针定义,因此也称为父资源的子资源链表。r1、r2和r3的parent指针均指向他们的父资源节点,在这里也就是图中的root节点。
假设想在root节点中分配一段I/O资源(由图中的阴影区域表示)。函数request_resource()实现这一功能。它有两个参数:①root指针,表示要在哪个资源根节点中进行分配;②new指针,指向描述所要分配的资源(即图中的阴影区域)的resource结构。该函数的源代码如下(kernel/resource.c):
int request_resource(struct resource *root, struct resource *new)
{
struct resource *conflict;
write_lock(&resource_lock);
conflict = __request_resource(root, new);
write_unlock(&resource_lock);
return conflict ? -EBUSY : 0;
}
对上述函数的NOTE如下:
①资源锁resource_lock对所有资源树进行读写保护,任何代码段在访问某一颗资源树之前都必须先持有该锁。其定义如下(kernel/Resource.c):
static rwlock_t resource_lock = RW_LOCK_UNLOCKED;
②可以看出,函数实际上是通过调用内部静态函数__request_resource()来完成实际的资源分配工作。如果该函数返回非空指针,则表示有资源冲突;否则,返回NULL就表示分配成功。
③最后,如果conflict指针为NULL,则request_resource()函数返回返回值0,表示成功;否则返回-EBUSY表示想要分配的资源已被占用。
函数__request_resource()完成实际的资源分配工作。如果参数new所描述的资源中的一部分或全部已经被其它节点所占用,则函数返回与new相冲突的resource结构的指针。否则就返回NULL。该函数的源代码如下
(kernel/Resource.c):
/* Return the conflict entry if you can't request it */
static struct resource * __request_resource
(struct resource *root, struct resource *new)
{
unsigned long start = new->;start;
unsigned long end = new->;end;
struct resource *tmp, **p;
if (end < start)
return root;
if (start < root->;start)
return root;
if (end >; root->;end)
return root;
p = &root->;child;
for (; {
tmp = *p;
if (!tmp || tmp->;start >; end) {
new->;sibling = tmp;
*p = new;
new->;parent = root;
return NULL;
}
p = &tmp->;sibling;
if (tmp->;end < start)
continue;
return tmp;
}
}
对函数的NOTE:
①前三个if语句判断new所描述的资源范围是否被包含在root内,以及是否是一段有效的资源(因为end必须大于start)。否则就返回root指针,表示与根结点相冲突。
②接下来用一个for循环遍历根节点root的child链表,以便检查是否有资源冲突,并将new插入到child链表中的合适位置(child链表是以I/O资源物理地址从低到高的顺序排列的)。为此,它用tmp指针指向当前正被扫描的resource结构,用指针p指向前一个resource结构的sibling指针成员变量,p的初始值为指向root->;sibling。For循环体的执行步骤如下:
l 让tmp指向当前正被扫描的resource结构(tmp=*p)。
l 判断tmp指针是否为空(tmp指针为空说明已经遍历完整个child链表),或者当前被扫描节点的起始位置start是否比new的结束位置end还要大。只要这两个条件之一成立的话,就说明没有资源冲突,于是就可以把new链入child链表中:①设置new的sibling指针指向当前正被扫描的节点tmp(new->;sibling=tmp);②当前节点tmp的前一个兄弟节点的sibling指针被修改为指向new这个节点(*p=new);③将new的parent指针设置为指向root。然后函数就可以返回了(返回值NULL表示没有资源冲突)。
l 如果上述两个条件都不成立,这说明当前被扫描节点的资源域有可能与new相冲突(实际上就是两个闭区间有交集),因此需要进一步判断。为此它首先修改指针p,让它指向tmp->;sibling,以便于继续扫描child链表。然后,判断tmp->;end是否小于new->;start,如果小于,则说明当前节点tmp和new没有资源冲突,因此执行continue语句,继续向下扫描child链表。否则,如果tmp->;end大于或等于new->;start,则说明tmp->;[start,end]和new->;[start,end]之间有交集。所以返回当前节点的指针tmp,表示发生资源冲突。
3.2.2 资源的释放
函数release_resource()用于实现I/O资源的释放。该函数只有一个参数——即指针old,它指向所要释放的资源。起源代码如下:
int release_resource(struct resource *old)
{
int retval;
write_lock(&resource_lock);
retval = __release_resource(old);
write_unlock(&resource_lock);
return retval;
}
可以看出,它实际上通过调用__release_resource()这个内部静态函数来完成实际的资源释放工作。函数__release_resource()的主要任务就是将资源区域old(如果已经存在的话)从其父资源的child链表重摘除,它的源代码如下:
static int __release_resource(struct resource *old)
{
struct resource *tmp, **p;
p = &old->;parent->;child;
for (; {
tmp = *p;
if (!tmp)
break;
if (tmp == old) {
*p = tmp->;sibling;
old->;parent = NULL;
return 0;
}
p = &tmp->;sibling;
}
return -EINVAL;
}
对上述函数代码的NOTE如下:
同函数__request_resource()相类似,该函数也是通过一个for循环来遍历父资源的child链表。为此,它让tmp指针指向当前被扫描的资源,而指针p则指向当前节点的前一个节点的sibling成员(p的初始值为指向父资源的child指针)。循环体的步骤如下:
①首先,让tmp指针指向当前被扫描的节点(tmp=*p)。
②如果tmp指针为空,说明已经遍历完整个child链表,因此执行break语句推出for循环。由于在遍历过程中没有在child链表中找到参数old所指定的资源节点,因此最后返回错误值-EINVAL,表示参数old是一个无效的值。
③接下来,判断当前被扫描节点是否就是参数old所指定的资源节点。如果是,那就将old从child链表中去除,也即让当前结点tmp的前一个兄弟节点的sibling指针指向tmp的下一个节点,然后将old->;parent指针设置为NULL。最后返回0值表示执行成功。
④如果当前被扫描节点不是资源old,那就继续扫描child链表中的下一个元素。因此将指针p指向tmp->;sibling成员。
3.2.3 检查资源是否已被占用,
函数check_resource()用于实现检查某一段I/O资源是否已被占用。其源代码如下:
int check_resource(struct resource *root, unsigned long start, unsigned long len)
{
struct resource *conflict, tmp;
tmp.start = start;
tmp.end = start + len - 1;
write_lock(&resource_lock);
conflict = __request_resource(root, &tmp);
if (!conflict)
__release_resource(&tmp);
write_unlock(&resource_lock);
return conflict ? -EBUSY : 0;
}
对该函数的NOTE如下:
①构造一个临时资源tmp,表示所要检查的资源[start,start+end-1]。
②调用__request_resource()函数在根节点root申请tmp所表示的资源。如果tmp所描述的资源还被人使用,则该函数返回NULL,否则返回非空指针。因此接下来在conflict为NULL的情况下,调用__release_resource()将刚刚申请的资源释放掉。
③最后根据conflict是否为NULL,返回-EBUSY或0值。
3.2.4 寻找可用资源
函数find_resource()用于在一颗资源树中寻找未被使用的、且满足给定条件的(也即资源长度大小为size,且在[min,max]区间内)的资源。其函数源代码如下:
/*
* Find empty slot in the resource tree given range and alignment.
*/
static int find_resource(struct resource *root, struct resource *new,
unsigned long size,
unsigned long min, unsigned long max,
unsigned long align,
void (*alignf)(void *, struct resource *, unsigned long),
void *alignf_data)
{
struct resource *this = root->;child;
new->;start = root->;start;
for(; {
if (this)
new->;end = this->;start;
else
new->;end = root->;end;
if (new->;start < min)
new->;start = min;
if (new->;end >; max)
new->;end = max;
new->;start = (new->;start + align - 1) & ~(align - 1);
if (alignf)
alignf(alignf_data, new, size);
if (new->;start < new->;end && new->;end - new->;start + 1 >;= size)
{
new->;end = new->;start + size - 1;
return 0;
}
if (!this)
break;
new->;start = this->;end + 1;
this = this->;sibling;
}
return -EBUSY;
}
对该函数的NOTE如下:
同样,该函数也要遍历root的child链表,以寻找未被使用的资源空洞。为此,它让this指针表示当前正被扫描的子资源节点,其初始值等于root->;child,即指向child链表中的第一个节点,并让new->;start的初始值等于root->;start,然后用一个for循环开始扫描child链表,对于每一个被扫描的节点,循环体执行如下操作:
①首先,判断this指针是否为NULL。如果不为空,就让new->;end等于this->;start,也即让资源new表示当前资源节点this前面那一段未使用的资源区间。
②如果this指针为空,那就让new->;end等于root->;end。这有两层意思:第一种情况就是根结点的child指针为NULL(即根节点没有任何子资源)。因此此时先暂时将new->;end放到最大。第二种情况就是已经遍历完整个child链表,所以此时就让new表示最后一个子资源后面那一段未使用的资源区间。
③根据参数min和max修正new->;[start,end]的值,以使资源new被包含在[min,max]区域内。
④接下来进行对齐操作。
⑤然后,判断经过上述这些步骤所形成的资源区域new是否是一段有效的资源(end必须大于或等于start),而且资源区域的长度满足size参数的要求(end-start+1>;=size)。如果这两个条件均满足,则说明我们已经找到了一段满足条件的资源空洞。因此在对new->;end的值进行修正后,然后就可以返回了(返回值0表示成功)。
⑥如果上述两条件不能同时满足,则说明还没有找到,因此要继续扫描链表。在继续扫描之前,我们还是要判断一下this指针是否为空。如果为空,说明已经扫描完整个child链表,因此就可以推出for循环了。否则就将new->;start的值修改为this->;end+1,并让this指向下一个兄弟资源节点,从而继续扫描链表中的下一个子资源节点。
3.2.5 分配接口allocate_resource()
在find_resource()函数的基础上,函数allocate_resource()实现:在一颗资源树中分配一条指定大小的、且包含在指定区域[min,max]中的、未使用资源区域。其源代码如下:
/*
* Allocate empty slot in the resource tree given range and alignment.
*/
int allocate_resource(struct resource *root, struct resource *new,
unsigned long size,
unsigned long min, unsigned long max,
unsigned long align,
void (*alignf)(void *, struct resource *, unsigned long),
void *alignf_data)
{
int err;
write_lock(&resource_lock);
err = find_resource(root, new, size, min, max, align, alignf, alignf_data);
if (err >;= 0 && __request_resource(root, new))
err = -EBUSY;
write_unlock(&resource_lock);
return err;
}
3.2.6 获取资源的名称列表
函数get_resource_list()用于获取根节点root的子资源名字列表。该函数主要用来支持/proc/文件系统(比如实现proc/ioports文件和/proc/iomem文件)。其源代码如下:
int get_resource_list(struct resource *root, char *buf, int size)
{
char *fmt;
int retval;
fmt = " %08lx-%08lx : %s
";
if (root->;end < 0x10000)
fmt = " %04lx-%04lx : %s
";
read_lock(&resource_lock);
retval = do_resource_list(root->;child, fmt, 8, buf, buf + size) - buf;
read_unlock(&resource_lock);
return retval;
}
可以看出,该函数主要通过调用内部静态函数do_resource_list()来实现其功能,其源代码如下:
/*
* This generates reports for /proc/ioports and /proc/iomem
*/
static char * do_resource_list(struct resource *entry, const char *fmt,
int offset, char *buf, char *end)
{
if (offset < 0)
offset = 0;
while (entry) {
const char *name = entry->;name;
unsigned long from, to;
if ((int) (end-buf) < 80)
return buf;
from = entry->;start;
to = entry->;end;
if (!name)
name = "";
buf += sprintf(buf, fmt + offset, from, to, name);
if (entry->;child)
buf = do_resource_list(entry->;child, fmt, offset-2, buf, end);
entry = entry->;sibling;
}
return buf;
}
函数do_resource_list()主要通过一个while{}循环以及递归嵌套调用来实现,较为简单,这里就不在详细解释了。
3.3 管理I/O Region资源
Linux将基于I/O映射方式的I/O端口和基于内存映射方式的I/O端口资源统称为“I/O区域”(I/O Region)。I/O Region仍然是一种I/O资源,因此它仍然可以用resource结构类型来描述。下面我们就来看看Linux是如何管理I/O Region的。
3.3.1 I/O Region的分配
在函数__request_resource()的基础上,Linux实现了用于分配I/O区域的函数__request_region(),如下:
struct resource * __request_region(struct resource *parent,
unsigned long start, unsigned long n, const char *name)
{
struct resource *res = kmalloc(sizeof(*res), GFP_KERNEL);
if (res) {
memset(res, 0, sizeof(*res));
res->;name = name;
res->;start = start;
res->;end = start + n - 1;
res->;flags = IORESOURCE_BUSY;
write_lock(&resource_lock);
for (; {
struct resource *conflict;
conflict = __request_resource(parent, res);
if (!conflict)
break;
if (conflict != parent) {
parent = conflict;
if (!(conflict->;flags & IORESOURCE_BUSY))
continue;
}
/* Uhhuh, that didn't work out.. */
kfree(res);
res = NULL;
break;
}
write_unlock(&resource_lock);
}
return res;
}
NOTE:
①首先,调用kmalloc()函数在SLAB分配器缓存中分配一个resource结构。
②然后,相应的根据参数值初始化所分配的resource结构。注意!flags成员被初始化为IORESOURCE_BUSY。
③接下来,用一个for循环开始进行资源分配,循环体的步骤如下:
l 首先,调用__request_resource()函数进行资源分配。如果返回NULL,说明分配成功,因此就执行break语句推出for循环,返回所分配的resource结构的指针,函数成功地结束。
l 如果__request_resource()函数分配不成功,则进一步判断所返回的冲突资源节点是否就是父资源节点parent。如果不是,则将分配行为下降一个层次,即试图在当前冲突的资源节点中进行分配(只有在冲突的资源节点没有设置IORESOURCE_BUSY的情况下才可以),于是让parent指针等于conflict,并在conflict->;flags&IORESOURCE_BUSY为0的情况下执行continue语句继续for循环。
l 否则如果相冲突的资源节点就是父节点parent,或者相冲突资源节点设置了IORESOURCE_BUSY标志位,则宣告分配失败。于是调用kfree()函数释放所分配的resource结构,并将res指针置为NULL,最后用break语句推出for循环。
④最后,返回所分配的resource结构的指针。
3.3.2 I/O Region的释放
函数__release_region()实现在一个父资源节点parent中释放给定范围的I/O Region。实际上该函数的实现思想与__release_resource()相类似。其源代码如下:
void __release_region(struct resource *parent,
unsigned long start, unsigned long n)
{
struct resource **p;
unsigned long end;
p = &parent->;child;
end = start + n - 1;
for (; {
struct resource *res = *p;
if (!res)
break;
if (res->;start <= start && res->;end >;= end) {
if (!(res->;flags & IORESOURCE_BUSY)) {
p = &res->;child;
continue;
}
if (res->;start != start' 'res->;end != end)
break;
*p = res->;sibling;
kfree(res);
return;
}
p = &res->;sibling;
}
printk("Trying to free nonexistent resource <%08lx-%08lx>;
", start, end);
}
类似地,该函数也是通过一个for循环来遍历父资源parent的child链表。为此,它让指针res指向当前正被扫描的子资源节点,指针p指向前一个子资源节点的sibling成员变量,p的初始值为指向parent->;child。For循环体的步骤如下:
①让res指针指向当前被扫描的子资源节点(res=*p)。
②如果res指针为NULL,说明已经扫描完整个child链表,所以退出for循环。
③如果res指针不为NULL,则继续看看所指定的I/O区域范围是否完全包含在当前资源节点中,也即看看[start,start+n-1]是否包含在res->;[start,end]中。如果不属于,则让p指向当前资源节点的sibling成员,然后继续for循环。如果属于,则执行下列步骤:
l 先看看当前资源节点是否设置了IORESOURCE_BUSY标志位。如果没有设置该标志位,则说明该资源节点下面可能还会有子节点,因此将扫描过程下降一个层次,于是修改p指针,使它指向res->;child,然后执行continue语句继续for循环。
l 如果设置了IORESOURCE_BUSY标志位。则一定要确保当前资源节点就是所指定的I/O区域,然后将当前资源节点从其父资源的child链表中去除。这可以通过让前一个兄弟资源节点的sibling指针指向当前资源节点的下一个兄弟资源节点来实现(即让*p=res->;sibling),最后调用kfree()函数释放当前资源节点的resource结构。然后函数就可以成功返回了。
3.3.3 检查指定的I/O Region是否已被占用
函数__check_region()检查指定的I/O Region是否已被占用。其源代码如下:
int __check_region(struct resource *parent, unsigned long start, unsigned long n)
{
struct resource * res;
res = __request_region(parent, start, n, "check-region";
if (!res)
return -EBUSY;
release_resource(res);
kfree(res);
return 0;
}
该函数的实现与__check_resource()的实现思想类似。首先,它通过调用__request_region()函数试图在父资源parent中分配指定的I/O Region。如果分配不成功,将返回NULL,因此此时函数返回错误值-EBUSY表示所指定的I/O Region已被占用。如果res指针不为空则说明所指定的I/O Region没有被占用。于是调用__release_resource()函数将刚刚分配的资源释放掉(实际上是将res结构从parent的child链表去除),然后调用kfree()函数释放res结构所占用的内存。最后,返回0值表示指定的I/O Region没有被占用。
3.4 管理I/O端口资源
我们都知道,采用I/O映射方式的X86处理器为外设实现了一个单独的地址空间,也即“I/O空间”(I/O Space)或称为“I/O端口空间”,其大小是64KB(0x0000-0xffff)。Linux在其所支持的所有平台上都实现了“I/O端口空间”这一概念。
由于I/O空间非常小,因此即使外设总线有一个单独的I/O端口空间,却也不是所有的外设都将其I/O端口(指寄存器)映射到“I/O端口空间”中。比如,大多数PCI卡都通过内存映射方式来将其I/O端口或外设内存映射到CPU的RAM物理地址空间中。而老式的ISA卡通常将其I/O端口映射到I/O端口空间中。
Linux是基于“I/O Region”这一概念来实现对I/O端口资源(I/O-mapped 或 Memory-mapped)的管理的。
3.4.1 资源根节点的定义
Linux在kernel/Resource.c文件中定义了全局变量ioport_resource和iomem_resource,来分别描述基于I/O映射方式的整个I/O端口空间和基于内存映射方式的I/O内存资源空间(包括I/O端口和外设内存)。其定义如下:
struct resource ioport_resource =
{ "CI IO", 0x0000, IO_SPACE_LIMIT, IORESOURCE_IO };
struct resource iomem_resource =
{ "CI mem", 0x00000000, 0xffffffff, IORESOURCE_MEM };
其中,宏IO_SPACE_LIMIT表示整个I/O空间的大小,对于X86平台而言,它是0xffff(定义在include/asm-i386/io.h头文件中)。显然,I/O内存空间的大小是4GB。
3.4.2 对I/O端口空间的操作
基于I/O Region的操作函数__XXX_region(),Linux在头文件include/linux/ioport.h中定义了三个对I/O端口空间进行操作的宏:①request_region()宏,请求在I/O端口空间中分配指定范围的I/O端口资源。②check_region()宏,检查I/O端口空间中的指定I/O端口资源是否已被占用。③release_region()宏,释放I/O端口空间中的指定I/O端口资源。这三个宏的定义如下:
#define request_region(start,n,name)
__request_region(&ioport_resource, (start), (n), (name))
#define check_region(start,n)
__check_region(&ioport_resource, (start), (n))
#define release_region(start,n)
__release_region(&ioport_resource, (start), (n))
其中,宏参数start指定I/O端口资源的起始物理地址(是I/O端口空间中的物理地址),宏参数n指定I/O端口资源的大小。
3.4.3 对I/O内存资源的操作
基于I/O Region的操作函数__XXX_region(),Linux在头文件include/linux/ioport.h中定义了三个对I/O内存资源进行操作的宏:①request_mem_region()宏,请求分配指定的I/O内存资源。②check_ mem_region()宏,检查指定的I/O内存资源是否已被占用。③release_ mem_region()宏,释放指定的I/O内存资源。这三个宏的定义如下:
#define request_mem_region(start,n,name)
__request_region(&iomem_resource, (start), (n), (name))
#define check_mem_region(start,n)
__check_region(&iomem_resource, (start), (n))
#define release_mem_region(start,n)
__release_region(&iomem_resource, (start), (n))
其中,参数start是I/O内存资源的起始物理地址(是CPU的RAM物理地址空间中的物理地址),参数n指定I/O内存资源的大小。
3.4.4 对/proc/ioports和/proc/iomem的支持
Linux在ioport.h头文件中定义了两个宏:
get_ioport_list()和get_iomem_list(),分别用来实现/proc/ioports文件和/proc/iomem文件。其定义如下:
#define get_ioport_list(buf) get_resource_list(&ioport_resource, buf, PAGE_SIZE)
#define get_mem_list(buf) get_resource_list(&iomem_resource, buf, PAGE_SIZE)
3.5 访问I/O端口空间
在驱动程序请求了I/O端口空间中的端口资源后,它就可以通过CPU的IO指定来读写这些I/O端口了。在读写I/O端口时要注意的一点就是,大多数平台都区分8位、16位和32位的端口,也即要注意I/O端口的宽度。
Linux在include/asm/io.h头文件(对于i386平台就是include/asm-i386/io.h)中定义了一系列读写不同宽度I/O端口的宏函数。如下所示:
⑴读写8位宽的I/O端口
unsigned char inb(unsigned port);
void outb(unsigned char value,unsigned port);
其中,port参数指定I/O端口空间中的端口地址。在大多数平台上(如x86)它都是unsigned short类型的,其它的一些平台上则是unsigned int类型的。显然,端口地址的类型是由I/O端口空间的大小来决定的。
⑵读写16位宽的I/O端口
unsigned short inw(unsigned port);
void outw(unsigned short value,unsigned port);
⑶读写32位宽的I/O端口
unsigned int inl(unsigned port);
void outl(unsigned int value,unsigned port);
3.5.1 对I/O端口的字符串操作
除了上述这些“单发”(single-shot)的I/O操作外,某些CPU也支持对某个I/O端口进行连续的读写操作,也即对单个I/O端口读或写一系列字节、字或32位整数,这就是所谓的“字符串I/O指令”(String Instruction)。这种指令在速度上显然要比用循环来实现同样的功能要快得多。
Linux同样在io.h文件中定义了字符串I/O读写函数:
⑴8位宽的字符串I/O操作
void insb(unsigned port,void * addr,unsigned long count);
void outsb(unsigned port ,void * addr,unsigned long count);
⑵16位宽的字符串I/O操作
void insw(unsigned port,void * addr,unsigned long count);
void outsw(unsigned port ,void * addr,unsigned long count);
⑶32位宽的字符串I/O操作
void insl(unsigned port,void * addr,unsigned long count);
void outsl(unsigned port ,void * addr,unsigned long count);
3.5.2 Pausing I/O
在一些平台上(典型地如X86),对于老式总线(如ISA)上的慢速外设来说,如果CPU读写其I/O端口的速度太快,那就可能会发生丢失数据的现象。对于这个问题的解决方法就是在两次连续的I/O操作之间插入一段微小的时延,以便等待慢速外设。这就是所谓的“Pausing I/O”。
对于Pausing I/O,Linux也在io.h头文件中定义了它的I/O读写函数,而且都以XXX_p命名,比如:inb_p()、outb_p()等等。下面我们就以out_p()为例进行分析。
将io.h中的宏定义__OUT(b,”b”char)展开后可得如下定义:
extern inline void outb(unsigned char value, unsigned short port) {
__asm__ __volatile__ ("outb %" "b " "0,%" "w" "1"
: : "a" (value), "Nd" (port));
}
extern inline void outb_p(unsigned char value, unsigned short port) {
__asm__ __volatile__ ("outb %" "b " "0,%" "w" "1"
__FULL_SLOW_DOWN_IO
: : "a" (value), "Nd" (port));
}
可以看出,outb_p()函数的实现中被插入了宏__FULL_SLOWN_DOWN_IO,以实现微小的延时。宏__FULL_SLOWN_DOWN_IO在头文件io.h中一开始就被定义:
#ifdef SLOW_IO_BY_JUMPING
#define __SLOW_DOWN_IO "
jmp 1f
1: jmp 1f
1:"
#else
#define __SLOW_DOWN_IO "
outb %%al,$0x80"
#endif
#ifdef REALLY_SLOW_IO
#define __FULL_SLOW_DOWN_IO __SLOW_DOWN_IO
__SLOW_DOWN_IO __SLOW_DOWN_IO __SLOW_DOWN_IO
#else
#define __FULL_SLOW_DOWN_IO __SLOW_DOWN_IO
#endif
显然,__FULL_SLOW_DOWN_IO就是一个或四个__SLOW_DOWN_IO(根据是否定义了宏REALLY_SLOW_IO来决定),而宏__SLOW_DOWN_IO则被定义成毫无意义的跳转语句或写端口0x80的操作(根据是否定义了宏SLOW_IO_BY_JUMPING来决定)。
3.6 访问I/O内存资源
尽管I/O端口空间曾一度在x86平台上被广泛使用,但是由于它非常小,因此大多数现代总线的设备都以内存映射方式(Memory-mapped)来映射它的I/O端口(指I/O寄存器)和外设内存。基于内存映射方式的I/O端口(指I/O寄存器)和外设内存可以通称为“I/O内存”资源(I/O Memory)。因为这两者在硬件实现上的差异对于软件来说是完全透明的,所以驱动程序开发人员可以将内存映射方式的I/O端口和外设内存统一看作是“I/O内存”资源。
从前几节的阐述我们知道,I/O内存资源是在CPU的单一内存物理地址空间内进行编址的,也即它和系统RAM同处在一个物理地址空间内。因此通过CPU的访内指令就可以访问I/O内存资源。
一般来说,在系统运行时,外设的I/O内存资源的物理地址是已知的,这可以通过系统固件(如BIOS)在启动时分配得到,或者通过设备的硬连线(hardwired)得到。比如,PCI卡的I/O内存资源的物理地址就是在系统启动时由PCI BIOS分配并写到PCI卡的配置空间中的BAR中的。而ISA卡的I/O内存资源的物理地址则是通过设备硬连线映射到640KB-1MB范围之内的。但是CPU通常并没有为这些已知的外设I/O内存资源的物理地址预定义虚拟地址范围,因为它们是在系统启动后才已知的(某种意义上讲是动态的),所以驱动程序并不能直接通过物理地址访问I/O内存资源,而必须将它们映射到核心虚地址空间内(通过页表),然后才能根据映射所得到的核心虚地址范围,通过访内指令访问这些I/O内存资源。
3.6.1 映射I/O内存资源
Linux在io.h头文件中声明了函数ioremap(),用来将I/O内存资源的物理地址映射到核心虚地址空间(3GB-4GB)中,如下:
void * ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags);
void iounmap(void * addr);
函数用于取消ioremap()所做的映射,参数addr是指向核心虚地址的指针。这两个函数都是实现在mm/ioremap.c文件中。具体实现可参考《情景分析》一书。
3.6.2 读写I/O内存资源
在将I/O内存资源的物理地址映射成核心虚地址后,理论上讲我们就可以象读写RAM那样直接读写I/O内存资源了。但是,由于在某些平台上,对I/O内存和系统内存有不同的访问处理,因此为了确保跨平台的兼容性,Linux实现了一系列读写I/O内存资源的函数,这些函数在不同的平台上有不同的实现。但在x86平台上,读写I/O内存与读写RAM无任何差别。如下所示(include/asm-i386/io.h):
#define readb(addr) (*(volatile unsigned char *) __io_virt(addr))
#define readw(addr) (*(volatile unsigned short *) __io_virt(addr))
#define readl(addr) (*(volatile unsigned int *) __io_virt(addr))
#define writeb(b,addr) (*(volatile unsigned char *) __io_virt(addr) = (b))
#define writew(b,addr) (*(volatile unsigned short *) __io_virt(addr) = (b))
#define writel(b,addr) (*(volatile unsigned int *) __io_virt(addr) = (b))
#define memset_io(a,b,c) memset(__io_virt(a),(b),(c))
#define memcpy_fromio(a,b,c) memcpy((a),__io_virt(b),(c))
#define memcpy_toio(a,b,c) memcpy(__io_virt(a),(b),(c))
上述定义中的宏__io_virt()仅仅检查虚地址addr是否是核心空间中的虚地址。该宏在内核2.4.0中的实现是临时性的。具体的实现函数在arch/i386/lib/Iodebug.c文件。
显然,在x86平台上访问I/O内存资源与访问系统主存RAM是无差别的。但是为了保证驱动程序的跨平台的可移植性,我们应该使用上面的函数来访问I/O内存资源,而不应该通过指向核心虚地址的指针来访问。
[目录]
--------------------------------------------------------------------------------
from smth
[目录]
--------------------------------------------------------------------------------
基本结构
1.UNIX下设备驱动程序的基本结构
在UNIX系统里,对用户程序而言,设备驱动程序隐藏了设备的具体细节,对各种不同设备提供了一致的接口,一般来说是把设备映射为一个特殊的设备文件,用户程序可以象对其它文件一样对此设备文件进行操作。UNIX对硬件设备支持两个标准接口:块特别设备文件和字符特别设备文件,通过块(字符)特别 设备文件存取的设备称为块(字符)设备或具有块(字符)设备接口。 块设备接口仅支持面向块的I/O操作,所有I/O操作都通过在内核地址空间中的I/O缓冲区进行,它可以支持几乎任意长度和任意位置上的I/O请求,即提供随机存取的功能。
字符设备接口支持面向字符的I/O操作,它不经过系统的快速缓存,所以它们负责管理自己的缓冲区结构。字符设备接口只支持顺序存取的功能,一般不能进行任意长度的I/O请求,而是限制I/O请求的长度必须是设备要求的基本块长的倍数。显然,本程序所驱动的串行卡只能提供顺序存取的功能,属于是字符设备,因此后面的讨论在两种设备有所区别时都只涉及字符型设备接口。设备由一个主设备号和一个次设备号标识。主设备号唯一标识了设备类型,即设备驱动程序类型,它是块设备表或字符设备表中设备表项的索引。次设备号仅由设备驱动程序解释,一般用于识别在若干可能的硬件设备中,I/O请求所涉及到的那个设备。
设备驱动程序可以分为三个主要组成部分:
(1) 自动配置和初始化子程序,负责检测所要驱动的硬件设备是否存在和是否能正常工作。如果该设备正常,则对这个设备及其相关的、设备驱动程序需要的软件状态进行初始化。这部分驱动程序仅在初始化的时候被调用一次。
(2) 服务于I/O请求的子程序,又称为驱动程序的上半部分。调用这部分是由于系统调用的结果。这部分程序在执行的时候,系统仍认为是和进行调用的进程属于同一个进程,只是由用户态变成了核心态,具有进行此系统调用的用户程序的运行环境,因此可以在其中调用sleep()等与进程运行环境有关的函数。
(3) 中断服务子程序,又称为驱动程序的下半部分。在UNIX系统中,并不是直接从中断向量表中调用设备驱动程序的中断服务子程序,而是由UNIX系统来接收硬件中断,再由系统调用中断服务子程序。中断可以产生在任何一个进程运行的时候,因此在中断服务程序被调用的时候,不能依赖于任何进程的状态,也就不能调用任何与进程运行环境有关的函数。因为设备驱动程序一般支持同一类型的若干设备,所以一般在系统调用中断服务子程序的时候,都带有一个或多个参数,以唯一标识请求服务的设备。
在系统内部,I/O设备的存取通过一组固定的入口点来进行,这组入口点是由每个设备的设备驱动程序提供的。一般来说,字符型设备驱动程序能够提供如下几个入口点:
(1) open入口点。打开设备准备I/O操作。对字符特别设备文件进行打开操作,都会调用设备的open入口点。open子程序必须对将要进行的I/O操作做好必要的准备工作,如清除缓冲区等。如果设备是独占的,即同一时刻只能有一个程序访问此设备,则open子程序必须设置一些标志以表示设备处于忙状态。
(2) close入口点。关闭一个设备。当最后一次使用设备终结后,调用close子程序。独占设备必须标记设备可再次使用。
(3) read入口点。从设备上读数据。对于有缓冲区的I/O操作,一般是从缓冲区里读数据。对字符特别设备文件进行读操作将调用read子程序。
(4) write入口点。往设备上写数据。对于有缓冲区的I/O操作,一般是把数据写入缓冲区里。对字符特别设备文件进行写操作将调用write子程序。
(5) ioctl入口点。执行读、写之外的操作。
(6) select入口点。检查设备,看数据是否可读或设备是否可用于写数据。select系统调用在检查与设备特别文件相关的文件描述符时使用select入口点。如果设备驱动程序没有提供上述入口点中的某一个,系统会用缺省的子程序来代替。对于不同的系统,也还有一些其它的入口点。
[目录]
--------------------------------------------------------------------------------
驱动程序
2.LINUX系统下的设备驱动程序
具体到LINUX系统里,设备驱动程序所提供的这组入口点由一个结构来向系统进行说明,此结构定义为:
#include <linux/fs.h>;
struct file_operations {
int (*lseek)(struct inode *inode,struct file *filp,
off_t off,int pos);
int (*read)(struct inode *inode,struct file *filp,
char *buf, int count);
int (*write)(struct inode *inode,struct file *filp,
char *buf,int count);
int (*readdir)(struct inode *inode,struct file *filp,
struct dirent *dirent,int count);
int (*select)(struct inode *inode,struct file *filp,
int sel_type,select_table *wait);
int (*ioctl) (struct inode *inode,struct file *filp,
unsigned int cmd,unsigned int arg);
int (*mmap) (void);
int (*open) (struct inode *inode, struct file *filp);
void (*release) (struct inode *inode, struct file *filp);
int (*fsync) (struct inode *inode, struct file *filp);
};
其中,struct inode提供了关于特别设备文件/dev/driver(假设此设备名为driver)的信息,它的定义为:
#include <linux/fs.h>;
struct inode {
dev_t i_dev;
unsigned long i_ino; /* Inode number */
umode_t i_mode; /* Mode of the file */
nlink_t i_nlink;
uid_t i_uid;
gid_t i_gid;
dev_t i_rdev; /* Device major and minor numbers*/
off_t i_size;
time_t i_atime;
time_t i_mtime;
time_t i_ctime;
unsigned long i_blksize;
unsigned long i_blocks;
struct inode_operations * i_op;
struct super_block * i_sb;
struct wait_queue * i_wait;
struct file_lock * i_flock;
struct vm_area_struct * i_mmap;
struct inode * i_next, * i_prev;
struct inode * i_hash_next, * i_hash_prev;
struct inode * i_bound_to, * i_bound_by;
unsigned short i_count;
unsigned short i_flags; /* Mount flags (see fs.h) */
unsigned char i_lock;
unsigned char i_dirt;
unsigned char i_pipe;
unsigned char i_mount;
unsigned char i_seek;
unsigned char i_update;
union {
struct pipe_inode_info pipe_i;
struct minix_inode_info minix_i;
struct ext_inode_info ext_i;
struct msdos_inode_info msdos_i;
struct iso_inode_info isofs_i;
struct nfs_inode_info nfs_i;
} u;
};
struct file主要用于与文件系统对应的设备驱动程序使用。当然,其它设备驱动程序也可以使用它。它提供关于被打开的文件的信息,定义为:#include <linux/fs.h>;
struct file {
mode_t f_mode;
dev_t f_rdev; /* needed for /dev/tty */
off_t f_pos; /* Curr. posn in file */
unsigned short f_flags; /* The flags arg passed to open */
unsigned short f_count; /* Number of opens on this file */
unsigned short f_reada;
struct inode *f_inode; /* pointer to the inode struct */
struct file_operations *f_op;/* pointer to the fops struct*/
};
在结构file_operations里,指出了设备驱动程序所提供的入口点位置,分别是
(1) lseek,移动文件指针的位置,显然只能用于可以随机存取的设备。
(2) read,进行读操作,参数buf为存放读取结果的缓冲区,count为所要读取的数据长度。返回值为负表示读取操作发生错误,否则返回实际读取的字节数。对于字符型,要求读取的字节数和返回的实际读取字节数都必须是inode->;i_blksize的的倍数。
(3) write,进行写操作,与read类似。
(4) readdir,取得下一个目录入口点,只有与文件系统相关的设备驱动程序才使用。
(5) selec,进行选择操作,如果驱动程序没有提供select入口,select操作将会认为设备已经准备好进行任何的I/O操作。
(6) ioctl,进行读、写以外的其它操作,参数cmd为自定义的的命令。
(7) mmap,用于把设备的内容映射到地址空间,一般只有块设备驱动程序使用。
( open,打开设备准备进行I/O操作。返回0表示打开成功,返回负数表示失败。如果驱动程序没有提供open入口,则只要/dev/driver文件存在就认为打开成功。
(9) release,即close操作。
设备驱动程序所提供的入口点,在设备驱动程序初始化的时候向系统进行登记,以便系统在适当的时候调用。LINUX系统里,通过调用register_chrdev向系统注册字符型设备驱动程序。register_chrdev定义为:
#include <linux/fs.h>;
#include <linux/errno.h>;
int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);
其中,major是为设备驱动程序向系统申请的主设备号,如果为0则系统为此驱动程序动态地分配一个主设备号。name是设备名。fops就是前面所说的对各个调用的入口点的说明。此函数返回0表示成功。返回-EINVAL表示申请的主设备号非法,一般来说是主设备号大于系统所允许的最大设备号。返回-EBUSY表示所申请的主设备号正在被其它设备驱动程序使用。如果是动态分配主设备号成功,此函数将返回所分配的主设备号。如果register_chrdev操作成功,设备名就会出现在/proc/devices文件里。
初始化部分一般还负责给设备驱动程序申请系统资源,包括内存、中断、时钟、I/O端口等,这些资源也可以在open子程序或别的地方申请。在这些资源不用的时候,应该释放它们,以利于资源的共享。在UNIX系统里,对中断的处理是属于系统核心的部分,因此如果设备与系统之间以中断方式进行数据交换的话,就必须把该设备的驱动程序作为系统核心的一部分。设备驱动程序通过调用request_irq函数来申请中断,通过free_irq来释放中断。它们的定义为:
#include <linux/sched.h>;
int request_irq(unsigned int irq,
void (*handler)(int irq,void dev_id,struct pt_regs *regs),
unsigned long flags,
const char *device,
void *dev_id);
void free_irq(unsigned int irq, void *dev_id);
参数irq表示所要申请的硬件中断号。handler为向系统登记的中断处理子程序,中断产生时由系统来调用,调用时所带参数irq为中断号,dev_id为申请时告诉系统的设备标识,regs为中断发生时寄存器内容。device为设备名,将会出现在/proc/interrupts文件里。flag是申请时的选项,它决定中断处理程序的一些特性,其中最重要的是中断处理程序是快速处理程序(flag里设置了SA_INTERRUPT)还是慢速处理程序(不设置SA_INTERRUPT),快速处理程序运行时,所有中断都被屏蔽,而慢速处理程序运行时,除了正在处理的中断外,其它中断都没有被屏蔽。
在LINUX系统中,中断可以被不同的中断处理程序共享,这要求每一个共享此中断的处理程序在申请中断时在flags里设置SA_SHIRQ,这些处理程序之间以dev_id来区分。如果中断由某个处理程序独占,则dev_id可以为NULL。request_irq返回0表示成功,返回-INVAL表示irq>;15或handler==NULL,返回-EBUSY表示中断已经被占用且不能共享。作为系统核心的一部分,设备驱动程序在申请和释放内存时不是调用malloc和free,而代之以调用kmalloc和kfree,它们被定义为:
#include <linux/kernel.h>;
void * kmalloc(unsigned int len, int priority);
void kfree(void * obj);
参数len为希望申请的字节数,obj为要释放的内存指针。priority为分配内存操作的优先级,即在没有足够空闲内存时如何操作,一般用GFP_KERNEL。与中断和内存不同,使用一个没有申请的I/O端口不会使CPU产生异常,也就不会导致诸如“segmentation fault"一类的错误发生。任何进程都可以访问任何一个I/O端口。此时系统无法保证对I/O端口的操作不会发生冲突,甚至会因此而使系统崩溃。因此,在使用I/O端口前,也应该检查此I/O端口是否已有别的程序在使用,若没有,再把此端口标记为正在使用,在使用完以后释放它。这样需要用到如下几个函数:
int check_region(unsigned int from, unsigned int extent);
void request_region(unsigned int from, unsigned int extent, const char *name);
void release_region(unsigned int from, unsigned int extent);
调用这些函数时的参数为:from表示所申请的I/O端口的起始地址;extent为所要申请的从from开始的端口数;name为设备名,将会出现在/proc/ioports文件里。check_region返回0表示I/O端口空闲,否则为正在被使用。
在申请了I/O端口之后,就可以如下几个函数来访问I/O端口:
#include <asm/io.h>;
inline unsigned int inb(unsigned short port);
inline unsigned int inb_p(unsigned short port);
inline void outb(char value, unsigned short port);
inline void outb_p(char value, unsigned short port);
其中inb_p和outb_p插入了一定的延时以适应某些慢的I/O端口。在设备驱动程序里,一般都需要用到计时机制。在LINUX系统中,时钟是由系统接管,设备驱动程序可以向系统申请时钟。与时钟有关的系统调用有:
#include <asm/param.h>;
#include <linux/timer.h>;
void add_timer(struct timer_list * timer);
int del_timer(struct timer_list * timer);
inline void init_timer(struct timer_list * timer);
struct timer_list的定义为:
struct timer_list {
struct timer_list *next;
struct timer_list *prev;
unsigned long expires;
unsigned long data;
void (*function)(unsigned long d);
};
其中expires是要执行function的时间。系统核心有一个全局变量JIFFIES表示当前时间,一般在调用add_timer时jiffies=JIFFIES+num,表示在num个系统最小时间间隔后执行function。系统最小时间间隔与所用的硬件平台有关,在核心里定义了常数HZ表示一秒内最小时间间隔的数目,则num*HZ表示num秒。系统计时到预定时间就调用function,并把此子程序从定时队列里删除,因此如果想要每隔一定时间间隔执行一次的话,就必须在function里再一次调用add_timer。function的参数d即为timer里面的data项。在设备驱动程序里,还可能会用到如下的一些系统函数:
#include <asm/system.h>;
#define cli() __asm__ __volatile__ ("cli":
#define sti() __asm__ __volatile__ ("sti":
这两个函数负责打开和关闭中断允许。
#include <asm/segment.h>;
void memcpy_fromfs(void * to,const void * from,unsigned long n);
void memcpy_tofs(void * to,const void * from,unsigned long n);
在用户程序调用read 、write时,因为进程的运行状态由用户态变为核心态,地址空间也变为核心地址空间。而read、write中参数buf是指向用户程序的私有地址空间的,所以不能直接访问,必须通过上述两个系统函数来访问用户程序的私有地址空间。memcpy_fromfs由用户程序地址空间往核心地址空间复制,memcpy_tofs则反之。参数to为复制的目的指针,from为源指针,n为要复制的字节数。在设备驱动程序里,可以调用printk来打印一些调试信息,用法与printf类似。printk打印的信息不仅出现在屏幕上,同时还记录在文件syslog里。
[目录]
--------------------------------------------------------------------------------
具体实现
3.LINUX系统下的具体实现
在LINUX里,除了直接修改系统核心的源代码,把设备驱动程序加进核心里以外,还可以把设备驱动程序作为可加载的模块,由系统管理员动态地加载它,使之成为核心地一部分。也可以由系统管理员把已加载地模块动态地卸载下来。
LINUX中,模块可以用C语言编写,用gcc编译成目标文件(不进行链接,作为*.o文件存在),为此需要在gcc命令行里加上-c的参数。在编译时,还应该在gcc的命令行里加上这样的参数:-D__KERNEL__ -DMODULE。由于在不链接时,gcc只允许一个输入文件,因此一个模块的所有部分都必须在一个文件里实现。编译好的模块*.o放在/lib/modules/xxxx/misc下(xxxx表示核心版本,如在核心版本为2.0.30时应该为/lib/modules/2.0.30/misc),然后用depmod -a使此模块成为可加载模块。模块用insmod命令加载,用rmmod命令来卸载,并可以用lsmod命令来查看所有已加载的模块的状态。
编写模块程序的时候,必须提供两个函数,一个是int init_module(void),供insmod在加载此模块的时候自动调用,负责进行设备驱动程序的初始化工作。init_module返回0以表示初始化成功,返回负数表示失败。另一个函数是voidcleanup_module (void),在模块被卸载时调用,负责进行设备驱动程序的清除工作。
在成功的向系统注册了设备驱动程序后(调用register_chrdev成功后),就可以用mknod命令来把设备映射为一个特别文件,其它程序使用这个设备的时候,只要对此特别文件进行操作就行了。
[目录]
--------------------------------------------------------------------------------
PCI
PCI是一种广泛采用的总线标准,它提供了优于其他总线标准(比如EISA)的特性。在大多数奔腾主板上,PCI是高速、高带宽(32-bit和64-bit)、处理器无关的总线。对PCI的支持第一次加入Linux中时,其内核接口是PCI BIOS32函数的堆砌。这样做有几个问题:
* PCI BIOS仅存在于PC上;
* PCI BIOS只代表特定的结构,非PC类机器的某些PCI设置不能用PCI BIOS来描述;
* 个别机子的PCI BIOS函数不象预期的那样工作。
Linux 2.2提供了一个通用的PCI接口。Linux x86内核实际上努力直接驱动硬件,只有当它发现某些东西不能理解时,它才会调用PCI BIOS32函数。
驱动程序可以继续使用老的PCI接口,但是为了兼容将来的内核,可能需要更新。
如果驱动程序将要跨平台工作,那就更加需要更新了。多数新、老函数有简单的对应关系。PCI BIOS基于总线号/设备号/功能号的思想,而新的代码使用pci_bus和pci_dev结构。第一个新PCI函数是:
pci_present()
这个函数检查机器是否存在一条或更多的PCI总线。老内核有一个pcibios_present()函数,它们的用法完全相同。
确认PCI存在之后,你可以扫描PCI总线来查找设备。PCI设备通过几个配置寄存器来标识,主要是供应商ID和设备ID。每个供应商被分配了一个唯一的标识(ID),并且假设供应商给他们的设备(板子、芯片等)分配唯一的设备ID。PCI的一个好处是它提供了版本和编程接口信息,因此可以发现板子的变化。
在Linux 2.2中,扫描PCI总线一般用pci_find_device()函数。范例如下:
struct pci_dev *pdev = NULL;
while ((pdev = pci_find_device(PCI_MY_VENDOR,
PCI_MY_DEVICE, pdev)) != NULL)
{
/* Found a device */
setup_device(pdev);
}
pci_find_device()有3个参数:第一个是供应商ID,第二个是设备ID,第三个是函数的返回值,NULL表示你想从头开始查找。在这个例子中,对找到的设备调用setup_device()来进行设置。
另一个值得高兴的事情,是PCI为你处理了所有资源配置工作。一般来说PCI BIOS具体做这些工作,但是在其他平台上,这项工作由固件或者体系结构相关的Linux代码来做。到你的驱动程序查找PCI卡的时候,它已经被分配了系统资源。
Linux在pci_dev结构中提供了PCI相关的核心信息。同时还允许读写每个卡的PCI配置空间。当你可以直接查找资源数据时应该小心,对许多系统来说,卡上配置的数据与内核提供的数据并不相符。因为许多非PC机器有多条PCI总线,PCI总线以设备卡不知道的方式映射到系统中。
Linux直接提供了IRQ和PCI BARs(基址寄存器)。为了避免代码在非PC平台上出现意外,你应该总是使用内核提供的数据。下面代码列出了setup_device()例程:
Listing One: The setup_device () Function
void setup_device(struct pci_dev *dev)
{
int io_addr = dev->;base_address[0] & PCI_BASE_ADDRESS_IO_MASK;
int irq = dev->;irq;
u8 rev;
pci_read_config_byte(dev, PCI_REVISION_ID, &rev);
if (rev<64)
printk("Found a WonderWidget 500 at I/O 0x%04X, IRQ %d.\n",
io_addr, irq);
else
printk("Found a WonderWidget 600 at I/O 0x%04X, IRQ %d.\n",
io_addr, irq);
/* Check for a common BIOS problem - if you
* expect an IRQ you might not get it */
if (irq==0)
{
printk(KERN_ERR "BIOS has not assigned the WonderWidget"
" an interrupt.\n";
return;
}
/* Now do the board initialization knowing the resources */
init_device(io_addr, irq, rev<64 ? 0 : 1);
pci_set_master(dev);
}
当你的卡被BIOS配置后,某些特性可能会被屏蔽掉。比如,多数BIOS都会清掉“master”位,这导致板卡不能随意向主存中拷贝数据。Linux 2.2提供了一个辅助函数:
pci_set_master(struct pci_dev *)
这个函数会检查是否需要设置标志位,如果需要,则会将“master”位置位。例子函数setup_device还使用了pci_read_config_byte来读取配置空间数据。内核提供了一整套与配置空间相关的函数:
pci_read_config_byte,
pci_read_config_word,
和pci_read_config_dword
分别从配置空间获取8,16和32位数据;
pci_write_config_byte,
pci_write_config_word,
和pci_write_config_dword
分别向配置空间写入8,16和32位数据。PCI配置空间独立于I/O和内存空间,只能通过这些函数访问。
最后一组有用的PCI函数以不同的方式扫描PCI总线。pci_find_class查找符合给定类别(class)的设备。PCI规范把设备分为不同的类别,你可以根据类别查找设备。例如,为了查找一个USB控制器,可以用
struct pci_dev *pdev = NULL;
while((pdev=pci_find_class
(PCI_CLASS_SERIAL_USB <<8, pdev))!=NULL)
{
u8 type;
pci_read_config_byte(dev,
PCI_CLASS_PROG, &type);
if(type!=0)
continue;
/* FOUND IT */
}
另一个例子是I2O。这时,供应商ID只用来确定板卡的实际类型(type),偶尔用来对付特定板卡的bug。
扫描PCI设备的最后一种途径是pci_find_slot,使你按照特定的顺序扫描PCI插槽和功能。它很少使用,但是,如果你要控制查找某一类型设备时扫描PCI总线的顺序,你可以用它。这种情况通常出现在你需要遵照主板BIOS报告设备的顺序时,或者你想使Linux和非Linux驱动程序以相同的顺序报告设备时。传递给pci_find_slot()的是总线号slot和设备-功能号function(slot<<3 | function)。
PCI中断和其他注意事项
PCI总线一个重要的概念是共享中断处理,这在ISA总线设备中一般是看不到的。PCI总线中断也是电平触发的(level-triggered),也就是说,中断一直在那里,直到设备去清除它。这些特性给驱动程序处理中断加上了一些重要的限制。
驱动程序注册PCI中断时,总是应该带上SA_SHIRQ标志,用来指明中断线是可以共享的。如果不这样做,那么系统中的其他设备有可能不能正常工作,用户也可能遇到麻烦。
由于中断是共享的,PCI设备驱动程序和内核都需要与每个中断处理例程进行沟通的方法。你必须用一个非空(non-NULL)的dev_id来注册共享中断,否则,当你需要用free_irq来释放一个中断时,内核不能区分不同的中断处理例程。dev_id被送到中断处理例程,因此它非常重要。例如,你可以这样:
if (request_irq(dev->;irq, dev_interrupt,
SA_SHIRQ, "wonderwidget",
dev))
return -EAGAIN;
结束时,用下面的语句来正确释放中断:
free_irq(dev->;irq, dev)
中断处理例程被调用时收到dev参数,这使事情很简单了。你不必搜寻使用该中断的设备,通常可以这样做:
Listing Two: Using the dev_id
static void dev_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
struct wonderwidget *dev = dev_id;
u32 status;
/* It is important to exit interrupt handlers
* that are not for us as fast as possible */
if((status=inl(dev->;port))==0) /* Not our interrupt */
return;
if(status&1)
handle_rx_intr(dev);
....
}
你必须总是小心处理中断。永远不要在安装中断处理例程之前产生中断。因为PCI中断是电平触发的,如果你产生了中断而又不能处理它,可能会导致死机。这意味着写初始化代码时必须特别小心,你必须在打开设备的中断之前注册中断处理例程。同样,关闭时必须在注销中断处理例程之前屏蔽设备的中断。与ISA总线相比,Linux对PCI总线的支持用到较多的函数,并且要小心处理中断。
作为回报,不需要你的介入,系统把一切都配置好了。
[目录]
--------------------------------------------------------------------------------
loopback
各位大侠,最近我看Linux源码中的网络驱动部分。
先从loopback.c入手的。
loopback.c中的loopback_xmit函数中有这么一段:
static int loopback_xmit(struct sk_buff * skb,struct net_device * dev)
{
struct net_device_stats * stats = (struct net_device_stats *)dev_priv;
if (atomic_read(&skb->;users)!=1){
/*判断有几个人用skb. 是会有多出用skb,例如一边运行一边sniff.有些时候会修改skb, 这就要clone,如果这/个skb也被其他人用了.. */
struct sk_buff * skb2 = skb;
skb=skb_clone(skb,GFP_ATOMIC);
if(skb==NULL){
kfree_skb(skb2);
return 0;/*这里系统内存不足,为什么不报错?因为对kernel来说,mem 不够不是错,是会出现的实际情况,. 在这里的处理方式就是把这个包drop调.不loopback了. */
}
kfree_skb(skb2);
}
else
skb_orphan(skb);/*查<linux/skbuff.h>;中定义:
skb_orphan ---- orphan a buffer
@skb: buffer to orphan
If a buffer currently has an owner then we
call the owner's destructor function and
make the @skb unowned.The buffer continues
to exist but is no longer charged to its
former owner
那么skb_orphan以后,原来skb所指向的sk_buff
结构如何使用呢?skb是否成了一个空指针?
skb_orphan和kfree_skb有什么本质的区别?
其实这里应该不是free调的.还是可以用的.但是取消
原来的owner的引用而已. */
.
.
.
}
[目录]
--------------------------------------------------------------------------------
Sis 900
SIS 900 是一个可以用来实作 10/100 网络卡的控制芯片。它提供了对 PCI mastermode , MII, 802.3x 流量控制等各种标准的支援。这篇文章将告诉大家,如何写一个 Linux 的网络驱动程序,它将比大家想像中简单很多。这篇文章将以 Linux 2.4 版为对象, 2.2 版提供的界面略有不同,但差别并不太大,读完本文后再读 2.2 版的程序码应该不会有太大困难才是。 本文所参考的驱动程序是在 2.4.3 版中 drivers/net/sis900.c 这个档案。你可以在 http://xxx.xxx.xxx.xxx/linux-2.4.3/drivers/net/sis900.c 找到它。如果你能有一份硬件的 databook 在手边,读起驱动程序的码可能会更简单。 SIS900的 databook 可以直接在http://www.sis.com.tw/ftp/Databook/900/sis900.exe下载。
PCI 驱动程序
对一个 PCI 驱动程序而言, Linux 提供了很完整的支援,大部份的 PCI 资讯都由内建的程序读出。对个别的驱动程序而言直接使用就可以了。所以在这个部份,唯一要做的事只是告知 PCI 子系统一个新的驱动程序己经被加入系统之中了。在档案的最末端,你会看到下面的程序,
static struct pci_driver sis900_pci_driver = {
name: SIS900_MODULE_NAME,
id_table: sis900_pci_tbl,
probe: sis900_probe,
remove: sis900_remove,
};
static int __init sis900_init_module(void)
{
printk(KERN_INFO "%s", version);
return pci_module_init(&sis900_pci_driver);
}
static void __exit sis900_cleanup_module(void)
{
pci_unregister_driver(&sis900_pci_driver);
}
pci_module_init 是用来向 PCI 子系统注册一个 PCI 驱动程序。根据 id_table 中所提供的资料, PCI 子系统会在发现符合驱动程序要求的装置时使用它。那 PCI 子系统如何做到这件事呢 ? 我们先看一下 id_table 的内容就很清楚了。
static struct pci_device_id sis900_pci_tbl [] __devinitdata = {
{PCI_VENDOR_ID_SI, PCI_DEVICE_ID_SI_900,
PCI_ANY_ID, PCI_ANY_ID, 0, 0, SIS_900},
{PCI_VENDOR_ID_SI, PCI_DEVICE_ID_SI_7016,
PCI_ANY_ID, PCI_ANY_ID, 0, 0, SIS_7016},
{0,}
};
MODULE_DEVICE_TABLE (pci, sis900_pci_tbl);
看懂了吗 ? 嗯,我想你懂了。不过我还是解释一下。前面四个分别是
vendor id : PCI_VENDOR_ID_SI
device id : PCI_DEVICE_ID_SI_900
sub vendor id : PCI_ANY_ID
sub device id : PCI_ANY_ID
意思是说这个驱动程序支援 SIS 出的 SIS900 系列所有的硬件,我们不介意 subvendor id 和 sub device id 。你可以加入任何你想要的项目。对于不同的网络卡制造商,它们可能会有不同的 sub vendor id 和 sub device id 。但只要它们用SIS900 这个芯片,那这个驱动程序就可能适用。我们可以说这是一个『公版』的驱动程序。初始化好了,那其它的部份呢 ? 还记意 sis900_pci_driver 中其它的二个项目 probe 和remove 吗 ? 它们是用来初始化和移除一个驱动程序的呼叫。你可以把它们想成驱动程序物件的 constructor 和 destructor 。在 probe 中,你应该由硬件中把一些将来可能会用到的资讯准备好。由于这是一个 PCI 驱动程序,你不必特意去检查装置是否真的存在。但如果你的驱动程序只支援某些特定的硬件,或是你想要检查系统中是否有一些特别的硬件存在,你可以在这里做。例如在这个驱动程序中,对不同版本的硬件,我们用不
同的方法去读它的 MAC 位址。
pci_read_config_byte(pci_dev, PCI_CLASS_REVISION, &revision);
if (revision == SIS630E_900_REV || revision == SIS630EA1_900_REV)
ret = sis630e_get_mac_addr(pci_dev, net_dev);
else if (revision == SIS630S_900_REV)
ret = sis630e_get_mac_addr(pci_dev, net_dev);
else
ret = sis900_get_mac_addr(pci_dev, net_dev);
对于 SIS630E SIS630EA1 和 SIS630S 这些整合式芯片而言,其 MAC 位址被储存在 APC CMOS RAM 之中。但对其它独立的芯片而言则是存在网络卡的 EEPROM 之上。为了不要让这篇文章像流水帐一般,我不仔细的说明 probe 的过程。大家自己揣摸一下吧 !
在 probe 中还有一段比较和后文有关的程序码
net_dev->;open = &sis900_open;
net_dev->;hard_start_xmit = &sis900_start_xmit;
net_dev->;stop = &sis900_close;
net_dev->;get_stats = &sis900_get_stats;
net_dev->;set_config = &sis900_set_config;
net_dev->;set_multicast_list = &set_rx_mode;
net_dev->;do_ioctl = &mii_ioctl;
net_dev->;tx_timeout = sis900_tx_timeout;
net_dev->;watchdog_timeo = TX_TIMEOUT;
我想这很清楚,我们透过 net_dev 这个结构告诉 Linux 网络子系统如何来操作这个装置。当你使用 ifconfig 这个 R 令时,系统会使用 sis900_open 打开这个驱动程序,并使用 set_config 来说定装置的参数,如 IP address 。当有资料需要被传送时, sis900_start_xmit 被用来将资料送入装置之中。接下来,我们就一一的检视这些函数。
初始化装置
sis900_open(struct net_device *net_dev);
这个函数会在我们使用 ifconfig 将一网络装置激活时被呼叫。当驱动程序被插入系统之后,通常并不会马上开始接收或传送封包。一般来说,在 probe 的阶段,我们只是单纯的判断装置是否存在。实际激活硬件的动作在这里才会被实际执行。以 SIS900 为例,在其硬件中只有一个大约 2K 的缓冲区。也就是说在装置上只有一个
封包的缓冲区。当一个封包被传送后,装置必须产生一个中断要求操作系统将下一个封包传入。如果由中断到中断驱动程序被执行需要 5ms 的时间,那一秒至多我们可以送出 200 个封包。也就是说网络传送是不可能大于 400K/s ,这对于一般的情况下是不太可能接受的事。SIS900 虽然在装置上只有很小的缓冲区,但它可以透过 PCI master 模式直接控制主机板上的记忆体。事实上,它使用下面的方式来传送资料。你必须在记忆体中分配一组串接成环状串列的缓冲区,然后将 TXDP 指向缓冲区的第一个位址。 SIS900 会在第一个缓冲区传送完后自动的由第二个缓冲区取资料,并更新记忆中的资料将己传送完缓冲区的 OWN 位元清除。当 CPU 将缓冲区串列设定完成后,这个动作可以在完全没有 CPU 的介入下完成。所以硬件不必等待作业系统将新的资料送入,而可以连续的送出多个封包。操作系统只要能来的及让环状串列不会进入空的状态就可以了。
同样的,我们也需要一个接收缓冲区,使用进来的封包不至因操作系统来不及处理而遗失。在 sis900_open 中, sis900_init_rx_ring 和 sis900_init_tx_ring 就是用来负处初始化这二个串列。
在初始化串列之后,我们便可以要求 SIS900 开始接收封包。下面二行程序码便是用来做这件事。
outl((RxSOVR|RxORN|RxERR|RxOK|TxURN|TxERR|TxIDLE), ioaddr + imr);
outl(RxENA, ioaddr + cr);
outl(IE, ioaddr + ier);
第一行设定硬件在下列情况发出一个系统中断,
接收失败时
接收成功 时
传送失败时
所有缓冲区中的资料都传送完时
第二行则告诉硬件操作系统己经准备好要接收资料了。第三行则时硬件实际开始送出中断。
在这个函数的最后,我们安装一个每秒执行五次的 timer 。在它的处理函数 sis900_timer 中,我们会检查目前的连结状态,这包括了连结的种类 (10/100)和连接的状态 ( 网络卡是否直的被接到网络上去 ) 。
如果各位用过 Window 2000 ,另人印象最深刻的是当你将网络线拔出时, GUI 会自动警言网络己经中断。其实 Linux 也可以做到这件事,只是你需要一个比较好的图形界面就是了。
传送一个封包的 descriptor 给网络卡
sis900_start_xmit(struct sk_buff *skb, struct net_device *net_dev);
这个函数是用来将一个由 skb 描述的网络资料缓冲区送进传送缓冲区中准备传送。其中最重要的程序码为
sis_priv->;tx_ring[entry].bufptr = virt_to_bus(skb->;data);
sis_priv->;tx_ring[entry].cmdsts = (OWN | skb->;len);
outl(TxENA, ioaddr + cr);
SIS900 会使用 DMA 由缓冲区中取得封包的资料。由于缓冲区的数目有限,我们必须在缓冲区用完的时后告诉上层的网络协定不要再往下送资料了。在这里我们用下面的程序来做这件事。
if (++sis_priv->;cur_tx - sis_priv->;dirty_tx < NUM_TX_DESC) {
netif_start_queue(net_dev);
} else {
sis_priv->;tx_full = 1;
netif_stop_queue(net_dev);
}
netif_start_queue 用来告诉上层网络协定这个驱动程序还有空的缓冲区可用,请把下一个封包送进来。 netif_stop_queue 则是用来告诉上层网络协定所有的封包都用完了,请不要再送。
接收一个或多个封包
int sis900_rx(struct net_device *net_dev);
这个函式在会在有封包进入系统时被呼叫,因为可能有多于一个的封包在缓冲区之中。这个函数会逐一检查所有的缓冲区,直到遇到一个空的缓冲区为止。当我们发现一个有资料的缓冲区时,我们需要做二件事。首先是告知上层网络协定有一个新的封包进入系统,这件事由下面的程序完成
skb = sis_priv->;rx_skbuff[entry];
skb_put(skb, rx_size);
skb->;protocol = eth_type_trans(skb, net_dev);
netif_rx(skb);
前三行根据封包的内容更新 skbuff 中的档头。最后一行则是正式通知上层处理封包。
请注意 Linux 为了增加处理效能,在 netif_rx 并不会真的做完整接收封包的动作,而只是将这个封包记下来。真实的动作是在 bottom half 中才去处理。因为如此,原先储存封包的缓冲区暂时不能再被使用,我们必须重新分配一个新的缓冲区供下一个封包使用。下面的程序码是用来取得一个新的缓冲区。
if ((skb = dev_alloc_skb(RX_BUF_SIZE)) == NULL) {
sis_priv->;rx_skbuff[entry] = NULL;
sis_priv->;rx_ring[entry].cmdsts = 0;
sis_priv->;rx_ring[entry].bufptr = 0;
sis_priv->;stats.rx_dropped++;
break;
}
skb->;dev = net_dev;
sis_priv->;rx_skbuff[entry] = skb;
sis_priv->;rx_ring[entry].cmdsts = RX_BUF_SIZE;
sis_priv->;rx_ring[entry].bufptr = virt_to_bus(skb->;tail);
sis_priv->;dirty_rx++;
这个函数其馀的部份其实只是用来记录一些统计资料而己。
传送下一个封包
void sis900_finish_xmit (struct net_device *net_dev);
这个函数用来处理传送中断。在收到一个 TX 中断,表示有一个或多数缓冲区中的资料己经传送完成。我们可以把原先的缓冲区释出来供其它的封包使用,并且用下面的程序告诉上层协定可以送新的封包下来了。
if (sis_priv->;tx_full && netif_queue_stopped(net_dev) &&
sis_priv->;cur_tx - sis_priv->;dirty_tx < NUM_TX_DESC - 4) {
sis_priv->;tx_full = 0;
netif_wake_queue (net_dev);
}
netif_wake_queue() 会使得上层协定开始传送新的资料下来。
改变装置的设定
int sis900_set_config(struct net_device *dev, struct ifmap *map);
处理
经验
[目录]
--------------------------------------------------------------------------------
新手入门
入门
针对好多Linux 爱好者对内核很有兴趣却无从下口,本文旨在介绍一种解读linux内核源码的入门方法,而不是解说linux复杂的内核机制;
一.核心源程序的文件组织:
1.Linux核心源程序通常都安装在/usr/src/linux下,而且它有一个非常简单的编号约定:任何偶数的核心(例如2.0.30)都是一个稳定地发行的核心,而任何奇数的核心(例如2.1.42)都是一个开发中的核心。
本文基于稳定的2.2.5源代码,第二部分的实现平台为 Redhat Linux 6.0。
2.核心源程序的文件按树形结构进行组织,在源程序树的最上层你会看到这样一些目录:
●Arch :arch子目录包括了所有和体系结构相关的核心代码。它的每一个子目录都代表一种支持的体系结构,例如i386就是关于intel cpu及与之相兼容体系结构的子目录。PC机一般都基于此目录;
●Include: include子目录包括编译核心所需要的大部分头文件。与平台无关的头文件在 include/linux子目录下,与 intel cpu相关的头文件在include/asm-i386子目录下,而include/scsi目录则是有关scsi设备的头文件目录;
●Init: 这个目录包含核心的初始化代码(注:不是系统的引导代码),包含两个文件main.c和Version.c,这是研究核心如何工作的一个非常好的起点。
●Mm :这个目录包括所有独立于 cpu 体系结构的内存管理代码,如页式存储管理内存的分配和释放等;而和体系结构相关的内存管理代码则位于arch/*/mm/,例如arch/i386/mm/Fault.c
●Kernel:主要的核心代码,此目录下的文件实现了大多数linux系统的内核函数,其中最重要的文件当属sched.c;同样,和体系结构相关的代码在arch/*/kernel中;
●Drivers: 放置系统所有的设备驱动程序;每种驱动程序又各占用一个子目录:如,/block 下为块设备驱动程序,比如ide(ide.c)。如果你希望查看所有可能包含文件系统的设备是如何初始化的,你可以看drivers/block/genhd.c中的device_setup()。它不仅初始化硬盘,也初始化网络,因为安装nfs文件系统的时候需要网络其他: 如, Lib放置核心的库代码; Net,核心与网络相关的代码; Ipc,这个目录包含核心的进程间通讯的代码; Fs ,所有的文件系统代码和各种类型的文件操作代码,它的每一个子目录支持一个文件系统,例如fat和ext2;
●Scripts, 此目录包含用于配置核心的脚本文件等。
一般,在每个目录下,都有一个 .depend 文件和一个 Makefile 文件,这两个文件都是编译时使用的辅助文件,仔细阅读这两个文件对弄清各个文件这间的联系和依托关系很有帮助;而且,在有的目录下还有Readme 文件,它是对该目录下的文件的一些说明,同样有利于我们对内核源码的理解;
二.解读实战:为你的内核增加一个系统调用
虽然,Linux 的内核源码用树形结构组织得非常合理、科学,把功能相关联的文件都放在同一个子目录下,这样使得程序更具可读性。然而,Linux 的内核源码实在是太大而且非常复杂,即便采用了很合理的文件组织方法,在不同目录下的文件之间还是有很多的关联,分析核心的一部分代码通常会要查看其它的几个相关的文件,而且可能这些文件还不在同一个子目录下。
体系的庞大复杂和文件之间关联的错综复杂,可能就是很多人对其望而生畏的主要原因。当然,这种令人生畏的劳动所带来的回报也是非常令人着迷的:你不仅可以从中学到很多的计算机的底层的知识(如下面将讲到的系统的引导),体会到整个操作系统体系结构的精妙和在解决某个具体细节问题时,算法的巧妙;而且更重要的是:在源码的分析过程中,你就会被一点一点地、潜移默化地专业化;甚至,只要分析十分之一的代码后,你就会深刻地体会到,什么样的代码才是一个专业的程序员写的,什么样的代码是一个业余爱好者写的。
为了使读者能更好的体会到这一特点,下面举了一个具体的内核分析实例,希望能通过这个实例,使读者对 Linux的内核的组织有些具体的认识,从中读者也可以学到一些对内核的分析方法。
以下即为分析实例:
【一】操作平台:
硬件:cpu intel Pentium II ;
软件:Redhat Linux 6.0; 内核版本2.2.5【二】相关内核源代码分析:
1.系统的引导和初始化:Linux 系统的引导有好几种方式:常见的有 Lilo, Loadin引导和Linux的自举引导
(bootsect-loader),而后者所对应源程序为arch/i386/boot/bootsect.S,它为实模式的汇编程序,限于篇幅在此不做分析;无论是哪种引导方式,最后都要跳转到 arch/i386/Kernel/setup.S, setup.S主要是进行时模式下的初始化,为系统进入保护模式做准备;此后,系统执行 arch/i386/kernel/head.S (对经压缩后存放的内核要先执行 arch/i386/boot/compressed/head.S); head.S 中定义的一段汇编程序setup_idt ,它负责建立一张256项的 idt 表(Interrupt Descriptor Table),此表保存着所有自陷和中断的入口地址;其中包括系统调用总控程序 system_call 的入口地址;当然,除此之外,head.S还要做一些其他的初始化工作;
2.系统初始化后运行的第一个内核程序asmlinkage void __init start_kernel(void) 定义在/usr/src/linux/init/main.c中,它通过调用usr/src/linux/arch/i386/kernel/traps.c 中的一个函数
void __init trap_init(void) 把各自陷和中断服务程序的入口地址设置到 idt 表中,其中系统调用总控程序system_cal就是中断服务程序之一;void __init trap_init(void) 函数则通过调用一个宏
set_system_gate(SYSCALL_VECTOR,&system_call); 把系统调用总控程序的入口挂在中断0x80上;
其中SYSCALL_VECTOR是定义在 /usr/src/linux/arch/i386/kernel/irq.h中的一个常量0x80; 而 system_call 即为中断总控程序的入口地址;中断总控程序用汇编语言定义在/usr/src/linux/arch/i386/kernel/entry.S中;
3.中断总控程序主要负责保存处理机执行系统调用前的状态,检验当前调用是否合法, 并根据系统调用向量,使处理机跳转到保存在 sys_call_table 表中的相应系统服务例程的入口; 从系统服务例程返回后恢复处理机状态退回用户程序;
而系统调用向量则定义在/usr/src/linux/include/asm-386/unistd.h 中;sys_call_table 表定义在/usr/src/linux/arch/i386/kernel/entry.S 中; 同时在 /usr/src/linux/include/asm-386/unistd.h 中也定义了系统调用的用户编程接口;
4.由此可见 , linux 的系统调用也象 dos 系统的 int 21h 中断服务, 它把0x80 中断作为总的入口, 然后转到保存在 sys_call_table 表中的各种中断服务例程的入口地址 , 形成各种不同的中断服务;
由以上源代码分析可知, 要增加一个系统调用就必须在 sys_call_table 表中增加一项 , 并在其中保存好自己的系统服务例程的入口地址,然后重新编译内核,当然,系统服务例程是必不可少的。
由此可知在此版linux内核源程序中,与系统调用相关的源程序文件就包括以下这些:
1.arch/i386/boot/bootsect.S
2.arch/i386/Kernel/setup.S
3.arch/i386/boot/compressed/head.S
4.arch/i386/kernel/head.S
5.init/main.c
6.arch/i386/kernel/traps.c
7.arch/i386/kernel/entry.S
8.arch/i386/kernel/irq.h
9.include/asm-386/unistd.h
当然,这只是涉及到的几个主要文件。而事实上,增加系统调用真正要修改文件只有include/asm-386/unistd.h和arch/i386/kernel/entry.S两个;
【三】 对内核源码的修改:
1.在kernel/sys.c中增加系统服务例程如下:
asmlinkage int sys_addtotal(int numdata)
{
int i=0,enddata=0;
while(i<=numdata)
enddata+=i++;
return enddata;
}
该函数有一个 int 型入口参数 numdata , 并返回从 0 到 numdata 的累加值; 当然也可以把系统服务例程放在一个自己定义的文件或其他文件中,只是要在相应文件中作必要的说明;
2.把 asmlinkage int sys_addtotal( int) 的入口地址加到sys_call_table表中:
arch/i386/kernel/entry.S 中的最后几行源代码修改前为:
... ...
.long SYMBOL_NAME(sys_sendfile)
.long SYMBOL_NAME(sys_ni_syscall) /* streams1 */
.long SYMBOL_NAME(sys_ni_syscall) /* streams2 */
.long SYMBOL_NAME(sys_vfork) /* 190 */
.rept NR_syscalls-190
.long SYMBOL_NAME(sys_ni_syscall)
.endr
修改后为:
... ...
.long SYMBOL_NAME(sys_sendfile)
.long SYMBOL_NAME(sys_ni_syscall) /* streams1 */
.long SYMBOL_NAME(sys_ni_syscall) /* streams2 */
.long SYMBOL_NAME(sys_vfork) /* 190 */
/* add by I */
.long SYMBOL_NAME(sys_addtotal)
.rept NR_syscalls-191
.long SYMBOL_NAME(sys_ni_syscall)
.endr
3. 把增加的 sys_call_table 表项所对应的向量,在include/asm-386/unistd.h 中进行必要申明,以供用户进程和其他系统进程查询或调用:
增加后的部分 /usr/src/linux/include/asm-386/unistd.h 文件如下:
... ...
#define __NR_sendfile 187
#define __NR_getpmsg 188
#define __NR_putpmsg 189
#define __NR_vfork 190
/* add by I */
#define __NR_addtotal 191
4.测试程序(test.c)如下:
#include
#include
_syscall1(int,addtotal,int, num)
main()
{
int i,j;
do
printf("lease input a number\n";
while(scanf("%d",&i)==EOF);
if((j=addtotal(i))==-1)
printf("Error occurred in syscall-addtotal();\n";
printf("Total from 0 to %d is %d \n",i,j);
}
对修改后的新的内核进行编译,并引导它作为新的操作系统,运行几个程序后可以发现一切正常;在新的系统下对测试程序进行编译(*注:由于原内核并未提供此系统调用,所以只有在编译后的新内核下,此测试程序才能可能被编译通过),运行情况如下:
$gcc -o test test.c
$./test
Please input a number
36
Total from 0 to 36 is 666
可见,修改成功;
而且,对相关源码的进一步分析可知,在此版本的内核中,从/usr/src/linux/arch/i386/kernel/entry.S
文件中对 sys_call_table 表的设置可以看出,有好几个系统调用的服务例程都是定义在/usr/src/linux/kernel/sys.c 中的同一个函数:
asmlinkage int sys_ni_syscall(void)
{
return -ENOSYS;
}
例如第188项和第189项就是如此:
... ...
.long SYMBOL_NAME(sys_sendfile)
.long SYMBOL_NAME(sys_ni_syscall) /* streams1 */
.long SYMBOL_NAME(sys_ni_syscall) /* streams2 */
.long SYMBOL_NAME(sys_vfork) /* 190 */
... ...
而这两项在文件 /usr/src/linux/include/asm-386/unistd.h 中却申明如下:
... ...
#define __NR_sendfile 187
#define __NR_getpmsg 188 /* some people actually want streams */
#define __NR_putpmsg 189 /* some people actually want streams */
#define __NR_vfork 190
由此可见,在此版本的内核源代码中,由于asmlinkage int sys_ni_syscall(void) 函数并不进行任何操作,所以包括 getpmsg, putpmsg 在内的好几个系统调用都是不进行任何操作的,即有待扩充的空调用; 但它们却仍然占用着sys_call_table表项,估计这是设计者们为了方便扩充系统调用而安排的; 所以只需增加相应服务例程(如增加服务例程getmsg或putpmsg),就可以达到增加系统调用的作用。
[目录]
--------------------------------------------------------------------------------
一个简单程序的分析----深至内核
A small trail through the Linux kernel
Andries Brouwer, aeb@cwi.nl 2001-01-01
A program
---------------------------------------------------------------------------------------------------
#include <unistd.h>;
#include <fcntl.h>;
int main(){
int fd;
char buf[512];
fd = open("/dev/hda", O_RDONLY);
if (fd >;= 0)
read(fd, buf, sizeof(buf));
return 0;
}
---------------------------------------------------------------------------------------------------
This little program opens the block special device referring to the first IDE disk, and if the open succeeded reads the first sector. What happens in the kernel? Let us read 2.4.0 source.
[目录]
--------------------------------------------------------------------------------
open
The open system call is found in fs/open.c:
---------------------------------------------------------------------------------------------------
int sys_open(const char *filename, int flags, int mode) {
char *tmp = getname(filename);
int fd = get_unused_fd();
struct file *f = filp_open(tmp, flags, mode);
fd_install(fd, f);
putname(tmp);
return fd;
}
---------------------------------------------------------------------------------------------------
The routine getname() is found in fs/namei.c. It copies the file name from user space to kernel space:
---------------------------------------------------------------------------------------------------
#define __getname() kmem_cache_alloc(names_cachep, SLAB_KERNEL)
#define putname(name) kmem_cache_free(names_cachep, (void *)(name))
char *getname(const char *filename) {
char *tmp = __getname(); /* allocate some memory */
strncpy_from_user(tmp, filename, PATH_MAX + 1);
return tmp;
}
---------------------------------------------------------------------------------------------------
The routine get_unused_fd() is found in fs/open.c again. It returns the first unused filedescriptor:
---------------------------------------------------------------------------------------------------
int get_unused_fd(void) {
struct files_struct *files = current->;files;
int fd = find_next_zero_bit(files->;open_fds,
files->;max_fdset, files->;next_fd);
FD_SET(fd, files->;open_fds); /* in use now */
files->;next_fd = fd + 1;
return fd;
}
---------------------------------------------------------------------------------------------------
Here current is the pointer to the user task struct for the currently executing task.
The routine fd_install() is found in include/linux/file.h. It just stores the information returned by filp_open()
---------------------------------------------------------------------------------------------------
void fd_install(unsigned int fd, struct file *file) {
struct files_struct *files = current->;files;
files->;fd[fd] = file;
}
---------------------------------------------------------------------------------------------------
So all the interesting work of sys_open() is done in filp_open(). This routine is found in fs/open.c:
---------------------------------------------------------------------------------------------------
struct file *filp_open(const char *filename, int flags, int mode) {
struct nameidata nd;
open_namei(filename, flags, mode, &nd);
return dentry_open(nd.dentry, nd.mnt, flags);
}
---------------------------------------------------------------------------------------------------
The struct nameidata is defined in include/linux/fs.h. It is used during lookups.
---------------------------------------------------------------------------------------------------
struct nameidata {
struct dentry *dentry;
struct vfsmount *mnt;
struct qstr last;
};
---------------------------------------------------------------------------------------------------
The routine open_namei() is found in fs/namei.c:
---------------------------------------------------------------------------------------------------
open_namei(const char *pathname, int flag, int mode, struct nameidata *nd) {
if (!(flag & O_CREAT)) {
/* The simplest case - just a plain lookup. */
if (*pathname == '/') {
nd->;mnt = mntget(current->;fs->;rootmnt);
nd->;dentry = dget(current->;fs->;root);
} else {
nd->;mnt = mntget(current->;fs->;pwdmnt);
nd->;dentry = dget(current->;fs->;pwd);
}
path_walk(pathname, nd);
/* Check permissions etc. */
...
return 0;
}
...
}
---------------------------------------------------------------------------------------------------
An inode (index node) describes a file. A file can have several names (or no name at all), but it has a unique inode. A dentry (directory entry)describes a name of a file: the inode plus the pathname used to find it. Avfsmount describes the filesystem we are in.
So, essentially, the lookup part op open_namei() is found in path_walk():
---------------------------------------------------------------------------------------------------
path_walk(const char *name, struct nameidata *nd) {
struct dentry *dentry;
for(; {
struct qstr this;
this.name = next_part_of(name);
this.len = length_of(this.name);
this.hash = hash_fn(this.name);
/* if . or .. then special, otherwise: */
dentry = cached_lookup(nd->;dentry, &this);
if (!dentry)
dentry = real_lookup(nd->;dentry, &this);
nd->;dentry = dentry;
if (this_was_the_final_part)
return;
}
}
---------------------------------------------------------------------------------------------------
Here the cached_lookup() tries to find the given dentry in a cache of recently used dentries. If it is not found, the real_lookup() goes to the filesystem, which probably goes to disk, and actually finds the thing.After path_walk() is done, the nd argument contains the required dentry,which in turn has the inode information on the file. Finally we do dentry_open() that initializes a file struct:
---------------------------------------------------------------------------------------------------
struct file *
dentry_open(struct dentry *dentry, struct vfsmount *mnt, int flags) {
struct file *f = get_empty_filp();
f->;f_dentry = dentry;
f->;f_vfsmnt = mnt;
f->;f_pos = 0;
f->;f_op = dentry->;d_inode->;i_fop;
...
return f;
}
---------------------------------------------------------------------------------------------------
So far the open. In short: walk the tree, for each component hope the information is in cache, and if not ask the file system. How does this work? Each file system type provides structs super_operations,file_operations, inode_operations, address_space_operations that contain the addresses of the routines that can do stuff. And thus
---------------------------------------------------------------------------------------------------
struct dentry *real_lookup(struct dentry *parent, struct qstr *name, int flags) {
struct dentry *dentry = d_alloc(parent, name);
parent->;d_inode->;i_op->;lookup(dir, dentry);
return dentry;
}
---------------------------------------------------------------------------------------------------
calls on the lookup routine for the specific fiilesystem, as found in the struct inode_operations in the inode of the dentry for the directory in which we do the lookup.
And this file system specific routine must read the disk data and search the directory for the file we are looking for. Good examples of file systems are minix and romfs because they are simple and small. For example,in fs/romfs/inode.c:
---------------------------------------------------------------------------------------------------
romfs_lookup(struct inode *dir, struct dentry *dentry) {
const char *name = dentry->;d_name.name;
int len = dentry->;d_name.len;
char fsname[ROMFS_MAXFN];
struct romfs_inode ri;
unsigned long offset = dir->;i_ino & ROMFH_MASK;
for (; {
romfs_copyfrom(dir, &ri, offset, ROMFH_SIZE);
romfs_copyfrom(dir, fsname, offset+ROMFH_SIZE, len+1);
if (strncmp (name, fsname, len) == 0)
break;
/* next entry */
offset = ntohl(ri.next) & ROMFH_MASK;
}
inode = iget(dir->;i_sb, offset);
d_add (dentry, inode);
return 0;
}
romfs_copyfrom(struct inode *i, void *dest,
unsigned long offset, unsigned long count) {
struct buffer_head *bh;
bh = bread(i->;i_dev, offset>;>;ROMBSBITS, ROMBSIZE);
memcpy(dest, ((char *)bh->;b_data) + (offset & ROMBMASK), count);
brelse(bh);
}
(All complications, all locking, and all error handling deleted.)
---------------------------------------------------------------------------------------------------
[目录]
--------------------------------------------------------------------------------
read
Given a file descriptor (that keeps the inode and the file position of the file) we want to read. In fs/read_write.c we find:
---------------------------------------------------------------------------------------------------
ssize_t sys_read(unsigned int fd, char *buf, size_t count) {
struct file *file = fget(fd);
return file->;f_op->;read(file, buf, count, &file->;f_pos);
}
---------------------------------------------------------------------------------------------------
That is, the read system call asks the file system to do the reading,starting at the current file position. The f_op field was filled in the dentry_open() routine above with the i_fop field of an inode.
For romfs the struct file_operations is assigned in romfs_read_inode(). For a regular file (case 2) it assigns generic_ro_fops. For a block special file (case 4) it calls init_special_inode() (see devices.c) which assigns
def_blk_fops.
How come romfs_read_inode() was ever called? When the filesystem was mounted, the routine romfs_read_super() was called, and it assigned romfs_ops to the s_op field of the superblock struct.
---------------------------------------------------------------------------------------------------
struct super_operations romfs_ops = {
read_inode: romfs_read_inode,
statfs: romfs_statfs,
};
---------------------------------------------------------------------------------------------------
And the iget() that was skipped over in the discussion above (in romfs_lookup()) finds the inode with given number ino in a cache, and if it cannot be found there creates a new inode struct by calling get_new_inode()(see fs/inode.c):
---------------------------------------------------------------------------------------------------
struct inode * iget(struct super_block *sb, unsigned long ino) {
struct list_head * head = inode_hashtable + hash(sb,ino);
struct inode *inode = find_inode(sb, ino, head);
if (inode) {
wait_on_inode(inode);
return inode;
}
return get_new_inode(sb, ino, head);
}
struct inode *
get_new_inode(struct super_block *sb, unsigned long ino,
struct list_head *head) {
struct inode *inode = alloc_inode();
inode->;i_sb = sb;
inode->;i_dev = sb->;s_dev;
inode->;i_ino = ino;
...
sb->;s_op->;read_inode(inode);
}
---------------------------------------------------------------------------------------------------
So that is how the inode was filled, and we find that in our case (/dev/hda is a block special file) the routine that is called by sys_read is def_blk_fops.read, and inspection of block_dev.c shows that that is the routine block_read():
---------------------------------------------------------------------------------------------------
ssize_t block_read(struct file *filp, char *buf, size_t count, loff_t *ppos) {
struct inode *inode = filp->;f_dentry->;d_inode;
kdev_t dev = inode->;i_rdev;
ssize_t blocksize = blksize_size[MAJOR(dev)][MINOR(dev)];
loff_t offset = *ppos;
ssize_t read = 0;
size_t left, block, blocks;
struct buffer_head *bhreq[NBUF];
struct buffer_head *buflist[NBUF];
struct buffer_head **bh;
left = count; /* bytes to read */
block = offset / blocksize; /* first block */
offset &= (blocksize-1); /* starting offset in block */
blocks = (left + offset + blocksize - 1) / blocksize;
bh = buflist;
do {
while (blocks) {
--blocks;
*bh = getblk(dev, block++, blocksize);
if (*bh && !buffer_uptodate(*bh))
bhreq[bhrequest++] = *bh;
}
if (bhrequest)
ll_rw_block(READ, bhrequest, bhreq);
/* wait for I/O to complete,
copy result to user space,
increment read and *ppos, decrement left */
} while (left >; 0);
return read;
}
---------------------------------------------------------------------------------------------------
So the building blocks here are getblk(), ll_rw_block(), and wait_on_buffer().
The first of these lives in fs/buffer.c. It finds the buffer that already contains the required data if we are lucky, and otherwise a buffer that is going to be used.
---------------------------------------------------------------------------------------------------
struct buffer_head * getblk(kdev_t dev, int block, int size) {
struct buffer_head *bh;
int isize;
try_again:
bh = __get_hash_table(dev, block, size);
if (bh)
return bh;
isize = BUFSIZE_INDEX(size);
bh = free_list[isize].list;
if (bh) {
__remove_from_free_list(bh);
init_buffer(bh);
bh->;b_dev = dev;
bh->;b_blocknr = block;
...
return bh;
}
refill_freelist(size);
goto try_again;
}
---------------------------------------------------------------------------------------------------
The real I/O is started by ll_rw_block(). It lives in drivers/block/ll_rw_blk.c.
---------------------------------------------------------------------------------------------------
ll_rw_block(int rw, int nr, struct buffer_head * bhs[]) {
int i;
for (i = 0; i < nr; i++) {
struct buffer_head *bh = bhs;
bh->;b_end_io = end_buffer_io_sync;
submit_bh(rw, bh);
}
}
---------------------------------------------------------------------------------------------------
Here bh->;b_end_io specifies what to do when I/O is finished. In this case:
---------------------------------------------------------------------------------------------------
end_buffer_io_sync(struct buffer_head *bh, int uptodate) {
mark_buffer_uptodate(bh, uptodate);
unlock_buffer(bh);
}
---------------------------------------------------------------------------------------------------
So, ll_rw_block() just feeds the requests it gets one by one to submit_bh():
---------------------------------------------------------------------------------------------------
submit_bh(int rw, struct buffer_head *bh) {
bh->;b_rdev = bh->;b_dev;
bh->;b_rsector = bh->;b_blocknr * (bh->;b_size >;>; 9);
generic_make_request(rw, bh);
}
---------------------------------------------------------------------------------------------------
So, submit_bh() just passes things along to generic_make_request(), the routine to send I/O requests to block devices:
---------------------------------------------------------------------------------------------------
generic_make_request (int rw, struct buffer_head *bh) {
request_queue_t *q;
q = blk_get_queue(bh->;b_rdev);
q->;make_request_fn(q, rw, bh);
}
---------------------------------------------------------------------------------------------------
Thus, it finds the right queue and calls the request function for that queue.
---------------------------------------------------------------------------------------------------
struct blk_dev_struct {
request_queue_t request_queue;
queue_proc *queue;
void *data;
} blk_dev[MAX_BLKDEV];
request_queue_t *blk_get_queue(kdev_t dev)
{
return blk_dev[MAJOR(dev)].queue(dev);
}
---------------------------------------------------------------------------------------------------
In our case (/dev/hda), the blk_dev struct was filled by hwif_init (from drivers/ide/ide-probe.c):
and this ide_get_queue() is found in drivers/ide/ide.c:
---------------------------------------------------------------------------------------------------
blk_dev[hwif->;major].data = hwif;
blk_dev[hwif->;major].queue = ide_get_queue;
#define DEVICE_NR(dev) (MINOR(dev) >;>; PARTN_BITS)
request_queue_t *ide_get_queue (kdev_t dev) {
ide_hwif_t *hwif = (ide_hwif_t *) blk_dev[MAJOR(dev)].data;
return &hwif->;drives[DEVICE_NR(dev) & 1].queue;
}
---------------------------------------------------------------------------------------------------
This .queue field was filled by ide_init_queue():
And blk_init_queue() (from ll_rw_blk.c again):
---------------------------------------------------------------------------------------------------
ide_init_queue(ide_drive_t *drive) {
request_queue_t *q = &drive->;queue;
q->;queuedata = HWGROUP(drive);
blk_init_queue(q, do_ide_request);
}
blk_init_queue(request_queue_t *q, request_fn_proc *rfn) {
...
q->;request_fn = rfn;
q->;make_request_fn = __make_request;
q->;merge_requests_fn = ll_merge_requests_fn;
...
}
---------------------------------------------------------------------------------------------------
Aha, so we found the q->;make_request_fn. Here it is:
---------------------------------------------------------------------------------------------------
__make_request(request_queue_t *q, int rw, struct buffer_head *bh) {
/* try to merge request with adjacent ones */
...
/* get a struct request and fill it with device, start,length, ... */
...
add_request(q, req, insert_here);
if (!q->;plugged)
q->;request_fn(q);
}
add_request(request_queue_t *q, struct request *req,
struct list_head *insert_here) {
list_add(&req->;queue, insert_here);
}
---------------------------------------------------------------------------------------------------
When the request has been queued, q->;request_fn is called. What is that? We can see it above - it is do_ide_request() and lives in ide.c.
---------------------------------------------------------------------------------------------------
do_ide_request(request_queue_t *q) {
ide_do_request(q->;queuedata, 0);
}
ide_do_request(ide_hwgroup_t *hwgroup, int masked_irq) {
ide_startstop_t startstop;
while (!hwgroup->;busy) {
hwgroup->;busy = 1;
drive = choose_drive(hwgroup);
startstop = start_request(drive);
if (startstop == ide_stopped)
hwgroup->;busy = 0;
}
}
ide_startstop_t
start_request (ide_drive_t *drive) {
unsigned long block, blockend;
struct request *rq;
rq = blkdev_entry_next_request(&drive->;queue.queue_head);
block = rq->;sector;
block += drive->;part[minor & PARTN_MASK].start_sect;
SELECT_DRIVE(hwif, drive);
return (DRIVER(drive)->;do_request(drive, rq, block));
}
---------------------------------------------------------------------------------------------------
So, in the case of a partitioned disk it is only at this very low level that we add in the starting sector of the partition in order to get an absolute sector.
The first actual port access happened already:
---------------------------------------------------------------------------------------------------
#define SELECT_DRIVE(hwif,drive) \
OUT_BYTE((drive)->;select.all,
hwif->;io_ports[IDE_SELECT_OFFSET]);
---------------------------------------------------------------------------------------------------
but this do_request function must do the rest. For a disk it is defined in ide-disk.c, in the ide_driver_t idedisk_driver, and the function turns out to be do_rw_disk().
---------------------------------------------------------------------------------------------------
ide_startstop_t
do_rw_disk (ide_drive_t *drive, struct request *rq, unsigned long
block) {
if (IDE_CONTROL_REG)
OUT_BYTE(drive->;ctl,IDE_CONTROL_REG);
OUT_BYTE(rq->;nr_sectors,IDE_NSECTOR_REG);
if (drive->;select.b.lba) {
OUT_BYTE(block,IDE_SECTOR_REG);
OUT_BYTE(block>;>;=8,IDE_LCYL_REG);
OUT_BYTE(block>;>;=8,IDE_HCYL_REG);
OUT_BYTE(((block>;>;&0x0f)|drive->;select.all,IDE_SELECT_REG);
} else {
unsigned int sect,head,cyl,track;
track = block / drive->;sect;
sect = block % drive->;sect + 1;
OUT_BYTE(sect,IDE_SECTOR_REG);
head = track % drive->;head;
cyl = track / drive->;head;
OUT_BYTE(cyl,IDE_LCYL_REG);
OUT_BYTE(cyl>;>;8,IDE_HCYL_REG);
OUT_BYTE(head|drive->;select.all,IDE_SELECT_REG);
}
if (rq->;cmd == READ) {
ide_set_handler(drive, &read_intr, WAIT_CMD, NULL);
OUT_BYTE(WIN_READ, IDE_COMMAND_REG);
return ide_started;
}
...
}
---------------------------------------------------------------------------------------------------
This fills the remaining control registers of the interface and starts the actual I/O. Now ide_set_handler() sets up read_intr() to be called when we get an interrupt. This calls ide_end_request() when a request is done, which calls
end_that_request_first() (which calls bh->;b_end_io() as promised earlier) and end_that_request_last() which calls
blkdev_release_request() which wakes up whoever waited for the block.
[目录]
--------------------------------------------------------------------------------
lisolog文章检索
[目录]
--------------------------------------------------------------------------------
index
索引的内容:
我比较喜欢的帖子. 和精华收藏多有冲突.
索引的使用:
支持分类, 从这里找帖子, 然后分类,要比在论坛里搜索方便. 有这一点作用, 也不枉我遍历一次论坛.( 折叠起来看,不然太乱了)
索引的更新:
我们记录了索引更新的时间. 下一次更新时, 比方说一个月后, 只需选择显示一个月内发表的文章. 这样就不会丢掉文章. 但是,有可能重复, 因为新的回应会使帖子位置前移.这个问题这样解决:凡是我收集的文章统统加入我的收藏夹, 这样,更新时我只向索引里加入那些可以加入我的收藏夹的文章.
索引的局限:
水平有限, 不免错漏. 我尽力保留有价值的帖子. 不敢说没有在索引中的帖子就没有价值.但我一直在努力.
编辑者: hyl (07/12/02 13:56)
[目录]
--------------------------------------------------------------------------------
list1
关于faq----征求建议和合作者
请教linux内核版本2.0.35的进程切换
诚征版主,祝内核版越办越好!
编译内核时有很多东东不明白什么含义,哪位知道什么地方可以找到比较全面的资料
高手是怎么编译内核的啊?
编译内核之一
编译内核之三
编译内核之四
编译内核之五
编译内核之六(后记)
大家对NeXT,BeOS,Darwin这些变种如何看待,似乎国内很少有人谈及?
编译内核之二
提问:the STRUCTURE of Linux?
编译内核时,在哪部分把PCMIA卡编译掉?
微内核?进程调度?
linux的非微内核一直受竞争对手的非议,请问未来linux的发展在这方面有什么计划?(null)
一点题外话
书上说段页式内存管理是最好的内存管理方式,但LINUX的内存管理使用的是页式,为什莫?
1f是指什么,好象并没有1f的标签
编译内核是否只是简单的make config,若不是,请给我详细步骤。
那位大侠知道如何记录通过ipchains网关的数据报
要怎样建立放火墙
模块程序却出错如下:invalid parameter parm_a
块设备驱动程序的注册似乎都是通过调用register_blkdev(),
调用了netif_rx 函数。
请问tunable parameter
关于进程数
如何编写自己的设备驱动程序,又如何在C语言中调用
where the kernel start? why I can not find the function 'main()'?
请问可不可以在Linux下改网卡的硬件地址(将每个发送出去的数据包的硬件地址改为特定的值)?
编译的时候他说我的最后一行:missing seprator
lilo怎么改?image=? initrd
init在哪儿
__asm__是干什么的?
编译setup.S为什么有一大堆错误
想让内核将printk输出到messages文件
get_current(void)
在内核态,进程如何访问用户态空间的数据?
Bug大侠,该如何研究Linux的源代码
head.s中调用setup_paging时,内存0x1000起开始的几页全被清零,岂不是将内核代码head.s部分覆盖了,这是怎么回事?
内核首先读入内存0x10000处,但后又移至0x1000处,这样岂不是与内核页目录swapper_pg_dir地址冲突吗
inw()/outw()、inl()/outl(),其中b、w、l各是什么意思
如何安装3块网卡,每个网卡在一个网段
要写一个PCI卡(自制的)driver
加载modules时就提示有些目标文件中存在unresolved simbol
linux -- driver的编写 -- file_operations
装载lilo时会显示:“LILO:”,请问如何改变这个显示
关于BluePoint2.0的几个烂点:
include头文件modversions.h时,一般用什么条件?为什么我用的时候它总是和#include asm/uaccess.h冲突
用insmod装载模块时,出现了unresolved symbol
Linux的整体式结构决定了其内核的高效性
我用命令 mount -t vfat /dev/hda1 /mnt/c 发现,我的linux好象,不支持vfat
如何查看当前内核的配置参数
我们也做嵌入式,欢迎一起创业
kmalloc(),返回的地址不用设置页表,而vmalloc()需要。 这说明什么
__get_free_pages()返回的是物理地址还是虚拟地址
分析do_fork chldreg指针的赋值的问题小弟感觉很糊涂
linux -- driver -- __put_user
linux -- driver -- ioctl
How can I do a Linux Boot/Root Disk
编写驱动程序时,需要将硬件的物理地址为虚地址. LINUX内核如何保证这种影射对每个进程都是有效的.
如何在Windows或者DOS下编译内核
内核过程所允许使用的堆栈空间有多大
system.map到底有什么用
想利用时钟中断,想自己加入一些代码形成自己的中断服务程序.
用signal 的确可以做当异步地运行某个函数, 信号是否是以中断的形式运行的
起动盘为什么mount不上去
看不出head.s是如何调用start_kernal()函数的
start_kernel()中我怎么没有发现初始化网络部分的函数
head.s究竟是怎样调用start_kernel()函数的呢
Linux是不是对中断控制器重新编程过了
系统初始化
阅读setup.S原程序时,对下面进入保护模式程序段有点不理解
kernel_thread()是用来产生init进程的,然后由init全权处理进程,它怎么会初始化网络呢
段描述符高速缓存寄存器和描述符投影寄存器有什么区别
段描述符中有一位p用来区分此段是否在内存中,问题是若一段中部分叶在内存,部分不在内存,此位如何赋值
如何有效阅读内核代码?
init 进程启动之后,所有其他进程就由INIT进程全权处理。这时可以说系统内核已经完全启动起来吗?
在head.s-->;start_kernel()-->;启动init这个过程我还没有发现内核调用调度函数
能不能将内核的地址空间动态映射到用户空间的0xc0000000-0xffffffff 上去
已分配并不再使用的堆栈页面是对换到交换空间中还是直接被系统回收
Why I can't boot linux form fd(volume 1.722M) but fd(volume 1.44) can do?
调入系统模块到0x1000:0处时,为虾米还要判断es为64k对齐 为什么还要调用kill_motor
Pentium CPU CR0中的WP位是干什么用的?
CPU从用户的特权级3进入到内核的特权级0,请问这时是CPU如何完成这个中断指令的?
setup.S的bootsect_helper程序时,对于bios的15h中断的87号移低端内存到高端内存的参数有点不明白
do_mmap()函数 off&~PAGE_MASK的含义是什么?mm->;map_count 是何含义?mm->;locked_vm不是被锁定的vm的个数吗?
Oops是什么意思
为什么要去分析内核 我是菜鸟,但我是鹰的后代
setup.S中为什么需要置A20线
setup.S中移动剩余setup代码区的一个问题
那么当系统物理内存为最大值4G时,内核似乎只能管理它自已地址空间中的1G? 应用程序0-0x8048000有何作用?
pmd是指什麽
vfork的原意是什么
东东太多,我不知从哪下手!
8295A的断口号是怎么分配的
一篇ELF格式的详细说明
SYMBOL_NAME(...)和__asm__volatile(...)的功能是什么呢?
Linux中,局部描述符表LDT有何用呢?
__pa(x)是干什么用的
内核不使用虚拟内存,但是却把0xc0000000-4g的地址做为内核保留地址,这是怎么一会事
vmalloc分配的内存能否被swap out
ping的源代码
MEM_ALT_K是什么
__volatile__是什么功能
将一个极小的GUI机制引入内核可否?
Linux console font operation
在共享库定义共享段
Linux交换内存的一个缺陷
do_page_fault部分时,对里边提及的Pentium cpu缺陷(f00fc7c8冻结指令)很迷惑
解读vmlinux.lds.S
fixmap_init()函数是用来干虾米的?
lcall7 读trap_init()
paging_init()中的end_mem = (end_mem + ~mask) & mask;
static定义的变量放于哪个段
_edata和_end在哪儿定义
如何往内核加载模块
勇敢地蜕去你的陈年旧皮!
do_IRQ()中断号错误码的正负问题
build_irq"#"前缀
编译内核VFS:can't open root device 08:0a
mem_map结构数组中每个页面的age值是在什么时候更新的
GCC为2.96版,这是个非稳定的版本
说说用户进程的页面切换
kernel_threadregs.esp由从何而来呢?
Too many open files"问题
3g的虚拟内存到物理内存的映射是否都在task_struct中
为什么要有8M的隔离带?
为何总是报错VFS:unable to mount root fs on 08:01?
\linux\kernel\init\main.c开始的部分其偏移地址是否都被编译为从大于3G开始
lds定义了内核映象所有符号从PAGE_OFFSET(3G)处开始编址
Linux目前的体系只能管理2G物理内存?
编译好的内核为什么要要经过压缩?
系统状态保存在当前进程任务结构和内核堆栈中。进程间的切换点都在schedule()中
setup_arch()函数是如何得到命令行参数的
进程数据结构页面是如何保证不被换出内存的?
问_end的定义
mount和insmod一个模块的区别?网卡的混杂模式是什么意思?
mmap用于进程间文件共享,SYSV IPC用于进程间内存共享
当当前进程current的调度策略是FIFO时,其时间片current.counter为何没有重新赋值
加载模块时,想给参数赋值
开机后不要人干与就直接可起动我的应用
FIFO策略进程的时间片(counter)没有重新赋值
如何使linux进程的堆栈是不可运行的?
可重入 和 ret_from_intr
FIFO的进程其时间片在schedule()中被忽视而没有重新赋值.请问这是否会导致它被频繁的重新调度呢?
推荐内核分析风格
__asm__ __volitile__
我的看法
如何编程接收ppp0上的裸IP包?
PS/2鼠标工作原理和硬件编程的技术资料
请教如何读linux的Kernel
模块的版本相关性指什么?
当物理内存超过3G时就管理不了?
lcall7的入口处比system_call多压栈了一项pushfl
原代码看的工具lxr是怎么用的?
调用_free_page()后,该内存页真的被释放了吗
free_area_init()中bitmap_size 是否有问题?
对try_to_swap_out()的一点改进
Where is file for mapping kernel virtual address
有没有关于gunzip()的解压算法
gunzip()函数在解压缩piggy.o(真正的内核)时,是如何定位或寻址到piggy.o的内存地址的呢
堆中内存是如何分配的
这里的set_pte使我很疑惑,为什么用的不是物理地址,而是加上0xC0000000的虚拟地址呢
编译时决定virtual address吗?
boot.S, setup.S ... is running with real-mode?Need not mmu settings?
idle是内核线程,init线程已转变为普通进程
a paper about debugger
由于ELF中允许用.section自定义段
trampoline乱谈
SMP不太熟悉,有几个入门性的问题
如何在内核中获得键盘和鼠标的消息
引用内核中变量求助 EXPORT_SYMBOL()宏添加相应内核符号
outb_p :where define?what does "%w1" mean ?
Linux太难学了
读kmem_cache_estimate ()这个函数
kmem_cache_estimate 中的 L1_CACHE_ALIGN
slab分配器的设计思想
page aligned 是什么意思
用nice等命令设定的进程优先级有什么确切的含义
模块中的全局符号自动添加到系统符号表中,模块中不能使用EXPORT_SYMBOL(), 可用EXPORT
在内核态与用户态转变时,地址映射是相同的,堆栈区域不同
linux核心程序中怎样打开设备呢
怎样调试内核???gdb行吗?
硬件内存在系统内存空间中的映射问题
和malloc,calloc一样,一般是用sbrk系统调用实现的
Linux头文件中定义了进行串操作的函数,驱动程序可以使用它们来获得比C语言写的循环更好的性能
readahead使用的各个变量的意义
About kernel stack
section __ex_table,"a"和.previous 以及.fixed
请教各位如何使用SYSCTL的问题
System Call is a limitation??
About GET_CURRENT
Linux的TCP/IP协议栈阅读笔记(1)
Linux的TCP/IP协议栈阅读笔记(2)
Linux的TCP/IP协议栈阅读笔记(3)
Linux的TCP/IP协议栈阅读笔记(4)
Linux的TCP/IP协议栈阅读笔记(5)
Linux的TCP/IP协议栈阅读笔记(6)
Linux的TCP/IP协议栈阅读笔记(7)
About the Linux Kernel Analysis Book
Linux的TCP/IP协议栈阅读笔记(
请大虾们推柬一下读核工具
请问内核2.4版本的zone分配器的设计思想
可否在内核中进行截短文件操作?
在模块中调用系统调用
请问怎样正确查找函数原型?
MTRR是什么?
Linux-2.4.0网络部分改变
About multiple txt segment in an elf
建议阅读linux device drivers
模块化编程可否替换所有的系统调用
How to release a Module by itself ?
Linux设备驱动程序勘误表(部分)
内核中替代realloc()函数 的具体实现方法
什么是NR?
MAP_FIXED是什么固定映射?
正交持续性 自反系统
为何要保存flags? 只用cli()和sti()不可以么?
如何写直接读写硬盘的驱动程序
这种kmalloc,优先级参数应为GFP_AUTOMIC?
因为"Hello,World"在内核段中,使用段超越试试看
##是文本连接运算符
详细介绍slab的linux实现的文章连接
Linux防火墙程序设计
编译内核后,一定要重新链接System.map到新的System.map吗?
tty 到底是谁的abbreviation
Linux 模块调度问题和抢占
about EXPORT_SYMBOL
什么叫映象文件
mm/memory.c和mm/vmalloc.c各负责什么功能
怎样把自行编写的设备驱动程序添加到Linux核心中
谁知道netstat下的TIME_WAIT如何产生和避免
内核代码中经常使用固定数组而不是链表是为了编程简单?
用ioperm申请要存取的端口范围的访问权
TIME_WAIT状态有什么用
要先包含<linux/module.h>; <linux/kernel.h>;然后sleep_on_interruptible_timeout就正常了,不会崩掉了
函数init()的最后执行了 execve()函数,为什么内核代码没有被execve()所创建的新进程覆盖掉
dput()和dget()
个时间片大小是固定的吗?
kmem_cache_grow()开头的一串标志检测也让我不理解
[目录]
--------------------------------------------------------------------------------
list2
Shaper是一个限制网络速率的虚拟网络设备
jmpi go,INITSEG 是什么意思?
关于进程的flag,以及调度的一些概念
为何在 console_init()之后还不能打开/dev/console呀
do_wp_page
内存管理--end_mem解读
Unable to handle kernel paging request at virtual address...是从哪个模块报出来的?
按照原来配置重新编译2.4.0,重启后,屏幕显示:Uncompressing Linux ...Ok,booting the kernel.后就死机了
如何研究内核
有关kdev_t结构与次设备号的问题?
内存管理--free_area结构解读
内核地址手工转换,多是在填写页表时用到
进程管理--在时钟中断处理中为什么没有调用schedule函数
内存管理--memmap解读
请教Gcc源码的阅读问题
Unable to handle kernel paging request at virtual address....
Linux源代码讨论专用线索
我所看到的switch过程以及我的理解
我实现了一个进程切换方法
VFS: can't mount root filessystem . 这是怎么回事儿
内核栈
head.s中的LGDT装入gdt_descr处的内容...
内核空间在0xc0000000之上,但是如何使内核程序中访问的变量等的线形地址能够达到这个范围之上呢(因为段描述符的基地址是0),这是怎么做到的?(lds?)
汇编语言的语法
增加系统调用
块设备驱动,DMA内存,IDE硬盘的预读
traceroute的问题
在用户空间编写驱动程序
内核中的高端内存选项是怎么回事
在内核空间访问用户空间的问题
在地址0000开始的作了个中断向量表,这个是bios引导后实现的,还是dos获得系统控制权利后才做的事情??
中国的操作系统为什么停滞不前
在文件系统中struct dentry 是用来描述什么?
在x86平台上,io空间跟内存空间是分别编址的吗
Heap and Internal Fragmentation
文档最新动向 3月5日
/dev/ram,/dev/tty1等等,这些i节点是如何被创建的
消息可以代替信号?
About brk value, malloc, and heap
About contigeous virtual memory
About brk explaination from book.
在linux中,是怎么实现动态连接库的共享?地址和重入
raw disk I/O 的资料
todo & 遗留问题 & 计划介绍
一些资源
专题认领
源代码学习专题认领
编译过多次,但还是有些问题不能理解
外部中断的驱动怎么知道自己的3.硬件产生哪个中断?
对bottom half概念的一点理解
进程切换的时机
http://www.xfree86.org/
ftp://metalab.unc.edu/
http://www.xfree.org/FAQ/
http://www.xfree.org/#resources/
http://www.kde.org/
http://www.qt.org/
http://www.gnome.org/
http://www.gtk.org/
http://www.enlightenment.org/
http://www.opengroup.org/openmotif/
http://www.lesstif.org/
http://www.windowmaker.org/
http://www.gnustep.org/
http://www.itresearch.com/
ftp://ftp.funet.fi/pub/Linux/PEOPLE/Linus/SillySounds/
http://members.xoom.com/gnulix_guy/geek-gourmet/
http://www.csustan.edu/bazaar/
init process
在理解linux虚存的时候,我确总有些绕不过来
希望解决:3.硬件产生哪个中断的判别问题
Makefile 初探
所有的进程都公用_LDT(0)?
BUILD_IRQ宏
虚存难绕
Makefile解读之二
进程陷入内核时CR3的内容会改变吗?如果不变,如何存取内核空间呢?
barrier()的作用
Makefile解读之三: 模块的版本化处理
LDT:有点眉目了
系统调用流程
LINUX的系统内核空间的保护
Makefile解读之四: Rules.make的注释
HELP! printk() does not work in device module
as86汇编语言的语法说明
中断嵌套的问题
问题犹在:BUILD_COMMON_IRQ的宏展开
中断的部分代码解读
内核初起时如何从核心态进入用户态?
ASM格式简介
增加系统调用时的问题,虽已解决,但有的地方不太明白。
8259A的工作原理
Linux下的jiffies是10ms吗?
fork进程的学习
netfilter.c剖析1
getpid()
netfilter各个HOOK的关系
分析sockfs套接字文件系统
netfilter剖析2
backlog field in sock struct
所有进程在内核态的地址空间是一致的,可以相互访问的么
外设中的目录项(以EXT2为例)和内存中的"目录项"的比较
关于文件系统的安装与访问
Kernel 2.4中bottom half好象已经演化到soft-interrupt了
从系统调用open看源码
分析内核对gzip压缩文件进行解压的方法
sock 中zapped成员表示什
ip_tables.h分析
initrd 是干什么用的?
内核对以太网设备的探测过程
ISA网卡驱动程序的探测过程
sock结构的链接问题
read_lock()和write_lock()的过程描述如下
UNIX系统技术内幕》的第七章,介绍了自旋锁,看不懂他的改进
ISA网卡驱动程序发送和接收过程
EXT2的超级块与组描述符
新兵笔记--ULK(C2) beta版 Segmentation in Linux
网络包的排队发送过程
Linux secret.... (maybe)[Cross post]
Export了怎么还是无法resolve?
新兵笔记--ULK(Understanding the Linux Kernel) 序
关于netfilter的一点问题
ip_tables.c中组织规则的方式
内核打印的限速函数 net_ratelimit()
Confirm SA_INTERRUPT
trap和中断有什么不同啊
假如我要把PLT映射到内存的低端
Linux的硬件地址解析过程
inode和block之间有什么联系和区别
ip_tables.c的防火墙规则处理
内核堆栈与GET_CURRENT
根文件系统指的是哪个? 如何安装?
进程映象的过约束方法(overcommit_memory)
新兵笔记--ULK(C2) beta版 Paging in Hardware
ip_tables.c中防火墙规则环的检测
建立双向链表的一种常见方法
将root文件系统也放在一张软盘上
伙伴(buddy)算法的页面释放过程
ERR_PTR PTR_ERR(ptr) IS_ERR(ptr)
路由缓冲表的基本结构
IP包的接收过程
ip_tables.c的面向应用程序的接口
关于目录文件对象操作的一致性问题
IP包碎片重组过程
缓冲区管理与块设备
IP包的本地分发过程
vm_struct vs. vm_area_struct
从一个函数返回时,做什么?
套接字的基本结构
AT&T汇编与Intel汇编的比较1
AT&T汇编与Intel汇编比较2
设备驱动几个数据结构关系
对数据报套接字文件的select过程
MODULE_PARM(var,type)的功能是什么
EXT2:超级块----外部结构
EXT2:超级块----读入
EXT2:组描述符----外部结构
EXT2:组描述符----读入
EXT2:组描述符----查找
EXT2:inode位图----外部结构
EXT2:inode位图----读入
EXT2:概述
新兵笔记--ULK(C2) Paging in Linux
EXT2:inode----外部结构
EXT2:inode----查询与读入
EXT2:inode----分配
包缓冲区操作的几个内嵌函数
包队列操作的一些内嵌函数(续)
About init_mm and processes' mm
包缓冲的分配操作
信号处理问题
ip_tables.c的各个注册函数解释
execve的疑问
FD_SETSIZE问题,我补充了一点说明
get_user的使用
AT&T汇编语言的帮助文件
扩展的行内汇编手册
IP包的生成和发送接口
Linux网络接口层分析(之一):netif_rx
Kernel Hacking
open系统调用中对用户指针的检测
movl $pg0-_PAGE_OFFSET,%edi 其中的$pg0-_PAGE_OFFSET是什么意思?
关于Linux的路由
Segments are as follows:ds=es=ss=cs-INITSEG?
__get_free_pages()和free_pages()是最低层的内存分配和释放函数
新兵笔记--ULK(C3) Process Switching
EXT2:块----外部结构
[目录]
--------------------------------------------------------------------------------
list3
LOOPBACK网络回送设备
网络设备的IP地址结构
pci_bios_find_device
网络过滤器的基本结构
表驱动IP过滤器的基本工作过程
barrier()的疑问
open打开一个设备时,内核做了一些什么事情后才去调用相应的驱动的呀
新兵笔记--ULK(C3) Process Descriptor
核心中内存访问是否要进行页表转换
__init具体是什么作用
do_softirq() 初步探讨
内核semaphore初步
[rainfall]系统调用
时钟概述
init进程如何从内核态切换到用户态。
init进程从内核态切换到用户态(2)
我觉得现在的进度比较慢。
8259A的编程原理(2?)
分析rwlock的结构
分析rt_sigframe结构
tasklet_action()初步
分析access_ok的结构
请问2.4中实现net_bh功能的函数到哪里去了
分析test_wp_bit的过程
ptype_all结构的一点理解和网络初始化以后的组织图
set_call_gate以及其中的汇编
zone allocator
虚拟内存的管理,搞不懂!!
分析内核检测CPU时钟频率的过程
Process Scheduling灌水版
at&t汇编说明
分析几个页目录处理的宏
Solar Designer设计的stack不可运行的patch
外部中断的上半部分(一)
怎样取得skb结构中的内容!
Linux调试技术介绍
如何编程获得系统时间
(*(unsigned long *)&jiffies)与jiffies有何不同
谁能改变中断的优先级别
分页,自己瞎琢磨,想破头都搞不定
什么情况下需要使用volatile修饰符
硬件中断:8259部分
关于保护模式
不是linux的问题-一个切入保护模式的问题
内核重入
kernel和modules的区别
分析进入和退出VM86模式的过程
分析get_wchan()过程(调度)
分析两个用于进程链表的宏:SET_LINKS和REMOVE_LINK
中断门的设置
强烈推荐大家看看《Linux内核原代码分析》
硬件中断的入口
关于ptype_all链表和pypte_base的理解
什么是footprint,hotpath?
VM86模式下的保护异常处理
分析一下FPU状态的切换
2.4.1中lock_kernel()和unlock_kernel()的问题
在什么情况下会触发一般保护故障(GFP)
linux内核内存分配初探
足印footprint
分析__udelay()算法
分析进程信号队列的结构.
如何捕获所有的ip包
分析spinlock的结构
从系统调用中返回
在mm_struct中,active_mm 表示什么
内核只是将用户虚存区域描述成vm_area_struct链表
Page Table and Memory Cost
解释一下__attribute__
对semaphore初步分析
ret_from_intr 非抢占的判断
map_user_kiobuf 注释
解释一下memcmp
小课题 & 焦点(请跟贴)
十分的困惑--get_user?
软中断「一」
粗略分析一下Linux对进程调试的支持
谈一谈gcc编绎出的跳板(trampolines)代码。
_end在那儿定义的
使Linux堆栈不可执行的简易补丁
我的错误,大家可以借鉴(driver)
map_user_kiobuf的注释
[ jkl ]active_mm是什么
对BUILD_IRQ中 -256的解释
对重入的理解
分析内核模块的结构
内核空间偏移3G的疑问
VFS文件系统(1)
防火墙技术分析讲义( for aka lecture)
linux的Kernel映像可以存放在压缩文件系统中吗
VFS文件系统(2)
About swap_page_dir, kmalloc
分析内核模块的加载过程
内核变量声明问题 (switch)
free_area 中的map
About 2 pages of task_struct
About vm_area
About symbol and loadable module
定时器:实现机制:思想
Linux Kernel中,对线程的支持如下
os的对于进程的内存分配是基于线性地址还是物理的
About Copy On Write--do_wp_page()
分析用户对文件访问权的算法
分析任务切换时虚存的切换
关于kmalloc和vmalloc的问题
VFS文件系统(3)
分析路径名搜索过程
分析文件页缓冲结构
解读softirq
内核也好,用户进程也好都是线性0-4G地址
Linux内核网络参数的意义及应用
我对spinlock中rep nop的理解
请教有关ELF文件格式
网络设备的初始化是在何时做的
set_fs(get_fs())应改为set_fs(KERNEL_DS)
2.4的file_operations结构问题
Qdisc链的含义
请教关于GDT和LDT的问题?
为什么找不到__initcall__start变量的赋值语句(lds)
分析文件名检索的散列算法
软中断
dev_queue_xmit真不知道是怎么“蹦”出来的??
分析IDE硬盘驱动器读写过程
"paranoia"问题
__init free_area_init 中bitmap_size = (end_mem -PAGE_OFFSET) >;>; (PAGE_SHIFT + i); 的问题
讲讲notifier机制-看到register_netdevice_notifier是的困惑
定时器:介绍&使用
malloc是如何调用内核的服务来实现的
mmap 的问题
分析list_head结构
可以自定义内存区的操作函数吗
内核中网络部分问题
进程调度
应将__MODULE__改成MODULE。
硬件中断处理的三个对象
关于内核内存分配, 依然需要努力
想读内核,可是lxr始终不能用
看了二个多月的源代码很气馁!
分析RAMFS文件系统
讲解一下文件系统中主要的数据结构
虚拟文件系统的基本原理
#define MODVERSIONS(模块的问题真多)
我想恐怕可以提前结束了。<Linux的文件系统分析>
示例clone系统调用的使用方法
__builtin_constant_p()在哪里
页IO
请教一个模块编程键盘中断的例子
几个非阻塞socket连续向一个ip的不同端口connect 时
分析应用程序加载时堆栈中的参数结构
分析ELF的加载过程
请问“:”是什么意思? (fixup
Linux动态链接技术
setup.S的到head.S的跳转在那里
fib_lookup的疑问:fib表是如何构造以及在何时构造的呢?
vm_area_struct 的vm_flags 有一位VM_SHARED是什么意思
某些语句会导致gcc隐含地生成一个memset()调用
系统调用时的NR_syscall中是什么地址
netfilter各个HOOK的关系
将核心空间的内存映射到用户空间,或者将用户空间的内存映射到核心空间
netfilter.h的解释
有关ext2 super_block 的疑问
分析ext2文件系统物理块的分配过程
connect超时时间的一点探讨
关于TCP连接的应答问题
可以把整个ext2的分区的东西移植到reiserfs的分区?
内核是怎么得到网卡的硬件地址的
关于as86的一些资料
ip_input,ip_forward和ip_output
分析ext2文件系统文件块的分配过程
lvm技术
网卡混杂模式(promisc)
分析EXT2文件系统目?
主机端口
关于pagh_hash函数
关于framebuffer的问题
BUG()?
about tss?
ip_nat_hash的参数问题?
What's numa and CONFIG_DISCONTIGMEM
net_family_read_lock,为什么要lock,其中的原子操作是什么
tty终端的写入过程
Linux-2.4等待队列头的结构有了很大变化,应该用DECLEARE_WAIT_QUEUE_HEAD()定义等待队列头
I can't catch some codes in kernel/sched.c , why?
LKM中分配内存的问题。
When I read do_fork() in kernel/fork.c,??
新兵笔记--ULK(C4) Returning from Interrupts and Except
tty终端文件的异步操作
Why not find tss in task_struct
请教关于LINUX设备驱动编程的问题
Linux下IP――分片与重组
发送、接受、转发包处理结构框图--again:)
rt_priority?
PPP驱动程序的基本原理
有没有人对Ipsec感兴趣?
FIB的数据结构问题?
驱动程序与用户进程
谁能告诉我atomic_inc(Atomic.h)函数是什么意思?
BSD伪终端设备驱动程序
Help me check some codes about bootmem
uname的系统调用?
请教:tty.h 中的宏定义
What's meaning of "mapping" in struct page
a question about memory_pressure
local_table是什么?在哪里初始化的?inet_addr_inet函数有点看不懂
如何获得网络因CRC校验错丢失包的个数
struct page {...struct buffer_head *buffers...};
转发表的检索过程(fib_lookup)
ZERONET(x)和BADCLASS(x)是怎样确定广播地址的
哪位大侠帮忙解释一下fn_hash_lookup函数的作用,谢谢!
策略路由的NAT和netfilter的NAT的区别
请问get_fs和set_fs的作用
__attribute__ 是何意?
do_wp_page()函数的疑问??
PPP帧的发送
各位大虾,键盘如何来处理??
LinuxKernel的错误?--“console的问题
Linux设备驱动程序的中断
在中断处理中分配内存时没有使用GFP_ATOMIC标志
make modules" error on rh7.1
Booting:BIOS POST?
tcp_hashinfo结构里的那些hash table都是什么作用?
关于mmap的问题
Linux上gdb如何跟踪调试进dlopen内??
请问msg_name派什么用处?
分析ip_route_output_key( )中的一个疑惑?
为什么check_region不可解析?
对volatile的解释
套接字地址的绑定
关于2.4的内核初始化的问题
关于free_area空闲块数组?
一个LKM的初级问题 (包含头文件)
关于Ext2文件系统
嵌入式Linux内核精解
freeswan中,谁在操纵SADB?
linux下几种地址的区别
http://www.sgi.com/processors/r10k/timing/perf_count.html
我们的CPU-胡伟武
一个关于通过/proc与内核通信的问题
关于BOOT引导的问题?
中断描述符分析小问.
分析块设备缓冲区结构
kmalloc等内核常用函数的文档有没有
关于buffer cache的问题
进程,轻量级进程,内核线程,库线程的切换分别是由谁来负责,切换时都分别保存了什么信息?
为什么 virt_to_ phys( )不能用
分析sigframe的结构
分析信号的执行过程
请教核心0.01中head.s的检测A20地址线代码的意义
我在Kernel里定义的函数,在用户空间里怎么才能用呢
关于内存布置的问题
lilo与bootsect.s,setup.s的关系
有谁知道这个结构多少字节
start_kernel(void)函数的分析
分析虚拟文件系统的结构
slab
在驱动程序中可以直接从I/O内存读数据到用户空间?
start_kernel()后是怎么工作的
内核程序中分配内存最大是多少?
有关gcc内嵌汇编代码的问题
请问.S和.s文件的不同?
关于汇编
http://www.linuxassembly.org
Linuxkernel推荐
关于AT&T格式:
http://www-106.ibm.com/developerworks/linux/library/l-ia.html
lisoleg推荐
关于汇编:
http://www-aig.jpl.nasa.gov/public/home/decoste/HTMLS/GNU/binutils/as_toc.html
http://www.linuxassembly.org/resources.html#tutorials
lucian推荐
别谢我,这都是从http://www2.linuxforum.net/ker_plan/index/main.htm
找到的,有空去看看。
///
你可以下载bochs:http://sf.net/projects/bochs/
其中就有BIOS的汇编源程序,你认为是C也可以(C也内嵌着汇编嘛!).
不过,这个BIOS比较简单, 功能比较弱.
/
__builtin_constant_p 与移植
象CTRL~C和CTRL~V这样的中断信号内核中何时被初始化
A20可以通过两种方式进行控制
大概的意思是将代码段放到.text.init中去,至于为什么要这样做,这里有对于__section__的讨论。
http://gcc.gnu.org/ml/gcc/2000-05/msg00536.html
你可以去看看。
calibrate-delay
最大分配128K?
Makefile中$<是什么?
请教有关usb编程的问题
[ chstar ]linux内核内存分配初探
links:
http://developer.intel.com/design/pentium4/manuals/
documents in pdf is good and enough for intel asm
Linux内核源代码漫游
386硬件支持得任务切换如何进行
linux0.0.1的内存转换问题
请问在内核中 HZ 是干什么用的
哪里有2.2内核的写驱动程序的文档
Ramdisk 和initrd,有什么用处?
诸位高手,看内核,从哪个入手的
哪位高人了解此函数usb_submint_urb
do_page_fault( )的几点疑惑
硬盘启动时,setup存储在哪里
内存转换问题
再问有关存储的三个问题
为什么动态连接库一定需要一个符号连结文件
我在查找内存分配错误的时候,找到了这 (ld 的_end)
build kernel步骤
.text .data .bss不要了会怎么样?
漫谈linux ieee1394
linux下能不能用c++来开发驱动程序呢
kpiod进程是做什么用的
关于内核内存初始化
请教一个linux下分段与分页的问题
spinlock_t?
请教mmap()的问题
有关ramdisk的问题
关于在proc下创建一个新项源代码的疑问
一个关于通过/proc与内核通信的问题
proc 文件系统分析(二)
对proc文件系统的分析(三)
proc文件系统分析(四)
关于南桥82371AB中断
创建进程和添加进程代码的概念
那位朋友讲一讲Linux对GDT的布置吗
增加系统调用的资料
http://202.113.16.117/cgi-bin/bbscon?linux/M.1005336295.A=4959
锁的实现
http://groups.yahoo.com/group/lisoleg/files/MultiProcessing/
请教关于内存访问的问题
更改后的发送、接受、转发包处理结构框图
386 boot代码分析
请教关于assert
对 proc 文件系统的分析(一)
逻辑地址如何转换为线性地址
Linux启动问题
内核中无法睡眠?
一个内核网络代码的问题:skbuff->;dst_entry跟rtable结构是什么关系?
pci_bios_read_config_byte
ODL-One Disk Linux
[jkl]宏#与##(例子见BUILD_IRQ)
bbs.zsu上讨论的一个关于arp的问题,至今没有好的答案
有关linux引导扇区的问题?
谁能介绍一下变量的属性是如何设置的?
内核程序无法睡眠(续?)
bbs.zsu上讨论的一个关于arp的问题,至今没有好的答案 (又贴了一遍?)
分析serial.c
HZ问题
就是为什么可以用send_sig_info使阻塞的函数返回的原因
为什么一执行lidt就重起
只知道可用宏put_user和get_user macrocs使内核能够存取用户内存缓冲区。但究竟怎么用呢
setjmp、longjmp的问题
在内核中如何得到当前系统的时间
给个内存管理的整体概念先
请问各位,printk()将调试信息写到哪去了
limit标志表示分配区内可分配页面的富余程度
双向规则是什么意思
What's diefference between vmlinux & bImage
rt cache的问题
用户程序可以自己将自己切换到内核态吗
keywords: execve,vfork,kernel_thread,vm
如何在系统加一个自己的ioctl命令号
Linux Notification 机制的分析
请问__builtin_return_address在哪儿定义的
打开A20地址线为什么要写0x60口?
[ Feiyun ]linux/arch/i386/kernel/head.S (2.4.9)
[ Feiyun ]linux/arch/i386/boot/bootsect.S (2.4.9)
[ Feiyun ]linux/arch/i386/boot/setup.S (2.4.9)
[ Feiyun ]linux/arch/i386/boot/compressed/head.S
[ Feiyun ]Linux makefiles (Linux 2.4.9)
在unix下可以加入自己的协议模块吗?......
如何将Module和整个内核分离开?
中断服务程序怎么没有运行
something about neighbour Table
关于netfilter 里nfcache字段
建议用module_init()宏来调用card_init
请教一个内存管理的问题
中断描述(todo用)
stack of kernel mode and user mode
交换缓冲
分析信号的发送过程
Signal 11 while compiling the kernel(转)
堆栈上的current指针
一大堆序列号呀?
关于local的含义
关于nmap的问题 (tcp)
LINUX TCP层分析之一-------------主动连接发送SYN包的tcp_connect
缺frame和缺page table是如何区分的?
C.O.W到底到那一级
do { } while (0)是什么意思
怎样得到kmalloc分配的内存地址的实际物理地址
asmlinkage
怎样获得系统的精确计时?越精确越好
这个bug是由于内核将用户进程在内核空间引起的页保护故障错误地当成了内核vmalloc区域的页不存在故障所致
Linux下IrDA方面的开发资料
关于smp的疑问
TLB到底存储了写什么
有关任务切换的问题
内核空间的预映射是怎么回事
我知道肯定是有些我没注意到的代码改动了skb->;len
about improved long long code sequences by linus
内核和用户态怎么共享内存
网络部分的proto_ops的方法是如何重载的
裁减内核到多少K等等,那么具体是那个文件的大小呢?
请教如何精简LINUX到8M以下
有关等待队列的问题
内核方式是否需要三级页表转化
[目录]
--------------------------------------------------------------------------------
list4
elf_check_arch是什么函数
请问#APP和#NOAPP是什么意思
什么是L1 cache aligned
d_cache 与 inode _cache的问题
linux网络部分重要结构体联
}
if (--cx == 0)
return TIMEOUT;
}
*pByte |= (c0 << 1) & 0xf0;
data_write(0x00); /* send ACK */
return OK;
}
为了能够在setup.S下收字符,特将字符接收子程序该为AT&T汇编语法(也没有什么好办法,在DOS下用TURBO C 2.0将上述代码编译成汇编代码,然后手工转换成AT&T格式,据说有程序可以自动进行这样的转换,有谁用过请指教):
rcvbyte:
pushw %bp
movw %sp, %bp
subw $6, %sp
movw $511, -2(%bp)
movw $-1024, -4(%bp)
jmp .L13
.L15:
movw $889, %dx
inb %dx, %al
movb %al, -6(%bp)
testb $128, -6(%bp)
jne .L16
inb %dx, %al
movb %al, -5(%bp)
movb -6(%bp), %al
cmpb -5(%bp), %al
jne .L17
jmp .L14
.L17:
.L16:
subw $1, -4(%bp)
sbbw $0, -2(%bp)
movw -2(%bp), %dx
movw -4(%bp), %ax
orw %ax, %dx
jne .L18
movw $1, %ax
jmp .L12
.L18:
.L13:
jmp .L15
.L14:
movb -6(%bp), %al
shrb $1, %al
shrb $1, %al
shrb $1, %al
andb $15, %al
movw 4(%bp), %bx
movb %al, (%bx)
movb $16, %al
movw $888, %dx
outb %al, %dx
movw $511, -2(%bp)
movw $-1024, -4(%bp)
jmp .L19
.L21:
movw $889, %dx
inb %dx, %al
movb %al, -6(%bp)
testb $128, %al
je .L22
inb %dx, %al
movb %al, -5(%bp)
movb -6(%bp), %al
cmpb -5(%bp), %al
jne .L23
jmp .L20
.L23:
.L22:
subw $1, -4(%bp)
sbbw $0, -2(%bp)
movw -2(%bp), %dx
movw -4(%bp), %ax
orw %ax, %dx
jne .L24
movw $1, %ax
jmp .L12
.L24:
.L19:
jmp .L21
.L20:
movb -6(%bp), %al
shlb $1, %al
andb $240, %al
movw 4(%bp), %bx
orb %al, (%bx)
xorw %ax, %ax
movw $888, %dx
outb %al, %dx
jmp .L12
.L12:
movw %bp, %sp
popw %bp
ret
能够收发字符还不行,作为协议,总得知道数据的起始和结束,也应该进行简单的检错。这里采用字符填充方式进行数据包编码,用‘\’表示转义字符,数据包头用\H表示,数据包结束用\T表示如果数据中有'\',则用\\表示(从printf的格式串中学来的),数据包后面跟一个字节的校验和,这样就可以收发数据包了,具体程序如下:
int rcvpack(unsigned char * pData, int * pLength)
{
int ret;
int length;
unsigned char checksum;
int maxlength;
int status;
maxlength = *pLength + 1;
if (maxlength<=0)
return FAIL;
if (pData == NULL)
return FAIL;
checksum = 0;
length = 0;
status = 0;
while (1)
{
unsigned char ch;
int count;
count = 10;
while (1)
{
if ((ret = rcvbyte(&ch)) != OK)
{
count--;
if (count==0)
{
printf("\nReceive byte timeout\n";
return ret;
}
}
else
break;
}
switch (status)
{
case 0:
{
if (ch == '\\')
{
status = 1;
}
}
break;
case 1:
{
if (ch == 'H')
status = 2;
else
status = 0;
}
break;
case 2:
{
if (ch == '\\')
{
status = 3;
}
else
{
length ++;
if (length>;maxlength)
{
printf("Buffer overflow(%d>;%d)\n", length, maxlength);
return FAIL;
}
*pData++ = ch;
checksum += ch;
}
}
break;
case 3:
{
if (ch == '\\')
{
length++;
if (length>;maxlength)
{
printf("Buffer overflow (%d>;%d)\n", length, maxlength);
return FAIL;
}
checksum += ch;
*pData++ = ch;
status = 2;
}
else
if (ch =='T')
{
unsigned char chk;
*pLength = length;
if (rcvbyte(&chk)!=OK)
return FAIL;
if (checksum==chk)
{
return OK;
}
else
{
printf("ERROR: Checksum is nozero(%d-%d)\n", checksum,chk);
return FAIL;
}
}
else
{
printf("ERROR: a '\\' or 'T' expected('%c')!\n ", ch);
return FAIL;
}
}
}
}
}
int sendpack(unsigned char * pData, int length)
{
int ret;
unsigned char checksum;
checksum = 0;
if (length<=0)
return OK;
if ((ret = sendbyte('\\')) != OK)
return 1;
if ((ret = sendbyte('H')) != OK)
return 2;
while (length>;0)
{
unsigned char ch;
ch = *pData++;
checksum += ch;
if ((ret = sendbyte(ch)) != OK)
return 3;
if (ch == '\\')
{
if ((ret = sendbyte(ch)) != OK)
return 4;
}
length--;
}
if ((ret = sendbyte('\\')) != OK)
return 5;
if ((ret = sendbyte('T')) != OK)
return 6;
if ((ret = sendbyte(checksum)) != OK)
return 7;
return OK;
}
同样,也将rcvpack改成AT&T汇编(减少了几个printf语句):
chbuffer:
.byte 0
overflow:
.string "Buffer overflow..."
rcvpack:
pushw %bp
movw %sp, %bp
subw $12, %sp
pushw %si
movw 4(%bp), %si
movw 6(%bp), %bx
movw (%bx), %ax
incw %ax
movw %ax, -6(%bp)
cmpw $0, -6(%bp)
jg .L26
leaw overflow, %si
call prtstr
movw $2, %ax
jmp .L25
.L26:
orw %si, %si
jne .L27
movw $2, %ax
jmp .L25
.L27:
movb $0,-8(%bp)
movw $0, -10(%bp)
movw $0, -4(%bp)
jmp .L28
.L30:
movw $10, -2(%bp)
jmp .L31
.L33:
# movw -4(%bp), %ax
# addb $'0', %al
# call prtchr
leaw chbuffer, %ax
pushw %ax
call rcvbyte
popw %cx
movw %ax, -12(%bp)
orw %ax, %ax
je .L34
decw -2(%bp)
cmpw $0, -2(%bp)
jne .L35
movw -12(%bp), %ax
jmp .L25
.L35:
jmp .L36
.L34:
jmp .L32
.L36:
.L31:
jmp .L33
.L32:
pushw %si
leaw chbuffer, %si
movb (%si), %al
movb %al, -7(%bp)
popw %si
# call prtchr
movw -4(%bp), %ax
cmpw $3, %ax
jbe .L58
jmp .L56
.L58:
cmpw $0, %ax
je .L38
cmpw $1, %ax
je .L40
cmpw $2, %ax
je .L43
cmpw $3, %ax
je .L47
jmp .L56
.L38:
cmpb $92, -7(%bp)
jne .L39
movw $1, -4(%bp)
.L39:
jmp .L37
.L40:
cmpb $72, -7(%bp)
jne .L41
movw $2, -4(%bp)
jmp .L42
.L41:
movw $0, -4(%bp)
.L42:
jmp .L37
.L43:
cmpb $92, -7(%bp)
jne .L44
movw $3, -4(%bp)
jmp .L45
.L44:
incw -10(%bp)
movw -10(%bp), %ax
cmpw -6(%bp), %ax
jle .L46
movw $2, %ax
jmp .L25
.L46:
movb -7(%bp), %al
movb %al, (%si)
incw %si
movb -7(%bp), %al
addb %al, -8(%bp)
.L45:
jmp .L37
.L47:
cmpb $92, -7(%bp)
jne .L48
incw -10(%bp)
movw -10(%bp), %ax
cmpw -6(%bp), %ax
jle .L49
movw $2, %ax
jmp .L25
.L49:
movb -7(%bp), %al
addb %al, -8(%bp)
movb -7(%bp), %al
movb %al, (%si)
incw %si
movw $2, -4(%bp)
jmp .L50
.L48:
cmpb $84, -7(%bp)
jne .L51
movw -10(%bp), %ax
movw 6(%bp), %bx
movw %ax, (%bx)
leaw chbuffer, %ax
pushw %ax
call rcvbyte
popw %cx
orw %ax, %ax
je .L52
movw $2, %ax
jmp .L25
.L52:
movb -8(%bp), %al
cmpb chbuffer, %al
jne .L53
xorw %ax, %ax
jmp .L25
jmp .L54
sChecksumFailed:
.string "Checksum error!"
.L53:
leaw sChecksumFailed, %si
call prtstr
movw $2, %ax
jmp .L25
.L54:
jmp .L55
.L51:
movw $2, %ax
jmp .L25
.L55:
.L50:
.L56:
.L37:
.L28:
jmp .L30
.L29:
.L25:
popw %si
movw %bp, %sp
popw %bp
ret
好了,万事具备了,先用上面的c代码写另外一台计算机上的“服务”程序(也用来测试),这台计算机运行DOS,用TURBO C 2.0编译运行:
运行时将initrd.img和内核编译后的/usr/src/linux/arch/i386/boot/compressed/bvmlinux.out拷贝到该计算机的c:\下,然后带参数 s c:\bvmlinux.out c:\initrd.img运行即可。
至于启动程序,还得进行少许修改,才能烧到boot rom 中,见后面的说明。
int main(int argc, char* argv[])
{
FILE* pFile;
int count = 2;
if (argc<3)
{
printf("Usage testspp [s | r] \n";
return 1;
}
while(count {
if (argv[1][0] == 's')
pFile = fopen(argv[count], "rb";
else
pFile = fopen(argv[count], "wb";
if (pFile==NULL)
{
printf("Can't open/create file %s\n", argv[2]);
return 2;
}
if (argv[1][0]=='r')/*receive*/
{
unsigned long filesize;
char buffer[10244];
int length;
/*get filelength */
length = 10244;
printf("Receiving filesize package\n";
while( (rcvpack(buffer, &length)!=OK) && (length!=4))
length = 10244;
filesize = *(long*)buffer;
printf("file size is:%ld\n", filesize);
while (filesize>;0)
{
length = 10244;
if (rcvpack(buffer, &length) != OK)
{
printf("Receive data package failed\n";
return 0;
}
if (length>;0)
fwrite(buffer, 1, length, pFile);
filesize-=length;
printf("\r%ld Bytes Left ", filesize);
}
}
else/*send*/
{
unsigned long filesize;
/*send file length*/
unsigned long stemp;
int ret;
fseek(pFile, 0, 2);
filesize = ftell(pFile);
fseek(pFile, 0, 0);
printf("\nfile size is:%ld\n", filesize);
/*
while ((ret = sendpack((char *)&filesize, 4)) != OK)
{
printf("send file size failed(%d)\n", ret);
}
*/
while (filesize>;0)
{
char buffer[10240];
long size;
int ret;
size = fread(buffer, 1, 10240, pFile);
if ((ret = sendpack(buffer, size)) != OK)
{
printf("Send data package failed(%d)\n", ret);
return 0;
}
filesize -= size;
printf("\r\t%ld Bytes Left", filesize);
}
}
fclose(pFile);
count++;
}/*while*/
return 0;
}
5、对bootsect.S的修改
目前的bootsect.S ,主要的问题是,它是从软盘上读数据,将这些代码换成对rcvpack的调用即可,另外,它不支持调入initrd,应该增加相应的代码。问题在于,bootsect.S中没有那么多空间来放rcvpack相关的代码(毕竟只有512字节,当然,如果烧在boot rom中,就不存在这个问题了,但是用软盘调试时就不行了,因此干脆编制load_kernel和load_initrd放在setup.S中,然后在bootsect.S中进行回调即可。
bootsect.S 修改如下(只给出修改部分):
.....
.....
ok_load_setup:
call kill_motor
call print_nl
# Now we will load kernel and initrd
loading:
# 先打印Loading字符
movw $INITSEG, %ax
movw %ax, %es # set up es
movb $0x03, %ah # read cursor pos
xorb %bh, %bh
int $0x10
movw $22, %cx
movw $0x0007, %bx # page 0, attribute 7 (normal)
movw $msg1, %bp
movw $0x1301, %ax # write string, move cursor
int $0x10 # tell the user we're loading..
load_kernel_img:
# 将load_kernel函数的指针放到0x22C处这里进行调用就行了(软盘启动过程中,此前已经将setup.S
# 从磁盘上调到bootsect.S,即0x0200之后,注意setup.S的头部是一张表,这里“提前”消费了)
# 0x22C is the load kernel routine
bootsect_readimage = 0x22C
lcall bootsect_readimage
load_initrd_img:
# 将load_initrd函数的指针放到0x220处
# 0x220 if the load initrd routine
bootsect_readinitrd = 0x220
lcall bootsect_readinitrd
# After that (everything loaded), we jump to the setup-routine
# loaded directly after the bootblock:
ljmp $SETUPSEG, $0
......
......
6、对setup.S的修改
对setup.S进行修改,主要是:修改setup.S头部,增加load_kernel和load_initrd函数等,具体如下。
修改setup.S头部如下(为好看,这里删除了原来的部分注释):
start:
jmp trampoline
.ascii "HdrS" # header signature
.word 0x0202 # header version number (>;= 0x0105)
realmode_swtch: .word 0, 0 # default_switch, SETUPSEG
start_sys_seg: .word SYSSEG
.word kernel_version # pointing to kernel version string
type_of_loader: .byte 0
loadflags:
LOADED_HIGH = 1
.byte LOADED_HIGH # 只支持bzImage
setup_move_size: .word 0x8000
code32_start: # here loaders can put a different
.long 0x100000 # 0x100000 = default for big kernel
ramdisk_image: .long 0xB00000 # ramdisk 调到12M处
ramdisk_size: .long 0 # 由load_initrd来设置长度
bootsect_kludge:
.word load_initrd, SETUPSEG #0x220, 放置load_initrd函数的指针
heap_end_ptr: .word modelist+1024 pad1: .word 0
cmd_line_ptr: .long 0
load_kernel_call:
.word load_kernel, SETUPSEG
trampoline: call start_of_setup
.space 1024
load_kernel和load_initrd:
load_imsg:
.byte 13, 10
.string "Load INITRD from PARPort(37"
load_kmsg:
.byte 13, 10
.string "Load Kernel From PARPort(37"
reading_suc:
.string "."
reading_failed:
.string " failed"
read_len:
.word 0, 0
read_total:
.word 0, 0
read_buffer:
# 如何在AT&T语法中完成intel语法中的 db 1280 dup(0),那位请指教
# AT&T汇编的语法何处寻?
.string "012345678901234567890123456789012345678901234567890123456789"
.string "012345678901234567890123456789012345678901234567890123456789"
.string "012345678901234567890123456789012345678901234567890123456789"
.string "012345678901234567890123456789012345678901234567890123456789"
.string "012345678901234567890123456789012345678901234567890123456789"
.string "012345678901234567890123456789012345678901234567890123456789"
.string "012345678901234567890123456789012345678901234567890123456789"
.string "012345678901234567890123456789012345678901234567890123456789"
.string "012345678901234567890123456789012345678901234567890123456789"
.string "012345678901234567890123456789012345678901234567890123456789"
.string "012345678901234567890123456789012345678901234567890123456789"
.string "012345678901234567890123456789012345678901234567890123456789"
.string "012345678901234567890123456789012345678901234567890123456789"
.string "012345678901234567890123456789012345678901234567890123456789"
.string "012345678901234567890123456789012345678901234567890123456789"
.string "012345678901234567890123456789012345678901234567890123456789"
.string "012345678901234567890123456789012345678901234567890123456789"
.string "012345678901234567890123456789012345678901234567890123456789"
.string "012345678901234567890123456789012345678901234567890123456789"
.string "012345678901234567890123456789012345678901234567890123456789"
load_initrd:
pushw %ds
pushw %es
pushw %cs
popw %ds
pushw %cs
popw %es
cld
leaw load_imsg, %si
call prtstr # 打印提示
movw $0x1000, %ax
movw %ax, %es
xorw %bx, %bx
movw $0x00B0, %ax # initrd数据先调到0x1000:0000处,
# 满64K即移动到12M(0xB00000)处
movw %ax, %fs
movw $0, %cs:move_es
movl $0, %cs:read_total
call movetohigh # 初始化数据移动部分
call .ld_img # 从并口上读入一个文件并移动到指定位置
movl %cs:read_total, %eax
movl %eax, %cs:ramdisk_size # 设置ramdisk_size和ramdisk_image
movl $0x00B00000, %eax
movl %eax, %cs:ramdisk_image
popw %es
popw %ds
lret
load_kernel:
pushw %ds
pushw %es
pushw %cs
popw %ds
pushw %cs
popw %es
cld
leaw load_kmsg, %si
call prtstr
movw $0x1000, %ax
movw %ax, %es
xorw %bx, %bx
movw $0x0010, %ax
movw %ax, %fs
movw $0, %cs:move_es
movl $0, %cs:read_total
call movetohigh
call .ld_img
popw %es
popw %ds
lret
.ld_img:
.ld_nextpack:
pushw %bx
pushw %es
leaw read_len, %si
movw $1124, %ax
movw %ax, (%si)
pushw %si
leaw read_buffer, %ax
pushw %ax
movw %bx, %ax
call rcvpack # 调用rcpack接收一个数据包read_buffer中
popw %cx
popw %cx
popw %es
popw %bx
cmpw $0, %ax # 成功?
je .ld_suc
leaw reading_failed, %si
call prtstr
.ld_panic:
jmp .ld_panic # 失败则死循环
.ld_suc:
leaw read_buffer, %si
movw %bx, %di
movw $256, %cx # move 1024 bytes
rep
movsl # 从read_buffer移动到es:bx处,强制假定一个数据包长度
# 就是1024字节,最后一个数据包除外。
addw $1024, %bx # 更新bx, 如果bx加到零,则表示已经满64K,后面的调用中
call movetohigh # 进行实际的数据移动
movw %ax, %dx #
cmpw $0, %ax # 如果进行了64K数据移动,就打印一个'.'
je .ld_1
leaw reading_suc, %si
call prtstr
.ld_1:
leaw read_len, %si
xorl %eax, %eax
movw (%si), %ax
addl %eax, %cs:read_total
cmpw $1024, %ax # 更新收到数据总字节数,如果收到的字节数少于1024,则表示
# 收到最后一个数据包,这得冒点风险,万一最后一个数据包刚好
# 是1024字节,怎么办好呢?赌一把吧!
jb .ld_lastestpack
jmp .ld_nextpack # 接着接收下一个数据包
.ld_lastestpack:
# 最后一个数据包收到后,不见得满64K,此时应该强制数据移动
cmpw $0, %dx
jne .ld_exit
xorw %bx, %bx
call movetohigh
.ld_exit:
ret
7、用软盘进行调试,将启动程序烧到bootrom中
好了,大功告成,对内核进行配置,然后make bzImage,将bvmlinux.out拷贝到“服务器”上,建立initrd也放在“服务器”上,然后放张软盘在软驱中,dd if=/usr/src/linux/arch/i386/boot/bzImage of=/dev/fd0 count=32将bootsect.S+setup.S部分拷贝到软盘上,重新启动(先连接好并口线)。启动后再在“服务器”上启动文件“服务”程序,终于可以将Linux从并口上启动了!
做少量调整(主要是去掉读setup.S部分的代码),即可以将此bzImage的前8(16?)K写在一个文件中,处理成boot rom映象,烧到boot rom中,插到网络卡上,启动机器即可。这就是用网络卡从并口上启动Linux。
标题 Re: 用网络卡从并口上启动Linux(I386) [re: raoxianhong]
作者 raoxianhong (journeyman)
时间 10/09/01 11:30 AM
网络上说可以将Bootrom写到BIOS中去,但是没有实验成功,不知道有什么讲究,哪位可曾试过?
寻找文件 cbrom.pdf
标题 推荐两篇讲述启动过程的文章 [re: feiyunw]
作者 raoxianhong (journeyman)
时间 10/11/01 09:08 AM
http://www.pcguide.com/ref/mbsys/bios/boot.htm
http://www2.csa.iisc.ernet.in/~kvs/LinuxBoot.html
标题 Re: 386 boot代码分析 [re: feiyunw]
作者 raoxianhong (member)
时间 10/25/01 05:09 PM
附加文件 181431-bootrom.zip
有几位老兄Mail问网卡启动的启动代码问题,这里总结如下:
1.系统自检完毕后在ROM空间中找(好象是2Kbytes为单位),如果某一段的前两表字节是0x55AA,那么第三个字节作为ROM程序的大小(512字节为单位)。然后将该段空间中的所有字节相加(计算校验和),结果为零时表示ROM程序有效。此时BIOS用一个长调用(lcall),调用该块的第四个字节起始处(自然该用lret返回)。
2.有个问题原来一直不明白,如果此时某个启动网卡启动系统,但是后面还有带ROM的卡(比如PCI),那么该段ROM程序岂不是没有机会运行了吗,当然,如果不运行任何设备的扩展ROM,不知道Linux内会不会有问题!后来查资料得知,实际上制作网卡启动程序时还没有这么简单。
3.事实上,系统在自检及运行所有的扩展硬件检测之后,是用int 19h启动操作系统的!因此在扩展ROM中不直接启动操作系统,而是将操作系统启动代码作为int 19h的中断调用(其实也不用返回,操作系统没有必要返回)代码就行了。
明白这一点后,制作一个网卡启动程序就容易多了,具体请看某个网卡的启动源代码即可,附件中有一个,记不住是从哪里抄来的了!
标题 通用的网络卡bootrom处理程序 [re: feiyunw]
作者 raoxianhong (member)
时间 12/06/01 08:05 PM
Bootrom写好后要进行一些处理才能烧到EPROM中去。这里提供一段代码可以完成这个功能,上面讲的用并口启动Linux的程序就是这么处理的。
基本的想法是,写一个通用的启动代码载入程序(stub),将bootsect.S+setup.S(也就是bzImage的前面一段)设置成0x19号中断的中断向量。在外面写一段代码将该段代码和启动代码进行合并,生成合法的bootrom映象就,可以烧到bootrom中去,在网络卡上启动。
下面是通用的启动代码载入程序:
.code16
RomHeader:
.byte 0x55, 0xaa #启动ROM标志
RomPageCount:
.byte 0x20 #假定bootrom是16K bytes
RomCode:
pushw %es
pushw %bx
pushw %ax
movb $0xc1, %al
call IntVectAddr
movw $0x6a6e, %ax
cmpw %es%bx), %ax
jz RomBootInit_x
movw %ax, %es%bx)
movw $0xc019, %ax
call MoveIntVector
movw $RomBootVect, %bx
pushw %cs
popw %es
call SetIntVector
RomBootInit_x:
popw %ax
popw %bx
popw %es
lret
IntVectAddr:
xorw %bx,%bx
movw %bx,%es
movb %al,%bl
addw %bx,%bx
addw %bx,%bx
ret
GetIntVector:
call IntVectAddr
GetIntVect_1:
les %es%bx), %bx
ret
SetIntVector:
pushf #; entry AL=vector to set, ES:BX=value
pushw %es #; exit: vector modified
pushw %bx #; all registers preserved
call IntVectAddr
cli
popw %es%bx)
addw $2, %bx
popw %es%bx)
subw $2, %bx
popf
jmp GetIntVect_1
MoveIntVector:
call GetIntVector #; entry AL=vect to get, AH=vect to set
xchgb %al,%ah #; exit: vector set, ES:BX=vector value
call SetIntVector #; other registers preserved
xchgb %al,%ah
ret
RomBootVect:
pushw %cs
popw %ds
movw $0x07c0, %ax
movw %ax, %es
movw $BootCode, %si
subw %di, %di
movw $8192, %cx
cld
rep
movsw
ljmp $0x07c0, $0
lret
.org 0x0200
BootCode:
在Linux下的编译方法与bootsect.S的编译方法一样,编译成可执行文件后,比如放在bootx文件中。
内核编译后(make bzImage,支持上面所说的启动方式),得到bzImage文件。
下面是将这两个文件复合在一起得到bootrom映象的程序:
/* mkbtrom.c */
int main(int argc, char* argv[])
{
char buf[16384];
char ch;
int i;
if (argc<4)
{
printf("Usage: mkbtrom \n";
return 1;
}
FILE * pFile;
pFile = fopen(argv[1], "rb";
if (pFile==NULL)
{
printf("File %s open failed\n", argv[1]);
return 2;
}
fread(buf, 1, 512, pFile);
fclose(pFile);
pFile = fopen(argv[2], "rb";
if (pFile==NULL)
{
printf("File %s open failed\n", argv[2]);
return 2;
}
fread(&buf[512], 1, 16384-512-1, pFile);
fclose(pFile);
ch = 0;
for (i = 0;i<18383;i++)
ch += buf[ i ];
buf[16383] = -ch;
pFile = fopen(argv[3], "wb";
fwrite(buf, 1, 16384, pFile);
fclose(pFile);
return 0;
}
编译成执行文件后,运行mkbtrom bootx bzImage boot16k.bin后,boot16k.bin就可以烧到eprom中,从网络卡中启动了。
asmlinkage unsigned int do_IRQ(struct pt_regs regs)
{
... ...
if (softirq_active(cpu) & softirq_mask(cpu))
do_softirq();
}
-----------------------------------------------------
还有,不是每个被标注的软中断都能在这次陷入内核的部分中完成,可能会延迟到下次中断。
其它地方的调用:
在entry.S中有一个调用点:
handle_softirq:
call SYMBOL_NAME(do_softirq)
jmp ret_from_intr
有两处调用它,一处是当系统调用处理完后:
ENTRY(ret_from_sys_call)
#ifdef CONFIG_SMP
movl processor(%ebx),%eax
shll $CONFIG_X86_L1_CACHE_SHIFT,%eax
movl SYMBOL_NAME(irq_stat)(,%eax),%ecx # softirq_active
testl SYMBOL_NAME(irq_stat)+4(,%eax),%ecx # softirq_mask
#else
movl SYMBOL_NAME(irq_stat),%ecx # softirq_active
testl SYMBOL_NAME(irq_stat)+4,%ecx # softirq_mask
#endif
jne handle_softirq
一处是当异常处理完后:
ret_from_exception:
#ifdef CONFIG_SMP
GET_CURRENT(%ebx)
movl processor(%ebx),%eax
shll $CONFIG_X86_L1_CACHE_SHIFT,%eax
movl SYMBOL_NAME(irq_stat)(,%eax),%ecx # softirq_active
testl SYMBOL_NAME(irq_stat)+4(,%eax),%ecx # softirq_mask
#else
movl SYMBOL_NAME(irq_stat),%ecx # softirq_active
testl SYMBOL_NAME(irq_stat)+4,%ecx # softirq_mask
#endif
jne handle_softirq
注意其中的irq_stat, irq_stat +4 对应的就是字段 active和mask
既然我们每次调用完硬中断后都马上调用软中断,为什么还要在这里调用呢?
原因可能都多方面的:
(1)在系统调用或者异常处理中同样可以标注软中断,这样它们在返回前就能得以迅速执行
(2)前面提到,有些软中断要延迟到下次陷入内核才能执行,系统调用和异常都陷入内核,所以可以尽早的把软中断处理掉
(3)如果在异常或者系统调用中发生中断,那么前面提到,可能还会有一些软中断没有处理,在这两个地方做一个补救工作,尽量避免到下次陷入内核才处理这些软中断。
另外,在切换前也调用。
bottom half
2.2.x中的bottom half :
2.2.x版本中的bottom half就相当于2.4.1中的softirq.它的问题在于只有32个,如果要扩充的话,需要task 队列(这里task不是进程,而是函数),还有一个比较大的问题,就是虽然bottom half在一个CPU上是串行的(由local_bh_count[cpu]记数保证),但是在多CPU上是不安全的,例如,一个CPU上在运行关于定时器的bottom half,另一个CPU也可以运行同一个bottom half,出现了重入。
2.4.1中的bottom half
2.4.1中,用tasklet表示bottom half, mark_bh就是将相应的tasklet挂到运行队列里tasklet_hi_vec[cpu].list,这个队列由HI_SOFTIRQ对应的softirq来执行。
另外,用一个全局锁来保证,当一个CPU上运行一个bottom half时,其它CPU上不能运行任何一个bottom half。这和以前的bottom half有所不同,不知道是否我看错了。
用32个tasklet来表示bottom half:
struct tasklet_struct bh_task_vec[32];
首先,初始化所有的bottom half:
void __init softirq_init()
{
... ...
for (i=0; i<32; i++)
tasklet_init(bh_task_vec+i, bh_action, i);
... ...
}
这里bh_action是下面的函数,它使得bottom half运行对应的bh_base。
static void bh_action(unsigned long nr)
{
int cpu = smp_processor_id();
/*1*/ if (!spin_trylock(&global_bh_lock))
goto resched;
if (!hardirq_trylock(cpu))
goto resched_unlock;
if (bh_base[nr])
bh_base[nr]();
hardirq_endlock(cpu);
spin_unlock(&global_bh_lock);
return;
resched_unlock:
spin_unlock(&global_bh_lock);
resched:
mark_bh(nr);
}
/*1*/试图上锁,如果得不到锁,则重新将bottom half挂上,下次在运行。
当要定义一个bottom half时用下面的函数:
void init_bh(int nr, void (*routine)(void))
{
bh_base[nr] = routine;
mb();
}
取消定义时,用:
void remove_bh(int nr)
{
tasklet_kill(bh_task_vec+nr);
bh_base[nr] = NULL;
}
tasklet_kill确保这个tasklet被运行了,因而它的指针也没有用了。
激活一个bottom half,就是将它挂到队列中 :
static inline void mark_bh(int nr)
{
tasklet_hi_schedule(bh_task_vec+nr);
}
[目录]
--------------------------------------------------------------------------------
from lisolog
[目录]
--------------------------------------------------------------------------------
index
中断流程
中断可以用下面的流程来表示:
中断产生源 -------->; 中断向量表 (idt) ----------->; 中断入口 ( 一般简单处理后调用相应的函数) --------->; 后续处理
根据中断产生源,我们可以把中断分成两个部分 :
内部中断( CPU 产生)
外部中断( 外部硬件产生 )
这些中断经过一些处理后,会有一些后续处理。
后面分别讨论:
内部中断
外部中断
后续处理
[目录]
--------------------------------------------------------------------------------
内部中断
内部中断
内部中断有两种产生方式:
CPU 自发产生的: 如除数为0 的中断, page_fault 等
程序调用 int : int 80h
CPU自发产生的中断对应 idt 向量表中确定的位置,例如除数为0的中断在对应idt中第0个向量,
因此,内核只需要在第0个向量中设定相应的处理函数即可。
程序调用 int 可以产生的任何中断, 因此,前者是后者的子集。 特别的有:
int 80h
这是系统调用的中断.( system call )是用户代码调用内核代码的入口。
这里面可以考察的专题至少有:
*系统调用
*其它内部中断
[目录]
--------------------------------------------------------------------------------
外部中断
外部中断
1.
外部中断是: 外部硬件(如时钟) ----->; 中断芯片 ---->; 中断向量表 ----->; 中断入口
完成一个完整的映射,有4件事情要做:
(1) 将外部设备和中断芯片相应的管脚接上
(2) 对中断芯片设置,使得特定管脚的中断能映射到CPU idt特定的位置
(3) 程序中包含了这些中断入口
(4) 在中断向量表里设置向量相应的入口地址
这些工作需要在外部中断流程里描述
2.
由于硬件设备可能共用一个中断,在统一的函数中会有相应的结构来处理,也就是有16个结构分别处理相应的16个中断
特定的硬件驱动需要将自己的处理函数挂接到特定的结构上.
3.
但是,有一个问题:驱动怎么知道自己的硬件产生哪个中断?
有一些是确定的,比如时钟是第0个, 软盘是第 5 个(right ??), 还有一些 PCI 设备是可以通过访问得到它们的中断号的,但是ISA设备需要通过探测(probe)来得到(详细情况可以参考 linux device driver )这涉及探测的工作
4.
因此,这里面要考察的工作至少包括:
1. i8259芯片的设置(包括上面的 (2) ), 以及一些其它属性的设置
2. 外部中断的流程
3. 处理外部中断的结构与相应的数据结构
下面是《LINUX系统分析...》中的一段,可供参考。
但有时一个设备驱动程序不知道设备将使用哪一个中断。在PCI结构中这不会成为一个问题,因为PCI的设备驱动程序总是知道它们的中断号。但对于ISA结构而言,一个设备驱动程序找到自己使用的中断号却并不容易。Linux系统通过允许设备驱动程序探测自己的中断来解决这个问题。
首先,设备驱动程序使得设备产生一个中断。然后,允许系统中所有没有指定的中断,这意味着设备挂起的中断将会通过中断控制器传送。Linux 系统读取中断状态寄存器然后将它的值返回到设备驱动程序。一个非0 的结果意味着在探测期间发生了一个或者多个的中断。设备驱动程序现在可以关闭探测,这时所有还未被指定的中断将继续被禁止。
一个ISA 设备驱动程序知道了它的中断号以后,就可以请求对中断的控制了。PCI 结构的系统中断比I S A 结构的系统中断要灵活得多。ISA设备使用中断插脚经常使用跳线设置,所以在设备驱动程序中是固定的。但PCI 设备是在系统启动过程中PCI初始化时由PCI BIOS或PCI子系统分配的。每一个PCI 设备都有可能使用A、B、C或者D这4 个中断插脚中的一个。缺省情况下设备使用插脚A。
每个PCI插槽的PCI中断A、B、C和D是通过路由选择连接到中断控制器上的。所以PCI插槽4的插脚A可能连接到中断控制器的插脚6 ,PCI 插槽4 的插脚B 可能连接到中断控制器的插脚7 ,以此类推。
PCI中断具体如何进行路由一般依照系统的不同而不同,但系统中一定存在PCI中断路由拓扑结构的设置代码。在Intel PC机中,系统的BIOS代码负责中断的路由设置。对于没有BIOS的系统,Linux系统内核负责设置。
PCI的设置代码将中断控制器的插脚号写入到每个设备的PCI设置头中。PCI的设置代码根据所知道的PCI中断路由拓扑结构、PCI设备使用的插槽,以及正在使用的PCI中断的插脚号来决定中断号,也就是IRQ号。
系统中可以有很多的PCI中断源,例如当系统使用了PCI-PCI桥时。这时,中断源的数目可能超过了系统可编程中断控制器上插脚的数目。在这种情况下,某些PCI设备之间就不得不共享一个中断,也就是说,中断控制器上的某一个插脚可以接收来自几个设备的中断。Linux系统通过让第一个中断源请求者宣布它使用的中断是否可以被共享来实现中断在几个设备之间共享的。中断共享使得irq_action数组中的同一个入口指向几个设备的irqaction结构。当一个共享的中断有中断发生时,Linux系统将会调用和此中断有关的所有中断处理程序。所有可以共享中断的设备驱动程序的中断处理程序都可能在任何时候被调用,即使在自身没有中断需要处理时。
[目录]
--------------------------------------------------------------------------------
后续处理
后续处理
后续部分主要完成下面的工作
1. bottom_half
2. 是否能进程切换?
3.是否需要进程切换?是则切换
4.信号处理
特别的,有一个重要的下半部分就是时钟中断的下半部分。
bottom_half
正如许多书所说的,它们继续完成中断处理(在开中断的状态下), 因此中断中的处理函数需要在一个32位变量中设置特定的bit来告诉do_softirq要执行哪个bottom_half(我们不妨把这个32位数想象成一个新的中断向量表,设置bit相当于产生中断,下半部分相当于handler,也许这是被称为软中断的原因吧)bottom_half有的时候需要借助一个特殊的结构: task_queue 来完成更多的工作,
task_queue
task_queue 是一个链表,每个节点是一个函数指针,这样,一 个 bottom_half 就可以执行一个链表上的函数列了
当然 task_queue 不一定只在 bottom_half 中应用, 我查了一下, 在一些驱动中也直接调用 run_task_queue 来执行一个特定的队列.
如果内核需要在某个中断产生后执行它的函数,只需要在它下半部分调用的 task_queue 上挂上它的函数( Linux Device Driver 中有步进马达的例子)现在的内核有些变化,增加了softirq_action tasklet, 不十分清楚是什么原因
是否需要进行切换
因为 linux是非抢占的,所以如果返回的代码段是内核级的话,就不允许进行切换。如果能切换判断一下是否需要切换, 如果是就切换
信号处理
看是否有信号要处理,如果要调用 do_signal
时钟中断的下半部分
在上面许多的外部中断中,有一个特殊的中断的处理 timer_interrupt, 它的下半部分主要处理:时间计算和校准定时器工作
因此,我们有了下面的工作
*下半部分(包括softirq, tasklet, bottom_half )
*后续处理的流程
*时钟中断的下半部分
*定时器
[目录]
--------------------------------------------------------------------------------
软中断代码线索
[声明]
#define DECLARE_TASK_QUEUE(q) LIST_HEAD(q)
#define LIST_HEAD(name) \
struct list_head name = LIST_HEAD_INIT(name)
struct list_head {
struct list_head *next, *prev;
};
#define LIST_HEAD_INIT(name) { &(name), &(name) }
''DECLARE_TASK_QUEUE'' [include/linux/tqueue.h, include/linux/list.h]
DECLARE_TASK_QUEUE(q) 宏用来申明一个名叫"q"的结构管理任务队列
[标明(MARK)]
这里是mark_bh过程的结构 [include/linux/interrupt.h]
|mark_bh(NUMBER)
|tasklet_hi_schedule(bh_task_vec + NUMBER)
|insert into tasklet_hi_vec
|__cpu_raise_softirq(HI_SOFTIRQ)
|soft_active |= (1 << HI_SOFTIRQ)
''mark_bh''[include/linux/interrupt.h]
例如,当IRQ handler需要推迟一些工作, 它会mark_bh(NUMBER), 这里的NUMBER是BH的标志
[执行]
通过do_IRQ[arch/i386/kernel/irq.c]
|do_softirq
|h->;action(h)->; softirq_vec[TASKLET_SOFTIRQ]->;action ->; tasklet_action
|tasklet_vec[0].list->;func
"h->;action(h);" 是队列向前.
.
[目录]
--------------------------------------------------------------------------------
2. 4软中断机制
一. 软中断概况
软中断是利用硬件中断的概念,用软件方式进行模拟,实现宏观上的异步执行效果。很多情况下,软中断和"信号"有些类似,同时,软中断又是和硬中断相对应的,"硬中断是外部设备对CPU的中断","软中断通常是硬中断服务程序对内核的中断","信号则是由内核(或其他进程)对某个进程的中断"(《Linux内核源代码情景分析》第三章)。软中断的一种典型应用就是所谓的"下半部"(bottom half),它的得名来自于将硬件中断处理分离成"上半部"和"下半部"两个阶段的机制:上半部在屏蔽中断的上下文中运行,用于完成关键性的处理动作;而下半部则相对来说并不是非常紧急的,通常还是比较耗时的,因此由系统自行安排运行时机,不在中断服务上下文中执行。bottom half的应用也是激励内核发展出目前的软中断机制的原因,因此,我们先从bottom half的实现开始。
二. bottom half
在Linux内核中,bottom half通常用"bh"表示,最初用于在特权级较低的上下文中完成中断服务的非关键耗时动作,现在也用于一切可在低优先级的上下文中执行的异步动作。最早的bottom half实现是借用中断向量表的方式,在目前的2.4.x内核中仍然可以看到:
static void (*bh_base[32])(void); /* kernel/softirq.c */
系统如此定义了一个函数指针数组,共有32个函数指针,采用数组索引来访问,与此相对应的是一套函数:
void init_bh(int nr,void (*routine)(void));
为第nr个函数指针赋值为routine。
void remove_bh(int nr);
动作与init_bh()相反,卸下nr函数指针。
void mark_bh(int nr);
标志第nr个bottom half可执行了。
由于历史的原因,bh_base各个函数指针位置大多有了预定义的意义,在v2.4.2内核里有这样一个枚举:
enum {
TIMER_BH = 0,
TQUEUE_BH,
DIGI_BH,
SERIAL_BH,
RISCOM8_BH,
SPECIALIX_BH,
AURORA_BH,
ESP_BH,
SCSI_BH,
IMMEDIATE_BH,
CYCLADES_BH,
CM206_BH,
JS_BH,
MACSERIAL_BH,
ISICOM_BH
};
并约定某个驱动使用某个bottom half位置,比如串口中断就约定使用SERIAL_BH,现在我们用得多的主要是TIMER_BH、TQUEUE_BH和IMMEDIATE_BH,但语义已经很不一样了,因为整个bottom half的使用方式已经很不一样了,这三个函数仅仅是在接口上保持了向下兼容,在实现上一直都在随着内核的软中断机制在变。现在,在2.4.x内核里,它用的是tasklet机制。
三. task queue
在介绍tasklet之前,有必要先看看出现得更早一些的task queue机制。显而易见,原始的bottom half机制有几个很大的局限,最重要的一个就是个数限制在32个以内,随着系统硬件越来越多,软中断的应用范围越来越大,这个数目显然是不够用的,而且,每个bottom half上只能挂接一个函数,也是不够用的。因此,在2.0.x内核里,已经在用task queue(任务队列)的办法对其进行了扩充,这里使用的是2.4.2中的实现。
task queue是在系统队列数据结构的基础上建成的,以下即为task queue的数据结构,定义在include/linux/tqueue.h中:
struct tq_struct {
struct list_head list; /* 链表结构 */
unsigned long sync; /* 初识为0,入队时原子的置1,以避免重复入队 */
void (*routine)(void *); /* 激活时调用的函数 */
void *data; /* routine(data) */
};
typedef struct list_head task_queue;
在使用时,按照下列步骤进行:
DECLARE_TASK_QUEUE(my_tqueue); /* 定义一个my_tqueue,实际上就是一个以tq_struct为元素的list_head队列 */
说明并定义一个tq_struct变量my_task;
queue_task(&my_task,&my_tqueue); /* 将my_task注册到my_tqueue中 */
run_task_queue(&my_tqueue); /* 在适当的时候手工启动my_tqueue */
大多数情况下,都没有必要调用DECLARE_TASK_QUEUE()定义自己的task queue,因为系统已经预定义了三个task queue:
tq_timer,由时钟中断服务程序启动;
tq_immediate,在中断返回前以及schedule()函数中启动;
tq_disk,内存管理模块内部使用。
一般使用tq_immediate就可以完成大多数异步任务了。
run_task_queue(task_queue *list)函数可用于启动list中挂接的所有task,可以手动调用,也可以挂接在上面提到的bottom half向量表中启动。以run_task_queue()作为bh_base[nr]的函数指针,实际上就是扩充了每个bottom half的函数句柄数,而对于系统预定义的tq_timer和tq_immediate的确是分别挂接在TQUEUE_BH和IMMEDIATE_BH上(注意,TIMER_BH没有如此使用,但TQUEUE_BH也是在do_timer()中启动的),从而可以用于扩充bottom half的个数。此时,不需要手工调用run_task_queue()(这原本就不合适),而只需调用mark_bh(IMMEDIATE_BH),让bottom half机制在合适的时候调度它。
四. tasklet
由上看出,task queue以bottom half为基础;而bottom half在v2.4.x中则以新引入的tasklet为实现基础。
之所以引入tasklet,最主要的考虑是为了更好的支持SMP,提高SMP多个CPU的利用率:不同的tasklet可以同时运行于不同的CPU上。在它的源码注释中还说明了几点特性,归结为一点,就是:同一个tasklet只会在一个CPU上运行。
struct tasklet_struct
{
struct tasklet_struct *next; /* 队列指针 */
unsigned long state; /* tasklet的状态,按位操作,目前定义了两个位的含义:
TASKLET_STATE_SCHED(第0位)或TASKLET_STATE_RUN(第1位) */
atomic_t count; /* 引用计数,通常用1表示disabled */
void (*func)(unsigned long); /* 函数指针 */
unsigned long data; /* func(data) */
};
把上面的结构与tq_struct比较,可以看出,tasklet扩充了一点功能,主要是state属性,用于CPU间的同步。
tasklet的使用相当简单:
定义一个处理函数void my_tasklet_func(unsigned long);
DECLARE_TASKLET(my_tasklet,my_tasklet_func,data); /*
定义一个tasklet结构my_tasklet,与my_tasklet_func(data)函数相关联,相当于DECLARE_TASK_QUEUE() */
tasklet_schedule(&my_tasklet); /*
登记my_tasklet,允许系统在适当的时候进行调度运行,相当于queue_task(&my_task,&tq_immediate)和mark_bh(IMMEDIATE_BH) */
可见tasklet的使用比task queue更简单,而且,tasklet还能更好的支持SMP结构,因此,在新的2.4.x内核中,tasklet是建议的异步任务执行机制。除了以上提到的使用步骤外,tasklet机制还提供了另外一些调用接口:
DECLARE_TASKLET_DISABLED(name,function,data); /*
和DECLARE_TASKLET()类似,不过即使被调度到也不会马上运行,必须等到enable */
tasklet_enable(struct tasklet_struct *); /* tasklet使能 */
tasklet_disble(struct tasklet_struct *); /* 禁用tasklet,只要tasklet还没运行,则会推迟到它被enable */
tasklet_init(struct tasklet_struct *,void (*func)(unsigned long),unsigned long); /* 类似DECLARE_TASKLET() */
tasklet_kill(struct tasklet_struct *); /* 清除指定tasklet的可调度位,即不允许调度该tasklet,但不做tasklet本身的清除 */
前面提到过,在2.4.x内核中,bottom half是利用tasklet机制实现的,它表现在所有的bottom half动作都以一类tasklet的形式运行,这类tasklet与我们一般使用的tasklet不同。
在2.4.x中,系统定义了两个tasklet队列的向量表,每个向量对应一个CPU(向量表大小为系统能支持的CPU最大个数,SMP方式下目前2.4.2为32)组织成一个tasklet链表:
struct tasklet_head tasklet_vec[NR_CPUS] __cacheline_aligned;
struct tasklet_head tasklet_hi_vec[NR_CPUS] __cacheline_aligned;
另外,对于32个bottom half,系统也定义了对应的32个tasklet结构:
struct tasklet_struct bh_task_vec[32];
在软中断子系统初始化时,这组tasklet的动作被初始化为bh_action(nr),而bh_action(nr)就会去调用bh_base[nr]的函数指针,从而与bottom half的语义挂钩。mark_bh(nr)被实现为调用tasklet_hi_schedule(bh_tasklet_vec+nr),在这个函数中,bh_tasklet_vec[nr]将被挂接在tasklet_hi_vec[cpu]链上(其中cpu为当前cpu编号,也就是说哪个cpu提出了bottom half的请求,则在哪个cpu上执行该请求),然后激发HI_SOFTIRQ软中断信号,从而在HI_SOFTIRQ的中断响应中启动运行。
tasklet_schedule(&my_tasklet)将把my_tasklet挂接到tasklet_vec[cpu]上,激发TASKLET_SOFTIRQ,在TASKLET_SOFTIRQ的中断响应中执行。HI_SOFTIRQ和TASKLET_SOFTIRQ是softirq子系统中的术语,下一节将对它做介绍。
五. softirq
从前面的讨论可以看出,task queue基于bottom half,bottom half基于tasklet,而tasklet则基于softirq。
可以这么说,softirq沿用的是最早的bottom half思想,但在这个"bottom half"机制之上,已经实现了一个更加庞大和复杂的软中断子系统。
struct softirq_action
{
void (*action)(struct softirq_action *);
void *data;
};
static struct softirq_action softirq_vec[32] __cacheline_aligned;
这个softirq_vec[]仅比bh_base[]增加了action()函数的参数,在执行上,softirq比bottom half的限制更少。
和bottom half类似,系统也预定义了几个softirq_vec[]结构的用途,通过以下枚举表示:
enum
{
HI_SOFTIRQ=0,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
TASKLET_SOFTIRQ
};
HI_SOFTIRQ被用于实现bottom half,TASKLET_SOFTIRQ用于公共的tasklet使用,NET_TX_SOFTIRQ和NET_RX_SOFTIRQ用于网络子系统的报文收发。在软中断子系统初始化(softirq_init())时,调用了open_softirq()对HI_SOFTIRQ和TASKLET_SOFTIRQ做了初始化:
void open_softirq(int nr, void (*action)(struct softirq_action*), void *data)
open_softirq()会填充softirq_vec[nr],将action和data设为传入的参数。TASKLET_SOFTIRQ填充为tasklet_action(NULL),HI_SOFTIRQ填充为tasklet_hi_action(NULL),在do_softirq()函数中,这两个函数会被调用,分别启动tasklet_vec[cpu]和tasklet_hi_vec[cpu]链上的tasklet运行。
static inline void __cpu_raise_softirq(int cpu, int nr)
这个函数用来激活软中断,实际上就是第cpu号CPU的第nr号软中断的active位置1。在do_softirq()中将判断这个active位。tasklet_schedule()和tasklet_hi_schedule()都会调用这个函数。
do_softirq()有4个执行时机,分别是:从系统调用中返回(arch/i386/kernel/entry.S::ENTRY(ret_from_sys_call))、从异常中返回(arch/i386/kernel/entry.S::ret_from_exception标号)、调度程序中(kernel/sched.c::schedule()),以及处理完硬件中断之后(kernel/irq.c::do_IRQ())。它将遍历所有的softirq_vec,依次启动其中的action()。需要注意的是,软中断服务程序,不允许在硬中断服务程序中执行,也不允许在软中断服务程序中嵌套执行,但允许多个软中断服务程序同时在多个CPU上并发。
六. 使用示例
softirq作为一种底层机制,很少由内核程序员直接使用,因此,这里的使用范例仅对其余几种软中断机制。
1.bottom half
原有的bottom half用法在drivers/char/serial.c中还能看到,包括三个步骤:
init_bh(SERIAL_BH,do_serial_bh); //在串口设备的初始化函数rs_init()中,do_serial_bh()是处理函数
mark_bh(SERIAL_BH); //在rs_sched_event()中,这个函数由中断处理例程调用
remove_bh(SERIAL_BH); //在串口设备的结束函数rs_fini()中调用
尽管逻辑上还是这么三步,但在do_serial_bh()函数中的动作却是启动一个task queue:run_task_queue(&tq_serial),而在rs_sched_event()中,mark_bh()之前调用的则是queue_task(...,&tq_serial),也就是说串口bottom half已经结合task queue使用了。而那些更通用一些的bottom half,比如IMMEDIATE_BH,更是必须要与task queue结合使用,而且一般情况下,task queue也很少独立使用,而是与bottom half结合,这在下一节task queue使用示例中可以清楚地看到。
2.task queue
一般来说,程序员很少自己定义task queue,而是结合bottom half,直接使用系统预定义的tq_immediate等,尤以tq_immediate使用最频繁。看以下代码段,节选自drivers/block/floppy.c:
static struct tq_struct floppy_tq; //定义一个tq_struct结构变量floppy_tq,不需要作其他初始化动作
static void schedule_bh( void (*handler)(void*) )
{
floppy_tq.routine = (void *)(void *) handler;
//指定floppy_tq的调用函数为handler,不需要考虑floppy_tq中的其他域
queue_task(&floppy_tq, &tq_immediate);
//将floppy_tq加入到tq_immediate中
mark_bh(IMMEDIATE_BH);
//激活IMMEDIATE_BH,由上所述可知,
这实际上将引发一个软中断来执行tq_immediate中挂接的各个函数
}
当然,我们还是可以定义并使用自己的task queue,而不用tq_immediate,在drivers/char/serial.c中提到的tq_serial就是串口驱动自己定义的:
static DECLARE_TASK_QUEUE(tq_serial);
此时就需要自行调用run_task_queue(&tq_serial)来启动其中的函数了,因此并不常用。
3.tasklet
这是比task queue和bottom half更加强大的一套软中断机制,使用上也相对简单,见下面代码段:
1: void foo_tasklet_action(unsigned long t);
2: unsigned long stop_tasklet;
3: DECLARE_TASKLET(foo_tasklet, foo_tasklet_action, 0);
4: void foo_tasklet_action(unsigned long t)
5: {
6: //do something
7:
8: //reschedule
9: if(!stop_tasklet)
10: tasklet_schedule(&foo_tasklet);
11: }
12: void foo_init(void)
13: {
14: stop_tasklet=0;
15: tasklet_schedule(&foo_tasklet);
16: }
17: void foo_clean(void)
18: {
19: stop_tasklet=1;
20: tasklet_kill(&foo_tasklet);
21: }
这个比较完整的代码段利用一个反复执行的tasklet来完成一定的工作,首先在第3行定义foo_tasklet,与相应的动作函数foo_tasklet_action相关联,并指定foo_tasklet_action()的参数为0。虽然此处以0为参数,但也同样可以指定有意义的其他参数值,但需要注意的是,这个参数值在定义的时候必须是有固定值的变量或常数(如上例),也就是说可以定义一个全局变量,将其地址作为参数传给foo_tasklet_action(),例如:
int flags;
DECLARE_TASKLET(foo_tasklet,foo_tasklet_action,&flags);
void foo_tasklet_action(unsigned long t)
{
int flags=*(int *)t;
...
}
这样就可以通过改变flags的值将信息带入tasklet中。直接在DECLARE_TASKLET处填写flags,gcc会报"initializer element is not constant"错。
第9、10行是一种RESCHEDULE的技术。我们知道,一个tasklet执行结束后,它就从执行队列里删除了,要想重新让它转入运行,必须重新调用tasklet_schedule(),调用的时机可以是某个事件发生的时候,也可以是像这样在tasklet动作中。而这种reschedule技术将导致tasklet永远运行,因此在子系统退出时,应该有办法停止tasklet。stop_tasklet变量和tasklet_kill()就是干这个的。
11、static inline void zap_pte_range(pmd_t * pmd, unsigned long address,
unsigned long size)
zap为zero all pages的缩写。该函数的作用是将在pmd中从虚拟地址address开始,长度为size的内存块通过循环调用pte_clear将其页表项清零,调用free_pte将所含空间中的物理内存或交换空间中的虚存页释放掉。在释放之前,必须检查从address开始长度为size的内存块有无越过PMD_SIZE.(溢出则可使指针逃出0~1023的区间)。
[目录]
--------------------------------------------------------------------------------
zap_pmd_range
12、static inline void zap_pmd_range(pgd_t * dir, unsigned long address, unsigned long size)
函数结构与zap_pte_range类似,通过调用zap_pte_range完成对所有落在address到address+size区间中的所有pte的清零工作。zap_pmd_range至多清除4M空间的物理内存。
[目录]
--------------------------------------------------------------------------------
zap_page_range
13、int zap_page_range(struct mm_struct *mm, unsigned long address, unsigned long size)
函数结构与前两个函数类似。将任务从address开始到address+size长度内的所有对应的pmd都清零。zap_page_range的主要功能是在进行内存收缩、释放内存、退出虚存映射或移动页表的过程中,将不在使用的物理内存从进程的三级页表中清除。(在讨论clear_page_tables时,就提到过当进程退出时,释放页表之前,先保证将页表对应项清零,保证在处于退出状态时,进程不占用0~3G的空间。)
[目录]
--------------------------------------------------------------------------------
zeromap_pte_range等
14、static inline void zeromap_pte_range(pte_t * pte, unsigned long address,
unsigned long size, pte_t zero_pte)
15、static inline int zeromap_pmd_range(pmd_t * pmd, unsigned long address,
unsigned long size, pte_t zero_pte)
16、int zeromap_page_range(unsigned long address, unsigned long size, pgprot_t prot)
这三个函数与前面的三个函数从结构上看很相似,他们的功能是将虚拟空间中从地址address开始,长度为size的内存块所对应的物理内存都释放掉,同时将指向这些区域的pte都指向系统中专门开出的长度为4K,全为0的物理页。zeromap_page_range在kernel代码中没有被引用,这个函数是旧版本的Linux遗留下来的,在新版本中已经被zap_page_range所替代。
[目录]
--------------------------------------------------------------------------------
remap_pte_range等
17、static inline void remap_pte_range(pte_t * pte, unsigned long address,
unsigned long size, unsigned long offset, pgprot_t prot)
18、static inline int remap_pmd_range(pmd_t * pmd, unsigned long address,
unsigned long size, unsigned long offset, pgprot_t prot)
19、int remap_page_range(unsigned long from, unsigned long offset, unsigned long size,
pgprot_t prot)
这三个函数也同前面的函数一样,层层调用,现仅介绍一下最后一个函数的作用。remap_page_range的功能是将原先被映射到虚拟内存地址from处的,大小为size的虚拟内存块映射到以偏移量offset为起始地址的虚拟内存中,同时将原来的pte、pmd项都清零。该函数也是逐级调用,在remap_pte_range中,通过set_pte将的物理页映射到新的虚拟内存页表项pte上。remap_page_range函数的功能与下文中的remap.c中介绍的功能相近,因此在kernel中也没有用到。
[目录]
--------------------------------------------------------------------------------
put_dirty_page
20、unsigned long put_dirty_page(struct task_struct * tsk, unsigned long page,
unsigned long address)
将虚拟内存页page链接到任务tsk中虚拟地址为address的虚拟内存中,其主要调用的流程如下:put_dirty_page->;setup_arg_page->;do_load_xxx_binary(xxx为aout或elf,这些函数都在fs\exec.c中),它的功能是将在载入可执行文件的时候,将其相关的堆栈信息、环境变量等复制到当前进程的空间上。
[目录]
--------------------------------------------------------------------------------
handle_mm_fault
21、void handle_mm_fault(struct vm_area_struct * vma, unsigned long address,
int write_access)
用于处理ALPHA机中的缺页中断
[目录]
--------------------------------------------------------------------------------
mmap.c
在mmap.c中,主要提供了对进程内存管理进行支持的函数,主要包括了do_mmap、do_munmap等对进程的虚拟块堆avl数进行管理的函数。
有关avl树的一些操作:
1、static inline void avl_neighbours (struct vm_area_struct * node, struct vm_area_struct * tree, struct vm_area_struct ** to_the_left, struct vm_area_struct ** to_the_right)
寻找avl树tree中的节点node的前序节点和后序节点,将结果放在指针to_the_left和to_the_right中,即使得*to_the_left->;next=node,node->;next=*to_the_right。在实际搜索中,过程是找到node节点中的左节点的最右节点和右节点的最左节点,采用avl树搜索可以提高效率。
2、static inline void avl_rebalance (struct vm_area_struct *** nodeplaces_ptr, int count)
将由于插入操作或删除操作而造成不平衡的avl树恢复成平衡状态。nodeplaces_ptr是指向的是需要调整的子树的根节点,count是该子树的高度。
static inline void avl_insert (struct vm_area_struct * new_node,
struct vm_area_struct ** ptree)
将新节点new_node插入avl树ptree中,并将该树重新生成平衡avl树。在创建avl树时,将vma模块不断的插入avl树中,构建一个大的avl树。当进程创建时,复制父进程后需要将以双向链表拷贝过来的vma链生成avl树。
4、static inline void avl_insert_neighbours (struct vm_area_struct * new_node, struct vm_area_struct ** ptree, struct vm_area_struct ** to_the_left, struct vm_area_struct ** to_the_right)
将新节点new_node插入avl树ptree中,并将该树重新生成平衡avl树,同时返回该新节点的前序节点和后序节点。
5、static inline void avl_remove (struct vm_area_struct * node_to_delete, struct vm_area_struct ** ptree)
将指定要删除的节点node_to_delete从avl树ptree中删除。并将该树重新生成平衡avl树。该函数在释放虚存空间和归并vma链表是使用。
7、static void printk_list (struct vm_area_struct * vma)
8、static void printk_avl (struct vm_area_struct * tree)
9、static void avl_checkheights (struct vm_area_struct * tree)
10、static void avl_checkleft (struct vm_area_struct * tree, vm_avl_key_t key)
11、static void avl_checkright (struct vm_area_struct * tree, vm_avl_key_t key)
12、static void avl_checkorder (struct vm_area_struct * tree)
13、static void avl_check (struct task_struct * task, char *caller)
这些函数都是系统调试时用以检测avl树结构的正确性
14、static inline int vm_enough_memory(long pages)
通过计算当前系统中所剩的空间判断是否足够调用。可使用的内存包括缓冲存储器、页缓存、主存中的空闲页、swap缓存等。
15、static inline unsigned long vm_flags(unsigned long prot, unsigned long flags)
提供宏功能将页的保护位和标志位合并起来。
16、unsigned long get_unmapped_area(unsigned long addr, unsigned long len)
从虚拟内存address开始找到未分配的连续空间大于len的虚拟空间块,并将该快的首地址返回。
17、unsigned long do_mmap(struct file * file, unsigned long addr, unsigned long len,
unsigned long prot, unsigned long flags, unsigned long off)
do_mmap在Linux虚拟内存管理中是一个很重要的函数,它的主要功能是将可执行文件的映象映射到虚拟内存中,或将别的进程中的内存信息映射到该进程的虚拟空间中。并将映射了的虚拟块的vma加入到该进程的vma avl树中。其运行的流程如下,更详细的分析请参阅林涛同学和徐玫峰同学的报告。
检验给定的映射长度len是大于1页,小于一个任务的最大长度3G且加上进程的加上偏移量off不会溢出。如不满足则退出。
如果当前任务的内存是上锁的,检验加上len后是否会超过当前进程上锁长度的界限。如是则退出。
如果从文件映射,检验文件是否有读的权限。如无在退出。
调用get_unmaped取得从地址address开始未映射的连续虚拟空间大于len的虚存块。
如从文件映射,保证该文件控制块有相应的映射操作。
为映射组织该区域申请vma结构。
调用vm_enough_memory有足够的内存。如无则释放6中申请的vma,退出。
如果是文件映射,调用file->;f_op_mmap将该文件映射如vma中。
调用insert_vm_struct将vma插入该进程的avl树中。
归并该avl树。
18、void merge_segments (struct mm_struct * mm, unsigned long start_addr,
unsigned long end_addr)
经过对进程虚拟空间不断的映射,在进程中的vma块有许多是可以合并的,为了提高avl树查找的效率,减少avl树中不必要的vma块,通常需要将这些块和并,merge_segments的功能为合并虚拟空间中从start_addr到end_addr中类型相同,首尾相连的vma块。由于只有经过增加操作采有可能合并,所有merge_segments只在do_mmap和unmap_fixup中被调用。该函数的流程如下:
根据起始地址start_addr从找到第一块满足vm_end>;start_addr的vma块mpnt。
调用avl_neighbours找到在vma双向链表上与mpnt前后相连的vma块prev和next。
如果prev和mpnt首尾相连,且有同样在swap file中的节点,同样的标志,同样的操作等则将其合并,反之转向6。
调用avl_remove将mpnt从avl树中删除,调整prev的结束地址和后序指针。
将结构mpnt所占的物理空间删除。
prev、mpnt、next依次下移,如未超过end_addr则返回3。
19、static void unmap_fixup(struct vm_area_struct *area, unsigned long addr, size_t len)
释放虚拟空间中的某些区域的时候,将会出现四种情况:
将整个vma释放掉
将vma的前半部分释放掉
将vma的后半部分释放掉
将vma的中间部分释放掉
为了正常维护vma树,当第一种情况是,将整个vma释放掉。同时释放vma结构所占的空间。第二种,释放后半部分,修改vma的相关信息。第二种,释放前半部分,修改vma的相关信息。第四种,由于在vma中出现了一个洞,则需增加一个vma结构描述新出现的vma块。unmap_fixup所执行的工作就是当释放空间时,修正对vma树的影响。
20、int do_munmap(unsigned long addr, size_t len)
do_munmap将释放落在从地址addr开始,长度为len空间内的vma所对应的虚拟空间。do_munmap被系统调用sys_munmap所调用(对sys_munmap如何工作的不甚了解)。下面是该函数的流程:
通过find_vma根据addr找到第一块vma->;end>;addr的vma块mpnt。
调用avl_neighbours找到mpnt在链表中的相邻指针prev和next。
将检查中所有与虚拟空间addr~addr+len相交的vma块放入free链表中。同时如果该vma链接在共享内存中,则将其从该环形链表中释放出来。
按序搜索free链表,调用unmap_fixup释放空间。
调用zap_page_range将指向释放掉的虚拟空间中的pte页表项清零。
调用kfree释放mpnt结构占用的空间。
remap.c
该文件提供了对虚拟内存重映射的若干函数。在下文中将介绍这些函数的功能,分析这些函数在虚拟内存管理中所起的作用。同时详细介绍其中主要函数的流程。
static inline pte_t *get_one_pte(struct mm_struct *mm, unsigned long addr)
根据输入的虚存地址返回其在虚拟内存中的对应的页表项pte。
static inline pte_t *alloc_one_pte(struct mm_struct *mm, unsigned long addr)
根据输入的虚存地址addr在pgd表中根据三级页表映射机制找pte,如果在pgd表中无对应的项,则分配给一个pgd(pmd)表项,在这个表项内分配根据addr分配pte,将pte返回。
static inline int copy_one_pte(pte_t * src, pte_t * dst)
将目的pte(dst)表项中的值赋成源pte(src)中的值,然后将源pte中的值清零,根据这函数的功能取move_one_pte更合适。
static int move_one_page(struct mm_struct *mm,
unsigned long old_addr, unsigned long new_addr)
根据输入的虚拟地址old_addr调用get_one_pte获取该地址在三级页表中的pte项,调用copy_one_pte将该pte对应的物理页指针移到根据new_addr对应的pte项上,即在虚拟空间内移动一虚拟内存页。
static int move_page_tables(struct mm_struct * mm,
unsigned long new_addr, unsigned long old_addr, unsigned long len)
将虚拟地址空间中从old_addr开始的长度为len的虚拟内存移动到以new_addr为起始地点的的虚拟空间中,以下为该函数的流程:
将所需移动的内存长度len赋值给偏移量offset如果offset=0,结束。反之转向2。
将偏移量offset减去一个页的长度,调用move_one_page将从old_addr+offset开始的一页移到new_addr+offset。若发生错误则转到4。
如果offset不为0,则转向1,反之结束。
调用move_one_page将所有已移动到新地址的页移回源地址,调用zap_page_range将从new_addr开始的移动过的页pte清零,并返回出错信息-1。
static inline unsigned long move_vma(struct vm_area_struct * vma,
unsigned long addr, unsigned long old_len, unsigned long new_len)
将虚存中的vma块vma的起始地址为addr,长度为old_len的内存块扩展为长度为new_len的内存块,并在虚存中找到可容纳长度为new_len块的连续区域,返回首地址。其工作流程如下:
给新的vma结构块new_vma分配空间,如果不成功返回出错信息。
调用get_unmap_area从addr开始找到第一个未被利用的虚存空洞,空洞长度大于给定的新的虚拟内存块的长度len,将其首地址赋给new_addr。如果未招到,则转向9。
调用move_page_tables将从addr开始的长度为old_len的内存区域移动到以new_addr为起始地址的虚拟空间中。
修改new_vma块中关于起始地址,结束地址的值。
将新的new_vma块插入到当前进程的虚拟内存所链成的双向链表和avl树中。
调用merge_segment将虚拟空间中地址可能连结在一起的不同的vma段连结成一个vma块,同时删除冗于的vma块。
将原有空间中的从addr开始,长度为old_len的虚拟空间释放掉。
修改mm结构中的所有虚存的长度,返回新的起始虚拟地址new_addr。
将vma块new_vma释放掉并返回出错信息。
asmlinkage unsigned long sys_mremap(unsigned long addr, unsigned long old_len,
unsigned long new_len unsigned long flags)
sys_remap是一个系统调用,其主要功能是扩展或收缩现有的虚拟空间。它的主要工作流程如下:
检查addr地址是否小于4096,如小于,则非法,返回。
将原始长度old_len和需要扩展或收缩的长度new_len页对齐。
如果有old_len>;new_len,则说明是收缩空间,调用do_munmap将虚存空间中从new_len到old_len的空间释放掉。返回收缩后的首地址addr。
根据addr找到第一块vma块满足vma->;end >; addr,检查addr是否落在虚存的空洞中,如是,则返回出错信息。
检查需要扩展的内存块是否落在该vma块中,越界则返回出错信息。
如果该vma是上锁的,则检测上锁的内存扩展后是否越界,如是,则7返回出错信息 。
检测当前进程的虚存空间经扩展后是否超过系统给该进程的最大空间。如是,则返回出错信息。
如果找到vma块从addr开始到块末尾的长度为old_len且(old_len的长度不等于new_len或该虚存是不可移动的),则转向9,反之转向10。
检测从跟随找到的vma块的未分配的空间是否大于需要扩展空间。如果大于,则直接将扩展的空间挂在找到的vma块后,修改vma块中相关的信息,并返回扩展后虚拟块的首地址。如小于转向10。
如果当前虚拟块是是不可移动的,则返回错误信息。反之,调用move_vma将需要扩展的虚拟块移动可以容纳其长度new_len的虚拟空间中。
[目录]
--------------------------------------------------------------------------------
伙伴(buddy)算法
2.4版内核的页分配器引入了"页区"(zone)结构, 一个页区就是一大块连续的物理页面. Linux 2.4将整个物理内存划分为3个页区, DMA页区(ZONE_DMA), 普通页区(ZONE_NORMAL)和高端页区(ZONE_HIGHMEM).
页区可以使页面分配更有目的性, 有利于减少内存碎片. 每个页区的页分配仍使用伙伴(buddy)算法.
伙伴算法将整个页区划分为以2为幂次的各级页块的集合, 相邻的同次页块称为伙伴, 一对伙伴可以合并到更高次页面集合中去.
下面分析一下伙伴算法的页面释放过程.
; mm/page_alloc.c:
#define BAD_RANGE(zone,x) (((zone) != (x)->;zone) || (((x)-mem_map) offset) || (((x)-mem_map) >;= (zone)->;offset+(zone)->;size))
#define virt_to_page(kaddr) (mem_map + (__pa(kaddr) >;>; PAGE_SHIFT))
#define put_page_testzero(p) atomic_dec_and_test(
void free_pages(unsigned long addr, unsigned long order)
{ order是页块尺寸指数, 即页块的尺寸有(2^order)页.
if (addr != 0)
__free_pages(virt_to_page(addr), order);
}
void __free_pages(struct page *page, unsigned long order)
{
if (!PageReserved(page) put_page_testzero(page))
__free_pages_ok(page, order);
}
static void FASTCALL(__free_pages_ok (struct page *page, unsigned long order));
static void __free_pages_ok (struct page *page, unsigned long order)
{
unsigned long index, page_idx, mask, flags;
free_area_t *area;
struct page *base;
zone_t *zone;
if (page->;buffers)
BUG();
if (page->;mapping)
BUG();
if (!VALID_PAGE(page))
BUG();
if (PageSwapCache(page))
BUG();
if (PageLocked(page))
BUG();
if (PageDecrAfter(page))
BUG();
if (PageActive(page))
BUG();
if (PageInactiveDirty(page))
BUG();
if (PageInactiveClean(page))
BUG();
page->;flags ~((1 page->;age = PAGE_AGE_START;
zone = page->;zone; 取page所在的页区
mask = (~0UL) base = mem_map + zone->;offset; 求页区的起始页
page_idx = page - base; 求page在页区内的起始页号
if (page_idx ~mask) 页号必须在页块尺寸边界上对齐
BUG();
index = page_idx >;>; (1 + order);
; 求页块在块位图中的索引, 每一索引位置代表相邻两个"伙伴"
area = zone->;free_area + order; 取该指数页块的位图平面
spin_lock_irqsave( flags);
zone->;free_pages -= mask; 页区的自由页数加上将释放的页数(掩码值为负)
while (mask + (1 struct page *buddy1, *buddy2;
if (area >;= zone->;free_area + MAX_ORDER) 如果超过了最高次平面
BUG();
if (!test_and_change_bit(index, area->;map)) 测试并取反页块的索引位
/*
* the buddy page is still allocated.
*/
break; 如果原始位为0, 则说明该页块原来没有伙伴, 操作完成
/*
* Move the buddy up one level. 如果原始位为1, 则说明该页块存在一个伙伴
*/
buddy1 = base + (page_idx ^ -mask); 对页块号边界位取反,得到伙伴的起点
buddy2 = base + page_idx;
if (BAD_RANGE(zone,buddy1)) 伙伴有没有越过页区范围
BUG();
if (BAD_RANGE(zone,buddy2))
BUG();
memlist_del( 删除伙伴的自由链
mask area++; 求更高次位图平面
index >;>;= 1; 求更高次索引号
page_idx mask; 求更高次页块的起始页号
}
memlist_add_head( + page_idx)->;list, 将求得的高次页块加入该指数的自由链
spin_unlock_irqrestore( flags);
/*
* We don't want to protect this variable from race conditions
* since it's nothing important, but we do want to make sure
* it never gets negative.
*/
if (memory_pressure >; NR_CPUS)
memory_pressure--;
}
[目录]
--------------------------------------------------------------------------------
页目录处理的宏
对于i386的2级分页机构,每个页目录字高20位是页号,低12位是页属性.
如果将页目录字的低12位屏蔽成0,整个页目录字就是相应页面的物理地址,下面是常用的一些页目录处理的宏.
typedef struct { unsigned long pgd; } pgd_t; 一级页目录字结构
typedef struct { unsigned long pmd; } pmd_t; 中级页目录字结构
typedef struct { unsigned long pte_low; } pte_t; 末级页目录字结构
typedef struct { unsigned long pgprot; } pgprot_t; 页属性字结构
pgd_t *pgd = pgd_offset(mm_struct,addr);
取进程虚拟地址addr的一级页目录字指针,扩展为
((mm_struct)->;pgd + ((address >;>; 22) 0x3FF))
pgd_t *pgd = pgd_offset_k(addr)
取内核地址addr的一级页目录字指针,扩展为
(init_mm.pgd + ((address >;>; 22) 0x3FF));
pmd_t *pmd = pmd_offset(pgd, addr) ;
从一级页目录字指针取addr的中级页录字指针,在2级分页系统中,它们的值是相同的,扩展为
(pmd_t *)(pgd);
pte_t *pte = pte_offset(pmd, addr)
从中级页目录字指针取addr的末级页目录字指针,扩展为
(pte_t *)((pmd->;pmd 0xFFFFF000) + 0xC0000000) + ((addr >;>; 12) 0x3FF);
struct page *page = pte_page(pte_val));
取末级页目录字pte_val的页面映射指针,扩展为
(mem_map+(pte_val.pte_low >;>; 12))
pte_t pte_val = ptep_get_and_clear(pte);
取末级页目录字指针pte的值,并将该目录字清零,扩展为
(pte_t){xchg(...
pte_t pte_val = mk_pte(page,pgprot);
将页面映射指针page与页属性字pgprot组合成页目录字,扩展为
(pte_t) { (page - mem_map)
pte_t pte_val = mk_pte_phys(physpage, pgprot);
将物理地址physpage所在页面与页属性字组合成页目录字,扩展为
(pte_t) { physpage >;>; 12
unsigned long addr = pmd_page(pmd_val);
取中级页目录字所表示的页目录虚拟地址,扩展为
((unsigned long) (pmd_val.pmd 0xFFFFF000 + 0xC0000000));
set_pte(pte,pte_val);
设置末级页目录字,扩展为
*pte = pteval;
set_pmd(pmd,pmd_val)
设置中级页目录字,扩展为
*pmd = pmd_val;
set_pgd(pgd,pgd_val)
设置一级页目录字,扩展为
*pgd = pgd_val;
[目录]
--------------------------------------------------------------------------------
MM作者的文章
Linux MM: design for a zone based memory allocator
Rik van Riel, July 1998
One of the biggest problems currently facing the Linux memory management subsystem is memory fragmentation. This is the result of several developments in other parts of the Linux kernel, most importantly the growth of each process'es kernel stack to 8 kB and the dynamic allocation of DMA and networking buffers. These factors, together with a general speedup of both peripheral hardware and the device drivers has lead to a situation where the currently used buddy allocator just can't cut it anymore. This white-paper is divided in 3 pieces, the problem, the solution and some actual code. I need a lot of comments and hints for possible improvement, so feel free to email them to me...
The problem
The problem is caused by the fact that memory is allocated in chunks of different sizes. For most types of usage we just allocate memory one page (4 kB on most machines) at a time, but sometimes we give out larger pieces of memory (2, 4, 8, 16 or 32 pages at once). Because of the fact that most UNIX (and Linux) machines have a completely full memory (free memory is wasted memory), it is next to impossible to free larger area's and the best we can do is be very careful not to hand out those large areas when we only need a small one.
There have been (and there are) several workarounds for this fragmentation issue; one of them (PTE chaining) even involves a physical to logical translating, almost reverse page table-like solution. With that project, we can swap out pages based on their physical address, thus force freeing that one page that blocked an entire 128 kB area. This would solve most of our problems, except when that last page is unswappable, for example a page table or a program's kernel stack. In that case, we're screwed regardlessly of what deallocation scheme we're using.
Because our inability to hand out larger chunks of memory has impact on system functionality and could even have impact on system stability it seems warranted to sacrifice a little bit of speed (the buddy system is fast!) in order to solve most of the above problems. The main problem with the current system is that it doesn't differentiate between swappable and unswappable memory, leading to a system where page tables and other cruft are scattered all over the system, making it impossible to free up one large contiguous area.
This problem is made even worse by the fact that on some architectures we can only do DMA to addresses under 16 MB and it will undoubtedly show up again in some years when we all have 16 GB of memory and try do do DMA to those oldie 32 bit PCI cards that don't support dual cycle address mode
The solution
The solution is to hand out free zones of 128 kB large, and to use each zone for one type of usage only. Then we can be sure that no page tables interfere with the freeing of a zone of user memory, and we can always just free an area of memory.
In the current Linux kernel, we have the following uses for memory:
reserved memory, kernel code and statically allocated kernel structures: after system boot we never much with the layout of this memory so it's a non issue wrt. the allocator
user memory: this memory can be swapped out and/or relocated at will, it is allocated one page at a time and gives us no trouble, apart from the fact that we always need more than we have physically available; no special requirements
kernel stack: we allocate 8 kB (2 pages) of unswappable kernel stack for each process; each of those stacks needs to be physically contiguous and it needs to be in fast memory (not in uncached memory)
page tables: page directories are unswappable, page tables and (on some machines) page middle directories can be moved/swapped with great caution; the memory for these is given out one page at a time; we only look up the page tables every once in a while so speed is not very critical; when we have uncached memory, we'd rather use it for page tables than for user pages
small SLAB: SLAB memory is used for dynamic kernel data; it is allocated and freed at will, unfortunately this will is not ours but that of the (device) driver that requested the memory; speed is critical
large SLAB: the same as small SLAB, but sometimes the kernel wants large chunks (>; 2 pages); we make the distinction between the two because we don't want to face hopeless fragmentation inside the SLAB zones...
DMA buffers: this memory needs to be physically below a certain boundary (16 MB for ISA DMA) and is often allocated in chunks of 32, 64 or 128 kB
For small (< 16 MB) machines, the above scheme is overkill and we treat several types of usage as one. We can, for instance, treat large SLAB and DMA the same, and small SLAB, kernel stack and page table can be allocated in the same zones too. Small slab and kernel stack will be treated the same on every machine; the distinction is only made because I want the documentation to be complete.
In addition to this, we can differentiate between 3 different kinds of memory:
DMA memory: this memory is located under the 16 MB limit and is cached by the L1 and L2 caches
'normal' memory: this memory is located above the DMA limit and is cached by the L1 and L2 caches, it can not be used for DMA buffers
slow memory: this memory is not cached or present on an add-on board, it can not be used for DMA buffers and using it for time critical kernel stack and SLAB would be disastrous for performance; we also don't want to use it for CPU intensive user applications
Since we don't want to waste the slow memory we might have, we can use that for page tables and user memory that isn't used very often. If we have user memory in slow memory and it turns out that it is used very often we can always use the swap code to relocate it to fast memory. DMA memory is scarce, so we want to allocate that only we specifically need it or when we don't have any other memory left.
This leads to the following zone allocation orders:
SLAB and kernel stack | user memory | page tables | DMA buffers
-----------------------+---------------+----------------+-------------
normal memory | normal memory | slow memory | DMA memory
DMA memory | slow memory | normal memory |
slow memory | DMA memory | DMA memory |
This means that when, for instance, we ran out of user memory and there is enough free memory available, we first try to grab a zone of 'normal memory', if that fails we look for a free area of slow memory and DMA memory is tried last.
Page allocation
For SLAB, page table and DMA memory we always try to allocate from the fullest zone available and we grab a free zone when we're out of our own memory. In order to grab the fullest zone, we keep these zones in a (partially?) sorted order. For large SLAB/DMA areas we will also want to keep in mind the sizes of the memory chunks previously allocated in this zone.
User pages are kept on a number of linked lists: active, inactive, clean and free. We allocate new pages in the inactive queue and perform allocations from the free queue first, moving to the clean queue when we're out of free pages. Inactive pages get either promoted to the active queue (when they're in heavy use) or demoted to the clean queue (when they're dirty, we have to clean them first). Pages in the clean queue are also unmapped from the page table and thus already 'halfway swapped out'. Pages only enter the free list when a program free()s pages or when we add a new zone to the user area.
In order to be able to free new zones (for when SLAB gets overly active), we need to be able to mark a relatively free zone force-freeable. Upon scanning such a page kswapd will free the page and make sure it isn't allocated again.When the PTE chaining system gets integrated into the kernel, we can just force-free a user zone with relatively few active pages when the system runs out of free zones. Until then we'll need to keep two free zones and walk the page tables to find and free the pages.
Actual code
There's not much of actual code yet but all the administrative details are ready. ALPHA status reached and the .h file is ready
/*
* The struct mem_zone is used to describe a 32 page memory area.
*/
struct mem_zone {
mem_zone * prev, next; /* The previous and next zone on this list */
unsigned long used; /* Used pages bitmap for SLAB, etc !!! count for user */
unsigned long flags;
};
/*
* Flags for struct_mem->;flags
*/
#define ZONE_DMA 0x00000001 /* DMA memory */
#define ZONE_SLOW 0x00000002 /* uncached/slow memory */
#define ZONE_USER 0x00000004 /* usermode pages, these defines are for paranoia only */
#define ZONE_SLAB 0x00000008 /* large SLAB */
#define ZONE_STK 0x00000010 /* kernel stack and order-1 SLAB (and order-0 SLAB if there is slow memory) */
#define ZONE_PTBL 0x00000020 /* page tables and one-page SLAB (except when there is slow memory) */
#define ZONE_DMA 0x00000040 /* DMAbuffers */
#define ZONE_RECL 0x00000080 /* We are reclaiming this zone */
#define ZONE_0 0x00000100 /* loose pages allocated */
#define ZONE_1 0x00000200 /*order-1 (2^1 = 2 page)chunks allocated */
#define ZONE_2 0x00000400 /* etc... In order to help in buddy-like allocation for */
#define ZONE_3 0x00000800 /* large SLAB zones on small memory machines. */
#define ZONE_4 0x00001000
#define ZONE_5 0x00002000
/*
* Memory statistics
*/
typedef struct {
unsigned long used;
unsigned long free;
} zone_stats_t;
struct memstats {
struct zone_stats_t ptbl;
struct zone_stats_t stk;
struct zone_stats_t slab;
struct zone_stats_t dma;
/* Slightly different structs for these */
struct user {
unsigned long active;
unsigned long inactive;
unsigned long clean; /* we do lazy reclamation */
unsigned long free;
};
struct free {
unsigned long dma; /* different memory types */
unsigned long normal;
unsigned long slow;
};
struct misc {
unsigned long num_physpages;
unsigned long reserved; /* reserved pages */
unsigned long kernel; /* taken by static kernel stuff */
};
};
/* This is where we find the different zones */
struct memzones {
struct free {
struct mem_zone dma;
struct mem_zone normal;
struct mem_zone slow;
};
struct mem_zone dma;
struct mem_zone user;
struct mem_zone slab;
struct mem_zone stk;
struct mem_zone ptbl;
};
试图减小信号灯的数值,如果成功,信号灯的count取值成为0。这个进程现在可以继续运行并使用数据文件。但是,如果另一个进程需要使用这个文件,现在它试图减少信号灯的count数值,它会失败因为结果会是-1。这个进程会被挂起直到第一个进程处理完数据文件。当第一个进程处理完数据文件,它会增加信号灯的waking数值成为1。现在等待进程会被唤醒,这次它减小信号灯的尝试会成功。
每一个独立的信号灯操作可能都需要维护一个调整动作。Linux至少为每一个进程的每一个信号灯数组都维护一个sem_undo的数据结构。如果请求的进程没有,就在需要的时候为它创建一个。这个新的sem_undo数据结构同时在进程的task_struct数据结构和信号灯队列的semid_ds数据结构的队列中排队。对信号灯队列中的信号灯执行操作的时候,和这个操作值相抵消的值加到这个进程的sem_undo数据结构的调整队列这个信号灯的条目上。所以,如果操作值为2,那么这个就在这个信号灯的调整条目上增加-2。
当进程被删除,比如退出的时候,Linux遍历它的sem_undo数据结构组,并实施对于信号灯数组的调整。如果删除信号灯,它的sem_undo数据结构仍旧停留在进程的task_struct队列中,但是相应的信号灯数组标识符标记为无效。这种情况下,清除信号灯的代码只是简单地废弃这个sem_undo数据结构。
3.锁机制
lock_…();
unlock_…();
wait_on_…():TASK_RUNNING ->;TASK_UNINTERRUPTIBLE;
进程在RUNNING,WAITING状态间转换时,锁机制也是Linux中解决进程之间共享资源的一个方法。锁就是在资源的结构定义中加入一个锁成员,或为一个标志位,它的取值可以由多个进程检验和设置。锁可以用于实现对资源的共享竞争。具体来说当一个进程占用一个资源时,先对其上锁,然后再进行相关的操作,如果这时别的进程也要用这个资源,则必须等待这个锁被解开后,才可以进行下去。
但是,锁仅在某些数据结构和资源申请中才会用到,进程在申请某种特定资源时,会调用相应的__wait_on_… 函数来判断是否该资源已经被上锁,如果未上锁或已被解锁,则分配资源给进程,否则进程加入到等待队列中去。这种类型的申请有:__wait_on_dquot、__wait_on_buffer、__wait_on_inode、__wait_on_page、__wait_on_super等。
值得注意的是,如果申请不到这种资源,进程的状态都是转变成TASK_UNINTERRUPTIBLE。
定义锁的方式有两种:
专门的某项数据结构:
如:Superblock的数据结构中专门定义了锁数据项:s_lock;
置数据结构中某一项的某个标志位为锁标志:
如:
struct inode中定义了i_state的数据项,通过判断i_state 是否置了 I_LOCK,来判断该inode节点是否上锁。(2.2.8版本中定义)//注意:在2.2.0.34版本中是采用了专门的数据项i_lock来进行锁操作的。
struct buffer_head 中定义了b_state的数据项,通过判断b_state是否置了 BH_Lock位,来判断该缓冲区头是否上锁。
struct dquot中定义了dq_flags的数据项,通过判断dq_flags是否置了DQ_LOCKED位,来判断该dquot是否上锁。
struct page中定义了flags的数据项,通过判断flags是否置了PG_locked 位,来判断该页是否上锁。//注:程序中一般采用PageLocked(page)函数来判断是否上锁。
我们以buffer_head的加锁和解锁操作为例来解释一下通过锁机制进行的状态转换,在这里要申请buffer_head 资源,先要申请到锁,buffer_head的加锁和解锁就是通过置位和复位bh->;b_state来实现的:
//申请资源时将该缓冲区上锁,置锁位,如果申请不到,睡眠在等待队列上,等待该锁的释放。
extern inline void lock_buffer(struct buffer_head * bh)
{
while (set_bit(BH_Lock, &bh->;b_state))
__wait_on_buffer(bh);
}
//资源释放时,清该缓冲区锁位,并唤醒等待队列上的进程,参与竞争资源。
void unlock_buffer(struct buffer_head * bh)
{
......
clear_bit(BH_Lock, &bh->;b_state);
wake_up(&bh->;b_wait);
......
}
//检验该锁位是否已经置位
static inline int buffer_locked(struct buffer_head * bh)
{
return test_bit(BH_Lock, &bh->;b_state);
}
//在 \USR\SRC\LINUX\FS\BUFFER.C中定义了__wait_on_buffer(stuct buffer_head * bh);该函数判断该buffer_head是否已经被上了锁,如果是,则不能得到资源,将进程置成TASK_UNINTERRUPTIBLE,加入bh-->;b_wait队列中,调用schedule()转去调用其他的进程,否则,分配给资源,进程进入TASK_running状态。
void __wait_on_buffer(struct buffer_head * bh)
{
struct wait_queue wait = { current, NULL };
bh->;b_count++;
add_wait_queue(&bh->;b_wait, &wait);/*进程加入到等待锁的队列*/
repeat:
run_task_queue(&tq_disk);
current->;state = TASK_UNINTERRUPTIBLE;/*进程状态置为TASK_UNINTERRUPTIBLE*/
if (buffer_locked(bh)) {
schedule(); /*如果申请不到锁,重新调度CPU*/
goto repeat;
}
remove_wait_queue(&bh->;b_wait, &wait);/*进程从等待队列中删除*/
bh->;b_count--;
current->;state = TASK_RUNNING; /*进程状态置为TASK_ RUNNING*/
}
4. 管道(流)
管道做为系统的特殊设备文件,可以是内存方式的,也可以是外存方式的。管道的传输一般是单向的,即一个管道一向,若两个进程要做双向传输则需要2个管道.管道生成时即有两端,一端为读,一端为写,两个进程要协调好,一个进程从读方读,另一个进程向写方写。管道的读写使用流设备的读写函数,即:read(),write.管道的传输方式为FIFO,流方式的.不象消息队列可以按类型读取.管道分为有名管道和无名管道:
1. 有名管道
一般为系统特殊文件方式,使用的进程之间不一定要有父子关系或兄弟关系.
2. 无名管道
一般为内存方式,使用的进程之间一定要有父子关系或兄弟关系.
Linux shell允许重定向。例如:
$ ls | pr | lpr
把列出目录文件的命令ls的输出通过管道接到pr命令的标准输入上进行分页。最后,pr命令的标准输出通过管道连接到lpr命令的标准输入上,在缺省打印机上打印出结果。管道是单向的字节流,把一个进程的标准输出和另一个进程的标准输入连接在一起。没有一个进程意识到这种重定向,和它平常一样工作。是shell建立了进程之间的临时管道。在Linux中,使用指向同一个临时VFS INODE节点(本身指向内存中的一个物理页)的两个file数据结构来实现管道。当写进程向管道中写的时候,字节拷贝到了共享的数据页,当从管道中读的时候,字节从共享页中拷贝出来。Linux必须同步对于管道的访问。必须保证管道的写和读步调一致,它使用锁、等待队列和信号。
运用管道方式进行通讯的进程,由于都是调用sleep_on_interruptible,因此都是睡眠在TASK_INTERRUPTIBLE状态的。
管道结构的定义在include\linux\pipe_fs_i.h中,
struct pipe_inode_info {
struct wait_queue * wait;
char * base;
unsigned int start;
unsigned int len;
unsigned int lock; //用到了锁
unsigned int rd_openers;
unsigned int wr_openers;
unsigned int readers;
unsigned int writers;
};
对管道的操作主要有读和写两种:
1.向一个管道写pipe_write():
在\fs\pipe.c中定义了static int pipe_write(struct inode * inode, struct file * filp, const char * buf, int count);
实现机制:当写进程向管道写的时候,它使用标准的write库函数。这些库函数传递的文件描述符是进程的file数据结构组中的索引,每一个都表示一个打开的文件,在这种情况下,是打开的管道。Linux系统调用使用描述这个管道的file数据结构指向的write例程。这个write例程使用表示管道的VFS INODE节点存放的信息,来管理写的请求。如果有足够的空间把所有的字节都写导管到中,只要管道没有被读进程锁定,Linux为写进程上锁,并把字节从进程的地址空间拷贝到共享的数据页。如果管道被读进程锁定或者空间不够,当前进程睡眠,并放在管道INODE节点的等待队列中,并调用调度程序,运行另外一个进程。它是可以中断的,所以它可以接收信号。当管道中有了足够的空间写数据或者锁定解除,写进程就会被读进程唤醒。当数据写完之后,管道的VFS INODE 节点锁定解除,管道INODE节点的等待队列中的所有读进程都会被唤醒。
2.从一个管道读Pipe_read():
在\fs\pipe.c中定义了static int pipe_read(struct inode * inode, struct file * filp, char * buf, int count);
实现机制:从管道中读取数据和写数据非常相似。进程允许进行非阻塞的读(依赖于它们打开文件或者管道的模式),这时,如果没有数据可读或者管道被锁定,会返回一个错误。这意味着进程会继续运行。另一种方式是在管道的INODE节点的等待队列中等待,直到写进程完成。如果管道的进程都完成了操作,管道的INODE节点和相应的共享数据页被废弃。
进程在TASK_RUNNING和TASK_STOPPED间的转换:
1.进程从TASK_RUNNING->;TASK_STOPPED的转换:
TASK_STOPPED状态是一种暂停状态,和TASK_STOPPED状态配合工作的标志为PF_PTRACED和PF_TRACESYS,分别表示被跟踪和正在跟踪系统调用,一个是被动的,一个是主动的。
进程可通过两种途径进入TASK_STOPPED状态:
1).受其它进程的syscall_trace()系统调用的控制而暂时将CPU交给控制进程。
在调用syscall_trace()之前必须先调用sys_ptrace()(简称ptrace()),进行跟踪系统调用之前的准备工作。只有调用sys_ptrace()后,current的PF_PTRACED和PF_TRACESYS标志都已置位,跟踪和被跟踪的关系都已明确,再调用syscall_trace()才能真正使进程转入STOPPED状态。
syscall_trace()实现步骤:
(1)检验是否调用过ptrace()做过准备,没有则返回;
(2)置状态STOPPED ,通知父进程,子进程状态已变;
(3)进行CPU重新调度,将current进程从runqueue删除。
(4)如果exit_code非空,将它的值作为接收到的信号放到signal中。若是SIGTRAP
则current进程将一直处于stopped,直到收到其它信号。
sys_ptrace()实现步骤:
(1)如果request == PTRACE_TRACEME,则有进程要求跟踪current进程:
若current进程已被其它进程跟踪返回;
否则置current进程已被进程跟踪的标记;
(2)如果current进程想跟踪其它进程:
a.不能跟踪init进程;
b.找pid对应的进程child,找不到返回出错;
c.如果request为PTRACE_ATTACH
如果current进程试图ATTACH自己,出错;
如果试图attach的进程child已被attach,出错;
否则 child->;flags |= PF_PTRACED;做上标记,child已被attach;如果child
不是current的子进程,将它变成current的子进程;并且发SIGSTOP信号,暂
停它。
(3)进行其他合法性检查;
(4)判断request,执行相应操作:
case PTRACE_SYSCALL:继续执行,在下一个系统调用返回处停下。
case PTRACE_CONT:发信号后重新开始运行。
如果request == PTRACE_SYSCALL,置child标志位PF_TRACESYS;
否则 清child标志位PF_TRACESYS,以防止重新运行后因历史原因在下一个
系统调用返回处停下;
唤醒child进程。
case PTRACE_KILL: 想要结束child进程,唤醒child进程,并在退出信息
exit_code中存放SIGKILL信号。
case PTRACE_SINGLESTEP: 进行单步运行环境设置。
case PTRACE_DETACH: 恢复child进程的自由。清跟踪标志,并唤醒child进程 恢复child进程的原始亲属关系。
2).收到要求它暂停的信号。
另一种进入STOPPED状态的方法是信号,SIGSTOP信号使自由的current进程,打上PF_PTRACED标记,并将它转入STOPPED状态。do_signal在检查进程收到的信号时,若发现current进程已打上PF_PTRACED标记,则除收到的SIGKILL信号的情况外,current进程都将马上进入STOPPED状态。
do_signal()实现步骤:
(1)current进程已打上PF_PTRACED标记,则除收到的SIGKILL信号的情况外,进程都将进入TASK_STOPPED状态,通知父进程,并重新调度;
(2)如果收到信号SIGSTOP:如果当前进程标志位不是PF_PTRACED,则置当前进程状态为TASK_STOPPED; 通知父进程,并重新调度;
2.进程从TASK_STOPPED->;TASK_RUNNING的转换:
从TASK_STOPPED状态转到TASK_RUNNING状态通过“信号唤醒”。当有SIGKILL或SIGCONT信号发给TASK_STOPPED状态下的进程时,进程将被wake_up_process()唤醒。
int send_sig(unsigned long sig,struct task_struct * p,int priv)
{
………;
save_flags(flags); cli(); /*保存标志,关中断*/
if ((sig == SIGKILL) || (sig == SIGCONT)) {
if (p->;state == TASK_STOPPED)
wake_up_process(p); /*若进程p的状态是STOPPED,并且所发送的信号是SIGKILL和SIGCONT,将p状态赋成RUNNING,并挂到run-queue*/
p->;exit_code = 0; /*退出信息没有*/
p->;signal &= ~( (1<<(SIGSTOP-1)) | (1<<(SIGTSTP-1)) |
(1<<(SIGTTIN-1)) | (1<<(SIGTTOU-1)) ); /*处理过信号后,将p的可能曾接受到的SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU信号清掉*/
}
if (sig == SIGSTOP || sig == SIGTSTP || sig == SIGTTIN || sig == SIGTTOU)
p->;signal &= ~(1<<(SIGCONT-1)); /*若信号为SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU中的任一种,将p可能曾接受到的SIGCONT信号清掉*/
restore_flags(flags); /*恢复CPU标志同时打开中断*/
generate(sig,p); /*登记不能立即被处理的信号。*/
return 0;
}
进程的终止:从TASK_RUNNING->;TASK_ZOMBIE的转换
进程终止由可终止进程的系统调用通过调用do_exit()实现,do_exit()终止current进程,首先为current进程做上PF_EXITING的标记,释放current进程的存储管理信息、文件系统、文件信息、信号响应函数指针数组,将状态置成TASK_ZOMBIE,通知current的父进程,最后进行重新调度。do_exit()带一个参数code,用于传递终止进程的原因。
do_exit(long code)流程:
(1)如果进程在中断服务程序中调用do_exit(),则打印提示信息
(2)记录进程的记帐信息
(3)进程标志置为PF_EXITING
(4)释放定时器链表
(5)释放临界区数据
(6)将消息队列中和current进程有关项删除
(7)释放进程的存储管理信息
(8)释放进程已打开文件的信息
(9)释放进程的文件系统
(10)释放进程的信号响应函数指针数组等管理信息
(11)释放进程的LDT
(12)进程状态置为TASK_ZOMBIE
(13)置上退出信息,通知所有进程亲戚,它要退出了#
(14)exec_domain结构共享计数减1, binfmt结构共享计数减1
(15)重新调度,将current进程从run-queue中删除,交出CPU
exit_notify ()函数向所有和current进程有关的进程发相应的消息,以便它们开展工作,exit_notify ()还判断cueernt进程所在组是否会因current进程的退出而悬空,如果悬空并且组内有stopped状态的进程则发信号;同时进行一系列的指针调整,调整因current进程的死亡引起的进程关系转变。
exit_notify ()流程:
将所有原始进程为current的进程变成init进程的孙子。
如果父进程和current进程不在同一组,但在同一session内并且current进程组内所有进程的父进程和它在同一组,也就是说,current进程所在组会因current的退出而悬挂,同时current进程所在组内有stopped进程,就向整个组发SIGHUP和SIGCONT信号。
通知父进程进程死了。
调整所有current进程的子进程的父进程指针,将它们挂到它们的原始进程下,
将以往的跟踪被跟踪历史清除,调整它和新的兄弟的关系;检查每一个current
进程的子进程所在的组是否会悬挂,如果子进程和current进程不在同一组,并
且这个组已悬挂,组内有stopped的进程,就向组员发SIGHUP 和 SIGCONT信号。 (5)如果current进程是session的主管, 就和它所控制的tty脱离,向current
进程显示终端所在的组发SIGHUP 和 SIGCONT信号。
进程直接或间接地调用do_exit() 后,进程进入ZOMBIE状态,还有一块PCB未释放。PCB的释放必须由它的父进程执行,当父进程调用sys_wait4()时释放进入ZOMBIE状态的子进程的PCB。
具体调用do_exit()的函数有以下情况:
具体对应的系统调用出错,不得不终止进程,如:
do_page_fault():这个系统调用处理页面错误,它找到页面地址,出错原因,并将它转入相应的处理函数。当发生越界(out of memory)等bad page的致命错误。
sys_sigreturn():一般情况下sys_sigreturn()将sigcontext的内容保存到堆栈中,保存过程中当发现段寄存器越界了,这个致命错误就将导致进程结束。
setup_frame():setup_frame()建立信号响应函数的运行的环境,保存当前寄存器,将相应寄存器赋上信号响应函数的信息。在具体设定之前首先进行存储条件检验,不满足就不得不结束进程。
save_v86_state():save_v86_state()保存虚拟86模式下(virtual 86 mode)的信息,如果进程PCB中vm86的信息为空的,无法继续进行操作,只能结束进程。
(2)其他终止进程的情况,通过调用以下函数实现终止:
sys_exit():是一个系统调用,实现终止调用它的当前进程。
sys_reboot():sys_reboot()只能被特权级用户调用,用于重新启动系统。
do_signal():do_signal()是处理信号的一个函数。检查current进程每一个接收到的signal,如果是结束进程的信号,结束进程进行相应处理。
die_if_kernel()。
[目录]
--------------------------------------------------------------------------------
线程
1 概述
1.1 线程的定义(Introduction)
Threads can best be described as “lightweight processes”. The traditional UNIX-style notion of a process has been found to be inconvenient, if not inadequate for several applications in distributed systems development. The needs of these applications are best served by threads, which generalize the notion of a process so that it can be associated with multiple activities. The popularity of threads has resulted in their implementation on UNIX systems and thread libraries are now widely available to programmers for the development of concurrent applications.
1.2 Threads Implementation
Threads can be implemented in one of two ways:
1. User-level threads:
There is no kernel support for multi-threaded processes. Hence, the kernel only has a single-threaded process abstraction, but multi-threaded processes are implemented in a library of procedures linked with application programs. The kernel has no knowledge of lightweight processes (threads), and therefore cannot schedule them independently. A threads run-time library organizes the scheduling of threads. A thread would block the process and therefore all threads within it if it made a blocking system call, so the asynchronous I/O facilities of UNIX are used. The major disadvantage of this scheme is that threads within a process cannot take advantage of a multi-processor.
(上段译文)User-level没有核心支持的多线程的进程。因此,核心只有单线程进程概念,而多线程进程由与应用程序连接的过程库实现。核心不知道线程的存在,也就不能独立的调度这些线程了。一个线程运行库组织线程的调度。如果一个线程调用了一个阻塞的系统调用,进程可能被阻塞,当然其中的所有线程也同时被阻塞,所以UNIX使用了异步I/O工具。这种机制的的最大缺点是不能发挥多处理器的优势。
The advantages include:
(系统消耗小)Certain thread operations are significantly less costly. For example, switching between threads belonging to the same process do not necessarily involve a system call, and hence save this over-head.
(可以修改以适应特殊的应用)User-level thread implementations can be customized or changed to suit the particular application requirements. This is particularly useful for real-time multimedia processing etc. Also, it is possible to support many more user-level threads than can by default by a kernel.
2. Kernel-level threads:
This implementation allows threads within different processes to be scheduled according to a single scheme of relative prioritizing. This is suited for exploiting the concurrence of multiprocessors.
核心级线程如许不同进程里的线程按照同一相对优先方法调度,这适合于发挥多处理器的并发优点。
Most of the current thread library implementations available today implement user-level threads. There have been several research projects that have implemented some form of Kernel-level threads. Notable among these are the Mach distributed OS, which combines the advantages of user-level and kernel-level threads by allowing user-level code to provide scheduling hints to the kernel thread scheduler. By providing such a two-level scheduling scheme, the kernel retains control over the allocation of processor time, but also allows a process to take advantage of multiple processors.
1.3 Thread Libraries
The two most widely used thread libraries are POSIX and Solaris thread libraries. Both implementations are inter-operable, their functionality is similar, and can be used within the same application. However, only POSIX threads are guaranteed to be fully portable to other POSIX-compliant environments.
Similarities:
Most of the functions in both libraries, libpthread and libthread, have a counterpart in the other library. POSIX functions and Solaris functions, whose names have similar endings, usually have similar functionality, number of arguments, and use of arguments. All POSIX threads function names begin with the prefix pthread? where as the Solaris threads function names begin with the prefix thr?
Differences:
POSIX
is more portable
establishes characteristics for each thread according to configurable attribute objects
implements thread cancellation
enforces scheduling algorithms
allows for clean-up handlers for fork(2) calls
Solaris
threads can be suspended and continued
implements an optimized mutex, readers/writer locking
may increase the concurrency
implements daemon threads, for whose demise the process does not wait
1.4 Threads Standards
There are three different definitions for thread libraries competing for attention today: Win32, OS/2, and POSIX. The first two are proprietary and limited to their individual platforms (Win32 threads run only under NT and Win95, OS/2 threads on OS/2). The POSIX specification (IEEE 1003.1c, aka Pthreads) is intended for all computing platforms, and implementations are available or in the works for almost all major UNIX systems (including Linux), along with VMS.
POSIX Threads
The POSIX standard defines the API and behavior that all the Pthreads libraries must meet. It is part of the extended portion of POSIX, so it is not a requirement for meeting XPG4, but it is required for X/Open UNIX 98, and all major UNIX vendors have committed to meeting this standard. As of this writing, (7/97) almost all UNIX vendors have released a library.
Win32 and OS/2 Threads
Both the NT and OS/2 implementations contain some fairly radical differences
from the POSIX standard--to the degree that even porting from one or the other
to POSIX will prove moderately challenging. Microsoft has not announced any
plans to adopt POSIX. There are freeware POSIX libraries for Win32 (see
Commercial Products on page 249), and OS/2 also has an optional POSIX library.
DCE Threads
Before POSIX completed work on the standard, it produced a number of drafts which it published for comment. Draft 4 was used as the basis for the threads library in DCE. It is similar to the final spec, but it does contain a number of significant differences. Presumably, no one is writing any new DCE code.
Solaris Threads
Also known as UI threads, this is the library, which SunSoft used in developing Solaris 2 before the POSIX, committee completed their work. It will be available on Solaris 2 for the foreseeable future, although we expect most applications writers will opt for Pthreads. The vast majority of the two libraries are virtually identical.
1.5 Linux线程的思想及特点
1.5.1 LinuxThreads
http://pauillac.inria.fr/~xleroy/linuxthreads
Xavier Leroy at INRIA (Paris, France), with input from Pavel Krauz, Richard Henderson and others, has developed a Pthreads library that implements the One-to-One model, allowing it to take advantage of multiple processors. It is based on the new Linux system call, clone()2 . It runs on Linux 2.0 and up, on Intel, Alpha, SPARC, m68k, and MIPS machines. One limitation is its non-standard implementation of signal handling.
1.5.2 Implementation model for LinuxThreads
LinuxThreads follows the so-called "one-to-one" model: each thread is actually a separate process in the kernel. The kernel scheduler takes care of scheduling the threads, just like it schedules regular processes. The threads are created with the Linux clone() system call, which is a generalization of fork() allowing the new process to share the memory space, file descriptors, and signal handlers of the parent.
LinuxThreads采用称为1-1模型:每个线程实际上在核心是一个个单独的进程,核心的调度程序负责线程的调度,就象调度普通进程。线程是用系统调用clone()创建的,clone()系统调用是fork()的普遍形式,它允许新进程共享父进程的存储空间、文件描述符和软中断处理程序。
Advantages of the "one-to-one" model include:
Minimal overhead on CPU-intensive multiprocessing (with about one thread per processor); 最小限度消耗的CPU级多处理技术(每个CPU一个线程);
Minimal overhead on I/O operations; 最小限度消耗的I/O操作;
A simple and robust implementation (the kernel scheduler does most of the hard work for us);一种简单和强壮的实现(核心调度程序为我们做了大部分艰难的工作)。
The main disadvantage is more expensive context switches on mutex and condition operations, which must go through the kernel. This is mitigated by the fact that context switches in the Linux kernel are pretty efficient.
1.5.3 Consider other implementation models
There are basically two other models. The "many-to-one" model relies on a user-level scheduler that context-switches between the threads entirely in user code; viewed from the kernel, there is only one process running. This model is completely out of the question for me, since it does not take advantage of multiprocessors, and require unholy magic to handle blocking I/O operations properly. There are several user-level thread libraries available for Linux, but I found all of them deficient in functionality, performance, and/or robustness.
还有另外两种基本模型。多对一模型依赖于用户级的调度程序,线程切换完全由用户程序完成;从核心角度看,只有一个进程正在运行。这种模型不是我们所关心的,因为它无法利用多处理器的优点,而且要用不合理的方法处理I/O操作阻塞。
The "many-to-many" model combines both kernel-level and user-level scheduling: several kernel-level threads run concurrently, each executing a user-level scheduler that selects between user threads. Most commercial Unix systems (Solaris, Digital Unix and IRIX) implement POSIX threads this way. This model combines the advantages of both the "many-to-one" and the "one-to-one" model, and is attractive because it avoids the worst-case behaviors of both models -- especially on kernels where context switches are expensive, such as Digital Unix. Unfortunately, it is pretty complex to implement, and requires kernel supporting which Linux does not provide. Linus Torvalds and other Linux kernel developers have always been pushing the "one-to-one" model in the name of overall simplicity, and are doing a pretty good job of making kernel-level context switches between threads efficient. LinuxThreads is just following the general direction they set.
2 Linux核心对线程的支持
Linux核心对线程的支持主要是通过其系统调用,下文将进行系统的介绍。
2.1 系统调用clone()
以下是系统调用clone的代码:
asmlinkage int sys_clone(struct pt_regs regs)
{
unsigned long clone_flags;
unsigned long newsp;
clone_flags = regs.ebx;
newsp = regs.ecx;
if (!newsp)
newsp = regs.esp;
return do_fork(clone_flags, newsp, ®s);
}
与系统调用clone功能相似的系统调用有fork,但fork事实上只是clone的功能的一部分,clone与fork的主要区别在于传递了几个参数,而当中最重要的参数就是conle_flags,下表是系统定义的几个clone_flags标志:
标志 Value 含义
CLONE_VM 0x00000100 置起此标志在进程间共享VM
CLONE_FS 0x00000200 置起此标志在进程间共享文件系统信息
CLONE_FILES 0x00000400 置起此标志在进程间共享打开的文件
CLONE_SIGHAND 0x00000800 置起此标志在进程间共享信号处理程序
如果置起以上标志所做的处理分别是:
置起CLONE_VM标志:
mmget(current->;mm);
/*
* Set up the LDT descriptor for the clone task.
*/
copy_segments(nr, tsk, NULL);
SET_PAGE_DIR(tsk, current->;mm->;pgd);
置起CLONE_ FS标志:
atomic_inc(¤t->;fs->;count);
置起CLONE_ FILES标志:
atomic_inc(&oldf->;count);
置起CLONE_ SIGHAND标志:
atomic_inc(¤t->;sig->;count);
2.2 与线程调度相关的系统调用
以下是glibc-linuxthread用来进行调度的系统调度:
.long SYMBOL_NAME(sys_sched_setparam) /* 系统调用154 */
/*用来设置进程(或线程)的调度参数*/
.long SYMBOL_NAME(sys_sched_getparam)
/*用来获取进程(或线程)的调度参数*/
.long SYMBOL_NAME(sys_sched_setscheduler)
/*用来设置进程(或线程)的调度参数*/
.long SYMBOL_NAME(sys_sched_getscheduler)
/*用来获取进程(或线程)的调度参数*/
.long SYMBOL_NAME(sys_sched_yield)
/*用来强制核心重新调度进程(或线程)*/
.long SYMBOL_NAME(sys_sched_get_priority_max)
/*用来设置进程(或线程)的调度参数*/
.long SYMBOL_NAME(sys_sched_get_priority_min)
/*用来获取进程(或线程)的调度参数*/
.long SYMBOL_NAME(sys_sched_rr_get_interval) /* 系统调用161 */
/*用来获取进程(或线程)的调度时间间隔*/
3 Linux线程的实现
3.1 LinuxThreads概述
现在的0.8版LinuxThreads,是迄今为止在Linux下支持threads的最好的Runtime-library,而包含0.8版LinuxThreads的最好的Runtime-library是glibc- 2.1,下文所要分析的正是glibc-linuxthreads-2.1。
首先介绍一下0.8版LinuxThreads,它实现了一种BiCapitalized面向Linux的Posix 1003.1c"pthread"标准接口。LinuxThreads提供核心级线程即每个线程是一个独立的UNIX进程,通过调用新的系统调用与其它线程共享地址空间。线程由核心调度,就象UNIX进程调度一样。使用它的要求是:LINUX 版本2.0 或以上(要求有新的clone() 系统调用和新的实时调度程序)。对于Intel平台:要求有libc 5.2.18或后续版本,推荐使用5.2.18 或 5.4.12 及其后续版本;5.3.12和5.4.7有问题,也支持glibc 2,实际上是支持它的一个特别合适的版本。到目前支持Intel, Alpha, Sparc, Motorola 68k, ARM and MIPS平台,还支持多处理器
3.2 主要的数据结构及初始化
3.2.1 数据结构和部分数据初始化
/* Arguments passed to thread creation routine */
//传递给线程创建程序的参数
struct pthread_start_args {
void * (*start_routine)(void *); /* function to run */
void * arg; /* its argument */
sigset_t mask; /* initial signal mask for thread */
int schedpolicy; /* initial scheduling policy (if any) */
struct sched_param schedparam; /* initial scheduling parameters (if any) */
};
/* The type of thread descriptors */
//线程描述符类型
typedef struct _pthread_descr_struct * pthread_descr;
struct _pthread_descr_struct {
pthread_descr p_nextlive, p_prevlive;
/* Double chaining of active threads */
pthread_descr p_nextwaiting; /* Next element in the queue holding the thr */
pthread_t p_tid; /* Thread identifier */
int p_pid; /* PID of Unix process */
int p_priority; /* Thread priority (== 0 if not realtime) */
struct _pthread_fastlock * p_lock; /* Spinlock for synchronized accesses */
int p_signal; /* last signal received */
sigjmp_buf * p_signal_jmp; /* where to siglongjmp on a signal or NULL */
sigjmp_buf * p_cancel_jmp; /* where to siglongjmp on a cancel or NULL */
char p_terminated; /* true if terminated e.g. by pthread_exit */
char p_detached; /* true if detached */
char p_exited; /* true if the assoc. process terminated */
void * p_retval; /* placeholder for return value */
int p_retcode; /* placeholder for return code */
pthread_descr p_joining; /* thread joining on that thread or NULL */
struct _pthread_cleanup_buffer * p_cleanup; /* cleanup functions */
char p_cancelstate; /* cancellation state */
char p_canceltype; /* cancellation type (deferred/async) */
char p_canceled; /* cancellation request pending */
int * p_errnop; /* pointer to used errno variable */
int p_errno; /* error returned by last system call */
int * p_h_errnop; /* pointer to used h_errno variable */
int p_h_errno; /* error returned by last netdb function */
char * p_in_sighandler; /* stack address of sighandler, or NULL */
char p_sigwaiting; /* true if a sigwait() is in progress */
struct pthread_start_args p_start_args; /* arguments for thread creation */
void ** p_specific[PTHREAD_KEY_1STLEVEL_SIZE]; /* thread-specific data */
void * p_libc_specific[_LIBC_TSD_KEY_N]; /* thread-specific data for libc */
int p_userstack; /* nonzero if the user provided the stack */
void *p_guardaddr; /* address of guard area or NULL */
size_t p_guardsize; /* size of guard area */
pthread_descr p_self; /* Pointer to this structure */
int p_nr; /* Index of descriptor in __pthread_handles */
};
/* The type of thread handles. */
线程句柄
typedef struct pthread_handle_struct * pthread_handle;
struct pthread_handle_struct {
struct _pthread_fastlock h_lock; /* Fast lock for sychronized access */
pthread_descr h_descr; /* Thread descriptor or NULL if invalid */
char * h_bottom; /* Lowest address in the stack thread */
};
/* The type of messages sent to the thread manager thread */
//发送给线程管理线程的请求
struct pthread_request {
pthread_descr req_thread; /* Thread doing the request */
enum { /* Request kind */
REQ_CREATE, REQ_FREE, REQ_PROCESS_EXIT, REQ_MAIN_THREAD_EXIT,
REQ_POST, REQ_DEBUG
} req_kind;
union { /* Arguments for request */
struct { /* For REQ_CREATE: */
const pthread_attr_t * attr; /* thread attributes */
void * (*fn)(void *); /* start function */
void * arg; /* argument to start function */
sigset_t mask; /* signal mask */
} create;
struct { /* For REQ_FREE: */
pthread_t thread_id; /* identifier of thread to free */
} free;
struct { /* For REQ_PROCESS_EXIT: */
int code; /* exit status */
} exit;
void * post; /* For REQ_POST: the semaphore */
} req_args;
};
/* One end of the pipe for sending requests to the thread manager. */
//向管理线程发送请求的管道的一端;初始化为-1表示管理线程还没有创建
int __pthread_manager_request = -1;
/* Other end of the pipe for sending requests to the thread manager. */
int __pthread_manager_reader;
//线程的堆栈大小
#define STACK_SIZE (2 * 1024 * 1024)
//线程的初始堆栈大小
#define INITIAL_STACK_SIZE (4 * PAGE_SIZE)
/* Attributes for threads. */
//线程的属性
typedef struct
{
int __detachstate;
int __schedpolicy;
struct __sched_param __schedparam;
int __inheritsched;
int __scope;
size_t __guardsize;
int __stackaddr_set;
void *__stackaddr;
size_t __stacksize;
} pthread_attr_t;
//每个进程的最大线程数
#define PTHREAD_THREADS_MAX 1024
3.2.2 Main thread and manager thread initializing
/* Thread creation */
int __pthread_create_2_1(pthread_t *thread, const pthread_attr_t *attr,
void * (*start_routine)(void *), void *arg)
{
pthread_descr self = thread_self();
struct pthread_request request;
if (__pthread_manager_request < 0) { //检查是否启动线程机制
//初始化管理线程,启动线程机制
if (__pthread_initialize_manager() < 0) return EAGAIN;
}
request.req_thread = self;
request.req_kind = REQ_CREATE;
request.req_args.create.attr = attr;
request.req_args.create.fn = start_routine;
request.req_args.create.arg = arg;
sigprocmask(SIG_SETMASK, (const sigset_t *) NULL,
&request.req_args.create.mask);
//向管理线程发送请求
__libc_write(__pthread_manager_request, (char *) &request, sizeof(request));
suspend(self);
if (THREAD_GETMEM(self, p_retcode) == 0)
*thread = (pthread_t) THREAD_GETMEM(self, p_retval);
return THREAD_GETMEM(self, p_retcode);
}
int __pthread_initialize_manager(void)
{
int manager_pipe[2];
int pid;
struct pthread_request request;
/* If basic initialization not done yet (e.g. we're called from a constructor run before our constructor), do it now */
//初始化初始线程
if (__pthread_initial_thread_bos == NULL) pthread_initialize();
/* Setup stack for thread manager */建立管理线程堆栈
__pthread_manager_thread_bos = malloc(THREAD_MANAGER_STACK_SIZE);
if (__pthread_manager_thread_bos == NULL) return -1;
__pthread_manager_thread_tos =
__pthread_manager_thread_bos + THREAD_MANAGER_STACK_SIZE;
/* Setup pipe to communicate with thread manager */
//建立与管理线程通信的管道
if (pipe(manager_pipe) == -1) {
free(__pthread_manager_thread_bos);
return -1;
}
/* Start the thread manager */启动管理线程
pid = __clone(__pthread_manager, (void **) __pthread_manager_thread_tos,
CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND
, (void *)(long)manager_pipe[0]);
if (pid == -1) {
free(__pthread_manager_thread_bos);
__libc_close(manager_pipe[0]);
__libc_close(manager_pipe[1]);
return -1;
}
__pthread_manager_request = manager_pipe[1]; /* writing end */
__pthread_manager_reader = manager_pipe[0]; /* reading end */
__pthread_manager_thread.p_pid = pid;
/* Make gdb aware of new thread manager */
if (__pthread_threads_debug && __pthread_sig_debug >; 0)
{
raise(__pthread_sig_debug);
/* We suspend ourself and gdb will wake us up when it is
ready to handle us. */
suspend(thread_self());
}
/* Synchronize debugging of the thread manager */
request.req_kind = REQ_DEBUG;
__libc_write(__pthread_manager_request, (char *) &request, sizeof(request));
return 0;
}
//初始化初始线程
static void pthread_initialize(void)
{
struct sigaction sa;
sigset_t mask;
struct rlimit limit;
int max_stack;
/* If already done (e.g. by a constructor called earlier!), bail out */
if (__pthread_initial_thread_bos != NULL) return;
#ifdef TEST_FOR_COMPARE_AND_SWAP
/* Test if compare-and-swap is available */
__pthread_has_cas = compare_and_swap_is_available();
#endif
/* For the initial stack, reserve at least STACK_SIZE bytes of stack below the current stack address, and align that on a STACK_SIZE boundary. */
//当前堆栈下为初始堆栈留出至少STACK_SIZE,并按STACK_SIZE对齐
__pthread_initial_thread_bos =
(char *)(((long)CURRENT_STACK_FRAME - 2 * STACK_SIZE) & ~(STACK_SIZE - 1));
/* Play with the stack size limit to make sure that no stack ever grows
beyond STACK_SIZE minus two pages (one page for the thread descriptor
immediately beyond, and one page to act as a guard page). */
//调整堆栈大小限制使其不能增长超过STACK_SIZE减两页(一页给线程
//描述符,一页作为保护页)
getrlimit(RLIMIT_STACK, &limit);
max_stack = STACK_SIZE - 2 * __getpagesize();
if (limit.rlim_cur >; max_stack) {
limit.rlim_cur = max_stack;
setrlimit(RLIMIT_STACK, &limit);
}
/* Update the descriptor for the initial thread. */
__pthread_initial_thread.p_pid = __getpid();
/* If we have special thread_self processing, initialize that for the
main thread now. */
#ifdef INIT_THREAD_SELF
INIT_THREAD_SELF(&__pthread_initial_thread, 0);
#endif
/* The errno/h_errno variable of the main thread are the global ones. */
__pthread_initial_thread.p_errnop = &_errno;
__pthread_initial_thread.p_h_errnop = &_h_errno;
#ifdef SIGRTMIN
/* Allocate the signals used. */分配使用的软中断号
__pthread_sig_restart = __libc_allocate_rtsig (1);
__pthread_sig_cancel = __libc_allocate_rtsig (1);
__pthread_sig_debug = __libc_allocate_rtsig (1);
if (__pthread_sig_restart < 0 ||
__pthread_sig_cancel < 0 ||
__pthread_sig_debug < 0)
{
/* The kernel does not support real-time signals. Use as before
the available signals in the fixed set.
Debugging is not supported in this case. */
__pthread_sig_restart = DEFAULT_SIG_RESTART;
__pthread_sig_cancel = DEFAULT_SIG_CANCEL;
__pthread_sig_debug = 0;
}
#endif
/* Setup signal handlers for the initial thread.
Since signal handlers are shared between threads, these settings
will be inherited by all other threads. */
//设置初始进程的信号处理程序
#ifndef __i386__
sa.sa_handler = pthread_handle_sigrestart;
#else
sa.sa_handler = (__sighandler_t) pthread_handle_sigrestart;
#endif
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
__sigaction(__pthread_sig_restart, &sa, NULL);
#ifndef __i386__
sa.sa_handler = pthread_handle_sigcancel;
#else
sa.sa_handler = (__sighandler_t) pthread_handle_sigcancel;
#endif
sa.sa_flags = 0;
__sigaction(__pthread_sig_cancel, &sa, NULL);
if (__pthread_sig_debug >; 0) {
sa.sa_handler = pthread_handle_sigdebug;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
__sigaction(__pthread_sig_debug, &sa, NULL);
}
/* Initially, block __pthread_sig_restart. Will be unblocked on demand. */
sigemptyset(&mask);
sigaddset(&mask, __pthread_sig_restart);
sigprocmask(SIG_BLOCK, &mask, NULL);
/* Register an exit function to kill all other threads. */
/* Do it early so that user-registered atexit functions are called
before pthread_exit_process. */
__on_exit(pthread_exit_process, NULL);
}
3.3 线程的创建
Manager thread 接到创建线程请求后调用下函数。
static int pthread_handle_create(pthread_t *thread, const pthread_attr_t *attr,
void * (*start_routine)(void *), void *arg,
sigset_t * mask, int father_pid)
{
size_t sseg;
int pid;
pthread_descr new_thread;
char * new_thread_bottom;
pthread_t new_thread_id;
char *guardaddr = NULL;
size_t guardsize = 0;
int pagesize = __getpagesize();
/* First check whether we have to change the policy and if yes, whether
we can do this. Normally this should be done by examining the
return value of the __sched_setscheduler call in pthread_start_thread
but this is hard to implement. FIXME */
//检查是否需要调整调度策略,如果需要,是否能够做到
if (attr != NULL && attr->;__schedpolicy != SCHED_OTHER && geteuid () != 0)
return EPERM;
/* Find a free segment for the thread, and allocate a stack if needed */
//找出一个空段,如果需要再分配堆栈
for (sseg = 2; ; sseg++)
{
if (sseg >;= PTHREAD_THREADS_MAX)
return EAGAIN;
if (__pthread_handles[sseg].h_descr != NULL)
continue;
if (pthread_allocate_stack(attr, thread_segment(sseg), pagesize,
&new_thread, &new_thread_bottom,
&guardaddr, &guardsize) == 0)
break;
}
__pthread_handles_num++;
/* Allocate new thread identifier */分配新线程的标识符
pthread_threads_counter += PTHREAD_THREADS_MAX;
new_thread_id = sseg + pthread_threads_counter;
/* Initialize the thread descriptor */初始化新线程描述符
new_thread->;p_nextwaiting = NULL;
new_thread->;p_tid = new_thread_id;
new_thread->;p_priority = 0;
new_thread->;p_lock = &(__pthread_handles[sseg].h_lock);
new_thread->;p_signal = 0;
new_thread->;p_signal_jmp = NULL;
new_thread->;p_cancel_jmp = NULL;
new_thread->;p_terminated = 0;
new_thread->;p_detached = attr == NULL ? 0 : attr->;__detachstate;
new_thread->;p_exited = 0;
new_thread->;p_retval = NULL;
new_thread->;p_joining = NULL;
new_thread->;p_cleanup = NULL;
new_thread->;p_cancelstate = PTHREAD_CANCEL_ENABLE;
new_thread->;p_canceltype = PTHREAD_CANCEL_DEFERRED;
new_thread->;p_canceled = 0;
new_thread->;p_errnop = &new_thread->;p_errno;
new_thread->;p_errno = 0;
new_thread->;p_h_errnop = &new_thread->;p_h_errno;
new_thread->;p_h_errno = 0;
new_thread->;p_in_sighandler = NULL;
new_thread->;p_sigwaiting = 0;
new_thread->;p_guardaddr = guardaddr;
new_thread->;p_guardsize = guardsize;
new_thread->;p_userstack = attr != NULL && attr->;__stackaddr_set;
memset (new_thread->;p_specific, '\0',
PTHREAD_KEY_1STLEVEL_SIZE * sizeof (new_thread->;p_specific[0]));
new_thread->;p_self = new_thread;
new_thread->;p_nr = sseg;
/* Initialize the thread handle */
__pthread_init_lock(&__pthread_handles[sseg].h_lock);
__pthread_handles[sseg].h_descr = new_thread;
__pthread_handles[sseg].h_bottom = new_thread_bottom;
/* Determine scheduling parameters for the thread */
//确定线程的调度参数
new_thread->;p_start_args.schedpolicy = -1;
if (attr != NULL) {
switch(attr->;__inheritsched) {
case PTHREAD_EXPLICIT_SCHED:
new_thread->;p_start_args.schedpolicy = attr->;__schedpolicy;
memcpy (&new_thread->;p_start_args.schedparam, &attr->;__schedparam,
sizeof (struct sched_param));
break;
case PTHREAD_INHERIT_SCHED:
/* schedpolicy doesn't need to be set, only get priority */
__sched_getparam(father_pid, &new_thread->;p_start_args.schedparam);
break;
}
new_thread->;p_priority =
new_thread->;p_start_args.schedparam.sched_priority;
}
/* Finish setting up arguments to pthread_start_thread */
//设置pthread_start_thread的参数
new_thread->;p_start_args.start_routine = start_routine;
new_thread->;p_start_args.arg = arg;
new_thread->;p_start_args.mask = *mask;
/* Raise priority of thread manager if needed */根据需要调整管理线程的优先级
__pthread_manager_adjust_prio(new_thread->;p_priority);
/* Do the cloning */创建新线程
pid = __clone(pthread_start_thread, (void **) new_thread,
CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND |
__pthread_sig_cancel, new_thread);
/* Check if cloning succeeded */
if (pid == -1) {
/* Free the stack if we allocated it */
if (attr == NULL || !attr->;__stackaddr_set)
{
munmap((caddr_t)((char *)(new_thread+1) - INITIAL_STACK_SIZE),
INITIAL_STACK_SIZE);
if (new_thread->;p_guardsize != 0)
munmap(new_thread->;p_guardaddr, new_thread->;p_guardsize);
}
__pthread_handles[sseg].h_descr = NULL;
__pthread_handles[sseg].h_bottom = NULL;
__pthread_handles_num--;
return errno;
}
/* Insert new thread in doubly linked list of active threads */
//将新线程插入双向链表
new_thread->;p_prevlive = __pthread_main_thread;
new_thread->;p_nextlive = __pthread_main_thread->;p_nextlive;
__pthread_main_thread->;p_nextlive->;p_prevlive = new_thread;
__pthread_main_thread->;p_nextlive = new_thread;
/* Set pid field of the new thread, in case we get there before the
child starts. */
new_thread->;p_pid = pid;
/* We're all set */
*thread = new_thread_id;
return 0;
}
3.4 线程的堆栈分配和管理
STACK_SIZE 2*1024*1024
INITIAL_STACK_SIZE 4*PAGE_SIZE
THREAD_MANAGER_STACK_SIZE 2*PAGE_SIZE-32
static int pthread_allocate_stack(const pthread_attr_t *attr,
pthread_descr default_new_thread,
int pagesize,
pthread_descr * out_new_thread,
char ** out_new_thread_bottom,
char ** out_guardaddr,
size_t * out_guardsize)
{
pthread_descr new_thread;
char * new_thread_bottom;
char * guardaddr;
size_t stacksize, guardsize;
if (attr != NULL && attr->;__stackaddr_set)
{
/* The user provided a stack. */用户提供堆栈
new_thread =
(pthread_descr) ((long)(attr->;__stackaddr) & -sizeof(void *)) - 1;
new_thread_bottom = (char *) attr->;__stackaddr - attr->;__stacksize;
guardaddr = NULL;
guardsize = 0;
__pthread_nonstandard_stacks = 1;
}
else
{
/* Allocate space for stack and thread descriptor at default address */
//在缺省地址分配堆栈和描述符
new_thread = default_new_thread;
new_thread_bottom = (char *) new_thread - STACK_SIZE;
if (mmap((caddr_t)((char *)(new_thread + 1) - INITIAL_STACK_SIZE), INITIAL_STACK_SIZE, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED | MAP_GROWSDOWN, -1, 0) == MAP_FAILED)
/* Bad luck, this segment is already mapped. */
return -1;
/* We manage to get a stack. Now see whether we need a guard
and allocate it if necessary. Notice that the default
attributes (stack_size = STACK_SIZE - pagesize and
guardsize = pagesize) do not need a guard page, since
the RLIMIT_STACK soft limit prevents stacks from
running into one another. */
//判断是否需要保护页,如果需要就分配
if (attr == NULL ||
attr->;__guardsize == 0 ||
(attr->;__guardsize == pagesize &&
attr->;__stacksize == STACK_SIZE - pagesize))
{
/* We don't need a guard page. */
guardaddr = NULL;
guardsize = 0;
}
else
{
/* Put a bad page at the bottom of the stack */
stacksize = roundup(attr->;__stacksize, pagesize);
if (stacksize >;= STACK_SIZE - pagesize)
stacksize = STACK_SIZE - pagesize;
guardaddr = (void *)new_thread - stacksize;
guardsize = attr->;__guardsize;
if (mmap ((caddr_t) guardaddr, guardsize, 0, MAP_FIXED, -1, 0)
== MAP_FAILED)
{
/* We don't make this an error. */
guardaddr = NULL;
guardsize = 0;
}
}
}
*out_new_thread = new_thread;
*out_new_thread_bottom = new_thread_bottom;
*out_guardaddr = guardaddr;
*out_guardsize = guardsize;
return 0;
}
3.5 线程的调度
Common threads 的调度和普通进程并无大的区别,创建者可以自己设定线程的优先级。但是Manager thread则需要实时响应各进程提出的请求,所以Manager thread被设置成高于其它线程的优先级,方法是在创建每个新线程时调整Manager thread的优先级。
/* Adjust priority of thread manager so that it always run at a priority
higher than all threads */
void __pthread_manager_adjust_prio(int thread_prio)
{
struct sched_param param;
if (thread_prio <= __pthread_manager_thread.p_priority) return;
param.sched_priority =
thread_prio < __sched_get_priority_max(SCHED_FIFO)
? thread_prio + 1 : thread_prio;
__sched_setscheduler(__pthread_manager_thread.p_pid, SCHED_FIFO, ¶m);
__pthread_manager_thread.p_priority = thread_prio;
}
[目录]
--------------------------------------------------------------------------------
进程描述符
标题 新兵笔记--ULK(C3) Process Descriptor
作者 Big John (stranger )
时间 05/19/01 06:01 PM
Process Descriptor
description:
进程描述符:也就是结构体task_struct,它有很多域,包含了一个进程的所有信息,主要有它的属性、当前的状态、它所占有的资料,还有一些指针用于把按不同的需求把它链进不同的链表中。
进程描述符与进程的kernel栈:每个进程都有个自己的kernel栈,当它进入kernel态时(比如进行系统调用),kernel会把栈指针指向当前进程的kernel栈。在2.2中,进程的描述符和kernel栈是捆在一起的,
union task_union {
struct task_struct task;
unsigned long stack[2048];
};
kernel每次分配一个进程描述符总会按task_union的大小(即8k)"顺手"分配一个kernel栈,这样做一个最重要的目的就是为了能方便的得到当前运行进程的描述符,当系统需要时,总是使用current来得到当前运行的进程,在2.0中current可能是个全局变量或全局数组(当多CPU时),这样一方面是不方便使用,另一方面,在多CPU中还得去找当前CPU才能确定当前的current(我当初看过,但忘了它是怎么找的了)。现在使用了这个结构,kernel随时可以通过栈底指针减8K就可得到描述符的地址。大家可以看到现在的current实际是一个函数get_current,它所做的是就是把esp减去8K,然后返回这个地址。
进程描述符的分配与释放:由这两个函数完成,alloc_task_struct和free_task_struct。这两个函数没什么花头,还是由于分配物理页帧的代码过大,这里也有一个缓存static struct task_struct * task_struct_stack[EXTRA_TASK_STRUCT],它能缓存16项,分配和释放都尽量在这进行,除非它已经满了或空了,才去与分页系统打交道。
进程数组:struct task_struct *task[NR_TASKS];它是一个指针数组,其中NR_TASKS在i386下应该4090,实际可同时运行的进程应该是4092个,因为还得加上Process 0和Procces 1,这两个进程是不在进程数组中的。
当创建一个进程时,kernel会申请一片内存并把它加入task数组中。如何加入呢?出于效率考虑,task数组并不象我们平时处理那样把没有用的项置空,当要加入时顺序的去找数组中为空的项。它使用了类似于第二章中页表缓存链表的管理方法,tarray_freelist是这个链表的表头,具体操作如下:
初始化:
struct task_struct **tarray_freelist = NULL;
void __init sched_init(void)
{
。。。
int nr = NR_TASKS;
while(--nr >; 0)
add_free_taskslot(&task[nr]); // 把task数组的每一项加到空闲链表中。
。。。
}
函数add_free_taskslot:
*t = (struct task_struct *) tarray_freelist; // 把当前指针当next用,指向空闲链表的第一项(可能是NULL)
tarray_freelist = t; // tarray_freelist指向当前项
函数get_free_taskslot:
tslot = tarray_freelist; // *tslot是第一个空闲项
tarray_freelist = (struct task_struct **) *tslot; // *tslot的内容是下一个空闲项
return tslot;
各种各样的进程指针:在进程描述符中有很多task_struct的指针,用于把它链进不同的链表或树中。最简单的是next_task和prev_task,它们把当前系统中的所有进程链成一条双向链表;其次是next_run和prev_run,它们把当前可运行的进程(state为TASK_RUNNING,这和current不同,并不表示它正在运行,只表示它可以被CPU调度运行而已)链成一条双向链表,不过我的源码里并没有作者所说的runqueue指针头,好象是init_task取代了runqueue的位置;pidhash_next和pidhash_pprev是用来链进以进程号索引的hash表的,因为大多调用都使用进程号标识进程,所以需要建立一个hash表来加快以进程号来查找进程的速度;最后是p_opptr,p_pptr,p_cptr,p_ysptr,p_osptr,这些是用来标识进程的父子,兄弟等树状关系的,作者书中的图已经很清楚了,不再需要我多说了。
等待队列:一般来说,当某个任务不能马上完成时,kernel不会陪着进程在那死等,它只是简单把进程挂进某个队列,然后继续运行,等这个任务完成,kernel会从队列中取出等待的进程,并把它链进可运行队列中。等待队列的结构体如下:
struct wait_queue {
struct task_struct * task;
struct wait_queue * next;
};
很简单的结构,一个进程指针,一个next指针,应用时它将会是一个环形队列,add_wait_queue加入一项,remove_wait_queue移去新旧的一项,都不是很难理解。麻烦的是它的init_waitqueue,内容为
#define WAIT_QUEUE_HEAD(x) ((struct wait_queue *)((x)-1))
static inline void init_waitqueue(struct wait_queue **q)
{
*q = WAIT_QUEUE_HEAD(q);
}
结合作者的解释,它实际上是把当前队列指针加上前面的四个字节假设为一项了,然后"这一项"的next指针指向它自己。这个方法代码倒很简单,但是我却不是很喜欢,可读性实在有点。。。如果是我,宁愿加一空项做表头。
sleep和wakeup:刚才所说的kernel把进程挂进队一般是通过sleep_on来做的,而取出队列是通过wake_up来做的。现在来看看它们是怎么运行的吧。比如你要读取软盘内容,指令发出去了,但要等很久才有回应,这时会调用sleep_on把你的进程标识为TASK_UNINTERRUPTIBLE或TASK_INTERRUPTIBLE状态,然后把进程挂在某一个队列上,然后调用schedule,这样就会调度其它状态为TASK_RUNNING的进程继续运行。当指令完成时,比如软盘的内容已经读到内存中了,这时可能会产生一个中断,这个中断会从等待队列中把你的进程取出来,标识为TASK_RUNNING,然后放入可运行队列,又过了若干时间,你的进程真的开始运行了,这时会执行sleep_on中schedule后的语句,即把进程从进程从等待队列中移出去,然后就可以执行下面的操作了,这时你要读的内容已经读出来了。
进程限制:谁都不希望某个用户的进程会占用所有的内存,所有的CPU,所有的任何资源,这样就要对进程有所限制,kernel用了这样一个结构:
struct rlimit {
long rlim_cur;
long rlim_max;
};
其中rlim_cur为进程现在用到的资源数,rlim_max为进程可用的最大资源数。
结构task_struct中有一项为struct rlimit rlim[RLIM_NLIMITS],其中RLIM_NLIMITS为10,即你对进程可以有10种限制,这10种限制作者有讲解的,我也不说了。
question:
1、我的印象中,在get_current中,esp应该是栈顶指针,而且随时会变的,用它去减去8K,能得到正确的地址吗?
标题 Re: 新兵笔记--ULK(C3) Process Descriptor [re: Big John]
作者 lucian_yao (addict)
时间 05/20/01 09:16 AM
1应该是栈顶向下8K对齐得到task_struct指针。
2在2.4中最大进程数没有了,由于基本不用TSS结构,所以不受GDT大小限制。
标题 Re: 新兵笔记--ULK(C3) Process Descriptor [re: lucian_yao]
作者 Big John (stranger )
时间 05/22/01 04:24 PM
1、是我的错,把
__asm__("andl %%esp,%0; ":"=r" (current) : "0" (~8191UL));
中的andl看成addl了,所以百思而不得,呵。其实很简单,系统分配进程描述符时,都是偶数页对齐的,把sp后面的13位清0,当然也就是描述符的位置了。:)
2、2.4对进程没有限制,那当然就不会再用task_struct的数组了,那它是怎么组织的呢?不会是链表吧。
标题 Re: 新兵笔记--ULK(C3) Process Descriptor [re: Big John]
作者 iobject (stranger)
时间 05/29/01 04:08 PM
static inline struct task_struct * get_current(void)
{
struct task_struct *current;
__asm__("andl %%esp,%0; ":"=r" (current) : "" (~8191UL));
return current;
}
对于,%0,从语法上似乎是指current,但是这样的话这句话就说不通了,难道我对%0的理解有错吗
哪位指点一下,谢谢!
标题 Re: 新兵笔记--ULK(C3) Process Descriptor [re: iobject]
作者 Big John (stranger )
时间 05/29/01 05:33 PM
asm ("combine %2,%0" : "=r" (foo) : "0" (foo), "g" (bar));
The constraint `"0"' for operand 1 says that it must occupy the same
location as operand 0. A digit in constraint is allowed only in an
input operand and it must refer to an output operand.
这段来自gcc的info,大概意思是说,%1和%0将占用同一个寄存器,所以引用%0也就是引用%1了。
这样看来
__asm__("andl %%esp,%0; ":"=r" (current) : "0" (~8191UL));
展开应该是这样的:
movl $(~8191UL),%eax
#APP
andl %esp, %eax
#NO_APP
movl %eax,current
我也是现学现用,不知道对不对。
[目录]
--------------------------------------------------------------------------------
init进程从内核态切换到用户态
标题 init进程如何从内核态切换到用户态。
作者 chstar (stranger )
时间 03/08/01 01:24 PM
init进程从内核态切换到用户态。
//谢谢lucian_yao 邀请,在此灌水一篇
大家都知道如何产生一个新的进程。
通过sys_fork,之后再调用sys_execve
系统初启后(核心态)的第一个用户态进程是init。
这要涉及到内层(特权级高)向外层(特权级低)转移的问题。
通常情况下,内核是不会调用用户层的代码,要想实现这逆向的转移,一般做法是在用户进程的核心栈(tss->;esp0)压入用户态的SS,ESP,EFLAGS,CS,EIP,伪装成用户进程是通过陷阱门进入核心态,之后通过iret返回用户态。
那么linux 2.2.14中的用户态进程init是如何实现的?
首先在kernel_thread(init...)函数中,
利用系统调用sys_clone fork出一个内核级进程(此时要给该进程分配核心栈<-esp0),之后call init函数,init函数还会再起几个kernel_thread,然后会加载/sbin/init(通过execve调用)
在sys_execve中,要完成内核态到用户态的转移。
大体流程是sys_execve-->;do_execve-->;load_elf_binary()
-->;do_load_elf_binary()-->;do_mmap()
start_thread(reg,newip,newsp) (processor.h)
请大家关注do_mmap()及start_thread()很重要哦
do_mmap完成从文件虚拟空间到内存虚拟空间的映射。
而start_thread就是要在进程核心栈中的相应位置填入进程用户态的xss,esp and xcs,eip.
最后进程从ret_from_sys_call返回,iret指令从核心栈pop出xcs, eip完成特权及指令的转移, pop出 xss,esp,完成堆栈的切换。
以上我也是刚看代码悟出的,如有不对之处,还望高手指出。
[目录]
--------------------------------------------------------------------------------
SET_LINKS
宏:SET_LINKS(p)将进程p插入到进程系中
struct task_struct {
struct task_struct *next_task, *prev_task;
...
struct task_struct *p_opptr, *p_pptr, *p_cptr, *p_ysptr,*p_osptr; ...};
next_task和prev_task 为描述进程先后关系的环形队列
p_opptr 指向原始的父进程
p_pptr 指向当前的父进程
p_cptr 指向最年轻的子进程
p_ysptr 指向弟进程
p_osptr 指向兄进程
include/linux/sched.h
#define SET_LINKS(p) do {
\
(p)->;next_task = &init_task;
\ 进程p的下一个进程是初始化进程
(p)->;prev_task = init_task.prev_task;
\ 进程p的前一个进程是初始化进程的前一个进程
init_task.prev_task->;next_task = (p);
\ 进程p的进一进程指向p
init_task.prev_task = (p);
\初始化进程的前一进程指向p; 即将进程p加入到环形进程队列的尾部
(p)->;p_ysptr = NULL; \ 进程p现在是最年轻的进程
if (((p)->;p_osptr = (p)->;p_pptr->;p_cptr) != NULL)
(p)->;p_osptr->;p_ysptr = p;
\ 原来的最年轻进程变成p的兄进程
(p)->;p_pptr->;p_cptr = p; \ 父进程指向新的子进程p
} while (0)
[目录]
--------------------------------------------------------------------------------
REMOVE_LINKS
宏:REMOVE_LINKS(p)将进程p从进程系中删除
struct task_struct {
struct task_struct *next_task, *prev_task;
...
struct task_struct *p_opptr, *p_pptr, *p_cptr, *p_ysptr,*p_osptr; ...};
next_task和prev_task 为描述进程先后关系的环形队列
p_opptr 指向原始的父进程
p_pptr 指向当前的父进程
p_cptr 指向最年轻的子进程
p_ysptr 指向弟进程
p_osptr 指向兄进程
include/linux/sched.h
#define REMOVE_LINKS(p) do { \
(p)->;next_task->;prev_task = (p)->;prev_task;
\ 让进程p的下一进程指向p的前一进程
(p)->;prev_task->;next_task = (p)->;next_task;
\ 让进程p的前一进程指向p的下一进程
if ((p)->;p_osptr)
\ 如果进程p有兄进程,则让兄进程指向p的弟进程
(p)->;p_osptr->;p_ysptr = (p)->;p_ysptr;
if ((p)->;p_ysptr)
\ 如果进程p有弟进程,则让弟进程指向p的兄进程
(p)->;p_ysptr->;p_osptr = (p)->;p_osptr; \
else \ 如果p没有弟进程,说明p最年轻,则让父进程指向p的兄进程 (p)->;p_pptr->;p_cptr = (p)->;p_osptr;
\
} while (0)
[目录]
--------------------------------------------------------------------------------
get_wchan()
get_wchan()给出了某个睡眠进程schedule()的调用点.
; arch/i386/kernel/process.c
unsigned long get_wchan(struct task_struct *p)
{
unsigned long ebp, esp, eip;
unsigned long stack_page;
int count = 0;
if (!p || p == current || p->;state == TASK_RUNNING)
return 0;
stack_page = (unsigned long)p;
esp = p->;thread.esp; 取switch_to之前内核堆栈指针
if (!stack_page || esp 8188+stack_page)
return 0;
/* include/asm-i386/system.h:switch_to() pushes ebp last. */
ebp = *(unsigned long *) esp; 取保存在切换现场的schedule的ebp
do {
if (ebp 8184+stack_page)
return 0;
eip = *(unsigned long *) (ebp+4);
; (ebp+0)为上一级函数的ebp,(ebp+4)为schedule()的返回地址
; kernel/sched.c编绎加了-fno-omit-frame-pointer编绎标志,就是在这儿起作用.
; first_sched和last_sched是schedule()函数所在的地址范围
if (eip = last_sched)
return eip;
ebp = *(unsigned long *) ebp;
} while (count++ return 0;
}
现在的问题是,在什么情况下需要用count循环几次? 现有的代码好象不需要循环.
[目录]
--------------------------------------------------------------------------------
sigframe的结构
struct pt_regs {
long ebx;
long ecx;
long edx;
long esi;
long edi;
long ebp;
long eax;
int xds;
int xes;
long orig_eax;
long eip;
int xcs;
long eflags;
long esp;
int xss;
};
typedef void (*__sighandler_t)(int);
struct sigaction {
__sighandler_t sa_handler; 用户的信号处理函数指针
unsigned long sa_flags;
void (*sa_restorer)(void); 用户自定义的信号恢复函数指针
sigset_t sa_mask;
};
struct k_sigaction {
struct sigaction sa;
};
struct exec_domain {
const char *name;
lcall7_func handler;
unsigned char pers_low, pers_high;
unsigned long * signal_map;
unsigned long * signal_invmap;
struct module * module;
struct exec_domain *next;
};
struct sigcontext {
unsigned short gs, __gsh;
unsigned short fs, __fsh;
unsigned short es, __esh;
unsigned short ds, __dsh;
unsigned long edi;
unsigned long esi;
unsigned long ebp;
unsigned long esp;
unsigned long ebx;
unsigned long edx;
unsigned long ecx;
unsigned long eax;
unsigned long trapno;
unsigned long err;
unsigned long eip;
unsigned short cs, __csh;
unsigned long eflags;
unsigned long esp_at_signal;
unsigned short ss, __ssh;
struct _fpstate * fpstate;
unsigned long oldmask;
unsigned long cr2;
};
struct _fpstate {
unsigned long cw;
unsigned long sw;
unsigned long tag;
unsigned long ipoff;
unsigned long cssel;
unsigned long dataoff;
unsigned long datasel;
struct _fpreg _st[8];
unsigned short status;
unsigned short magic;
unsigned long _fxsr_env[6];
unsigned long mxcsr;
unsigned long reserved;
struct _fpxreg _fxsr_st[8];
struct _xmmreg _xmm[8];
unsigned long padding[56];
};
struct sigframe
{
char *pretcode; 指向retcode
int sig; sa_handler的sig参数
struct sigcontext sc; CPU状态
struct _fpstate fpstate;如果进程使用过FPU的话保存FPU状态
unsigned long extramask[(64 / 32 ) -1];
char retcode[8]; "popl % eax; movl $__NR_sigreturn,% eax; int $0x80"
};
static void setup_frame(int sig, struct k_sigaction *ka,
sigset_t *set, struct pt_regs * regs)
{
struct sigframe *frame;
int err = 0;
;取信号帧的起始地址
frame = get_sigframe(ka, regs, sizeof(*frame));
;检测frame指针是否越界
if (!access_ok(VERIFY_WRITE, frame, sizeof(*frame)))
goto give_sigsegv;
;每个进程可以对应于不同的运行域,如果需要的话就进行相应的信号转换
err |= __put_user((current->;exec_domain
current->;exec_domain->;signal_invmap
sig ? current->;exec_domain->;signal_invmap[sig]
: sig),
if (err)
goto give_sigsegv;
;继续在用户堆栈上填充sigframe结构
err |= setup_sigcontext( regs, set->;sig[0]);
if (err)
goto give_sigsegv;
;如果系统信号集的描述字多于1个的话,在extramask在保存多出来的部分,
;set->;sig[0]已在sigframe->;sc.oldmask保存
if (_NSIG_WORDS >; 1) {
err |= __copy_to_user(frame->;ex
ip_fw_check
{
从传入的skb参数中提取源地址src,目的地址dst,源端口src_port,目的端口dst_port,
TCP发起连接标志tcpsyn,分片包位移offset,IP包TOS消息oldtos;
......
f = chain->;chain; //取出规则链的的一条规则,规则链由chain参数传入
count = 0;
do {
for (; f; f = f->;next) { //遍历规则链中的规则,直到匹配(ip_rule_match返回1)
count++;
if (ip_rule_match(f,rif,ip,
tcpsyn,src_port,dst_port,offset)) {
if (!testing
&& !ip_fw_domatch(f, ip, rif, chain->;label,//作些标记,一般返回1
skb, slot,
src_port, dst_port,
count, tcpsyn)) {
ret = FW_BLOCK;
goto out;
}
break;
}
}
if(f) { //找到匹配规则
......
}else { //这次遍历根本没找到
是从别的地方跳转过来的,则转回去,然后继续遍历;
否则应用这条链的缺省规则;
}
} while (ret == FW_SKIP+2);
out:
......
return ret;
}
碎片:
根据第一个片的消息进行过滤,其他分片则允许通过。如果规则是丢弃的话,虽然后面的分片都可到达主机,
但由于第一片被滤掉了,无法重组成功,因此从效果上也相当于整个IP包被丢弃。
存在的漏洞等.
2.3 规则:
from 192.168.7.0/24 to 192.168.6.32/32 tcp 80 BLOCK
规则的数据结构表示:
规则链
struct ip_chain
{
ip_chainlabel label; /* Defines the label for each block */
struct ip_chain *next; /* Pointer to next block */
struct ip_fwkernel *chain; /* Pointer to first rule in block */
__u32 refcount; /* Number of refernces to block */
int policy; /* Default rule for chain. Only *
* used in built in chains */
struct ip_reent reent[0]; /* Actually several of these */
};
规则
struct ip_fwkernel
{
struct ip_fw ipfw;
struct ip_fwkernel *next; /* where to go next if current
* rule doesn't match */
struct ip_chain *branch; /* which branch to jump to if
* current rule matches */
int simplebranch; /* Use this if branch == NULL */
struct ip_counters counters[0]; /* Actually several of these */
};
待匹配的数据包消息
struct ip_fw
{
struct in_addr fw_src, fw_dst; /* Source and destination IP addr */
struct in_addr fw_smsk, fw_dmsk; /* Mask for src and dest IP addr */
__u32 fw_mark; /* ID to stamp on packet */
__u16 fw_proto; /* Protocol, 0 = ANY */
__u16 fw_flg; /* Flags word */
__u16 fw_invflg; /* Inverse flags */
__u16 fw_spts[2]; /* Source port range. */
__u16 fw_dpts[2]; /* Destination port range. */
__u16 fw_redirpt; /* Port to redirect to. */
__u16 fw_outputsize; /* Max amount to output to
NETLINK */
char fw_vianame[IFNAMSIZ]; /* name of interface "via" */
__u8 fw_tosand, fw_tosxor; /* Revised packet priority */
};
2.4 地址转换
ip_fw_demasquerade
ip_fw_masquerade
三 Linux下防火墙的实现之二(2.4内核):
3.1
A Packet Traversing the Netfilter System:
--->RE------>;[ROUTE]--->;FWD---------->OST------>;
Conntrack | Filter ^ NAT (Src)
Mangle | | Conntrack
NAT (Dst) | [ROUTE]
(QDisc) v |
IN Filter OUT Conntrack
| Conntrack ^ Mangle
| | NAT (Dst)
v | Filter
3.2 例子
## Insert connection-tracking modules (not needed if built into kernel).
# insmod ip_conntrack
# insmod ip_conntrack_ftp
## Create chain which blocks new connections, except if coming from inside.
# iptables -N block
# iptables -A block -m state --state ESTABLISHED,RELATED -j ACCEPT
# iptables -A block -m state --state NEW -i ! ppp0 -j ACCEPT
# iptables -A block -j DROP
## Jump to that chain from INPUT and FORWARD chains.
# iptables -A INPUT -j block
# iptables -A FORWARD -j block
3.3 规则的描述
一条规则分为三部分:
struct ipt_entry //主要用来匹配IP头
struct ip_match //额外的匹配(tcp头,mac地址等)
struct ip_target //除缺省的动作外(如ACCEPT,DROP),可以增加新的(如REJECT)。
3.4 代码提炼
ip_input.c:
/*
* Main IP Receive routine.
*/
int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt)
{
...
return NF_HOOK(PF_INET, NF_IP_PRE_ROUTING, skb, dev, NULL,
ip_rcv_finish);
...
}
netfilter.h:
#ifdef CONFIG_NETFILTER
#define NF_HOOK(pf, hook, skb, indev, outdev, okfn) \
(list_empty(&nf_hooks[(pf)][(hook)]) \
? (okfn)(skb) \
: nf_hook_slow((pf), (hook), (skb), (indev), (outdev), (okfn)))
#else /* !CONFIG_NETFILTER */
#define NF_HOOK(pf, hook, skb, indev, outdev, okfn) (okfn)(skb)
#endif /*CONFIG_NETFILTER*/
大的框架:"HOOK表":
struct list_head nf_hooks[NPROTO][NF_MAX_HOOKS]; //netfilter.c
通过nf_register_hook和nf_unregister_hook完成添加删除工作,nf_iterate负责执行hook上的函数。
增加用户自定义的HOOK,参见【8】,【10】:
重要流程(建议结合netfilter hacking howto 4.1.3来看):
/* Returns one of the generic firewall policies, like NF_ACCEPT. */
unsigned int
ipt_do_table(struct sk_buff **pskb,
unsigned int hook,
const struct net_device *in,
const struct net_device *out,
struct ipt_table *table,
void *userdata)
{
struct ipt_entry *e;
struct ipt_entry_target *t;
unsigned int verdict = NF_DROP;
table_base = (void *)table->;private->;entries
+ TABLE_OFFSET(table->;private,
cpu_number_map(smp_processor_id()));
e = get_entry(table_base, table->;private->;hook_entry[hook]);
...
ip_packet_match(ip, indev, outdev, &e->;ip, offset);
...
IPT_MATCH_ITERATE(e, do_match, *pskb, in, out, offset, protohdr, datalen, &hotdrop)
...
t = ipt_get_target(e);
...
verdict = t->;u.kernel.target->;target(pskb, hook, in, out, t->;data, userdata);//非标准的target走这一步
...
return verdict;
}
要加强对这段话的理解(netfilter hacking howto 4.1节) :
>;iptables does not register with any netfilter hooks: it relies on
>;other modules to do that and feed it the packets as appropriate; a
>;module must register the netfilter hooks and ip_tables separately, and
>;provide the mechanism to call ip_tables when the hook is reached.
四 Linux下防火墙的实现之三(checkpoint FW1)
让我们看看checkpoint的在linux上的防火墙是如何实现的,最终我们会发现,竟然和lkm使用的手段差不多:)
fw1通过dev_add_pack的办法加载输入过滤函数,但是在net_bh()中,传往网络层的skbuff是克隆的,即
skb2=skb_clone(skb, GFP_ATOMIC);
if(skb2)
pt_prev->;func(skb2, skb->;dev, pt_prev);
而fw1是怎么解决这个问题的呢?见下面的代码:
输入一:
; 哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪?
align 4
; 圹圹圹圹圹圹圹?S U B R O U T I N E 圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹?
; Attributes: bp-based frame
public fwinstallin
fwinstallin proc near ; CODE XREF: fwinstall+E9p
; fwinstall+149p
var_18 = byte ptr -18h
arg_0 = dword ptr 8
push ebp
mov ebp, esp
sub esp, 10h
push esi
push ebx
mov esi, ds:dev_base
cmp [ebp+arg_0], 0
jz short loc_0_802CBD0
add esp, 0FFFFFFF4h
push offset fw_ip_packet_type
call dev_add_pack
mov ebx, fw_ip_packet_type+10h ;如果考虑字节对齐问题的话fw_ip_packet_type+10h这时应该是ip_packet_type
mov dword ptr ds:fw_type_list, ebx
jmp short loc_0_802CB9C
; 哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪?
align 4
loc_0_802CB90: ; CODE XREF: fwinstallin+41j
add esp, 0FFFFFFF4h
push ebx
call dev_remove_pack ;fw1把ip_packet_type歇载掉了,然后自己在自己的处理函数(fw_filterin)中调ip_recv
mov ebx, [ebx+10h]
loc_0_802CB9C: ; CODE XREF: fwinstallin+2Dj
add esp, 10h
test ebx, ebx
jnz short loc_0_802CB90
test esi, esi
jz short loc_0_802CC14
loc_0_802CBA7: ; CODE XREF: fwinstallin+68j
test byte ptr fwdebug, 81h
jz short loc_0_802CBC3
add esp, 0FFFFFFF8h
mov eax, [esi]
push eax
push offset aFwinstallinS ; "fwinstallin: %s\n"
call fwkdebug_printf
add esp, 10h
loc_0_802CBC3: ; CODE XREF: fwinstallin+4Ej
mov esi, [esi+28h]
test esi, esi
jnz short loc_0_802CBA7
jmp short loc_0_802CC14
; 哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪?
align 8
loc_0_802CBD0: ; CODE XREF: fwinstallin+12j
cmp dword ptr ds:fw_type_list, 0
jz short loc_0_802CC14
add esp, 0FFFFFFF4h
push offset fw_ip_packet_type
call dev_remove_pack
add esp, 10h
cmp dword ptr ds:fw_type_list, 0
jz short loc_0_802CC14
loc_0_802CBF2: ; CODE XREF: fwinstallin+B2j
add esp, 0FFFFFFF4h
mov eax, dword ptr ds:fw_type_list
push eax
call dev_add_pack
mov eax, dword ptr ds:fw_type_list
add esp, 10h
mov eax, [eax+10h]
mov dword ptr ds:fw_type_list, eax
test eax, eax
jnz short loc_0_802CBF2
loc_0_802CC14: ; CODE XREF: fwinstallin+45j
; fwinstallin+6Aj ...
lea esp, [ebp+var_18]
xor eax, eax
pop ebx
pop esi
mov esp, ebp
pop ebp
retn
fwinstallin endp
输入二:
public fw_ip_packet_type
fw_ip_packet_type dd 8, 0, offset fw_filterin, 2 dup(0) ; DATA XREF: fwinstallin+17o
输出的挂载和lkm的手法一样,更改dev->;hard_start_xmit。dev结构在2.2版本的发展过程中变了一次,
为了兼容fw1对这点也做了处理。
输出一:
; 哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪?
align 4
; 圹圹圹圹圹圹圹?S U B R O U T I N E 圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹?
; Attributes: bp-based frame
public fwinstallout
fwinstallout proc near ; CODE XREF: fwinstall+FBp
; fwinstall+153p
var_18 = byte ptr -18h
arg_0 = dword ptr 8
push ebp
mov ebp, esp
sub esp, 0Ch
push edi
push esi
push ebx
mov edi, [ebp+arg_0]
xor esi, esi
mov ebx, ds:dev_base
jmp short loc_0_802D0A8
; 哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪?
loc_0_802D096: ; CODE XREF: fwinstallout+50j
add esp, 0FFFFFFFCh
push edi
push esi
push ebx
call installout_on_device
add esp, 10h
mov ebx, [ebx+28h]
inc esi
loc_0_802D0A8: ; CODE XREF: fwinstallout+14j
test ebx, ebx
jz short loc_0_802D0F8
test byte ptr fwdebug, 81h
jz short loc_0_802D0CD
xor eax, eax
mov ax, [ebx+50h]
push eax
mov eax, [ebx]
push eax
push esi
push offset aFwinstalloutIn ; "fwinstallout: interface %d: name=%s, fl"...
call fwkdebug_printf
add esp, 10h
loc_0_802D0CD: ; CODE XREF: fwinstallout+33j
cmp esi, 3Fh
jle short loc_0_802D096
add esp, 0FFFFFFF8h
push 40h
push offset aFw1CanOnlyHand ; "FW-1: Can only handle %d interfaces\n"
call fwkdebug_printf
add esp, 10h
test edi, edi
jz short loc_0_802D0F8
add esp, 0FFFFFFF4h
push offset aFw1NotAllInter ; "FW-1: Not all interfaces installed\n"
call fwkdebug_printf
add esp, 10h
loc_0_802D0F8: ; CODE XREF: fwinstallout+2Aj
; fwinstallout+66j
mov fw_nif, esi
test byte ptr fwdebug, 81h
jz short loc_0_802D124
add esp, 0FFFFFFFCh
mov eax, offset aUn ; "un"
test edi, edi
jz short loc_0_802D118
mov eax, offset unk_0_80687E4
loc_0_802D118: ; CODE XREF: fwinstallout+91j
push eax
push esi
push offset aFw1DInterfaces ; "FW-1: %d interfaces %sinstalled\n"
call fwkdebug_printf
loc_0_802D124: ; CODE XREF: fwinstallout+85j
lea esp, [ebp+var_18]
xor eax, eax
pop ebx
pop esi
pop edi
mov esp, ebp
pop ebp
retn
fwinstallout endp
输出二:
; 哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪?
align 10h
; 圹圹圹圹圹圹圹?S U B R O U T I N E 圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹?
; Attributes: bp-based frame
public installout_on_device
installout_on_device proc near ; CODE XREF: fwinstallout+1Cp
var_18 = byte ptr -18h
var_4 = dword ptr -4
arg_0 = dword ptr 8
arg_4 = dword ptr 0Ch
arg_8 = dword ptr 10h
push ebp
mov ebp, esp
sub esp, 0Ch
push edi
push esi
push ebx
mov edi, [ebp+arg_0]
mov esi, [ebp+arg_4]
mov ebx, [ebp+arg_8]
add esp, 0FFFFFFF4h
push edi
call xmit_func_addr
mov [ebp+var_4], eax
add esp, 10h
test ebx, ebx
jz short loc_0_802CFD4
mov ebx, esi
shl ebx, 4
cmp (oftab+4)[ebx], 0
jz short loc_0_802CF90
add esp, 0FFFFFFF4h
push offset aFw1OutputFilte ; "FW-1: Output filter already installed\n"
call fwkdebug_printf
mov eax, 6Ah
jmp loc_0_802D074
输出三:
; 哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪?
align 8
; 圹圹圹圹圹圹圹?S U B R O U T I N E 圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹圹?
; Attributes: bp-based frame
public xmit_func_addr
xmit_func_addr proc near ; CODE XREF: installout_on_device+16p
arg_0 = dword ptr 8
push ebp
mov ebp, esp
mov edx, [ebp+arg_0]
lea eax, [edx+0ACh]
cmp kver, 0Dh
jle short loc_0_802CB5B
lea eax, [edx+0B0h]
loc_0_802CB5B: ; CODE XREF: xmit_func_addr+13j
mov esp, ebp
pop ebp
retn
xmit_func_addr endp
FW1与linux的一些比较,可以参看参考文献【11】
五 参考文献
【1】了解Check Point FW-1状态表
http://magazine.nsfocus.com/detail.asp?id=538
【2】A Stateful Inspection of FireWall-1
http://www.dataprotect.com/bh2000/
【3】Linux IPCHAINS-HOWTO
http://www.linuxdoc.org
【4】防火墙新生代:Stateful-inspection
http://www.liuxuan.com/safe/anquan/html/firewall/04.htm
【5】netfilter站点上的文档
http://netfilter.kernelnotes.org
【6】Application Gateways and Stateful Inspection:A Brief Note Comparing and Contrasting
http://www.avolio.com/apgw+spf.html
【7】Internet Firewalls:Frequently Asked Questions
http://www.interhack.net/pubs/fwfaq
【8】Writing a Module for netfilter
http://www.linux-mag.com/2000-06/gear_01.html
【9】ipchains的源代码分析
http://www.lisoleg.net/lisoleg/network/ipchains.zip
【10】内核防火墙netfilter入门
http://magazine.nsfocus.com/detail.asp?id=637
【11】Check Point Firewall-1 on Linux, Part Two
http://www.securityfocus.com/frames/?focus=linux&content=/focus/linux/articles/checkpoint2.html
[目录]
--------------------------------------------------------------------------------
TCP/IP协议栈阅读笔记
下面是我看RH6.2(Kernel 2-2-14)的TCP/IP代码的笔记
[目录]
--------------------------------------------------------------------------------
启动以后
先从init/main.c的start_kernel函数说起。
在这个函数里面调用kernel_thread启动了init进程,这个进程对应的函数是同一个文件里面的init函数,在init函数里面调用了一个
叫do_basic_setup的在同一个文件里面的函数,这个函数调用了net/socket.c里面的sock_init函数,这个函数就是TCP/IP协议栈,也包括ipx等的入口。
首先sock_init函数里面有很多ifdef这样的东东,我觉得对于一个普通的主机来说,这些都不会配置的,它们包括:
SLAB_SKB,CONFIG_WAN_ROUTER,CONFIG_FIREWALL,CONFIG_RTNETLINK,CONFIG_NETLINK_DEV
去掉了这些编译选项以后就剩下这样的代码:
for (i = 0; i < NPROTO; i++)
net_families = NULL;
sk_init();
proto_init();
其中net_families在include/linux/net.h里面定义,是这样的:
struct net_proto_family
{
int family;
int (*create)(struct socket *sock, int protocol);
/* These are counters for the number of different methods of
each we support */
short authentication;
short encryption;
short encrypt_net;
};
其中有用的只有前两项,那个create的callback函数是每个协议,例如AF_INET等初始化上层协议如TCP/ICMP协议需要的,以后还会遇到的,这里先放着把
sk_init函数在net/core/sock.c里面,没什么说的..
struct sock *sk_alloc(int family, int priority, int zero_it)
{
struct sock *sk = kmem_cache_alloc(sk_cachep, priority);
if(sk) {
if (zero_it)
memset(sk, 0, sizeof(struct sock));
sk->;family = family;
}
return sk;
}
proto_init函数在同一个文件里面:
void __init proto_init(void)
{
extern struct net_proto protocols[];
struct net_proto *pro;
pro = protocols;
while (pro->;name != NULL)
{
(*pro->;init_func)(pro);
pro++;
}
}
struct net_proto在include/linux/net.h里面是这样的:
struct net_proto
{
const char *name; /* Protocol name */
void (*init_func)(struct net_proto *); /* Bootstrap */
};
这个protocols的数组是在net/protocols.c里面定义的,包含了一堆的协议初始化结构体,其中我只注意两个:AF_INET和AF_PACKET
它们的初始化函数分别是inet_proto_init和packet_proto_init
[目录]
--------------------------------------------------------------------------------
协议初始化-1
下面来看看IPv4协议和PACKET协议的初始化过程。
首先看PACKET协议,首先我们假定PACKET协议是编译在核心里面的,而不是一个MODULE,这样得到packet_proto_init函数在net/packet/af_packet.c里面是这样的:
void __init packet_proto_init(struct net_proto *pro)
{
sock_register(&packet_family_ops);
register_netdevice_notifier(&packet_netdev_notifier);
}
其中sock_register函数在net/socket.c里面,就是简单的设置前面说的net_families数组中间对应的值:
int sock_register(struct net_proto_family *ops)
{
if (ops->;family >;= NPROTO) {
printk(KERN_CRIT "protocol %d >;= NPROTO(%d)\n",
ops->;family, NPROTO);
return -ENOBUFS;
}
net_families[ops->;family]=ops;
return 0;
}
这里要说明的是packet_netdev_notifier是一个struct notifier_block类型,这个struct是在include/linux/notifier.h里面的:
struct notifier_block
{
int (*notifier_call)(struct notifier_block *self, unsigned long, void *);
struct notifier_block *next;
int priority;
};
而register_netdevice_notifier函数在net/core/dev.c里面,是这样的:
int register_netdevice_notifier(struct notifier_block *nb)
{
return notifier_chain_register(&netdev_chain, nb);
}
而notifier_chain_register函数在include/linux/notifier.h里面,是这样的:
extern __inline__ int notifier_chain_register(
struct notifier_block **list, struct notifier_block *n)
{
while(*list)
{
if(n->;priority >; (*list)->;priority)
break;
list= &((*list)->;next);
}
n->;next = *list;
*list=n;
return 0;
}
显然就是根据每个block的优先级把这个block排列在一个block的链表里面,在notifier_chain_register函数里面我们可以发现这个链表是netdev_chain。实际上这个链表的作用就是在每个interface打开,关闭状态改变或者外界调用相应的ioctl的时候通知这个链表上面的所有相关的设备,而每一个协议都调用register_netdevice_notifier注册了一个netdev_notifier的结构体,这样就可以在interface改变的时候得到通知了(通过调用每个notifier_call函数)。
下面来看inet_proto_init函数,这个函数在net/ipv4/af_inet.c中间,里面也有很多ifdef的编译选项,假定下面几个是没有定义的:CONFIG_NET_IPIP,CONFIG_NET_IPGRE,CONFIG_IP_FIREWALL,CONFIG_IP_MASQUERADE,CONFIG_IP_MROUTE
假定下面几个是定义了的:CONFIG_INET_RARP,CONFIG_PROC_FS
下面是整理后的代码:
(void) sock_register(&inet_family_ops);
for(p = inet_protocol_base; p != NULL {
struct inet_protocol *tmp=(struct inet_protocol *)p->;next;
inet_add_protocol(p);
printk("%s%s",p->;name,tmp?", ":"\n";
p = tmp;
}
arp_init();
ip_init();
tcp_v4_init(&inet_family_ops);
tcp_init();
icmp_init(&inet_family_ops);
rarp_ioctl_hook = rarp_ioctl;
proc_net_register(&proc_net_rarp);
proc_net_register(&proc_net_raw);
proc_net_register(&proc_net_snmp);
proc_net_register(&proc_net_netstat);
proc_net_register(&proc_net_sockstat);
proc_net_register(&proc_net_tcp);
proc_net_register(&proc_net_udp);
[目录]
--------------------------------------------------------------------------------
协议初始化-2
其中的sock_register函数的作用已经在前面说了,现在来看看struct inet_protocol和inet_add_protocol函数。前面的结构体
是在include/net/protocol.h里面:
struct inet_protocol
{
int (*handler)(struct sk_buff *skb, unsigned short len);
void (*err_handler)(struct sk_buff *skb, unsigned char *dp, int len);
struct inet_protocol *next;
unsigned char protocol;
unsigned char copy:1;
void *data;
const char *name;
};
第一个函数是用来接收数据的callback函数,第二个是错误处理函数,其它的copy是用来协议共享的,这个以后再说,data当然就是这个结构体的私有数据了。
inet_add_protocol函数是在net/ipv4/protocol.c里面的:
void inet_add_protocol(struct inet_protocol *prot)
{
unsigned char hash;
struct inet_protocol *p2;
hash = prot->;protocol & (MAX_INET_PROTOS - 1);
prot ->;next = inet_protos[hash];
inet_protos[hash] = prot;
prot->;copy = 0;
p2 = (struct inet_protocol *) prot->;next;
while(p2 != NULL)
{
if (p2->;protocol == prot->;protocol)
{
prot->;copy = 1;
break;
}
p2 = (struct inet_protocol *) p2->;next;
}
}
显然这个函数就是建立一个hash表,然后每个hash表项都是一个链表头,然后通过这个hash表加链表的方式访问每个协议结构体。在这里你也见到了copy成员的用法了把。
arp_init函数是在net/ipv4/arp.c里面的(假定没有定义CONFIG_SYSCTL):
neigh_table_init(&arp_tbl);
dev_add_pack(&arp_packet_type);
proc_net_register(&proc_net_arp);
不知道是不是有人眼睛一亮啊,呵呵,看到了dev_add_pack函数。
还是一步步来把。
neigh_table_init函数在net/core/neighbour.c中间:
void neigh_table_init(struct neigh_table *tbl)
{
unsigned long now = jiffies;
tbl->;parms.reachable_time = neigh_rand_reach_time(
tbl->;parms.base_reachable_time);
init_timer(&tbl->;gc_timer);
tbl->;gc_timer.data = (unsigned long)tbl;
tbl->;gc_timer.function = neigh_periodic_timer;
tbl->;gc_timer.expires = now + tbl->;gc_interval +
tbl->;parms.reachable_time;
add_timer(&tbl->;gc_timer);
init_timer(&tbl->;proxy_timer);
tbl->;proxy_timer.data = (unsigned long)tbl;
tbl->;proxy_timer.function = neigh_proxy_process;
skb_queue_head_init(&tbl->;proxy_queue);
tbl->;last_flush = now;
tbl->;last_rand = now + tbl->;parms.reachable_time*20;
tbl->;next = neigh_tables;
neigh_tables = tbl;
}
jiffies是当前系统的时间,在i386系统上面好象一个jiffies代表50ms,显然这个函数就是生成两个timer将一个放在系统的timerlist里面。那个gc_timer的意思是garbage collect timer,因为每过一段时间arp的cache就应该更新,所以要有一个expires时间,这段时间过了以后就要更新arp地址了,那个proxy_timer还没有看是什么,不过我假定我的机器不使用proxy也不做成proxy,所以proxy相关的都没有管
那个timer的function显然是时钟到期的回调函数,data是这个回调函数要使用的私有数据了。
下面是dev_add_pack函数,它在net/core/dev.c里面:
void dev_add_pack(struct packet_type *pt)
{
int hash;
#ifdef CONFIG_NET_FASTROUTE
/* Hack to detect packet socket */
if (pt->;data) {
netdev_fastroute_obstacles++;
dev_clear_fastroute(pt->;dev);
}
#endif
if(pt->;type==htons(ETH_P_ALL))
{
netdev_nit++;
pt->;next=ptype_all;
ptype_all=pt;
}
else
{
hash=ntohs(pt->;type)&15;
pt->;next = ptype_base[hash];
ptype_base[hash] = pt;
}
}
显然系统保留了两个表,一个是ptype_all,用来接收所有类型的包的链表,一个是一个hash数组+链表的结构,用来接收特定类型的包。
struct packet_type的定义在include/linux/netdevice.h里面,我保留原来的注释,这样就不用我多说了
{
unsigned short type;
/* This is really htons(ether_type). */
struct device *dev;
/* NULL is wildcarded here */
int (*func) (struct sk_buff *,
struct device *, struct packet_type *);
void *data;
/* Private to the packet type */
struct packet_type *next;
};
其中的func当然是回调函数了,举个例子来说,arp_packet_type是这样的:
static struct packet_type arp_packet_type =
{
__constant_htons(ETH_P_ARP),
NULL, /* All devices */
arp_rcv,
NULL,
NULL
};
arp_init函数还有最后一个proc_net_register函数,这个函数在include/linux/proc_fs.h里面:
static inline int proc_net_register(struct proc_dir_entry * x)
{
return proc_register(proc_net, x);
}
而proc_register在fs/proc/root.c里面,主要作用是在proc_net对应的目录下面生成每个协议的子目录,例如TCP等在/proc目录下面生成相应的目录,用户可以通过访问/proc/net目录下面的相应目录得到每个协议的统计参数。
[目录]
--------------------------------------------------------------------------------
协议初始化-3
下面是ip_init函数,它在net/ipv4/ip_output.c里面:(下面假定定义了CONFIG_PROC_FS,CONFIG_IP_MULTICAST和CONFIG_NET_CLS_ROUTE)
__initfunc(void ip_init(void))
{
dev_add_pack(&ip_packet_type);
ip_rt_init();
proc_net_register(&proc_net_igmp);
}
前面的dev_add_pack是说过的,这里就不再说了,而且proc_net_register也是前面提过的,这里都不说了,先来看看ip_rt_init函数把,它在net/ipv4/route.c里面,函数是这样的:
__initfunc(void ip_rt_init(void))
{
struct proc_dir_entry *ent;
devinet_init();
ip_fib_init();
rt_periodic_timer.function = rt_check_expire;
/* All the timers, started at system startup tend
to synchronize. Perturb it a bit.
*/
rt_periodic_timer.expires = jiffies + net_random()%
ip_rt_gc_interval + ip_rt_gc_interval;
add_timer(&rt_periodic_timer);
proc_net_register(&(struct proc_dir_entry) {
PROC_NET_RTCACHE, 8, "rt_cache",
S_IFREG | S_IRUGO, 1, 0, 0,
0, &proc_net_inode_operations,
rt_cache_get_info
});
ent = create_proc_entry("net/rt_acct", 0, 0);
ent->;read_proc = ip_rt_acct_read;
}
这个函数总的看来就是注册几个notifier(后面还要看的)和初始化路由表的timer,最后就在/proc目录下面创建一个目录项。其中proc_net_register函数就不说了,而create_proc_entry函数就是在/proc/net目录下面创建一个rt_acct,就是路由参数统计(account)目录,读函数就是ip_rt_acct_read,这个函数就是从全局变量ip_rt_acct中间拷贝数据到用户缓冲中而已。
devinet_init函数是net/ipv4/devinet.c里面的函数,整理后如下:
register_gifconf(PF_INET, inet_gifconf);
register_netdevice_notifier(&ip_netdev_notifier);
register_netdevice_notifier函数在说PACKET协议的时候提过,这里不说了,register_gifconf函数是用来注册对应SIOCGIFCONF这个系统调用的协议无关的一个回调函数,这个函数对于PF_INET来说就是inet_gifconf函数。
其中inet_gifconf函数是net/ipv4/devinet.c里面的,我大概的看了一点,主要好象是在所有的interface里面做一个循环,得到相应的name和address然后返回的。不过不是非常确定。大家参谋呀
而register_gifconf函数本身是在net/core/dev.c里面的,如下:
static gifconf_func_t * gifconf_list [NPROTO];
int register_gifconf(unsigned int family, gifconf_func_t * gifconf)
{
if (family>;=NPROTO)
return -EINVAL;
gifconf_list[family] = gifconf;
return 0;
}
这个函数的意义一目了然,就不说了。
gifconf_list里的函数会在dev_ifconf函数中间被调用,而dev_ifconf函数被dev_ioctl函数调用,dev_ioctl函数负责所有的针对interface的I/O控制。所以我们调用的interface的ioctl函数有一部分就会分到每个协议的gifconf函数里面来,我猜gifconf大概是generous interface configure的意思。就是通用接口配置的意思。
下面再看ip_fib_init函数,它在net/ipv4/fib_frontend.c中间,如下:
(假定没有define CONFIG_IP_MULTIPLE_TABLES,这个参数好象是要创建两个路由表,一个是local的,一个叫main)
__initfunc(void ip_fib_init(void))
{
proc_net_register(&(struct proc_dir_entry) {
PROC_NET_ROUTE, 5, "route",
S_IFREG | S_IRUGO, 1, 0, 0,
0, &proc_net_inode_operations,
fib_get_procinfo
});
fib_rules_init();
register_netdevice_notifier(&fib_netdev_notifier);
register_inetaddr_notifier(&fib_inetaddr_notifier);
}
其中proc_net_register和register_netdevice_notifier函数上面已经提过了,register_inetaddr_notifier函数的作用和register_netdevice_notifier差不多,这个函数也是调用的notifier_chain_register函数注册一个回调函数,这个回调函数在interface加上和删除的时候被调用,fib_rules_init函数其实也差不多,这个函数在net/ipv4/fib_rules.c里面,它其实就是调用一个
register_netdevice_notifier函数注册fib_rules_notifier回调结构体。
fib代表IPv4 Forwarding Information Base,就是IPv4转发信息的意思
[目录]
--------------------------------------------------------------------------------
协议初始化-4
下面是分析tcp_v4_init的时候了,这个函数在net/ipv4/tcp_ipv4.c里面:
__initfunc(void tcp_v4_init(struct net_proto_family *ops))
{
int err;
tcp_inode.i_mode = S_IFSOCK;
tcp_inode.i_sock = 1;
tcp_inode.i_uid = 0;
tcp_inode.i_gid = 0;
tcp_socket->;inode = &tcp_inode;
tcp_socket->;state = SS_UNCONNECTED;
tcp_socket->;type=SOCK_RAW;
if ((err=ops->;create(tcp_socket, IPPROTO_TCP))<0)
panic("Failed to create the TCP control socket.\n";
tcp_socket->;sk->;allocation=GFP_ATOMIC;
tcp_socket->;sk->;num = 256;
tcp_socket->;sk->;ip_ttl = MAXTTL;
}
tcp_inode当然就是一个inode节点了,而tcp_socket等于tcp_inode.u.socket_i,通过一个指针他们指向同一个内存.
tcp_socket是用来通信使用的,可以叫TCP的control socket或者是communicationsocket,当TCP通信没有相应的socket的时候这个socket就充当了socket的角色.比如在一个关闭端口上收到SYN时发送RST,或者是在三次握手的时候发送SYN(还没有accept产生新的socket)
值得注意的是ops->;create函数的调用,我们前面见过对于AF_INET来说这个回调函数是net/ipv4/af_inet.c的inet_create函数,这个函数是用来创建一个socket的时候用的,由于函数比较长,这里先略过分析,这第一次的分析只是一个大致流程的熟悉而已.
由于有socket创建和通信,所以这段代码是协议相关的,所以把这段代码从原来的tcp.c里面提取了出来
下面是tcp_init函数,它在net/ipv4/tcp.c里面,大体上来说就是创建了几个hash表和bucket.这段代码创建了下面几个全局对象:
tcp_openreq_cachep
tcp_bucket_cachep
tcp_timewait_cachep
tcp_ehash
tcp_bhash
其中ehash代表established hash, bhash代表bind hash,它们当然分别是所有的满足TCP_ESTABLISHED <= sk->;state < TCP_CLOSE状态的SOCK.但是我不清楚bucket在这里是什么意思.anyone knows?那几个cachep的作用也不是很清楚.由于整个函数主要是内存分配和错误处理,这里不贴了.
再下来就是icmp_init函数了,在net/ipv4/icmp.c里面,事实上,如果把tcp_v4_init里面的IPPROTO_TCP替换成IPPROTO_ICMP,基本都是一样的.
剩下的proc_net_register函数前面已经讲过了,这里就不说了.
到这里为止,Linux下面IP栈的开始的工作我们基本应该有了个了解,其中有几个关键的函数:
dev_add_pack:
注册一个链路层以上的处理函数,一般是用来使用新的网络层协议的,不过如果注册时重复也是可以的,这时候系统会设置一个copy位.如果是ETH_P_ALL则会接收所有的数据包.加入的元素保存在ptype_all链表和ptype_base hash链表中间.
inet_add_protocol:
注册一个建立在IP层以上的协议,例如TCP和UDP等
proc_net_register(还有类似的proc_register):
在/proc/net目录下面创建一个子目录项来使管理者能通过文件系统得到统计信息
现在迷惑的地方还有很多,一个是结构体sk_buff的每个成员的意义,一个是结构体sock的意义,不过这两个问题应该在以后看多了就知道了.下面我就打算一个个分析每个协议的处理了,包括状态转化/数据发送/接收.
[目录]
--------------------------------------------------------------------------------
bottom up
let's start from bottom up 有的时候用英语说话比汉语要简洁和有意思一点
一个lance得到数据以后总会这样处理:
skb = dev_alloc_skb (....);
skb->;protocol = eth_type_trans(skb, dev);
....
netif_rx (skb);
eth_type_trans函数在net/ethernet/eth.c里面,作用当然很简单了,大家可以自己看.
而netif_rx函数是在net/core/dev.c里面的,假定没有定义CONFIG_CPU_IS_SLOW(我觉得自己的CPU不慢和CONFIG_NET_HW_FLOWCONTROL(很少有人会意识到很多网卡有流量控制把,不过没有交换设备的支持,想凭这个东西达到Qos也没什么
用)以后的代码是这样的:
void netif_rx(struct sk_buff *skb)
{
skb->;stamp = xtime;
if (backlog.qlen <= netdev_max_backlog) {
if (backlog.qlen) {
if (netdev_dropping == 0) {
skb_queue_tail(&backlog,skb);
mark_bh(NET_BH);
return;
}
atomic_inc(&netdev_rx_dropped);
kfree_skb(skb);
return;
}
netdev_dropping = 0;
skb_queue_tail(&backlog,skb);
mark_bh(NET_BH);
return;
}
netdev_dropping = 1;
atomic_inc(&netdev_rx_dropped);
kfree_skb(skb);
}
xtime是当前的时间,一个struct timeval,利用gettimeofday函数得到的就是这个东西的内容.backlog是一个sk_buff的双向链表, netdev_dropping初始化为0,如果没有定义CONFIG_NET_HW_FLOWCONTROL,这个变量一直都将是0.skb_queue_tail就是把一个sk_buff加入到backlog双向队列中去.然后mark_bh是设置了一个全局变量相对位移NET_BH处的bit就返回了.这个bit的设置将使得内核下次schedule的时候从TIMER_BH向下处理时检查到NET_BH处发现有设置就会调用对应NET_BH优先级的函数net_bh来处理,这个回调函数是在net_dev_init函数里面调用init_bh设置的,呵呵,兄弟们,如果感兴趣可以自己再init_bh看看设置一个自己的处理backlog的函数啊.
Linux在这里采取了一个古怪的策略进行控制权的转移和处理机优先级的处理.另一个函数net_bh来处理从backlog中间得到包,它是这样的(假定没定义CONFIG_BRIDGE这个选项):
void net_bh(void)
{
struct packet_type *ptype;
struct packet_type *pt_prev;
unsigned short type;
unsigned long start_time = jiffies;
NET_PROFILE_ENTER(net_bh);
if (qdisc_head.forw != &qdisc_head)
qdisc_run_queues();
while (!skb_queue_empty(&backlog))
{
struct sk_buff * skb;
if (jiffies - start_time >; 1)
goto net_bh_break;
skb = skb_dequeue(&backlog);
#ifdef CONFIG_NET_FASTROUTE
if (skb->;pkt_type == PACKET_FASTROUTE) {
dev_queue_xmit(skb);
continue;
}
#endif
/* XXX until we figure out every place to modify.. */
skb->;h.raw = skb->;nh.raw = skb->;data;
if(skb->;mac.raw < skb->;head || skb->;mac.raw >; skb->;data){
printk(KERN_CRIT "%s: wrong mac.raw ptr, proto=%04x\n",
skb->;dev->;name, skb->;protocol);
kfree_skb(skb);
continue;
}
type = skb->;protocol;
pt_prev = NULL;
for (ptype = ptype_all; ptype!=NULL; ptype=ptype->;next)
{
if (!ptype->;dev || ptype->;dev == skb->;dev) {
if(pt_prev)
{
struct sk_buff *skb2=skb_clone(skb, GFP_ATOMIC);
if(skb2)
pt_prev->;func(skb2,skb->;dev, pt_prev);
}
pt_prev=ptype;
}
}
for (ptype = ptype_base[ntohs(type)&15]; ptype != NULL;
ptype = ptype->;next)
{
if (ptype->;type == type && (!ptype->;dev ||
ptype->;dev==skb->;dev))
{
if(pt_prev)
{
struct sk_buff *skb2;
skb2=skb_clone(skb, GFP_ATOMIC);
if(skb2)
pt_prev->;func(skb2, skb->;dev, pt_prev);
}
pt_prev=ptype;
}
} /* End of protocol list loop */
if(pt_prev)
pt_prev->;func(skb, skb->;dev, pt_prev);
else {
kfree_skb(skb);
}
} /* End of queue loop */
if (qdisc_head.forw != &qdisc_head)
qdisc_run_queues();
netdev_dropping = 0;
NET_PROFILE_LEAVE(net_bh);
return;
net_bh_break:
mark_bh(NET_BH);
NET_PROFILE_LEAVE(net_bh);
return;
}
这个函数其实很简单,NET_PROFILE_ENTER当然是一个宏展开了,它其实就是include/net/profile.h里面的net_profile_enter函数,而NET_PROFILE_LEAVE是profile.h文件里面的net_profile_leave函数,有兴趣的看看把.帮我解疑.
qdisc_head是一个Qdisc_head类型,是一个全局变量,看名字和处理顺序应该看作是一个Quick DISCovery的队列,如果不为空的话我们就要运行qdisc_run_queues函数进行清理了,不过我并不清楚这个queue的意义,这个变量和函数都在net/sched/sch_generic.c里面获得的.大家看了给我答疑把,xixi
下面的东西挺简单的,我就不说了,值得注意的是:
1.大家还记得ptype_all和ptype_base吗?就是调用dev_add_pack加入的数组啊,最终也调用了pt_prev->;func(....)
2.系统先处理ptype_all然后才处理的ptype_base
3.每处理一个sk_buff如果超过1jiffies(x86上为50ms)就再等待下次调用
4.sk_clone是一个快速拷贝,没有拷贝数据,只是复制头部而已
[目录]
--------------------------------------------------------------------------------
packet 函数
看看在net/packet/af_packet.c里面的packet_create函数,这个就是通过packet_proto_init加入的回调函数,假设定义了CONFIG_SOCK_PACKET,代码整理如下,这个函数是在用户创建链路层socket的时候被调用的:
static int packet_create(struct socket *sock, int protocol)
{
struct sock *sk;
int err;
if (!capable(CAP_NET_RAW))
return -EPERM;
if (sock->;type != SOCK_DGRAM && sock->;type != SOCK_RAW
&& sock->;type != SOCK_PACKET
)
return -ESOCKTNOSUPPORT;
//只有socket(AF_PACKET, [SOCK_DGRAM, SOCK_RAW],
//或者socket(AF_INET, SOCK_PACKET ,才能调用成功
sock->;state = SS_UNCONNECTED;
MOD_INC_USE_COUNT;
err = -ENOBUFS;
sk = sk_alloc(PF_PACKET, GFP_KERNEL, 1);
if (sk == NULL)
goto out;
sk->;reuse = 1;
sock->;ops = &packet_ops;
if (sock->;type == SOCK_PACKET)
sock->;ops = &packet_ops_spkt;
//如果是old_style的SOCK_PACKET,就使用packet_ops_spkt
//如果是AF_PACKET,就使用packet_ops作为对应的socket的
//回调函数
sock_init_data(sock,sk);
sk->;protinfo.af_packet = kmalloc(sizeof(struct packet_opt),
GFP_KERNEL);
//protinfo是一个union
if (sk->;protinfo.af_packet == NULL)
goto out_free;
memset(sk->;protinfo.af_packet, 0, sizeof(struct packet_opt));
sk->;zapped=0;
//这个zapped属性表示一个TCP的socket收到了RST
sk->;family = PF_PACKET;
sk->;num = protocol;
sk->;protinfo.af_packet->;prot_hook.func = packet_rcv;
if (sock->;type == SOCK_PACKET)
sk->;protinfo.af_packet->;prot_hook.func = packet_rcv_spkt;
sk->;protinfo.af_packet->;prot_hook.data = (void *)sk;
if (protocol) {
sk->;protinfo.af_packet->;prot_hook.type = protocol;
dev_add_pack(&sk->;protinfo.af_packet->;prot_hook);
//注意到了没有,如果protocol非零的话也可以dev_add_pack
//的,不过当然不能达到phrack55-12的目的,因为这时候你的
//数据已经在用户地址空间了,内核的数据也是改不了的
sk->;protinfo.af_packet->;running = 1;
}
sklist_insert_socket(&packet_sklist, sk);
//这个函数显然应该实现非常简单,在net/core/sock.c里面.
//packet_sklist是用来给每个socket通知interface状态变化
//的消息的,包括UP/DOWN/MULTICAST_LIST_CHANGE
//这个回调函数的实现是我们说过的register_netdev_notifier
return(0);
out_free:
sk_free(sk);
out:
MOD_DEC_USE_COUNT;
return err;
}
只有在创建了packet socket以后应用程序才能接收链路层的数据包.而只有你设置了一个非零的protocol以后才能dev_add_pack,你的socket才能接收数据的.现在看来,dev_add_pack确实是实现底层数据改写的一个重要的函数.所以下面我们
将注意dev_add_pack设置的回调函数func的使用.
[目录]
--------------------------------------------------------------------------------
packet_rcv
我们已经知道了,如果使用socket(AF_SOCKET, ..)产生一个PACKET SOCKET的话,dev_add_pack加入的函数是packet_rcv,下面是这个在net/packet/af_packet.c里面的函数:
static int packet_rcv(struct sk_buff *skb, struct device *dev,
struct packet_type *pt)
{
struct sock *sk;
struct sockaddr_ll *sll = (struct sockaddr_ll*)skb->;cb;
sk = (struct sock *) pt->;data;
//我们在packet_create中令data = sk了,remember?
if (skb->;pkt_type == PACKET_LOOPBACK) {
kfree_skb(skb);
return 0;
}
skb->;dev = dev;
sll->;sll_family = AF_PACKET;
sll->;sll_hatype = dev->;type;
sll->;sll_protocol = skb->;protocol;
sll->;sll_pkttype = skb->;pkt_type;
sll->;sll_ifindex = dev->;ifindex;
sll->;sll_halen = 0;
if (dev->;hard_header_parse)
sll->;sll_halen = dev->;hard_header_parse(skb, sll->;sll_addr);
if (dev->;hard_header)
if (sk->;type != SOCK_DGRAM)
skb_push(skb, skb->;data - skb->;mac.raw);
else if (skb->;pkt_type == PACKET_OUTGOING)
skb_pull(skb, skb->;nh.raw - skb->;data);
if (sock_queue_rcv_skb(sk,skb)<0)
{
kfree_skb(skb);
return 0;
}
return(0);
}
pkt_type属性是什么地方确定的?
这里还有几个函数要说明:
skb_pull在include/linux/skbuff.h中间:
extern __inline__ char *__skb_pull(struct sk_buff *skb,
unsigned int len)
{
skb->;len-=len;
return skb->;data+=len;
}
extern __inline__ unsigned char * skb_pull(struct sk_buff *skb,
unsigned int len)
{
if (len >; skb->;len)
return NULL;
return __skb_pull(skb,len);
}
不过是把头部的数据空出来,相应调整数据头部data的地址和长度.
同样skb_push在include/linux/skbuff.h中间:
extern __inline__ unsigned char *__skb_push(struct sk_buff *skb,
unsigned int len)
{
skb->;data-=len;
skb->;len+=len;
return skb->;data;
}
extern __inline__ unsigned char *skb_push(struct sk_buff *skb,
unsigned int len)
{
skb->;data-=len;
skb->;len+=len;
if(skb->;data head)
{
__label__ here;
skb_under_panic(skb, len, &&here);
here: ;
}
return skb->;data;
}
这个调整使数据长度加长,和skb_pull相反,不过skb_push显然更加安全一点.
在上面的程序中间,如果设备有一个明确的link_level_header,就考虑要不要调整数据长度和地址,如果sk->;type不是SOCK_DGRAM的话,说明程序对整个数据包包括ll地址都感兴趣.这样需要加长数据段使得数据包含ll头部.不然如果数据是向外走的,则需要把数据裁减到只包含从网络层数据包头开始的地方.所以是从nh.raw剪掉data,这就是差值.(nh=network header)
经过了这些处理以后,现在的skb已经是可以提交的了,这样就调用sock_queue_rcv_skb函数将这个skb加入到相应socket的接收缓冲区中去.
#if PAGE_SHIFT mempages >;>;= (13 - PAGE_SHIFT);
#endif
mempages *= sizeof(struct list_head);
for (order = 0; ((1UL
do {
unsigned long tmp;
nr_hash = (1UL sizeof(struct list_head);
d_hash_mask = (nr_hash - 1);
tmp = nr_hash;
d_hash_shift = 0;
while ((tmp >;>;= 1UL) != 0UL)
d_hash_shift++;
dentry_hashtable = (struct list_head *)
__get_free_pages(GFP_ATOMIC, order);
} while (dentry_hashtable == NULL --order >;= 0);
; 如果order太大,超过了__get_free_pages最大可分配尺寸,则减小order的值重试.
printk("Dentry-cache hash table entries: %d (order: %ld, %ld bytes)\n",
nr_hash, order, (PAGE_SIZE
if (!dentry_hashtable)
panic("Failed to allocate dcache hash table\n";
d = dentry_hashtable;
i = nr_hash;
do {
INIT_LIST_HEAD(d);
d++;
i--;
} while (i);
}
对opera的注释加点解释,读起来可能会更省力些。
1.为什么要用这个算法
例如要构造一个文件 /usr/local/cross/my_comp
这时要沿着上面这个文件名开始依次找直到cross的数据结构,也就是要找到
/usr/
/usr/local/
/usr/local/cross/
对应的数据结构dentry
假定我们已经找到/usr/对应的dentry, 现在必须能够从local找到它对应的dentry,这时就要从名字---->;dentry的快速映射,在Linux中般用哈希映射。
2. 查找方法
首先,通过d_hash粗分类,找到"local"所在的链表,然后顺序向下一一匹配。
3.一些操作如opera所述
4.初始化
首先通过__get_free_pages获得一些页面,这些页面构成了所有链表头数组。
[目录]
--------------------------------------------------------------------------------
permission(inode,mask)
permission(inode,mask)用来测试对文件(inode)是否有(mask)访问权.
; fs/namei.c
int permission(struct inode * inode,int mask)
{
if (inode->;i_op inode->;i_op->;permission) {
; 如果文件系统定义了自已的授权算法
int retval;
lock_kernel();
retval = inode->;i_op->;permission(inode, mask);
unlock_kernel();
return retval;
}
return vfs_permission(inode, mask); 缺省的授权算法
}
int vfs_permission(struct inode * inode,int mask)
{
int mode = inode->;i_mode;
; 如果对只读文件系统中的普通文件,目录文件,符号链接请求写访问
if ((mask S_IWOTH) IS_RDONLY(inode)
(S_ISREG(mode) || S_ISDIR(mode) || S_ISLNK(mode)))
return -EROFS; /* Nobody gets write access to a read-only fs */
; 如果对免疫文件请求写访问
if ((mask S_IWOTH) IS_IMMUTABLE(inode))
return -EACCES; /* Nobody gets write access to an immutable file */
if (current->;fsuid == inode->;i_uid)
mode >;>;= 6; 如果自已是文件的拥有者,取文件对拥有者的访问权
else if (in_group_p(inode->;i_gid))
mode >;>;= 3; 如果自已是文件所在组的成员,取文件对组成员的访问权
; 如果所请求的权限是实际对文件权限的子集或者被赋予了超越特权,则允许访问
if (((mode mask S_IRWXO) == mask) || capable(CAP_DAC_OVERRIDE))
return 0;
; 虽然自已对文件没有访问权限,但如果自已被赋予了读和检索的特权,
; 则允许读或检索目录.
/* read and search access */
if ((mask == S_IROTH) ||
(S_ISDIR(inode->;i_mode) !(mask ~(S_IROTH | S_IXOTH))))
if (capable(CAP_DAC_READ_SEARCH))
return 0;
return -EACCES;
}
; kernel/sys.c
/*
* Check whether we're fsgid/egid or in the supplemental group..
*/
int in_group_p(gid_t grp)
{
int retval = 1;
if (grp != current->;fsgid)
retval = supplemental_group_member(grp);
return retval;
}
static int supplemental_group_member(gid_t grp)
{
int i = current->;ngroups;
if (i) {
gid_t *groups = current->;groups;
do {
if (*groups == grp)
return 1;
groups++;
i--;
} while (i);
}
return 0;
}
[目录]
--------------------------------------------------------------------------------
IDE硬盘驱动器读写
Linux内核在缺省配置下最多支持10个IDE接口,IDE接口用ide_hwif_t结构来描述,每个IDE接口具有一对主-从驱动器接口,它们用ide_drive_t结构来描述,每个驱动器接口可接不同种类的IDE设备,如IDE硬盘,光驱等,它们用ide_driver_t结构来描述.
每个驱动器接口包含一个命令请求队列,用request_queue_t结构来描述,具体的请求用request结构来描述.
多个IDE驱动器可以共享一个中断,共享同一中断的驱动器形成一个组,用ide_hwgroup_t结构来描述.ide_intr()是所有的ide驱动器所共用的硬件中断入口,对之对应的ide_hwgroup_t指针将作为dev_id传递给ide_intr.
每次在读写某个驱动器之前,需要用ide_set_handler()来设置ide_intr将要调用的中断函数指针.中断产生以后,该函数指针被自动清除.
do_rw_disk(drive,rq,block) 从逻辑扇区号block开始向IDE硬盘驱动器drive写入rq所描述的内容.
以下是硬盘PIO传输模式的有关代码.
; drivers/ide/ide-disk.c
static ide_startstop_t do_rw_disk (ide_drive_t *drive, struct request *rq, unsigned long block)
{
if (IDE_CONTROL_REG)
OUT_BYTE(drive->;ctl,IDE_CONTROL_REG);
OUT_BYTE(rq->;nr_sectors,IDE_NSECTOR_REG);
if (drive->;select.b.lba) { 如果是逻辑块寻址模式
OUT_BYTE(block,IDE_SECTOR_REG);
OUT_BYTE(block>;>;=8,IDE_LCYL_REG);
OUT_BYTE(block>;>;=8,IDE_HCYL_REG);
OUT_BYTE(((block>;>;
} else {
unsigned int sect,head,cyl,track;
track = block / drive->;sect;
sect = block % drive->;sect + 1;
OUT_BYTE(sect,IDE_SECTOR_REG);
head = track % drive->;head;
cyl = track / drive->;head;
OUT_BYTE(cyl,IDE_LCYL_REG);
OUT_BYTE(cyl>;>;8,IDE_HCYL_REG);
OUT_BYTE(head|drive->;select.all,IDE_SELECT_REG);
}
if (rq->;cmd == READ) {{
ide_set_handler(drive, WAIT_CMD, NULL); WAIT_CMD为10秒超时
OUT_BYTE(drive->;mult_count ? WIN_MULTREAD : WIN_READ, IDE_COMMAND_REG);
return ide_started;
}
if (rq->;cmd == WRITE) {
ide_startstop_t startstop;
OUT_BYTE(drive->;mult_count ? WIN_MULTWRITE : WIN_WRITE, IDE_COMMAND_REG);
if (ide_wait_stat( drive, DATA_READY, drive->;bad_wstat, WAIT_DRQ)) {
printk(KERN_ERR "%s: no DRQ after issuing %s\n", drive->;name,
drive->;mult_count ? "MULTWRITE" : "WRITE";
return startstop;
}
if (!drive->;unmask)
__cli(); /* local CPU only */
if (drive->;mult_count) { 如果允许多扇区传送
ide_hwgroup_t *hwgroup = HWGROUP(drive);
/*
* Ugh.. this part looks ugly because we MUST set up
* the interrupt handler before outputting the first block
* of data to be written. If we hit an error (corrupted buffer list)
* in ide_multwrite(), then we need to remove the handler/timer
* before returning. Fortunately, this NEVER happens (right?).
*
* Except when you get an error it seems...
*/
hwgroup->;wrq = *rq; /* scratchpad */
ide_set_handler (drive, WAIT_CMD, NULL);
if (ide_multwrite(drive, drive->;mult_count)) {
unsigned long flags;
spin_lock_irqsave( flags);
hwgroup->;handler = NULL;
del_timer(
spin_unlock_irqrestore( flags);
return ide_stopped;
}
} else {
ide_set_handler (drive, WAIT_CMD, NULL);
idedisk_output_data(drive, rq->;buffer, SECTOR_WORDS); 写入一扇区SECTOR_WORDS=512/4
}
return ide_started;
}
printk(KERN_ERR "%s: bad command: %d\n", drive->;name, rq->;cmd);
ide_end_request(0, HWGROUP(drive));
return ide_stopped;
}
void ide_set_handler (ide_drive_t *drive, ide_handler_t *handler,
unsigned int timeout, ide_expiry_t *expiry)
{
unsigned long flags;
ide_hwgroup_t *hwgroup = HWGROUP(drive);
spin_lock_irqsave( flags);
if (hwgroup->;handler != NULL) {
printk("%s: ide_set_handler: handler not null; old=%p, new=%p\n",
drive->;name, hwgroup->;handler, handler);
}
hwgroup->;handler = handler;
hwgroup->;expiry = expiry;
hwgroup->;timer.expires = jiffies + timeout;
add_timer(
spin_unlock_irqrestore( flags);
}
static inline void idedisk_output_data (ide_drive_t *drive, void *buffer, unsigned int wcount)
{
if (drive->;bswap) {
idedisk_bswap_data(buffer, wcount);
ide_output_data(drive, buffer, wcount);
idedisk_bswap_data(buffer, wcount);
} else
ide_output_data(drive, buffer, wcount);
}
void ide_output_data (ide_drive_t *drive, void *buffer, unsigned int wcount)
{
byte io_32bit = drive->;io_32bit;
if (io_32bit) {
#if SUPPORT_VLB_SYNC
if (io_32bit 2) {
unsigned long flags;
__save_flags(flags); /* local CPU only */
__cli(); /* local CPU only */
do_vlb_sync(IDE_NSECTOR_REG);
outsl(IDE_DATA_REG, buffer, wcount);
__restore_flags(flags); /* local CPU only */
} else
#endif /* SUPPORT_VLB_SYNC */
outsl(IDE_DATA_REG, buffer, wcount);
} else {
#if SUPPORT_SLOW_DATA_PORTS
if (drive->;slow) {
unsigned short *ptr = (unsigned short *) buffer;
while (wcount--) {
outw_p(*ptr++, IDE_DATA_REG);
outw_p(*ptr++, IDE_DATA_REG);
}
} else
#endif /* SUPPORT_SLOW_DATA_PORTS */
outsw(IDE_DATA_REG, buffer, wcount }
}
int ide_multwrite (ide_drive_t *drive, unsigned int mcount)
{
ide_hwgroup_t *hwgroup= HWGROUP(drive);
/*
* This may look a bit odd, but remember wrq is a copy of the
* request not the original. The pointers are real however so the
* bh's are not copies. Remember that or bad stuff will happen
*
* At the point we are called the drive has asked us for the
* data, and its our job to feed it, walking across bh boundaries
* if need be.
*/
struct request *rq =
do {
unsigned long flags;
unsigned int nsect = rq->;current_nr_sectors;
if (nsect >; mcount)
nsect = mcount;
mcount -= nsect;
; 这时mcount为剩余的必需传送的扇区数
idedisk_output_data(drive, rq->;buffer, nsect spin_lock_irqsave( flags); /* Is this really necessary? */
#ifdef CONFIG_BLK_DEV_PDC4030
rq->;sector += nsect;
#endif
if (((long)(rq->;nr_sectors -= nsect)) spin_unlock_irqrestore( flags);
break;
}
if ((rq->;current_nr_sectors -= nsect) == 0) {
if ((rq->;bh = rq->;bh->;b_reqnext) != NULL) {{
rq->;current_nr_sectors = rq->;bh->;b_size>;>;9;
rq->;buffer = rq->;bh->;b_data;
} else {
spin_unlock_irqrestore( flags);
printk("%s: buffer list corrupted (%ld, %ld, %d)\n",
drive->;name, rq->;current_nr_sectors,
rq->;nr_sectors, nsect);
ide_end_request(0, hwgroup);
return 1;
}
} else {
/* Fix the pointer.. we ate data */
rq->;buffer += nsect }
spin_unlock_irqrestore( flags);
} while (mcount);
return 0;
}
; IDE接口共用中断入口
void ide_intr (int irq, void *dev_id, struct pt_regs *regs)
{
unsigned long flags;
ide_hwgroup_t *hwgroup = (ide_hwgroup_t *)dev_id;
ide_hwif_t *hwif;
ide_drive_t *drive;
ide_handler_t *handler;
ide_startstop_t startstop;
spin_lock_irqsave( flags);
hwif = hwgroup->;hwif;
if (!ide_ack_intr(hwif)) {
spin_unlock_irqrestore( flags);
return;
}
if ((handler = hwgroup->;handler) == NULL || hwgroup->;poll_timeout != 0) {
/*
* Not expecting an interrupt from this drive.
* That means this could be:
* (1) an interrupt from another PCI device
* sharing the same PCI INT# as us.
* or (2) a drive just entered sleep or standby mode,
* and is interrupting to let us know.
* or (3) a spurious interrupt of unknown origin.
*
* For PCI, we cannot tell the difference,
* so in that case we just ignore it and hope it goes away.
*/
#ifdef CONFIG_BLK_DEV_IDEPCI
if (IDE_PCI_DEVID_EQ(hwif->;pci_devid, IDE_PCI_DEVID_NULL))
#endif /* CONFIG_BLK_DEV_IDEPCI */
{
/*
* Probably not a shared PCI interrupt,
* so we can safely try to do something about it:
*/
unexpected_intr(irq, hwgroup);
#ifdef CONFIG_BLK_DEV_IDEPCI
} else {
/*
* Whack the status register, just in case we have a leftover pending IRQ.
*/
(void) IN_BYTE(hwif->;io_ports[IDE_STATUS_OFFSET]);
#endif /* CONFIG_BLK_DEV_IDEPCI */
}
spin_unlock_irqrestore( flags);
return;
}
drive = hwgroup->;drive;
if (!drive) {
/*
* This should NEVER happen, and there isn't much we could do about it here.
*/
spin_unlock_irqrestore( flags);
return;
}
if (!drive_is_ready(drive)) {
/*
* This happens regularly when we share a PCI IRQ with another device.
* Unfortunately, it can also happen with some buggy drives that trigger
* the IRQ before their status register is up to date. Hopefully we have
* enough advance overhead that the latter isn't a problem.
*/
spin_unlock_irqrestore( flags);
return;
}
if (!hwgroup->;busy) {
hwgroup->;busy = 1; /* paranoia */
printk("%s: ide_intr: hwgroup->;busy was 0 ??\n", drive->;name);
}
hwgroup->;handler = NULL;
del_timer(
spin_unlock(
if (drive->;unmask)
ide__sti(); /* local CPU only */
startstop = handler(drive); /* service this interrupt, may set handler for next interrupt */
spin_lock_irq(
/*
* Note that handler() may have set things up for another
* interrupt to occur soon, but it cannot happen until
* we exit from this routine, because it will be the
* same irq as is currently being serviced here, and Linux
* won't allow another of the same (on any CPU) until we return.
*/
set_recovery_timer(HWIF(drive));
drive->;service_time = jiffies - drive->;service_start;
if (startstop == ide_stopped) {
if (hwgroup->;handler == NULL) { /* paranoia */
hwgroup->;busy = 0;
ide_do_request(hwgroup, hwif->;irq);
} else {
printk("%s: ide_intr: huh? expected NULL handler on exit\n", drive->;name);
}
}
spin_unlock_irqrestore( flags);
}
; 单个扇区写入之后的中断处理
static ide_startstop_t write_intr (ide_drive_t *drive)
{
byte stat;
int i;
ide_hwgroup_t *hwgroup = HWGROUP(drive);
struct request *rq = hwgroup->;rq;
if (!OK_STAT(stat=GET_STAT(),DRIVE_READY,drive->;bad_wstat)) {
printk("%s: write_intr error1: nr_sectors=%ld, stat=0x%02x\n", drive->;name, rq->;nr_sectors, stat);
} else {
if ((rq->;nr_sectors == 1) ^ ((stat DRQ_STAT) != 0)) {
rq->;sector++;
rq->;buffer += 512;
rq->;errors = 0;
i = --rq->;nr_sectors;
--rq->;current_nr_sectors;
if (((long)rq->;current_nr_sectors) ide_end_request(1, hwgroup);
if (i >; 0) {
idedisk_output_data (drive, rq->;buffer, SECTOR_WORDS);
ide_set_handler (drive, WAIT_CMD, NULL);
return ide_started;
}
return ide_stopped;
}
return ide_stopped; /* the original code did this here (?) */
}
return ide_error(drive, "write_intr", stat);
}
; 多重扇区写入后的中断处理
static ide_startstop_t multwrite_intr (ide_drive_t *drive)
{
byte stat;
int i;
ide_hwgroup_t *hwgroup = HWGROUP(drive);
struct request *rq =
if (OK_STAT(stat=GET_STAT(),DRIVE_READY,drive->;bad_wstat)) {
if (stat DRQ_STAT) {
/*
* The drive wants data. Remember rq is the copy
* of the request
*/
if (rq->;nr_sectors) {
if (ide_multwrite(drive, drive->;mult_count))
return ide_stopped;
ide_set_handler (drive, WAIT_CMD, NULL);
return ide_started;
}
} else {
/*
* If the copy has all the blocks completed then
* we can end the original request.
*/
if (!rq->;nr_sectors) { /* all done? */
rq = hwgroup->;rq;
for (i = rq->;nr_sectors; i >; 0{
i -= rq->;current_nr_sectors;
ide_end_request(1, hwgroup);
}
return ide_stopped;
}
}
return ide_stopped; /* the original code did this here (?) */
}
return ide_error(drive, "multwrite_intr", stat);
}
; 读扇区的中断处理
static ide_startstop_t read_intr (ide_drive_t *drive)
{
byte stat;
int i;
unsigned int msect, nsect;
struct request *rq;
/* new way for dealing with premature shared PCI interrupts */
if (!OK_STAT(stat=GET_STAT(),DATA_READY,BAD_R_STAT)) {
if (stat (ERR_STAT|DRQ_STAT)) {
return ide_error(drive, "read_intr", stat);
}
/* no data yet, so wait for another interrupt */
ide_set_handler(drive, WAIT_CMD, NULL);
return ide_started;
}
msect = drive->;mult_count;
read_next:
rq = HWGROUP(drive)->;rq;
if (msect) {
if ((nsect = rq->;current_nr_sectors) >; msect)
nsect = msect;
msect -= nsect;
} else
nsect = 1;
idedisk_input_data(drive, rq->;buffer, nsect * SECTOR_WORDS);
rq->;sector += nsect;
rq->;buffer += nsect rq->;errors = 0;
i = (rq->;nr_sectors -= nsect);
if (((long)(rq->;current_nr_sectors -= nsect)) ide_end_request(1, HWGROUP(drive));
if (i >; 0) {
if (msect)
goto read_next;
ide_set_handler (drive, WAIT_CMD, NULL);
return ide_started;
}
return ide_stopped;
}
static inline void idedisk_input_data (ide_drive_t *drive, void *buffer, unsigned int wcount)
{
ide_input_data(drive, buffer, wcount);
if (drive->;bswap)
idedisk_bswap_data(buffer, wcount);
}
void ide_input_data (ide_drive_t *drive, void *buffer, unsigned int wcount)
{
byte io_32bit = drive->;io_32bit;
if (io_32bit) {
#if SUPPORT_VLB_SYNC
if (io_32bit 2) {
unsigned long flags;
__save_flags(flags); /* local CPU only */
__cli(); /* local CPU only */
do_vlb_sync(IDE_NSECTOR_REG);
insl(IDE_DATA_REG, buffer, wcount);
__restore_flags(flags); /* local CPU only */
} else
#endif /* SUPPORT_VLB_SYNC */
insl(IDE_DATA_REG, buffer, wcount);
} else {
#if SUPPORT_SLOW_DATA_PORTS
if (drive->;slow) {
unsigned short *ptr = (unsigned short *) buffer;
while (wcount--) {
*ptr++ = inw_p(IDE_DATA_REG);
*ptr++ = inw_p(IDE_DATA_REG);
}
} else
#endif /* SUPPORT_SLOW_DATA_PORTS */
insw(IDE_DATA_REG, buffer, wcount }
}
atomic_t queued_sectors;
#define blk_finished_io(nsects) \
atomic_sub(nsects, \
if (atomic_read( printk("block: queued_sectors atomic_set( 0); \
}
static inline void blkdev_dequeue_request(struct request * req)
{
list_del(
}
void ide_end_request (byte uptodate, ide_hwgroup_t *hwgroup)
{
struct request *rq;
unsigned long flags;
spin_lock_irqsave( flags);
rq = hwgroup->;rq;
if (!end_that_request_first(rq, uptodate, hwgroup->;drive->;name)) {
add_blkdev_randomness(MAJOR(rq->;rq_dev));
blkdev_dequeue_request(rq);
hwgroup->;rq = NULL;
end_that_request_last(rq);
}
spin_unlock_irqrestore( flags);
}
int end_that_request_first (struct request *req, int uptodate, char *name)
{
struct buffer_head * bh;
int nsect;
req->;errors = 0;
if (!uptodate)
printk("end_request: I/O error, dev %s (%s), sector %lu\n",
kdevname(req->;rq_dev), name, req->;sector);
if ((bh = req->;bh) != NULL) {
nsect = bh->;b_size >;>; 9;
blk_finished_io(nsect);
req->;bh = bh->;b_reqnext;
bh->;b_reqnext = NULL;
bh->;b_end_io(bh, uptodate);
if ((bh = req->;bh) != NULL) {
req->;hard_sector += nsect;
req->;hard_nr_sectors -= nsect;
req->;sector = req->;hard_sector;
req->;nr_sectors = req->;hard_nr_sectors;
req->;current_nr_sectors = bh->;b_size >;>; 9;
if (req->;nr_sectors current_nr_sectors) {
req->;nr_sectors = req->;current_nr_sectors;
printk("end_request: buffer-list destroyed\n";
}
req->;buffer = bh->;b_data;
return 1;
}
}
return 0;
}
void end_that_request_last(struct request *req)
{
if (req->;sem != NULL)
up(req->;sem);
blkdev_release_request(req);
}
void inline blkdev_release_request(struct request *req)
{
request_queue_t *q = req->;q;
int rw = req->;cmd;
req->;rq_status = RQ_INACTIVE;
req->;q = NULL;
/*
* Request may not have originated from ll_rw_blk. if not,
* asumme it has free buffers and check waiters
*/
if (q) {
/*
* we've released enough buffers to start I/O again
*/
if (waitqueue_active(
atomic_read( wake_up(
/*
* Add to pending free list and batch wakeups
*/
list_add(
if (++q->;pending_free[rw] >;= batch_requests) {
int wake_up = q->;pending_free[rw];
blk_refill_freelist(q, rw);
wake_up_nr( wake_up);
}
}
}
void inline blk_refill_freelist(request_queue_t *q, int rw)
{
if (q->;pending_free[rw]) {
list_splice(
INIT_LIST_HEAD(
q->;pending_free[rw] = 0;
}
}
[目录]
--------------------------------------------------------------------------------
proc
PROC文件系统是一个伪文件系统,它的文件和目录是由Linux 操作系统核心提供的,以文件系统的方式为访问系统内核数据的操作提供接口,它们不占用磁盘上的任何空间,有了这些文件和目录, 用户可以更容易的了解操作系统核心或各个进程的状态,并能对系统的一些参数进行配置。比如,一个系统内能打开的文件数最大缺省是1024,即系统最多能同时打开1024个文件,这在使用Linux做多用户的服务器时是不够用的,通过对/PROC下文件的修改可以在不修改核心,甚至不启动机器的情况下改变这个缺省值。由于系统的信息,如进程,是动态改变的,所以用户或应用程序读取PROC文件时,PROC文件系统是动态从系统内核读出所需信息并提交的。它的目录结构如下:
目录名称 目录内容
apm Advanced power management info
Cmdline Kernel command line
Cpuinfo Info about the CPU
Devices Available devices (block and character)
Dma Used DMS channels
Filesystems Supported filesystems
Interrupts Interrupt usage
Ioports I/O port usage
Kcore Kernel core image
Kmsg Kernel messages
Ksyms Kernel symbol table
Loadavg Load average
Locks Kernel locks
Meminfo Memory info
Misc Miscellaneous
Modules List of loaded modules
Mounts Mounted filesystems
Partitions Table of partitions known to the system
Rtc Real time clock
Slabinfo Slab pool info
Stat Overall statistics
Swaps Swap space utilization
Version Kernel version
Uptime System uptime
并不是所有这些目录在你的系统中都有,这取决于你的内核配置和装载的模块。另外,在/proc下还有三个很重要的目录:net,scsi和sys。Sys目录是可写的,可以通过它来访问或修改内核的参数,而net和scsi则依赖于内核配置。例如,如果系统不支持scsi,则scsi目录不存在。
除了以上介绍的这些,还有的是一些以数字命名的目录,它们是进程目录。系统中当前运行的每一个进程都有对应的一个目录在/PROC下,以进程的PID号为目录名,它们是读取进程信息的接口。而self目录则是读取进程本身的信息接口,是一个link。Proc文件系统的名字就是由之而起。进程目录的结构如下:
目录名称 目录内容
Cmdline Command line arguments
Environ Values of environment variables
Fd Directory, which contains all file descriptors
Mem Memory held by this process
Stat Process status
Status Process status in human readable form
Cwd Link to the current working directory
Exe Link to the executable of this process
Maps Memory maps
Statm Process memory status information
Root Link to the root directory of this process
用户如果要查看系统信息,可以用cat命令。例如:
>; cat /proc/interrupts
CPU0
0: 8728810 XT-PIC timer
1: 895 XT-PIC keyboard
2: 0 XT-PIC cascade
3: 531695 XT-PIC aha152x
4: 2014133 XT-PIC serial
5: 44401 XT-PIC pcnet_cs
8: 2 XT-PIC rtc
11: 8 XT-PIC i82365
12: 182918 XT-PIC PS/2 Mouse
13: 1 XT-PIC fpu
14: 1232265 XT-PIC ide0
15: 7 XT-PIC ide1
NMI: 0
2.修改内核参数
在/proc文件系统中有一个有趣的目录:/proc/sys。它不仅提供了内核信息,而且可以通过它修改内核参数,来优化你的系统。但是你必须很小心,因为可能会造成系统崩溃。最好是先找一台无关紧要的机子,调试成功后再应用到你的系统上。
要改变内核的参数,只要用vi编辑或echo参数重定向到文件中即可。下面有一个例子:
# cat /proc/sys/fs/file-max
4096
# echo 8192 >; /proc/sys/fs/file-max
# cat /proc/sys/fs/file-max
8192
如果你优化了参数,则可以把它们写成脚本文件,使它在系统启动时自动完成修改。
PROC文件系统的初始化过程概述
PROC文件系统总的初始化流程如下图所示,是从INIT/MAIN.C的START_KERNEL()函数中开始的,首先是PROC_ROOT_INIT(),在这个函数中用PROC_DIR_EMTRY注册了/PROC及其目录下的各个文件及目录的基本信息,包括注册INODE NUMBER,文件名长度,文件名,操作权限,连接数,用户标识号,组标识号,允许的INODE OPERATIONS和兄弟、父母、子文件的指针等,并生成文件目录之间的相互关系,即目录树。接下来在SYSCLT_INIT()里把ROOT_TABLE的各项内容挂到/PROC树的PROC_SYS_ROOT下面,PROC_SYS_ROOT是一个特殊的PROC项,即/PROC/SYS目录,在此目录下的部分文件是可写的,可以通过修改这些文件内容来修改系统配置参数。然后是FILESYSTEM_SETUP(),在这里产生一个新的FILE_SYSTEM_TYPE:PROC_FS_TYPE,把这个文件系统类型挂到FILE_SYSTEMS链表的末尾,其中包含了读取PROC文件系统SUPER BLOCK的函数指针,PROC_READ_SUPER,接下来在装载了ROOT文件系统后,如果还要把PROC文件系统MOUNT上来则需通过此函数把PROC文件系统的SUPER BLOCK读进内存里的SUPER_BLOCKS链表。从上面可以看到,PROC文件系统的初始化流程主要分两步,即登记注册和挂载文件系统,其中的核心内容是在登记注册里,下面就具体分析一下这两部分的具体初始化操作。
三、PROC文件系统的登记注册过程
从程序中来看,PROC文件系统的文件可以分为两大类,一类是ROOT部分,另一类是BASE部分,体现在初始化程序中分别叫做PROC_ROOT_INIT()和PROC_BASE_INIT()。ROOT部分主要是针对/PROC目录下的一些名称位置相对固定的常规性文件,如 CPUINFO,MEMINFO,KMESG,MODULES,DEVICES,INTERRUPTS,FILESYSTEMS,PARTITIONS,DMA,IOPORTS,CMDLINE,MOUNTS等;而BASE部分则是针对系统中每个进程的,每个运行中的进程在/PROC下都有一个以自己的进程号为名字的目录,里面记载了关于此进程运行状态的信息,如STATUS,MEM,CWD,ROOT,FD,EXE,CMDLINE,STAT,STATM等。下面将会分别介绍这两部分的初始化过程,首先介绍一下基本的数据结构。
1.基本数据结构
在PROC 文件系统中最重要的数据结构是一个叫PROC_DIR_ENTRY的结构类型(include/linux/proc_fs.h),所有该文件系中的文件及目录都除了通用文件操作用的INODE,FILE,DENTRY等结构外,都必须首先注册自己的一个PROC_DIR_ENTRY,在其中定义了针对自己的各项属性和操作函数指针。
struct proc_dir_entry {
unsigned short low_ino; /*注册inode号,实际inode的低16位*/
unsigned short namelen; /*名称字符串的长度*/
const char *name; /*文件名称*/
mode_t mode; /*文件权限*/
nlink_t nlink; /*连接到此文件的目录项个数*/
uid_t uid; /*用户标识*/
gid_t gid; /*组标识*/
unsigned long size; /*文件大小,均为0*/
struct inode_operations * ops; /*inode操作的函数指针*/
int (*get_info)(char *, char **, off_t, int, int);
void (*fill_inode)(struct inode *, int); /*填补inode信息的函数指针*/
struct proc_dir_entry *next, *parent, *subdir; /*兄弟,父亲,子文件指针*/
void *data;
int (*read_proc)(char *page, char **start, off_t off, int count,
int *eof, void *data);
int (*write_proc)(struct file *file, const char *buffer,
unsigned long count, void *data);
int (*readlink_proc)(struct proc_dir_entry *de, char *page);
unsigned int count; /*使用数*/
int deleted; /*删除标志*/
};
其次就是针对INODE,FILE和SUPER BLOCK的各种操作函数指针,这些结构与文件系统中使用的完全相同,此处不再赘述。
2.PROC_ROOT_INIT()
PROC_ROOT_INIT()函数的定义如下:
__initfunc(void proc_root_init(void))
{
proc_base_init();
proc_register(&proc_root, &proc_root_loadavg);
proc_register(&proc_root, &proc_root_uptime);
proc_register(&proc_root, &proc_root_meminfo);
proc_register(&proc_root, &proc_root_kmsg);
proc_register(&proc_root, &proc_root_version);
proc_register(&proc_root, &proc_root_cpuinfo);
proc_register(&proc_root, &proc_root_self);
#ifdef CONFIG_SYSCTL
proc_register(&proc_root, &proc_sys_root);
#endif
#ifdef CONFIG_MCA
proc_register(&proc_root, &proc_mca);
#endif
#ifdef CONFIG_DEBUG_MALLOC
proc_register(&proc_root, &proc_root_malloc);
#endif
#ifdef CONFIG_MODULES
proc_register(&proc_root, &proc_root_modules);
proc_register(&proc_root, &proc_root_ksyms);
#endif
proc_register(&proc_root, &proc_root_stat);
proc_register(&proc_root, &proc_root_devices);
proc_register(&proc_root, &proc_root_partitions);
proc_register(&proc_root, &proc_root_interrupts);
proc_register(&proc_root, &proc_root_filesystems);
proc_register(&proc_root, &proc_root_fs);
proc_register(&proc_root, &proc_root_dma);
proc_register(&proc_root, &proc_root_ioports);
proc_register(&proc_root, &proc_root_cmdline);
……
}
__initfunc是在inlucde/linux/init.h中定义的一个宏,表示此函数仅在系统初始化时使用,使用完毕即释放申请的内存资源。
PROC_REGISTER()函数在fs/proc/root.c中定义,程序如下:
int proc_register(struct proc_dir_entry * dir, struct proc_dir_entry * dp)
{
int i;
if (dp->;low_ino == 0) {
i = make_inode_number();
if (i < 0)
return -EAGAIN;
dp->;low_ino = i;
}
/*如果没有low_ino值,则产生一个新的赋给它*/
dp->;next = dir->;subdir;
dp->;parent = dir;
dir->;subdir = dp;
/*赋给兄弟、父母及子女的指针*/
if (S_ISDIR(dp->;mode)) {
if (dp->;ops == NULL)
dp->;ops = &proc_dir_inode_operations;
dir->;nlink++;
} else if (S_ISLNK(dp->;mode)) {
if (dp->;ops == NULL)
dp->;ops = &proc_link_inode_operations;
} else {
if (dp->;ops == NULL)
dp->;ops = &proc_file_inode_operations;
}
/*对于dp的不同属性调整操作函数指针*/
return 0;
}
初始化时首先要为每个文件或目录创建一个PROC_DIR_ENTRY的实例变量,内容包括注册的INODE号,名称,操作权限,连接数和INODE操作函数指针等,具体方法如下所示:
struct proc_dir_entry proc_root = {
PROC_ROOT_INO, 5, "/proc", S_IFDIR | S_IRUGO | S_IXUGO,
2, 0, 0, 0, &proc_root_inode_operations, NULL, NULL, NULL,
&proc_root, NULL };
struct proc_dir_entry proc_mca = {
PROC_MCA, 3, "mca", S_IFDIR | S_IRUGO | S_IXUGO,
2, 0, 0, 0, &proc_dir_inode_operations, NULL, NULL, NULL,
&proc_root, NULL };
static struct proc_dir_entry proc_root_loadavg = {
PROC_LOADAVG, 7, "loadavg", S_IFREG | S_IRUGO, 1, 0, 0,
0, &proc_array_inode_operations };
static struct proc_dir_entry proc_root_uptime = {
PROC_UPTIME, 6, "uptime", S_IFREG | S_IRUGO, 1, 0, 0,
0, &proc_array_inode_operations };
static struct proc_dir_entry proc_root_meminfo = {
PROC_MEMINFO, 7, "meminfo", S_IFREG | S_IRUGO, 1, 0, 0,
0, &proc_array_inode_operations };
static struct proc_dir_entry proc_root_kmsg = {
PROC_KMSG, 4, "kmsg", S_IFREG | S_IRUSR, 1, 0, 0,
0, &proc_kmsg_inode_operations };
......
PROC_REGISTER()函数首先检查这个新的PROC_DIR_ENTRY是否已有自己的PROC INODE号,如前所述,PROC文件系统是个“伪”文件系统,它并不存在于任何实际的物理设备上,所以它的文件INODE号与普通文件的INODE号的涵义是不同的,它不需要标识此文件的物理存在位置,只要能在本文件系统中唯一地标识这个文件即可。对于ROOT部分的文件在include/proc_fs.h中用一个枚举变量enum root_directory_inos来对其赋值,其中第一个文件即/PROC目录用PROC_ROOT_INO=1来定义。如果此文件在变量初始化时未赋值,LINUX用一个proc_alloc_map来表示INODE的使用情况,proc_alloc_map是一块有4096个bits的内存空间,每一位表示一个INODE的使用情况,如已经被使用则置位,放弃时恢复。分配时找其中第一位未使用的bit,在其位数上加上4096就是这个新登记文件的PROC INODE号。在此之后,PROC_REGISTER()调整父目录与子目录/文件之间的指针关系,及子目录/文件的INODE操作函数指针,最终结果是生成一棵以PROC_DIR_ENTRY为节点,PROC_ROOT为根的目录树,表明其相互之间的关系,在以后对文件或目录做标准操作(即用INODE,FILE,DENTRY等结构)的时候就可以从中获得需要的信息和函数指针。
在解释过PROC_REGISTER()函数后顺便再说明一下它的反向操作函数PROC_UNREGISTER(),即注销一个PROC_DIR_ENTRY项,其定义如下:
int proc_unregister(struct proc_dir_entry * dir, int ino)
{
struct proc_dir_entry **p = &dir->;subdir, *dp;
/*从dir的subdir指针开始进行搜索*/
while ((dp = *p) != NULL) { /*在dp指针尚不为空时*/
if (dp->;low_ino == ino) { /*如果low_ino==ino,说明仅对root部分*/
*p = dp->;next;
dp->;next = NULL;
/*兄弟指针置空*/
if (S_ISDIR(dp->;mode))
dir->;nlink--;
/*如果dp是目录,其父目录的连接数要减去1*/
if (ino >;= PROC_DYNAMIC_FIRST &&
ino < PROC_DYNAMIC_FIRST+PROC_NDYNAMIC)
clear_bit(ino-PROC_DYNAMIC_FIRST,
(void *) proc_alloc_map);
/*如果是在程序中生成的low_ino,则要清除对应的proc_allc_map中的*/
/*位标志*/
proc_kill_inodes(ino);
return 0;
}
p = &dp->;next; /*p指向dp的兄弟指针,继续搜索*/
}
return -EINVAL;
}
在PROC_UNREGISTER()中调用了另一个函数PROC_KILL_INODE(),该函数的功能是把一个已经被注销的PROC文件系统的INODE消灭掉,定义如下:
static void proc_kill_inodes(int ino)
{
struct file *filp;
/* inuse_filps is protected by the single kernel lock */
for (filp = inuse_filps; filp; filp = filp->;f_next) {
/*在正在使用中的文件链表中进行搜索*/
struct dentry * dentry;
struct inode * inode;
dentry = filp->;f_dentry;
if (!dentry)
continue;
if (dentry->;d_op != &proc_dentry_operations)
continue;
/*如果该项不属于PROC文件系统,则继续*/
inode = dentry->;d_inode;
if (!inode)
continue;
if (inode->;i_ino != ino)
continue;
filp->;f_op = NULL;
/*把该文件的操作函数指针置空,以后就无法使用了*/
}
}
3.PROC_BASE_INIT()
PROC_BASE_INIT()函数在PROC_ROOT_INIT()中调用,BASE部分的初始化与ROOT部分基本相同,首先为每个目录和文件初始化一个PROC_DIR_ENTRY结构,然后调用PROC_REGISTER()函数进行注册,并生成BASE部分的关系树,程序如下:
struct proc_dir_entry proc_pid = {
PROC_PID_INO, 5, "<pid>;", S_IFDIR | S_IRUGO | S_IXUGO, 2, 0, 0,
0, &proc_base_inode_operations, NULL, proc_pid_fill_inode,
NULL, &proc_root, NULL
};
static struct proc_dir_entry proc_pid_status = {
PROC_PID_STATUS, 6, "status", S_IFREG | S_IRUGO, 1, 0, 0,
0, &proc_array_inode_operations, NULL, proc_pid_fill_inode,
};
static struct proc_dir_entry proc_pid_mem = {
PROC_PID_MEM, 3, "mem", S_IFREG | S_IRUSR | S_IWUSR, 1, 0, 0,
0, &proc_mem_inode_operations, NULL, proc_pid_fill_inode,
};
static struct proc_dir_entry proc_pid_cwd = {
PROC_PID_CWD, 3, "cwd", S_IFLNK | S_IRWXU, 1, 0, 0,
0, &proc_link_inode_operations, NULL, proc_pid_fill_inode,
};
static struct proc_dir_entry proc_pid_root = {
PROC_PID_ROOT, 4, "root", S_IFLNK | S_IRWXU, 1, 0, 0,
0, &proc_link_inode_operations, NULL, proc_pid_fill_inode,
};
static struct proc_dir_entry proc_pid_exe = {
PROC_PID_EXE, 3, "exe", S_IFLNK | S_IRWXU, 1, 0, 0,
0, &proc_link_inode_operations, NULL, proc_pid_fill_inode,
};
static struct proc_dir_entry proc_pid_fd = {
PROC_PID_FD, 2, "fd", S_IFDIR | S_IRUSR | S_IXUSR, 2, 0, 0,
0, &proc_fd_inode_operations, NULL, proc_pid_fill_inode,
};
static struct proc_dir_entry proc_pid_environ = {
PROC_PID_ENVIRON, 7, "environ", S_IFREG | S_IRUSR, 1, 0, 0,
0, &proc_array_inode_operations, NULL, proc_pid_fill_inode,
};
static struct proc_dir_entry proc_pid_cmdline = {
PROC_PID_CMDLINE, 7, "cmdline", S_IFREG | S_IRUGO, 1, 0, 0,
0, &proc_array_inode_operations, NULL, proc_pid_fill_inode,
};
static struct proc_dir_entry proc_pid_stat = {
PROC_PID_STAT, 4, "stat", S_IFREG | S_IRUGO, 1, 0, 0,
0, &proc_array_inode_operations, NULL, proc_pid_fill_inode,
};
static struct proc_dir_entry proc_pid_statm = {
PROC_PID_STATM, 5, "statm", S_IFREG | S_IRUGO, 1, 0, 0,
0, &proc_array_inode_operations, NULL, proc_pid_fill_inode,
};
static struct proc_dir_entry proc_pid_maps = {
PROC_PID_MAPS, 4, "maps", S_IFIFO | S_IRUGO, 1, 0, 0,
0, &proc_arraylong_inode_operations, NULL, proc_pid_fill_inode, };
__initfunc(void proc_base_init(void))
{
#if CONFIG_AP1000
proc_register(&proc_pid, &proc_pid_ringbuf);
#endif
proc_register(&proc_pid, &proc_pid_status);
proc_register(&proc_pid, &proc_pid_mem);
proc_register(&proc_pid, &proc_pid_cwd);
proc_register(&proc_pid, &proc_pid_root);
proc_register(&proc_pid, &proc_pid_exe);
proc_register(&proc_pid, &proc_pid_fd);
proc_register(&proc_pid, &proc_pid_environ);
proc_register(&proc_pid, &proc_pid_cmdline);
proc_regis
ter(&proc_pid, &proc_pid_stat);
proc_register(&proc_pid, &proc_pid_statm);
proc_register(&proc_pid, &proc_pid_maps);
#ifdef __SMP__
proc_register(&proc_pid, &proc_pid_cpu);
#endif
};
这部分与ROOT部分的不同之处一是BASE部分的文件/目录的PROC INODE 号是用另一个枚举变量enum pid_directory_inos来赋值,其第一项PROC_PID_INO=2,即<pid>;目录。由于ROOT部分的每个文件/目录在/PROC下只有唯一的一个实例,而BASE部分对每个进程均有相同的一份拷贝,所以它的实际INODE号必须对不同的进程予以区分,在LINUX中,这种区分是通过把进程PID做为INODE的高16位,PROC_DIR_ENTRY中的LOW_INO做为INODE的低16位来实现的,这样可以保证每个文件INODE号在文件系统中的唯一性。另一个不同之处是BASE部分的文件在注册其PROC_DIR_ENTRY的时候都增加了FILL_INODE函数指针,指向PROC_PID_FILL_INODE函数,该函数的主要功能是把实际INODE号右移16位,获得对应此目录或文件的进程PID号,再把此进程的用户标识和组标识赋回给INODE结构。还有一点区别就是PROC_BASE_INIT()初始化的这棵PROC_DIR_ENTRY树是相对独立的(如图2),它以<pid>;目录,即PROC_PID项为根,并注册了该目录下各个文件及目录之间的相互关系,但它并不把PROC_PID挂到PROC_ROOT下面去,因为这是要在对PROC_ROOT做READDIR时动态加载的。
4.INODE OPERATIONS
有一点需要强调的是PROC文件系统在初始化时只是为每个目录和文件登记了一个PROC_DIR_ENTRY项,并为这些项生成了关系树,但对于VFS的通用文件操作做使用的数据结构,如INODE,FILE,DENTRY等此时都是不存在的,这些数据结构要等到在对PROC文件系统的文件做OPEN和READ等操作时才会根据PROC_DIR_ENTRY里定义的INODE OPERATION及其中的FILE OPERATIONS函数指针调用对应函数产生,这是PROC文件系统与其他基于设备的文件系统的一大区别之处。所以,在PROC_DIR_ENTRY中定义的INODE OPERATIONS决定了该文件的操作方式以及获取对应系统信息的渠道。举例来说,PROC_ROOT指向的PROC_ROOT_INODE_OPERATIONS中允许的INODE OPERATIONS函数是PROC_ROOT_LOOKUP,FILE OPERATIONS函数是PROC_ROOT_READDIR ;PROC_PID指向的PROC_BASE_INODE_OPERATIONS中允许的INODE OPERATIONS函数是PROC_LOOKUP,FILE OPERATIONS函数是PROC_ READDIR ;PROC_MEM_INFO指向的PROC_ARRAY_INODE_OPERATIONS中允许的INODE OPERATIONS函数均为空,FILE OPERATIONS函数是ARRAY_READ。下面我们来分析一下几个LOOKUP和READDIR函数,ARRAY_READ的功能主要是面向底层如何获取系统信息反馈上来,详见黄军同学的报告,这里就不做详述了。
PROC_LOOKUP、PROC_READDIR与PROC_ROOT_LOOKUP、PROC_ROOT_READDIR的功能基本上是相同的,只不过加上了ROOT后就加上了对<ID>;目录的处理功能。
程序如下所示:
1) proc_lookup()
int proc_lookup(struct inode * dir, struct dentry *dentry)
{
struct inode *inode;
struct proc_dir_entry * de;
int error;
error = -ENOTDIR;
if (!dir || !S_ISDIR(dir->;i_mode))
goto out;
/*如果dir空或dir不是目录,则退出*/
error = -ENOENT;
inode = NULL;
de = (struct proc_dir_entry *) dir->;u.generic_ip;
/*根据dir生成一个proc_dir_entry的指针*/
if (de) {
for (de = de->;subdir; de ; de = de->;next) {
/*在de的子目录和文件中搜索*/
if (!de || !de->;low_ino)
continue;
if (de->;namelen != dentry->;d_name.len)
continue;
if (!memcmp(dentry->;d_name.name, de->;name, de->;namelen)) {
/*如果dentry和由dir指向的proc_dir_entry名字相同*/
int ino = de->;low_ino | (dir->;i_ino & ~(0xffff));
error = -EINVAL;
inode = proc_get_inode(dir->;i_sb, ino, de);
/*申请一个inode节点,对应的proc_dir_entry节点为de,节点号*/
/*为ino。同时把de的数据填入inode */
break;
}
}
}
if (inode) {
dentry->;d_op = &proc_dentry_operations;
d_add(dentry, inode);
/*把dentry放到dentry_hash_table表中然后把inode的I_dentry和dentry的*/
/*d_alias相连*/
error = 0;
}
out:
return error;
}
2) proc_root_lookup()
static int proc_root_lookup(struct inode * dir, struct dentry * dentry)
{
unsigned int pid, c;
struct task_struct *p;
const char *name;
struct inode *inode;
int len;
if (dir->;i_ino == PROC_ROOT_INO) { /* check for safety... */
dir->;i_nlink = proc_root.nlink;
read_lock(&tasklist_lock); /*加读进程列表的锁*/
for_each_task(p) {
if (p->;pid)
dir->;i_nlink++; /*对于每个进程都要把proc_root的link数加1*/
}
read_unlock(&tasklist_lock); /*解读进程列表的锁*/
}
if (!proc_lookup(dir, dentry)) /*如果调用proc_lookup成功则返回*/
return 0;
/*如果调用proc_lookup不成功,说明要找的是pid部分的*/
pid = 0;
name = dentry->;d_name.name;
len = dentry->;d_name.len;
while (len-- >; 0) {
c = *name - '0';
name++;
if (c >; 9) {
pid = 0;
break;
}
pid *= 10;
pid += c;
/*把目录名的字符串转换成整数型的进程号*/
if (pid & 0xffff0000) {
pid = 0;
break;
}
}
read_lock(&tasklist_lock);
p = find_task_by_pid(pid);
read_unlock(&tasklist_lock);
inode = NULL;
if (pid && p) {
unsigned long ino = (pid << 16) + PROC_PID_INO;
inode = proc_get_inode(dir->;i_sb, ino, &proc_pid);
if (!inode)
return -EINVAL;
inode->;i_flags|=S_IMMUTABLE;
}
dentry->;d_op = &proc_dentry_operations;
d_add(dentry, inode);
return 0;
}
3) proc_readdir()
int proc_readdir(struct file * filp, void * dirent, filldir_t filldir)
{
struct proc_dir_entry * de;
unsigned int ino;
int i;
struct inode *inode = filp->;f_dentry->;d_inode;
if (!inode || !S_ISDIR(inode->;i_mode))
return -ENOTDIR;
ino = inode->;i_ino;
de = (struct proc_dir_entry *) inode->;u.generic_ip;
if (!de)
return -EINVAL;
i = filp->;f_pos;
switch (i) {
case 0:
if (filldir(dirent, ".", 1, i, ino) < 0)
return 0;
i++;
filp->;f_pos++;
/* fall through */
case 1:
if (filldir(dirent, "..", 2, i, de->;parent->;low_ino) < 0)
return 0;
i++;
filp->;f_pos++;
/* fall through */
default:
ino &= ~0xffff;
de = de->;subdir;
i -= 2;
for (; {
if (!de)
return 1;
if (!i)
break;
de = de->;next;
i--;
}
do {
if (filldir(dirent, de->;name, de->;namelen, filp->;f_pos, ino | de->;low_ino) < 0)
return 0;
filp->;f_pos++;
de = de->;next;
} while (de);
}
return 1;
}
4) get_pid_list()
static int get_pid_list(int index, unsigned int *pids)
{
struct task_struct *p;
int nr_pids = 0;
index -= FIRST_PROCESS_ENTRY;
read_lock(&tasklist_lock);
for_each_task(p) {
int pid = p->;pid;
if (!pid)
continue;
if (--index >;= 0)
continue;
pids[nr_pids] = pid;
nr_pids++;
if (nr_pids >;= PROC_MAXPIDS)
break;
}
read_unlock(&tasklist_lock);
return nr_pids;
}
5) proc_root_readdir()
static int proc_root_readdir(struct file * filp, void * dirent, filldir_t filldir)
{
unsigned int pid_array[PROC_MAXPIDS];
char buf[PROC_NUMBUF];
unsigned int nr = filp->;f_pos;
unsigned int nr_pids, i;
if (nr < FIRST_PROCESS_ENTRY) {
int error = proc_readdir(filp, dirent, filldir);
if (error <= 0)
return error;
filp->;f_pos = nr = FIRST_PROCESS_ENTRY;
}
nr_pids = get_pid_list(nr, pid_array);
for (i = 0; i < nr_pids; i++) {
int pid = pid_array;
ino_t ino = (pid << 16) + PROC_PID_INO;
unsigned long j = PROC_NUMBUF;
do {
j--;
buf[j] = '0' + (pid % 10);
pid /= 10;
} while (pid);
if (filldir(dirent, buf+j, PROC_NUMBUF-j, filp->;f_pos, ino) < 0)
break;
filp->;f_pos++;
}
return 0;
}
在这里通过inode_operation和file_operation注册的这些函数是在对/PROC下的文件及目录进行open及read操作时才被调用,生成对应的inode,file,dentry等数据结构并取得对应的系统信息反馈
PROC文件系统的挂载过程
FILESYSTEM_SETUP()主要就是调用了一系列的INIT_*_FS()函数(*代表各种不同的文件系统),对PROC文件系统就是INIT_PROC_FS(),在此函数中实例化了一个FILE_SYSTEM_TYPE类型的结构变量PROC_FS_TYPE,其中包括此文件系统的名字和PROC_READ_SUPER函数指针,然后通过REGISTER_FILESYSTEM函数把它挂到FILE_SYSTEMS链表的末尾。
/* fs/filesystems.c */
void __init filesystem_setup(void)
{
#ifdef CONFIG_EXT2_FS
init_ext2_fs(); /* 初始化ext2文件系统 */
#endif
......
#ifdef CONFIG_PROC_FS
init_proc_fs(); /* 初始化proc文件系统 */
#endif
......
} ;
/* fs/proc/procfs_syms.c */
static struct file_system_type proc_fs_type = {
"proc",
0 /* FS_NO_DCACHE doesn't work correctly */,
proc_read_super, /*针对本文件系统读取super_block的函数指针*/
NULL } ;
int init_proc_fs(void)
{
return register_filesystem(&proc_fs_type) == 0;
}
/* fs/super.c */
int register_filesystem(struct file_system_type * fs)
{
struct file_system_type ** tmp;
if (!fs)
return -EINVAL;
if (fs->;next)
return -EBUSY;
tmp = &file_systems;
while (*tmp) {
if (strcmp((*tmp)->;name, fs->;name) == 0)
return -EBUSY;
tmp = &(*tmp)->;next;
} /*遍历file_systems链表*/
*tmp = fs; /*把fs挂到file_systems链表末尾*/
return 0;
}
在系统启动时如果需要把PROC文件系统挂载上来则要根据PROC_FS_TYPE中注册的PROC_READ_SUPER()函数把SUPER BLOCK读入内存,并加入SUPER_BLOCKS链表。对于一般基于硬设备上的文件系统,READ_SUPER函数的操作就是在内存中创建一个新的空SUPER BLOCK,然后从设备上把SUPER BLOCK读入。但对于PROC文件系统,其READ_SUPER操作的内容有所不同,因为PROC文件系统是不存在于任何设备上的,所以PROC_READ_SUPER函数就是在产生一个新的空SUPER_BLOCK之后在内存中锁住该块后直接对其进行修改了。
/* fs/proc/inode.c */
struct super_block *proc_read_super(struct super_block *s,void *data, int silent)
{
struct inode * root_inode;
lock_super(s); /*锁住该super_block*/
s->;s_blocksize = 1024;
s->;s_blocksize_bits = 10;
s->;s_magic = PROC_SUPER_MAGIC;
s->;s_op = &proc_sops;
root_inode = proc_get_inode(s, PROC_ROOT_INO, &proc_root);
if (!root_inode)
goto out_no_root;
s->;s_root = d_alloc_root(root_inode, NULL);
if (!s->;s_root)
goto out_no_root;
parse_options(data, &root_inode->;i_uid, &root_inode->;i_gid);
unlock_super(s);
return s;
out_no_root:
printk("proc_read_super: get root inode failed\n";
iput(root_inode);
s->;s_dev = 0;
unlock_super(s);
return NULL;
}
处理由 ifconfig 送来的命令,在驱动程序中我们通常只处理 media type的改变。这个函数会根据 ifconfig 送来的值改变 MII 控制器的 media tyep ,你可以使用
# ifconfig eth0 media 10basT
将目前的输出入界面强迫改到 10basT 。对于某些自动媒体检测做的有问题的switch 而言这可能是必要的设定,但一般而言默认的 auto 是最好的设定。硬件会自动决定要使用那一个界面,使用者完全不必担心,当实体层的设定改变 ( 例如将网络线插到不同的地方 ) ,硬件会自动侦测并改变设定。
void set_rx_mode(struct net_device *net_dev);
改变目前封包过滤器的模式。当你使用
# ifconfig eth0 promisc
# ifconfig eth0 multicast
等命令时会被呼叫。一般而言,驱动程序的默认值是只接受目的位址和网络卡的 MAC address 相同的封包。你可以透过 ifconfig 命令控制驱动程序接受其它种类的封包。结语好了 ! 我己经解析完整个网络卡的驱动程序了。当你了解这个驱动程序后,再去了解其它的驱动程序变成一件很简单的事情。大部份网络驱动程序的架构其实都很类似。事实上, Linux 早期的网络卡驱动程序几乎是由同一个人完成的。而后来的驱动程序也几乎
都以这些驱动程序为蓝本,所以看起来都很类似。你要不要也试著再去读另一个网络驱动程序的源代码呢 ? 也许 你会开始抱怨怎么写驱动程序这么神秘的东西怎么变得如此简单了 !
多馀的一节
这一节多馀的,你不想看就算了 为了证明网络驱动程序之间有多类似我再简略的trace Intel eepro100 的驱程程序给大家看。不罗唆,马上开始。
初始化
static struct pci_device_id eepro100_pci_tbl[] __devinitdata = {
{ PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_82557,
PCI_ANY_ID, PCI_ANY_ID, },
{ PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_82559ER,
PCI_ANY_ID, PCI_ANY_ID, },
{ PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_ID1029,
PCI_ANY_ID, PCI_ANY_ID, },
{ PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_ID1030,
PCI_ANY_ID, PCI_ANY_ID, },
{ PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_82820FW_4,
PCI_ANY_ID, PCI_ANY_ID, },
{ 0,}
};
MODULE_DEVICE_TABLE(pci, eepro100_pci_tbl);
tatic struct pci_driver eepro100_driver = {
name: "eepro100",
id_table: eepro100_pci_tbl,
probe: eepro100_init_one,
remove: eepro100_remove_one,
#ifdef CONFIG_EEPRO100_PM
suspend: eepro100_suspend,
resume: eepro100_resume,
#endif
};
return pci_module_init(&eepro100_driver);
嗯 ! 一切都不出意类之外,是吧 !
初始化装置
eepro100_init_one()
这个看起来比 SIS900 的复杂多了。不过几个关鉴的函数还是一样,只是它的程序码看起比较乱。 BSD 的人喜欢说 Linux 的程序码太乱 ! 嗯,好像不承认不行 不过我说它乱的很可爱,行了吧 !
传送封包
speedo_start_xmit(struct sk_buff *skb, struct net_device *dev)
这个函数相似到我不必做任何讲解,也不必有任何文件你就可以知道它在做些什么事了 ! 程序码几乎到了一行对一行的程度 ( 夸张了一点 ! 不过很接近事实。我信相 SIS900 的 driver 是很整个程序 copy 过去再修改的 )
中断处理
void speedo_interrupt(int irq, void *dev_instance, struct pt_regs *regs);
这个函数,我再喜欢 Linux 也不得不抱怨一下了。 Donald Becker 先生,能麻烦程序写的好看一点好吗 ?
基本上,它把 sis900_rx 的内容直接放在中断处理函数之中。不过我想分开还是会清楚一些。
speedo_tx_buffer_gc 基本上就是 sis900_finish_xmit 。下面的程序是不是很眼熟呢 ?
dirty_tx = sp->;dirty_tx;
while ((int)(sp->;cur_tx - dirty_tx) >; 0) {
int entry = dirty_tx % TX_RING_SIZE;
int status = le32_to_cpu(sp->;tx_ring[entry].status);
}
连变数名字都很像呢 !
不过 eepro100 的驱动程序没有实作 set_config 的界面,所以你不能用ifconfig 来改变 media type 。不过 eepro100 提供了由模块命令列选项改变的功 能,当然它是不及 set_config 来的方便就是了。
还要再来一个吗 ? 你自己去做吧 !
[目录]
--------------------------------------------------------------------------------
ISA总线DMA的实现
Linux对ISA总线DMA的实现
(By 詹荣开,NUDT dep3)
Copyright ? 2002 by 詹荣开
E-mail:zhanrk@sohu.com
Linux-2.4.0 Version 1.0.0,2002-10-16
关键词:Linux、I/O、ISA总线、设备驱动程序
申明:这份文档是按照自由软件开放源代码的精神发布的,任何人可以免费获得、使用和重新发布,但是你没有限制别人重新发布你发布内容的权利。发布本文的目的是希望它能对读者有用,但没有任何担保,甚至没有适合特定目的的隐含的担保。更详细的情况请参阅GNU通用公共许可证(GPL),以及GNU自由文档协议(GFDL)。
你应该已经和文档一起收到一份GNU通用公共许可证(GPL)的副本。如果还没有,写信给:The Free Software Foundation, Inc., 675 Mass Ave, Cambridge,MA02139, USA
欢迎各位指出文档中的错误与疑问。
----------------------------------------------------
DMA是一种无需CPU的参与就可以让外设与系统RAM之间进行双向(to device 或 from device)数据传输的硬件机制。使用DMA可以使系统CPU从实际的I/O数据传输过程中摆脱出来,从而大大提高系统的吞吐率(throughput)。
由于DMA是一种硬件机制,因此它通常与硬件体系结构是相关的,尤其是依赖于外设的总线技术。比如:ISA卡的DMA机制就与PCI卡的DMA机制有区别。本站主要讨论ISA总线的DMA技术。
1.DMA概述
DMA是外设与主存之间的一种数据传输机制。一般来说,外设与主存之间存在两种数据传输方法:(1)Pragrammed I/O(PIO)方法,也即由CPU通过内存读写指令或I/O指令来持续地读写外设的内存单元(8位、16位或32位),直到整个数据传输过程完成。(2)DMA,即由DMA控制器(DMA Controller,简称DMAC)来完成整个数据传输过程。在此期间,CPU可以并发地执行其他任务,当DMA结束后,DMAC通过中断通知CPU数据传输已经结束,然后由CPU执行相应的ISR进行后处理。
DMA技术产生时正是ISA总线在PC中流行的时侯。因此,ISA卡的DMA数据传输是通过ISA总线控制芯片组中的两个级联8237 DMAC来实现的。这种DMA机制也称为“标准DMA”(standard DMA)。标准DMA有时也称为“第三方DMA”(third-party DMA),这是因为:系统DMAC完成实际的传输过程,所以它相对于传输过程的“前两方”(传输的发送者和接收者)来说是“第三方”。
标准DMA技术主要有两个缺点:(1)8237 DMAC的数据传输速度太慢,不能与更高速的总线(如PCI)配合使用。(2)两个8237 DMAC一起只提供了8个DMA通道,这也成为了限制系统I/O吞吐率提升的瓶颈。
鉴于上述两个原因,PCI总线体系结构设计一种成为“第一方DMA”(first-party DMA)的DMA机制,也称为“Bus Mastering”(总线主控)。在这种情况下,进行传输的PCI卡必须取得系统总线的主控权后才能进行数据传输。实际的传输也不借助慢速的ISA DMAC来进行,而是由内嵌在PCI卡中的DMA电路(比传统的ISA DMAC要快)来完成。Bus Mastering方式的DMA可以让PCI外设得到它们想要的传输带宽,因此它比标准DMA功能满足现代高性能外设的要求。
随着计算机外设技术的不断发展,现代能提供更快传输速率的Ultra DMA(UDMA)也已经被广泛使用了。本为随后的篇幅只讨论ISA总线的标准DMA技术在Linux中的实现。记住:ISA卡几乎不使用Bus Mastering模式的DMA;而PCI卡只使用Bus Mastering模式的DMA,它从不使用标准DMA。
2.Intel 8237 DMAC
最初的IBM PC/XT中只有一个8237 DMAC,它提供了4个8位的DMA通道(DMA channel 0-3)。从IBM AT开始,又增加了一个8237 DMAC(提供4个16位的DMA通道,DMA channel 4-7)。两个8237 DMAC一起为系统提供8个DMA通道。与中断控制器8259的级联方式相反,第一个DMAC被级联到第二个DMAC上,通道4被用于DMAC级联,因此它对外设来说是不可用的。第一个DMAC也称为“slave DAMC”,第二个DMAC也称为“Master DMAC”。
下面我们来详细叙述一下Intel 8237这个DMAC的结构。
每个8237 DMAC都提供4个DMA通道,每个DMA通道都有各自的寄存器,而8237本身也有一组控制寄存器,用以控制它所提供的所有DMA通道。
2.1 DMA通道的寄存器
8237 DMAC中的每个DMA通道都有5个寄存器,分别是:当前地址寄存器、当前计数寄存器、地址寄存器(也称为偏移寄存器)、计数寄存器和页寄存器。其中,前两个是8237的内部寄存器,对外部是不可见的。
(1)当前地址寄存器(Current Address Register):每个DMA通道都有一个16位的当前地址寄存器,表示一个DMA传输事务(Transfer Transaction)期间当前DMA传输操作的DMA物理内存地址。在每个DMA传输开始前,8237都会自动地用该通道的Address Register中的值来初始化这个寄存器;在传输事务期间的每次DMA传输操作之后该寄存器的值都会被自动地增加或减小。
(2)当前计数寄存器(Current Count Register):每个每个DMA通道都有一个16位的当前计数寄存器,表示当前DMA传输事务还剩下多少未传输的数据。在每个DMA传输事务开始之前,8237都会自动地用该通道的Count Register中的值来初始化这个寄存器。在传输事务期间的每次DMA传输操作之后该寄存器的值都会被自动地增加或减小(步长为1)。
(3)地址寄存器(Address Register)或偏移寄存器(Offset Register):每个DMA通道都有一个16位的地址寄存器,表示系统RAM中的DMA缓冲区的起始位置在页内的偏移。
(4)计数寄存器(Count Register):每个DMA通道都有一个16位的计数寄存器,表示DMA缓冲区的大小。
(5)页寄存器(Page Register):该寄存器定义了DMA缓冲区的起始位置所在物理页的基地址,即页号。页寄存器有点类似于PC中的段基址寄存器。
2.2 8237 DAMC的控制寄存器
(1)命令寄存器(Command Register)
这个8位的寄存器用来控制8237芯片的操作。其各位的定义如下图所示:
(2)模式寄存器(Mode Register)
用于控制各DMA通道的传输模式,如下所示:
(3)请求寄存器(Request Register)
用于向各DMA通道发出DMA请求。各位的定义如下:
(4)屏蔽寄存器(Mask Register)
用来屏蔽某个DMA通道。当一个DMA通道被屏蔽后,它就不能在服务于DMA请求,直到通道的屏蔽码被清除。各位的定义如下:
上述屏蔽寄存器也称为“单通道屏蔽寄存器”(Single Channel Mask Register),因为它一次只能屏蔽一个通道。此外含有一个屏蔽寄存器,可以实现一次屏蔽所有4个DMA通道,如下:
(5)状态寄存器(Status Register)
一个只读的8位寄存器,表示各DMA通道的当前状态。比如:DMA通道是否正服务于一个DMA请求,或者某个DMA通道上的DMA传输事务已经完成。各位的定义如下:
2.3 8237 DMAC的I/O端口地址
主、从8237 DMAC的各个寄存器都是编址在I/O端口空间的。而且其中有些I/O端口地址对于I/O读、写操作有不同的表示含义。如下表示所示:
Slave DMAC’s I/O port Master DMAC’sI/O port read write
0x000 0x0c0 Channel 0/4 的Address Register
0x001 0x0c1 Channel 0/4的Count Register
0x002 0x0c2 Channel 1/5 的Address Register
0x003 0x0c3 Channel 1/5的Count Register
0x004 0x0c4 Channel 2/6的Address Register
0x005 0x0c5 Channel 2/6的Count Register
0x006 0x0c6 Channel 3/7的Address Register
0x007 0x0c7 Channel 3/7的Count Register
0x008 0x0d0 Status Register Command Register
0x009 0x0d2 Request Register
0x00a 0x0d4 Single Channel Mask Register
0x00b 0x0d6 Mode Register
0x00c 0x0d8 Clear Flip-Flop Register
0x00d 0x0da Temporary Register Reset DMA controller
0x00e 0x0dc Reset all channel masks
0x00f 0x0de all-channels Mask Register
各DMA通道的Page Register在I/O端口空间中的地址如下:
DMA channel Page Register’sI/O port address
0 0x087
1 0x083
2 0x081
3 0x082
4 0x08f
5 0x08b
6 0x089
7 0x08a
注意两点:
1. 各DMA通道的Address Register是一个16位的寄存器,但其对应的I/O端口是8位宽,因此对这个寄存器的读写就需要两次连续的I/O端口读写操作,低8位首先被发送,然后紧接着发送高8位。
2. 各DMA通道的Count Register:这也是一个16位宽的寄存器(无论对于8位DMA还是16位DMA),但相对应的I/O端口也是8位宽,因此读写这个寄存器同样需要两次连续的I/O端口读写操作,而且同样是先发送低8位,再发送高8位。往这个寄存器中写入的值应该是实际要传输的数据长度减1后的值。在DMA传输事务期间,这个寄存器中的值在每次DMA传输操作后都会被减1,因此读取这个寄存器所得到的值将是当前DMA事务所剩余的未传输数据长度减1后的值。当DMA传输事务结束时,该寄存器中的值应该被置为0。
2.4 DMA通道的典型使用
在一个典型的PC机中,某些DMA通道通常被固定地用于一些PC机中的标准外设,如下所示:
Channel Size Usage
0 8-bit Memory Refresh
1 8-bit Free
2 8-bit Floppy Disk Controller
3 8-bit Free
4 16-bit Cascading
5 16-bit Free
6 16-bit Free
7 16-bit Free
2.5 启动一个DMA传输事务的步骤
要启动一个DMA传输事务必须对8237进行编程,其典型步骤如下:
1.通过CLI指令关闭中断。
2.Disable那个将被用于此次DMA传输事务的DMA通道。
3.向Flip-Flop寄存器中写入0值,以重置它。
4.设置Mode Register。
5.设置Page Register。
6.设置Address Register。
7.设置Count Register。
8.Enable那个将被用于此次DMA传输事务的DMA通道。
9.用STI指令开中断。
3 Linux对读写操作8237 DMAC的实现
由于DMAC的各寄存器是在I/O端口空间中编址的,因此读写8237 DMAC是平台相关的。对于x86平台来说,Linux在include/asm-i386/Dma.h头文件中实现了对两个8237 DMAC的读写操作。
3.1 端口地址和寄存器值的宏定义
Linux用宏MAX_DMA_CHANNELS来表示系统当前的DMA通道个数,如下:
#define MAX_DMA_CHANNELS 8
然后,用宏IO_DMA1_BASE和IO_DMA2_BASE来分别表示两个DMAC在I/O端口空间的端口基地址:
#define IO_DMA1_BASE 0x00
/* 8 bit slave DMA, channels 0..3 */
#define IO_DMA2_BASE 0xC0
/* 16 bit master DMA, ch 4(=slave input)..7 */
接下来,Linux定义了DMAC各控制寄存器的端口地址。其中,slave SMAC的各控制寄存器的端口地址定义如下:
#define DMA1_CMD_REG 0x08 /* command register (w) */
#define DMA1_STAT_REG 0x08 /* status register (r) */
#define DMA1_REQ_REG 0x09 /* request register (w) */
#define DMA1_MASK_REG 0x0A /* single-channel mask (w) */
#define DMA1_MODE_REG 0x0B /* mode register (w) */
#define DMA1_CLEAR_FF_REG 0x0C /* clear pointer flip-flop (w) */
#define DMA1_TEMP_REG 0x0D /* Temporary Register (r) */
#define DMA1_RESET_REG 0x0D /* Master Clear (w) */
#define DMA1_CLR_MASK_REG 0x0E /* Clear Mask */
#define DMA1_MASK_ALL_REG 0x0F /* all-channels mask (w) */
Master DMAC的各控制寄存器的端口地址定义如下:
#define DMA2_CMD_REG 0xD0 /* command register (w) */
#define DMA2_STAT_REG 0xD0 /* status register (r) */
#define DMA2_REQ_REG 0xD2 /* request register (w) */
#define DMA2_MASK_REG 0xD4 /* single-channel mask (w) */
#define DMA2_MODE_REG 0xD6 /* mode register (w) */
#define DMA2_CLEAR_FF_REG 0xD8 /* clear pointer flip-flop (w) */
#define DMA2_TEMP_REG 0xDA /* Temporary Register (r) */
#define DMA2_RESET_REG 0xDA /* Master Clear (w) */
#define DMA2_CLR_MASK_REG 0xDC /* Clear Mask */
#define DMA2_MASK_ALL_REG 0xDE /* all-channels mask (w) */
8个DMA通道的Address Register的端口地址定义如下:
#define DMA_ADDR_0 0x00 /* DMA address registers */
#define DMA_ADDR_1 0x02
#define DMA_ADDR_2 0x04
#define DMA_ADDR_3 0x06
#define DMA_ADDR_4 0xC0
#define DMA_ADDR_5 0xC4
#define DMA_ADDR_6 0xC8
#define DMA_ADDR_7 0xCC
8个DMA通道的Count Register的端口地址定义如下:
#define DMA_CNT_0 0x01 /* DMA count registers */
#define DMA_CNT_1 0x03
#define DMA_CNT_2 0x05
#define DMA_CNT_3 0x07
#define DMA_CNT_4 0xC2
#define DMA_CNT_5 0xC6
#define DMA_CNT_6 0xCA
#define DMA_CNT_7 0xCE
8个DMA通道的Page Register的端口地址定义如下:
#define DMA_PAGE_0 0x87 /* DMA page registers */
#define DMA_PAGE_1 0x83
#define DMA_PAGE_2 0x81
#define DMA_PAGE_3 0x82
#define DMA_PAGE_5 0x8B
#define DMA_PAGE_6 0x89
#define DMA_PAGE_7 0x8A
Mode Register的几个常用值的定义如下:
#define DMA_MODE_READ 0x44
/* I/O to memory, no autoinit, increment, single mode */
#define DMA_MODE_WRITE 0x48
/* memory to I/O, no autoinit, increment, single mode */
#define DMA_MODE_CASCADE 0xC0
/* pass thru DREQ->;HRQ, DACK<-HLDA only */
#define DMA_AUTOINIT 0x10
3.2 读写DMAC的高层接口函数
(1)使能/禁止一个特定的DMA通道
Single Channel Mask Register中的bit[2]为0表示使能一个DMA通道,为1表示禁止一个DMA通道;而该寄存器中的bit[1:0]则用于表示使能或禁止哪一个DMA通道。
函数enable_dma()实现使能某个特定的DMA通道,传输dmanr指定DMA通道号,其取值范围是0~DMA_MAX_CHANNELS-1。如下:
static __inline__ void enable_dma(unsigned int dmanr)
{
if (dmanr<=3)
dma_outb(dmanr, DMA1_MASK_REG);
else
dma_outb(dmanr & 3, DMA2_MASK_REG);
}
宏dma_outb和dma_inb实际上就是outb(或outb_p)和inb函数。注意,当dmanr取值大于3时,对应的是Master DMAC上的DMA通道0~3,因此在写DMA2_MASK_REG之前,要将dmanr与值3进行与操作,以得到它在master DMAC上的局部通道编号。
函数disable_dma()禁止一个特定的DMA通道,其源码如下:
static __inline__ void disable_dma(unsigned int dmanr)
{
if (dmanr<=3)
dma_outb(dmanr | 4, DMA1_MASK_REG);
else
dma_outb((dmanr & 3) | 4, DMA2_MASK_REG);
}
为禁止某个DMA通道,Single Channel Mask Register中的bit[2]应被置为1。
(2)清除Flip-Flop寄存器
函数Clear_dma_ff()实现对slave/Master DMAC的Flip-Flop寄存器进行清零操作。如下:
static __inline__ void clear_dma_ff(unsigned int dmanr)
{
if (dmanr<=3)
dma_outb(0, DMA1_CLEAR_FF_REG);
else
dma_outb(0, DMA2_CLEAR_FF_REG);
}
(3)设置某个特定DMA通道的工作模式
函数set_dma_mode()实现设置一个特定DMA通道的工作模式。如下:
static __inline__ void set_dma_mode(unsigned int dmanr, char mode)
{
if (dmanr<=3)
dma_outb(mode | dmanr, DMA1_MODE_REG);
else
dma_outb(mode | (dmanr&3), DMA2_MODE_REG);
}
DMAC 的Mode Register中的bit[1:0]指定对该DMAC上的哪一个DMA通道进行模式设置。
(4)为DMA通道设置DMA缓冲区的起始物理地址和大小
由于8237中的DMA通道是通过一个8位的Page Register和一个16位的Address Register来寻址位于系统RAM中的DMA缓冲区,因此8237 DMAC最大只能寻址系统RAM中物理地址在0x000000~0xffffff范围内的DMA缓冲区,也即只能寻址物理内存的低16MB(24位物理地址)。反过来讲,Slave/Master 8237 DMAC又是如何寻址低16MB中的物理内存单元的呢?
首先来看Slave 8237 DMAC(即第一个8237 DMAC)。由于Slave 8237 DMAC是一个8位的DMAC,因此DMA通道0~3在一次DMA传输操作(一个DMA传输事务又多次DMA传输操作组成)中只能传输8位数据,即一个字节。Slave 8237 DMAC将低16MB物理内存分成256个64K大小的页(Page),然后用Page Register来表示内存单元物理地址的高8位(bit[23:16]),也即页号;用Address Register来表示内存单元物理地址在一个Page(64KB大小)内的页内偏移量,也即24位物理地址中的低16位(bit[15:0])。由于这种寻址机制,因此DMA通道0~3的DMA缓冲区必须在一个Page之内,也即DMA缓冲区不能跨越64KB页边界。
再来看看Master 8237 DMAC(即第二个8237 DMAC)。这是一个16位宽的DMAC,因此DMA通道5~7在一次DMA传输操作时可以传输16位数据,也即一个字word。此时DMA通道的Count Register(16位宽)表示以字计的待传输数据块大小,因此数据块最大可达128KB(64K个字),也即系统RAM中的DMA缓冲区最大可达128KB。由于一次可传输一个字,因此Master 8237 DMAC所寻址的内存单元的物理地址肯定是偶数,也即物理地址的bit[0]肯定为0。此时物理内存的低16MB被化分成128个128KB大小的page,Page Register中的bit[7:1]用来表示页号,也即对应内存单元物理地址的bit[23:17],而Page Register的bit[0]总是被设置为0。Address Register用来表示内存单元在128KB大小的Page中的页内偏移,也即对应内存单元物理地址的bit[16:1](由于此时物理地址的bit[0]总是为0,因此不需要表示)。由于Master 8237 DMAC的这种寻址机制,因此DMA通道5~7的DMA缓冲区不能跨越128KB的页边界。
下面我们来看看Linux是如何实现为各DMA通道设置其Page寄存器的。NOTE!DMA通道5~7的Page Register中的bit[0]总是为0。如下所示:
static __inline__ void set_dma_page(unsigned int dmanr, char pagenr)
{
switch(dmanr) {
case 0:
dma_outb(pagenr, DMA_PAGE_0);
break;
case 1:
dma_outb(pagenr, DMA_PAGE_1);
break;
case 2:
dma_outb(pagenr, DMA_PAGE_2);
break;
case 3:
dma_outb(pagenr, DMA_PAGE_3);
break;
case 5:
dma_outb(pagenr & 0xfe, DMA_PAGE_5);
break;
case 6:
dma_outb(pagenr & 0xfe, DMA_PAGE_6);
break;
case 7:
dma_outb(pagenr & 0xfe, DMA_PAGE_7);
break;
}
}
在上述函数的基础上,函数set_dma_addr()用来为特定DMA通道设置DMA缓冲区的基地址,传输dmanr指定DMA通道号,传输a指定位于系统RAM中的DMA缓冲区起始位置的物理地址。如下:
/* Set transfer address & page bits for specific DMA channel.
* Assumes dma flipflop is clear.
*/
static __inline__ void set_dma_addr(unsigned int dmanr, unsigned int a)
{
set_dma_page(dmanr, a>;>;16);
if (dmanr <= 3) {
dma_outb( a & 0xff, ((dmanr&3)<<1) + IO_DMA1_BASE );
dma_outb( (a>;>; & 0xff, ((dmanr&3)<<1) + IO_DMA1_BASE );
} else {
dma_outb( (a>;>;1) & 0xff, ((dmanr&3)<<2) + IO_DMA2_BASE );
dma_outb( (a>;>;9) & 0xff, ((dmanr&3)<<2) + IO_DMA2_BASE );
}
}
函数set_dma_count()为特定DMA通道设置其Count Register的值。传输dmanr指定DMA通道,传输count指定待传输的数据块大小(以字节计),实际写到Count Register中的值应该是count-1。如下所示:
static __inline__ void set_dma_count(unsigned int dmanr, unsigned int count)
{
count--;
if (dmanr <= 3) {
dma_outb( count & 0xff, ((dmanr&3)<<1) + 1 + IO_DMA1_BASE );
dma_outb( (count>;>; & 0xff, ((dmanr&3)<<1) + 1 + IO_DMA1_BASE );
} else {
dma_outb( (count>;>;1) & 0xff, ((dmanr&3)<<2) + 2 + IO_DMA2_BASE );
dma_outb( (count>;>;9) & 0xff, ((dmanr&3)<<2) + 2 + IO_DMA2_BASE );
}
}
函数get_dma_residue()获取某个DMA通道上当前DMA传输事务的未传输剩余数据块的大小(以字节计)。DMA通道的Count Register的值在当前DMA传输事务进行期间会不断地自动将减小,直到当前DMA传输事务完成,Count Register的值减小为0。如下:
static __inline__ int get_dma_residue(unsigned int dmanr)
{
unsigned int io_port = (dmanr<=3)? ((dmanr&3)<<1) + 1 + IO_DMA1_BASE
: ((dmanr&3)<<2) + 2 + IO_DMA2_BASE;
/* using short to get 16-bit wrap around */
unsigned short count;
count = 1 + dma_inb(io_port);
count += dma_inb(io_port) << 8;
return (dmanr<=3)? count : (count<<1);
}
3.3 对DMAC的保护
DMAC是一种全局的共享资源,为了保证设备驱动程序对它的独占访问,Linux在kernel/dma.c文件中定义了自旋锁dma_spin_lock来保护它(实际上是保护DMAC的I/O端口资源)。任何想要访问DMAC的设备驱动程序都首先必须先持有自旋锁dma_spin_lock。如下:
static __inline__ unsigned long claim_dma_lock(void)
{
unsigned long flags;
spin_lock_irqsave(&dma_spin_lock, flags); /* 关中断,加锁*/
return flags;
}
static __inline__ void release_dma_lock(unsigned long flags)
{
spin_unlock_irqrestore(&dma_spin_lock, flags);/* 开中断,开锁*/
}
4 Linux对ISA DMA通道资源的管理
DMA通道是一种系统全局资源。任何ISA外设想要进行DMA传输,首先都必须取得某个DMA通道资源的使用权,并在传输结束后释放所使用DMA通道资源。从这个角度看,DMA通道资源是一种共享的独占型资源。
Linux在kernel/Dma.c文件中实现了对DMA通道资源的管理。
4.1 对DMA通道资源的描述
Linux在kernel/Dma.c文件中定义了数据结构dma_chan来描述DMA通道资源。该结构类型的定义如下:
struct dma_chan {
int lock;
const char *device_id;
};
其中,如果成员lock!=0则表示DMA通道正被某个设备所使用;否则该DMA通道就处于free状态。而成员device_id就指向使用该DMA通道的设备名字字符串。
基于上述结构类型dma_chan,Linux定义了全局数组dma_chan_busy[],以分别描述8个DMA通道资源各自的使用状态。如下:
static struct dma_chan dma_chan_busy[MAX_DMA_CHANNELS] = {
{ 0, 0 },
{ 0, 0 },
{ 0, 0 },
{ 0, 0 },
{ 1, "cascade" },
{ 0, 0 },
{ 0, 0 },
{ 0, 0 }
};
显然,在初始状态时除了DMA通道4外,其余DMA通道皆处于free状态。
4.2 DMA通道资源的申请
任何ISA卡在使用某个DMA通道进行DMA传输之前,其设备驱动程序都必须向内核提出DMA通道资源的申请。只有申请获得成功后才能使用相应的DMA通道。否则就会发生资源冲突。
函数request_dma()实现DMA通道资源的申请。其源码如下:
int request_dma(unsigned int dmanr, const char * device_id)
{
if (dmanr >;= MAX_DMA_CHANNELS)
return -EINVAL;
if (xchg(&dma_chan_busy[dmanr].lock, 1) != 0)
return -EBUSY;
dma_chan_busy[dmanr].device_id = device_id;
/* old flag was 0, now contains 1 to indicate busy */
return 0;
}
上述函数的核心实现就是用原子操作xchg()让成员变量dma_chan_busy[dmanr].lock和值1进行交换操作,xchg()将返回lock成员在交换操作之前的值。因此:如果xchg()返回非0值,这说明dmanr所指定的DMA通道已被其他设备所占用,所以request_dma()函数返回错误值-EBUSY表示指定DMA通道正忙;否则,如果xchg()返回0值,说明dmanr所指定的DMA通道正处于free状态,于是xchg()将其lock成员设置为1,取得资源的使用权。
4.3 释放DMA通道资源
DMA传输事务完成后,设备驱动程序一定要记得释放所占用的DMA通道资源。否则别的外设将一直无法使用该DMA通道。
函数free_dma()释放指定的DMA通道资源。如下:
void free_dma(unsigned int dmanr)
{
if (dmanr >;= MAX_DMA_CHANNELS) {
printk("Trying to free DMA%d
", dmanr);
return;
}
if (xchg(&dma_chan_busy[dmanr].lock, 0) == 0) {
printk("Trying to free free DMA%d
", dmanr);
return;
}
} /* free_dma */
显然,上述函数的核心实现就是用原子操作xchg()将lock成员清零。
4.4 对/proc/dma文件的实现
文件/proc/dma将列出当前8个DMA通道的使用状况。Linux在kernel/Dma.c文件中实现了函数个get_dma_list()函数来至此/proc/dma文件的实现。函数get_dma_list()的实现比较简单。主要就是遍历数组dma_chan_busy[],并将那些lock成员为非零值的数组元素输出到列表中即可。如下:
int get_dma_list(char *buf)
{
int i, len = 0;
for (i = 0 ; i < MAX_DMA_CHANNELS ; i++) {
if (dma_chan_busy.lock) {
len += sprintf(buf+len, "%2d: %s
",
i,
dma_chan_busy.device_id);
}
}
return len;
} /* get_dma_list */
5 使用DMA的ISA设备驱动程序
DMA虽然是一种硬件机制,但它离不开软件(尤其是设备驱动程序)的配合。任何使用DMA进行数据传输的ISA设备驱动程序都必须遵循一定的框架。
5.1 DMA通道资源的申请与释放
同I/O端口资源类似,设备驱动程序必须在一开始就调用request_dma()函数来向内核申请DMA通道资源的使用权。而且,最好在设备驱动程序的open()方法中完成这个操作,而不是在模块的初始化例程中调用这个函数。因为这在一定程度上可以让多个设备共享DMA通道资源(只要多个设备不同时使用一个DMA通道)。这种共享有点类似于进程对CPU的分时共享:-)
设备使用完DMA通道后,其驱动程序应该记得调用free_dma()函数来释放所占用的DMA通道资源。通常,最好再驱动程序的release()方法中调用该函数,而不是在模块的卸载例程中进行调用。
还需要注意的一个问题是:资源的申请顺序。为了避免死锁(deadlock),驱动程序一定要在申请了中断号资源后才申请DMA通道资源。释放时则要先释放DMA通道,然后再释放中断号资源。
使用DMA的ISA设备驱动程序的open()方法的如下:
int xxx_open(struct inode * inode, struct file * filp)
{
┆
if((err = request_irq(irq,xxx_ISR,SA_INTERRUPT,”YourDeviceName”,NULL))
return err;
if((err = request_dma(dmanr, “YourDeviceName”)){
free_irq(irq, NULL);
return err;
}
┆
return 0;
}
release()方法的范例代码如下:
void xxx_release(struct inode * inode, struct file * filp)
{
┆
free_dma(dmanr);
free_irq(irq,NULL);
┆
}
5.2 申请DMA缓冲区
由于8237 DMAC只能寻址系统RAM中低16MB物理内存,因此:ISA设备驱动程序在申请DMA缓冲区时,一定要以GFP_DMA标志来调用kmalloc()函数或get_free_pages()函数,以便在系统内存的DMA区中分配物理内存。
5.3 编程DMAC
设备驱动程序可以在他的read()方法、write()方法或ISR中对DMAC进行编程,以便准备启动一个DMA传输事务。一个DMA传输事务有两种典型的过程:(1)用户请求设备进行DMA传输;(2)硬件异步地将外部数据写道系统中。
用户通过I/O请求触发设备进行DMA传输的步骤如下:
1.用户进程通过系统调用read()/write()来调用设备驱动程序的read()方法或write()方法,然后由设备驱动程序read/write方法负责申请DMA缓冲区,对DMAC进行编程,以准备启动一个DMA传输事务,最后正确地设置设备(setup device),并将用户进程投入睡眠。
2.DMAC负责在DMA缓冲区和I/O外设之间进行数据传输,并在结束后触发一个中断。
3.设备的ISR检查DMA传输事务是否成功地结束,并将数据从DMA缓冲区中拷贝到驱动程序的其他内核缓冲区中(对于I/O device to memory的情况)。然后唤醒睡眠的用户进程。
硬件异步地将外部数据写到系统中的步骤如下:
1.外设触发一个中断通知系统有新数据到达。
2.ISR申请一个DMA缓冲区,并对DMAC进行编程,以准备启动一个DMA传输事务,最后正确地设置好外设。
3.硬件将外部数据写到DMA缓冲区中,DMA传输事务结束后,触发一个中断。
4. ISR检查DMA传输事务是否成功地结束,然后将DMA缓冲区中的数据拷贝驱动程序的其他内核缓冲区中,最后唤醒相关的等待进程。
网卡就是上述过程的一个典型例子。
为准备一个DMA传输事务而对DMAC进行编程的典型代码段如下:
unsigned long flags;
flags = claim_dma_lock();
disable_dma(dmanr);
clear_dma_ff(dmanr);
set_dma_mode(dmanr,mode);
set_dma_addr(dmanr, virt_to_bus(buf));
set_dma_count(dmanr, count);
enable_dma(dmanr);
release_dma_lock(flags);
检查一个DMA传输事务是否成功地结束的代码段如下:
int residue;
unsigned long flags = claim_dma_lock();
residue = get_dma_residue(dmanr);
release_dma_lock(flags);
ASSERT(residue == 0);
注:本节大部分内容来自于ldd2
图
linux网络部分重要结构体联系图(?)
用\续行, NR
请问ENTRY( ... ) 语法
在中断例程中如何与其他进程通讯??(用信号量还是全局变量)
内核模块编译需要优化
About task->;active_mm
驱动程序怎样锁定内存
linux 2.4.x initialization for IA-32 howto
Booting Linux for IA64
About task->;active_mm (bnn 续)
About Our China's CPU-Fang Zhou
我在你的帖子中注释了一下,不一定对.
RTL8139 驱动程序解析(转)
Sun YongYao--发送、接受、转发包处理结构框图修正
Linux内核分析文档计划
windowsXP和linux在系统结构上谁更好?
TCP协议的核心守护进程echod
请教一个中断方面的问题
再谈page cache和buffer cache
稀疏文件是怎么的概念呢
请教frees/wan的原理问题
块设备文件和普通文件有什么区别?普通文件不也是放在块设备上的吗?
关于I/O请求优化
请教一个MBR的问题
page cache与buffer cache的关系。
请问系统中有几个针对磁盘读写的request队列?
TCP传输控制块的初始状态
net_rx()网络数据流向
这样的内核程序能阻塞吗
Linux设备驱动程序的中断
页故障的处理过程
网卡的初始化过程
2.4内核网络部分的初始化
2.4网络代码阅读笔记(1)
2.4网络代码阅读笔记(2)
2.4网络代码阅读笔(3)
2.4网络代码阅读笔(4)
2.4网络代码阅读笔记(5)
RAM DISK 和 DiskOnChip 有什么关系?
文件映射区域的分配方式
未经accept的socket是如何处理数据的?
如何对拷不同长度的socket缓冲区?
Linux内核地址空间的布局
netfilter一问
请教GDT
我非得用SMP吗?
关于LDT的疑问
请教一个tcp连接性能的问题?
能解释一下A20地址线是怎么回事吗?为何要打开?
如何显示GDT,IDT,LDT表的内容
物理内存的分配结构
重入的疑惑
高端页面的映射
2.4.1内核中页结构PG_refrenced标志什么时候置位?
怎样在内核中写文件?
一些问题(网络方面)
网卡驱动程序(inw,insw提问)
向所有做内核分析的朋友提个问题
netfilter这里还是不明白
交换页分配方法
在网卡驱动程序中如何实现包转发?
请教一个内核寻址的问题
非分页、连续内存?
FreeBSD内核文件的结构
FreeBSD内核的编绎方法
virtual address to physical address - HOWTO?
关于printk的一个问题:
关于内存管理的一个问题
如何加一个文件系统到内核中
如何实现临界区
关于引导程序问题
不让root process使用1024以下的端口
get_current()的问题
hard_smp_processor_id()与smp_processor_id()有什么区别?
新兵笔记--ULK(C3) Exception Handing
TCP包处理的后备队列(backlog)和预备队列(prequeue)
解释一下__wait_queue_head结构?还有wake_up
TCP包的校验和
内核日志和内核控制台
minix文件系统的物理结构
关于buddy system的几个问题?
void page_cache_init(u_long mempages)
信号能否重入?
static __inline__和extern __inline__有什么区别?
void __remove_shared_vm_struct(vma)
spinlock
TCP PAWS处理
About Major Number
linux内核启动到底做了什么的资料
listen和accept过程的简要描述
真正的信号高手请进
有关页寿命的疑问
完了,没耐心看了,大侠帮忙,给我讲一下tcp_connect就好!
linux中如何设置stack的大小?
关于struct
GET_CURRENT还有疑问
谁有ARM远程调试的stub的源代码
[精华] 辞职申明 和 学习内核的一些感想
分析calibrate_delay过程
请教内存初始化的问题(reserve mem)
详细解释一下Thread(线程)???
linux 支持内核线程吗
asmlinkage
http://www.kernelnewbies.org/faq/index.php3#asmlinkage.xml
linuxdoc.org的对于Linux线程的一个比较完整的回答
行内汇编中,"o"(var)(不是零)"o"是什麽寄存器,谢谢!
如何在模块中增加系统调用
red black tree
iptables内核数据结构关系图
[serial]中断问题?
console注册与tty注册的区别
make_pages_present(),不知它的具体作用是什么
宏mb()是什么意思?
这是我搜集的linuxkernel兄的大作
linuxkernel兄大作之2
TCP层向外发包时为什么总是将skb进行克隆?
内核防火墙iptables的一条规则(图)
struct dentry中各项意义
关于read_super_block 的一点疑问!
端口读硬盘信息疑问
asmlinkage这个关键字是什么意思
fork()与vfork()的区别
pg0 的问题?
Linux的启动盘与DOS中的启动盘不一样
__attribute__是什么东西,怎么用呀
800 种文件格式
http://www.wotsit.org/default.asp
在linux下读取、修改(如果可能)主板bios的内容
制作基于软盘的Linux系统
调试信息过多造成dmesg无法完全显示怎么办?
Ctrl-D被按下后,产生什么信号?或者根本不产生信号?
关于中断响应
linux内核概念问题,欢迎讨论
介绍一下e820 map的内容
新兵笔记--ULK(C4) Initializing the idt
_set_gate分析
Linux A-Z书中实例的源程序
关于end_mem的一点疑惑
新兵笔记--ULK(C2) beta版
防火墙分析总结
网络包复制
head.S 中的问题
现转载对我帮助很大的文件,十分感谢那位作者!!!
关于MBR的一个问题
ELF的 e_ident[EI_NIDENT]是什么意思?
Makefile中Config.in是什麽文件
virtual address space and physical address
time quantum
Lisoleg的昨天、今天和明天
System.map中的几个标志T,t,B,b,..的问题
内存初始化mem_init中的两个问题
ldd2
Chapter 9: Interrupt Handling
http://www.xml.com/ldd/chapter/book/pdf/ch09.pdf
C & C
mmap
http://kernelnewbies.org/code/mmap/
内核代码可以读取文件或共享内存吗?
内核镜像是如果映射到3G去的?
进程的页目录表中的768项之后的项是不是都是有效的?
内核页目录
关于进程内存空间的映射问题*
Special expression from linux-kernel mailing list
discussion about MaxNumber Of Threads From KernelT
核心态对用户态的数据访问
内核是怎么访问PCI配置空间的?
这么做行不?(请大伙抽空看看)内核中共享内存的如何使用
which Compiler to Use for kernel compiling
GLIBC被多个进程共享疑问
fork()一个子进程时,IPC资源是否可以被子进程继承
malloc()在内核中对应的函数是什么
支持热插拔的驱动程序的框架疑问
回答“关于页面映射计数”
关于内核中加入自己得代码得可行性
用户空间和内核空间之间的数据传输
为什么需要copy_from_user?(转载)
内核启动过程问题(rush老哥看过来,我知道你看过启动代码)
软中断图解
早期的任务切换
谁能说一下iptables.c 和 netfilter.c 的关系?
最近看mm的人很多啊,和大家分享我给自己写的教程
一个疑问 --《Understanding Linux Kernel》
请教linux源代码中的数组问题
skb_reserve - adjust headroom
UNIX核心介绍
分配虚存空间有了mmap和munmap为什么还需要sys_brk?
Write Your Own Operating System Tutorial && FAQ !
some valuable HowtoS for KernelStudy
To get intelX86 docs
如何修改IP流程中的数据包???
请问怎样让我的一个可执行程序在开机过程中自动执行
关于ctags的问题?
关于copy sk_buff的问题
Kernel2.4.18的交换机制分析
系统初启时中断向量表放在哪里
swap in (2.4.0)
handle mm fault (2.4.0)
swap out for Kernel2.4.0
实时进程的时间片赋值问题?
关于驱动程序与实际设备的关联问题
本论坛关于时钟的一个悬而未决的问题
A20是什么东西?
bootsect.s和setup.s问题集锦
bios ox15中断ah=e801
http://www.ctyme.com/intr/int.htm
gas 文档
http://www.gnu.org/manual/gas-2.9.1/
用什么办法可以将系统的gdt给显示出来?
这么生动的赋值方法
在0x104000处的硬件参数表的详细说明
转贴一篇关于mm的文章。
这不是自己覆盖自己吗
do_irq()的问题
misc两个head.s, end, __PAGE_OFFSET
关于interrup和exception
一个setup.S与head.S连接的问题?
终于独立看懂了一个内嵌汇编,兼问个问题
draft BIOS use document for the kernel(里面有讲e820)
start_arch中copy e820_map的问题
2.2的一些task数据结构在2.4里是什么?
关于bottom half
mem_map array
__init 在那儿定义的
有个prototype用了pgprot来做变量。刚好pgprot_t的数据结构定义里又用了pgprot
fixed_addresses 的排列
how to smp
空闲页面free_area结构初试话一问?
setup_arch中的reserve resource!
slab里的free buffer
list_entry的疑惑
linux内核中的同步和互斥专题 (1)
linux内核中的同步和互斥专题(2)
setup_arch()中PFN_UP,PFN_DOWN,PFN_PHYS这三个函数是什么意思
cache_cache里的slab的大小是不是一个page
几个变量的意思(page,mm)
remap_page_range()的疑问!!??
start_kernel 里的 init_modules()问题
linux 内核原代码汇编中 .align 伪指令的意思是什么?
关于task_union
多个内核问题提问
请问关于页计数和页的使用情况的关系的一个问题?
CR3问题?
Linux字符设备基础(转贴)
又是start_kernel里的变量定义问题
ioremap是干什么用的?
Makefile :这行为什麽要有( )
关于flash的一些问题!
100个最佳Linux站点(推荐)
判断process是在user mode还是在kernel mode的标准是什么?
关于ext2的几个问题
谁知道unistd.h里的这段解释是什么意思
关于skbuff得资料
bdata.last_offset和last_pos在何处初始化?
内存管理问题?空闲页的bitmap是干什么的?
noncontiguous memory究竟指的是什么?
再聊聊free_area的bitmap
关于gcc内嵌汇编的一个问题
对bh_base[32]和bh_task_vec[32]的一些猜测
明白了free_area的bitmap了!
软中断softirq
路由问题
http://linux-mm.org/
super block是用来干什么的
研读Linux内核主要是为了做些什么?
Linux内核抢占补丁的基本原理
page描述符起名page,确实不好
请教一个术语--”堆“
#if 1肯定是满足条件,else后面的有什么用
内存管理中常常见到“round up”,它具体是什么意思呢?
我也来问几个关于gcc内嵌汇编的问题
"aka inode"? that is "as know as inode"
谁知道”16M以上的低位物理内存“到底有多大?占了多少百分比物理内存
如何做源程序的补丁?
local_table是什么?在哪里初始化的
汇编call命令调用C函数时,参数是如何通过stack传递的
一段boot的测试代码,有问题,来看看……
读do_basic_setup时碰到的问题(pci)
start_kernel里面的prof_shift干什么用的
HA开发,增强内核,学习好方式
关于spin_lock
vfs的索引节点号i_ino是怎么管理的
ip_route_input
想听听大家是怎么读核的
我的分析是应该首先发送第一片
谈一谈我的内核精简计划,以及目前的进展情况
IP分片与重组的分析与整理
“Practical Unix & Internet Security"。号称是”圣经”式著作
Linux源代码中的几个缩写
几个网络有关的变量
为什么不立刻用长跳转指令重载CS段寄存器呢
当物理空间的的线性地址>;1G的时候,虚拟空间如何解决
请教关于睡眠和唤醒进程的两个函数...
如何读取系统物理内存
freeswan是什么东西呢?
ip_rcv_finish函数一问
inflate.c文件的fill_inbuf()函数中insize的赋值的问题
裁减Linux内核的心得体会
linux下的一个文件的最大尺寸是多少?由什么决定?windows下呢?
请教一个Netfilter的应用问题
使用register_blkdev( ), 做完insmod后,为什么/proc/devices没有我的块设备
应用层->;pci->;disk,这种情况disk的驱动该如何写?
请教一个内存管理问题(挺典型的一个问题)
快速找到connect,socket函数实现的代码
致地讲一下IP_FORWARD那个函数
如何在内核中唤醒和睡眠用户进程??
如何打开.patch文件?
关于IP_FRAGMENT的问题
How It Works: DOS Floppy Disk Boot Sector
How It Works -- Master Boot Record
Minix代码分析之一:引导扇区代码
同一程序加载执行空间分布一样吗?
锁和信号量
内核与根文件系统
读kernel时容易漏掉的几个细节
推荐课题及"目的"
devfs_entry是干什么用的
2.2里的vfsmntlist,2.4改什么了
如何直接向网卡读写数据包?
如何在内核中加入PF_KEY套接字?
enable_irq(),disable_irq().和cli(),sti()之间有什么区别
关于slip模块中使用ioperm
gfp是什么的缩写阿?
关于中断例程中的子进程睡眠问题
连续空间分配的大小问题!
skb->;data为物理地址。为何在第一句中又进行转换
可加载模块写文件出现的奇怪现象 (程序设计经验)
SYMBOL_NAME( )
zone_struct
关于物理地址和虚拟地址的问题
请问linux文件系统层次结构的问题
kmalloc(size)返回的是物理地址,还是虚拟地址
VFS初始化过程中的一些问题
how to use create_proc_info_entry
中断上下文,进程上下文和任务上下文之间的区别---需要进一步深入
混杂模式
video adapter's ROM
本论坛和mailing list的区别!
很高兴能为大家服务, 及一些想法,大伙来参谋参谋
mkfs和fdisk的根本区别在哪儿?
amp - bios 资料
http://spr2.net/up_loader.php
内核等待队列机制介绍
http://www.linuxaid.com.cn/engineer/ideal/kernel/wait_queue.htm
linuxaid的网友ideal竹叶收藏
bootsect.s把大内核复制到高端内存的时候,还是实模式.如何实现的
page-》list
OWNOS操作系统设计书
need_resched 是怎么使用的
请教高手一个关于网络全局变量dev_base的问题
关于页面映射计数的讨论
free_area_init_core对其中struct page的virtual赋值为当前struct page所描述的物理页框的内核虚地址
再和4-1的反做与运算得到的又是什么呢?
__init , __exit , module_init, module_exit是什么意
写个小的OS项目,如何?加之一些感想(完全兴趣与学习目的)
mem_map数组
怎么解决这个include头文件时出现的矛盾
内核命令行是何时在何处传递给内核的
A20是什么?
关于brk与mmap的问题--已解决见另外一个帖子
在mm中,vm_area_struct为什么只用了单链表?
为什么要copy_from_user?
关于网络中接收软中断(net_rx_action)的问题
对UDP包在对包不做任何改变的情况下用udp_check()函数重新计算,结果与原校验和不符?
怎么初始化一个PCI设备?
[目录]
--------------------------------------------------------------------------------
list5
关于queue_task的一个问题
打造软盘上的linux
http://www.linuxfromscratch.org/ to define your own OS distribution.
www.linuxdoc.org to get howtos you want
可以参见下面链接处:
http://www.linuxeden.com/edu/doctext.php?docid=1582
Linux内核调试技术介绍 :)希望有帮助
我根据这个
从源代码打造一个最小化的Linux系统实作篇
http://www.linux.org.tw/CLDP/gb/mini/BuildMin.html#toc6
编linux,但不知道如何安装到指定路径
为你的Linux快速编译所需要的模块
by 刘军民 (fancao0515@0451.com)
http://www-900.ibm.com/developerWorks/linux/l-tip-prompt/tip13/index.shtml
copy_e820_map()问题
合并相邻区函数merge_segments()--2.0.33
关于module的问题
关于卖广告和我对linux的一点意见
timer_interrupt()函数看不明白,请指点
如何驱动键盘灯?
schedule(void)函数一定是可以重入的,对吗?
mmap (2.4.0)
Linux: Using Video RAM as System RAM
Linux: -aa VM in 2.5
一个linux驱动程序的问题
大侠指点一下get_pid的算法?
怎样修改PIPE_BUF的大小限制
操作系统理论的探索 (序言部分)
问题出在你新编译的内核不支持模块的版本识别
版本2.4.16下path_init和path_walk,link_path_walk分析报告
怎么理解记录块缓冲区的内容是一致的?
mark_bh(TIMER_BH)和timer_bh( )
当今日新月异,“对象”横飞,潜读LINUX源码除了提高自身素质外还有哪些令人兴奋的应用前景
一个困扰很久的问题--paging_init()
Linux核心调试环境的搭建
磁盘扇区与磁盘块如何定义以及如何区分使用?
也表处理宏的定义释疑
为什么学习LINUX源码、学到什么程度及怎样学习才能在最短的时间里取得最大的收获?
关于时间中断的一个问题
事物总是从陌生开始认识
关于/dev/shm
怎么在本机上配置一个lxf看源代码?在下对apache网络配置一窍不通
哪位知道哪有/proc的手册么
请问叶框0(0 ~ 4K物理地址空间)是否一直未使用?
剖析Linux任务切换的细节。
请问linux下有没有好的看源码的工具?
system call
低1G虚存是做什么的?
bootsect,setup,head的总结
增加系统调用的血汗经验 贡献给大家
How to use ramfs ???
能否将framebuffer的源码说一下
entry.S(续)
谁知道系统刚启动时BIOS建立的参数表
指点一下《Understanding the Linux Kernel》中的难点
Linux: O(1) Update For 2.4
links4RealtimeLinux
A new era: Carrier-grade Linux for Telecommunicati
lwIP - A Lightweight TCP/IP Stack
sys_arch interface for lwIP 0.5
Raw TCP/IP interface for lwIP 0.5
links4EmbededLinux
man slabinfo , 对其中的SMP部分不得其解
请教一个关于Linux处理外设中断的问题
异常处理函数的压栈
使用VIDEO_CHAR检查内核启动时在何处挂起
为什么用files->;fd[fd]而不用files->;fd_array[fd]
Linux 下网卡混杂模式
修改时间片DEFAUT_PRIORITY,为什么当系统
Lan网卡收到包后,应该比较mac地址。
如果手工触发request_irq登记的中断服务
请介绍关于cacheline相关知识,比方cacheline_pingpong
网络包的接收过程
关于pt_regs
How the PCI Hot Plug Driver Filesystem
iptable用户空间和内核关系的问题?
LINUX文件系统分析草稿--面向微内核的OS设计
希望大家讨论讨论?我们学源码要学到什么时候才能收手?
中国有多少人在做关于kernel的开发(真遗憾偶挖了坑没有人跳进来)
Linux内核中对硬盘控制的有关资料
head.s 中“jmp 1f”的1f是什么意思?
how to get size fo ram?
谈谈自已所尝试的内核分析方法
linux中进程与线程的问题
如何在LKM中执行一个用户空间的程序
为什么32位的系统不支持大于2G的文件?
Linux上是否有类似winsock2上的SPI相对应的接口层?
请问如何驱动这个怪物
用双网卡如何同时传输数据(?)
在内核中如何进行大空间内存的分配??
请帮忙修改程序(do you like this?)
关于Login的一个问题(basic)
offsetof怎么理解
为什么每个磁道有63个扇区,而不是64个
ldd2的Backward Compatibility章节
LDD第三章的一个问题
如何根据pid得到进程的名称
如何将内核编译成无版本检查的呢
about init section (vmlinux.lds)
udp报文转发不了,而icmp报文可以,为什么(undstand it?)
张贴一个别人推荐的很好的内核线程的例子
2.4.16软中断分析图 (again)
about interrupt number : 15 or 16
关于引用内核输出符号的问题
cache不命中的情况
请教/proc/pci(有个pci图)
连续内存与离散内存的问题
请问怎样能够得到一个进程的所有的通讯流量
如何让root不能修改其他用户的口令?
slab对象的地址可以不通过页表映射吗?
atomic_fool_gcc的作用是什么?如果不加gcc可能怎么处理?为什么2.4中没有了
关于缺叶中断的内存管理1
slab对象的地址可以不通过页表映射吗(...?)
about "jmp *%eax
insmod的模块放在哪里?
linux smp 求救
8259A可编程控制器在linux上的初始化
在网络层构造UDP包好像不太合理
如何将内核中printk的输出到屏幕上
Linux在i386处理器在PAE模式下寻址
shell系统流程(哈哈)
网上那有关于a20线的详细介绍
调度 schedule 概念
zone balance 是何意
多一些供献,少一些..
大家看看,是不是《情景分析》说错了?(文件读写)
欢迎calfe,garycao加入版面管理
裁减libc的两个工具
一个是Montavista公司的lib优化工具
基本思想是扫描你的应用程序的库函数调用.将你的应用程序没有用到的库函数删除.
一个是IBM组织的一个minilibc project
http://www-124.ibm.com/developer/opensource/linux/projects/minilibc/
这儿摘录了其中的几个非正式观点,共享.
Some write for the fun/eduction of it and want to to it all.
Some have ego problems and must do it all.
Some do not know how to adapt existing code so they do it all.
Some have really wierd systems with nothing close so must do it all.
Some have not searched enough to know of simulair systems so must do it all.
Some have ethical/comercial problems with licenceses and must do it all.
Some rename code and say he did it all.
Some DO use existing code.
uclinux 分析
http://hhcn.org/uclinux.html
gnu/hurd 介绍
http://www.gnu.org/software/hurd/
几个手册
http://www.refcards.org
netlink socket资料
http://qos.ittc.ukans.edu/netlink/html/
截包零拷贝能不能实现
收集内核FAQ了! 大家如在版面上发现适合的话题请转载一份贴在这儿
local_irq_disable()在smp下究竟是关了所有处理器的中断还是当前处理器中断
读setup.s中的几个问题(openarm的几个连接)
spinlock的设计和实现
在linux中一个进程如何修改其它进程的数据(关注)
Linux的页式内存内存管理
head.S 分析
内核信号量的简单演示(几个进程并发调用down的情况 ppt)
i386内存结构示意图(intel and mine)
内存观点(2.4.0)(精华收集和自己的一点理解)
如何关闭linux的单用户模式
flush_tlb_xxx()这类函数都是干什么用的
设备初始化问题(basic)
System.map有什么用处?
Linux 内核笔记 -- 信号
如何读取指定硬盘扇区号的扇区数据?
关于swapper的问题
linus关于debug tools的一篇旧帖子
What is the meaning of "volatile"?
重新编译内核后不支持VFAT的中文文件名?(内核?)
忙里偷闲:Slab简要分析(一张图)
volatile的含义
kgdb文档
引导与启动部分之head.s部分的注释
系统引导与启动(suzhe前辈的一次报告)-----by linux3-III
系统引导与启动之深入lilo-----(zz by linux3-III)
有那位大虾做过内核裁减
jiffies耗尽后会发生什么
ip层及tcp层的处理究竟谁在做
关于tcp/ip不明白的地方
请问在Linux下如何得知每个CPU的使用率等信息
[ jkl ]__attribute__
问几个很弱地问题...
Ext2 文件系统的硬盘布局
如何修改或去掉以framebuffer方式启动的内核的小企鹅的标志
__copy_user的实现原理
boot loader 的一些研究
ip数据包接收发送
Linux的存储管理(一)
内核启动时传入的参数存放在甚么位置
编译linux内核及注意事项(支持ipv6)(看起来步骤详细)
我是怎样精通了linux内核的?(??)
pci_read_config_byte
[精华] LDD2-CH12-块设备
<<Linux设备驱动程序第二版>;>;第八章 硬件访问
<<Linux设备驱动程序第二版>;>;第六章 时钟管理
《Linux设备驱动程序》第二版译稿 第九章中断处理 by kongming etc
<<Linux设备驱动程序第二版>;>;第九章 中断处理
<<Linux设备驱动程序第二版>;>;导读计划
Linux 设备驱动程序》第二版翻译稿样章(前言)
《Linux 设备驱动程序》第二版中文翻译稿样章(第 6 章 时间流)
《Linux 设备驱动程序》第二版中文译稿样章(第 4 章 调试技术)
Linux设备驱动程序(第一版)完整勘误表
编译内核出错(为什么这种话题很多?)
gdtr,idtr,cr3乃至页目录表,他们之中的地址都是物理地址吗?
vm area的地址映射(多看些讨论有好处)
difference between "return" and "exit"?
关于booting部分的一些链接
[精华] Linux 内核笔记2 - 进程调度
Linux的存储管理(二)
linux内核调试(讨论)
推荐一本浅显易懂的书
此图如何看(e820)
论文“Linux内核进程调度与控制 --源代码分析与研究”
怎样用bochs和vmware测试内核
关于使用kgdb调试内核
Linux中的NAT能同时支持多少连接?
为什么会kernel panic(多多讨论)
task_struct结构中的comm成员是代表什么意思啊
《Linux 设备驱动程序》第二版中文译稿样章(第 15 章 外设总线概述)
注册设备文件处理函数的方法有问题(算个经验)
根文件在那里初始
关于in_interrupt的问题
为你的Linux快速编译所需要的模块 (zz)
关于《Linux 设备驱动程序》第二版中文译稿样章的说明
MODR/M and SIB,whar's the means?
What's the maximum preemption latency
LINUX下的设备驱动程序
[转贴]如何编写Linux操作系统下的设备驱动程序
关于vmalloc的疑问!!!
into entry-armv.s
设备驱动程序如何在启动时加载(faq)
bonding.c的原码分析
驱动开发的基础问题(硬件厂商提供驱动吗?)
LINUX的网络设备中,利用master 和slave来实现负载平衡
学习内核和驱动需要多少硬件知识
Linux 内核笔记3 --内核同步-- 第一部分
uClinux - BFLT Binary Flat Format
[精华] LINUX实时调度研讨。
mmap函数问题
Setup Terminal Systems
首席培训官教新手使用最新的内核
PG_active和PG_referenced
内核中的所有struct semaphore型变量都共享一个spinlock_t变量
进程互斥,PV操作
ldd2第十五章PCI总线阅读随记
Write Once protocol for multi processor
一个pci插槽的pci设备的中断是不是固定的
do_IRQ()问题
page->;mapping和page->;buffer分别是做什么用的?
do_softirq()问题
Linux内核进程调度与控制 --源代码分析与研究 by minipuss
可否核态和用户态直接进行通讯
Linux 内核笔记3 --内核同步-- 第三部分
Linux 内核笔记3 --内核同步-- 第二部分
How to open/read a file in interrupt_context?
Why "scheduling in interrupt" ?
when should i define a variable with "volatile" ?
Kernel/Device Driver questions?(mmap,kmalloc,irq)
"gdb-serial.txt" from "gdbstub.tar.gz"
gdtr中为什么放线形地址 ,而不是物理地址?
代表设备文件ext2_inode节点保存了什么信息保证找到设备的?
[精华] Linux 内核笔记3 - 内核同步(完整版)
内核与用户进程通讯的几种方式比较
我把内存管理FAQ重新整理了一下。
“逻辑地址”、“线性地址”、“物理地址”的区别和联系
关于在内核中向应用程序发送信号的问题
实际一个程序同时可能有多个进程,所以一般由pid找程序名较合理
refill_inactive()函数
又关于page->;buffers和page->;mapping
关于video.s的疑问
关于内存的几个问题
怎么添加上ip伪装和ip端口转发
Running Linux on Linux(User-Mode Linux )?
如何生成可被lilo引导的image?
同志们,为什么浪费0~08048000的进程地址空间不用
关于添加系统调用的问题
作linux向另一个硬件平台的移植工作应该如何入手?要做哪些工作(链接)
kernel精华!(文档包)已经上传到我公司的服务器上了! (有钱出钱)
Kongming给出了ldd2中文版的部分章节,大家抽空读读,对其中细节给点建议
请问如何使网关支持内网上任意IP的上网通讯?
那么MAIN这个进程是什么时候fork的呢?
怎样在内核中读写文件?
LINUX ALLOCATED DEVICES
Interview with Robert Love
请xshell结合这段代码对于cacheline做些解释
HZ 表示每两次时钟中断相隔的微秒数
head.S 中的__PAGE_OFFSET 和其他
谁能讲一讲Console和tty之间的关系?
wmb是干吗的函数 (zz)
这是我在学习设备驱动程序的一点问题?希望大家赐教!
2.4.16下网络结构全景图1.0 (包含模块接口变量名称)
谁有真正能用的Linux启动盘的资料?
Encrypted Ethernet Bridging
More Explicative Kernel Tainting
setup.S中"in al #0x92" 0x92端口的含义
BIOS 数据区域(BDA)位置在哪里?
Why pay someone to port Linux for you?
中断上下文中如何实现文件读写?
wait和completion数据结构的解释
user_struct
谁能解释一下tty,tty0,ttyS0,console,终端,控制台这几个词
Linux MTD源代码分析
请问内核中读写配置文件的源码在哪个目录的哪个文件中
写时复制中关于empty_zero_page的问题
Linux 内核使用的 GNU C 扩展
我的一个Object-oriented operating environment 设想.
是不是所有的包都要用到这里的match,target,在哪个hook用,看得糊涂了
为什么在转发之前需要COPY一下SK_BUFF???
转载一篇文章"泛系、自由与“一、百、万”工程"
2.4.16内核icmp报文处理分析
请问各hook函数之间怎么传递信息:在同一个hook点上的和不同hook点上的函数各自如何传递
关于中断问题的请教
大家有否考虑在Linux上实现一个硬件抽象层?(空)
Linux Kernel Module Programming Guide
可动态编译的嵌入式操作系统
CMOS里的东西在启动后是不是COPY到内存中去了
问个基础的问题
vmalloc的问题
超级宝书http://suse.chinasec.com/ulk.pdf
哪有LINUX内核分析(进程部分)的文章?
写时复制的问题
守护进程里如何发声?
关于swapper_pg_dir初始化的问题
linux 与 minix的共存
我编驱动程序的时候的问题(PCI)
如何实现内存映射?
关于在转发前sk_buff结构copy的问题
Are you familiar with Architecture or OS ?
为什么谁都可?
[目录]
--------------------------------------------------------------------------------
AKA推荐书籍
====================================================================
《The Design of The UNIX Operating System》 Author: Maurice J. Bach
中译本:《UNIX 操作系统设计》
---------------------
翻译者:陈葆钰等
北大版: 32开,黄皮,9.40¥
机工版: 16开,褐皮,30¥
---------------------
一本老书,但绝对是bible级的。其内容覆盖了经典UNIX操作系统中的所有基本概念
以及设计思想。阅读者最好能懂一些基本的操作系统理论知识。
====================================================================
====================================================================
《Unix Internals - The New Frontiers》 Author: Uresh Vahalia
中译本:《UNIX 高级教程:系统技术内幕》
---------------------
翻译者:聊鸿斌等
清华大学出版社出版,16开,桔皮,58¥
---------------------
一本新书,也是bible级的。其主要特点是80%的内容都是现代UNIX操作系统的新思想
和新概念。对各UNIX大家的精彩设计点都有很详尽的阐述。阅读者最好先看看贝奇那
本书。
====================================================================
====================================================================
《Linux Core Kernel Commentary》
---------------------
Author: Scott Maxwell
---------------------
有中译本,但具体信息不详 对照源码讲Linux内核,版本还比较高,2.2.X。有些新
东西可看看。可惜没讲网络部分的实现。
====================================================================
====================================================================
《Linux Kernel Internals: 2nd version》
---------------------
Author: M Beck ...
---------------------
目前无中译本 以数据结构为主体来介绍内核,内容很丰富,但版本太低(2.0.x),有
些陈旧的东西容易令人误入歧途。
====================================================================
====================================================================
《The Design and Implementation of the 4.4BSD Operating System》
---------------------
Author: McKusick, Bostic, Karels, Quarterman
---------------------
目前无中译本 讲述BSD操作系统最权威的书。书的作者亦即BSD最早的几名开发者。
====================================================================
====================================================================
《Linux 操作系统及实验教程》
---------------------
作者:李善平,郑扣根(浙大教师)
机工出版
---------------------
有些内容不错,讲得还比较深入。也缺少网络部分的描述。
====================================================================
====================================================================
《UNIX 系统下的80386》
---------------------
作者:周明德, 张淑玲
清华出版
---------------------
讲X86的体系结构。要想看懂/arch/i386下的辕马,最好先看看这本书。
====================================================================
====================================================================
《UNIX Systems for Modern Architectures》
---------------------
Author: Curt Schimmel
---------------------
目前无中译本 如果想了解辕马中SMP的实现。可参考看这本书。
====================================================================
====================================================================
《保护模式下的80386及其编程》
---------------------
出版社:清华大学出版社
---------------------
本书全面地介绍了80386的结构。首先是80386实模式和保护模式下的寄存器集和
指令集,接着从保护模式下的虚存管理、保护级、多任务支持、中断和异常等方
面深入地剖析386的强大功能,再接着提供几个典型的编程实例以及一个完整的从
386加电初始化开始到形成一个有基本的多任务和虚拟存储能力的系统的例子并作
了详细解释,最后还清楚地说明了80386与8086/80286的兼容性和差别。本书的特
点是严谨深入,对CPU各种条件下的动作和反应用形式化的微程序讲解得十分清楚,
尤其适合系统程序员阅读。总之,这实在是一本不可多得的好书.
====================================================================
====================================================================
《Linux 操作系统的内核分析》 作者:陈莉君编著
---------------------
价格:40 元
出版社:人民邮电出版社
---------------------
无
====================================================================
[目录]
--------------------------------------------------------------------------------
linux论坛推荐资源
~~~~~~~
一般文献
~~~~~~~
利索脚组织的网站:
http://www.lisoleg.net
linux kernel internals 2.4(不是特别全)
http://www.moses.uklinux.net/patches/lki.html
http://tzhang.xiloo.com/unix/kernel/
UNIX高级教程 系统技术内幕
Uresh Vahalia 清华大学出版社
书。这是一本非常好的书,思想性很强,对学习Linux以及操作系统非常有帮助。
Understanding the Linux Kernel
Daniel P. Bovet Marco Cesati
O'Reilly出版
超级宝书!2001年内会有中译本。
这里是第10章
Process Scheduling
Linux 操作系统及实验教程
李善平 郑扣根 编著
机械工业出版社
Linux 操作系统的内核分析
陈莉君编著
人民邮电出版社
Linux Device Driver
书。有中译本。帮助理解中断,任务队列等内核机制,帮助掌握驱动编程。
Operating Systems
resource center
jkl 推荐
关于操作系统的介绍,有许多专题文章
http://www.nondot.org/sabre//os/
关于Linux的重要链接
Pengcheng Zou 推荐
给出了许多重要的链接
http://www.linuxlinks.com/Kernel/
非常完备的CPU开发手册
Intel Architecture Software Developer’s Manual
Volume 3:
System Programming
" target="_new">;http://developer.intel.com/design/pentiumii/manuals/24319202.pdf
对i386的机制讲得非常详细,可以帮助深入了解i386保护模式机制,对理解Linux的相关实现非常有帮助
~~~~~~~
关于内存
~~~~~~~
Outline of the Linux Memory Management System
黑饭 推荐
http://home.earthlink.net/~jknapka/linux-mm/vmoutline.html
已经是一个比较细致的介绍内存(包括内核的和用户空间的)了。
SGI公司的公开项目
http://oss.sgi.com/projects/
SGI公司的关于伸缩性的技术非常棒!Linux的NUMA就是由他们做的,这是他们的网页
Linux内存开发小组的网站
jkl 推荐
http://www.linux-mm.org
这是Linux负责开发内存管理的核心成员的网站
下面有他们关于zone设计思想的描述:
http://surriel.com/zone-alloc.html
~~~~~~~
关于中断
~~~~~~~
关于中断上锁的专题文档
http://netfilter.kernelnotes.org/unreliable-guides/kernel-locking/lklockingguide.html
我们知道在中断中写程序要小心,不小心会发生race condition.这篇文献对这方面做了介绍
~~~~~~~~~~
关于文件系统
~~~~~~~~~~
Linux Commentary
dream_seeker 推荐
http://www.cse.unsw.edu.au/~neilb/oss/linux-commentary/
主要介绍文件系统, 另外,下面的
linux-vm-readme
主要介绍VM和交换,也很值得一看
关于汇编
[http://www.linuxassembly.org http://www.linuxassembly.org ]
Linuxkernel推荐
关于AT
http://www-106.ibm.com/developerworks/linux/library/l-ia.html
lisoleg推荐
关于汇编:
http://www-aig.jpl.nasa.gov/public/home/decoste/HTMLS/GNU/binutils/as_toc.html
http://www.linuxassembly.org/resources.html#tutorials
lucian推荐
关于驱动,专门研究linux驱动设计的bbs
http://short.xilubbs.com
joyfire推荐
Edited by lucian_yao on 05/20/01 08:05 PM.
[目录]
--------------------------------------------------------------------------------
数据结构
以下是linux操作系统核心常用的数据结构
block_dev_struct
此结构用于向核心登记块设备,它还被buffer
cache实用。所有此类结构都位于blk_dev数组中。
struct blk_dev_struct {
void (*request_fn)(void);
struct request * current_request;
struct request plug;
struct tq_struct plug_tq;
};
buffer_head
此结构包含关于buffer cache中一块缓存的信息。
/* bh state bits */
#define BH_Uptodate 0 /* 1 if the buffer contains valid data */
#define BH_Dirty 1 /* 1 if the buffer is dirty */
#define BH_Lock 2 /* 1 if the buffer is locked */
#define BH_Req 3 /* 0 if the buffer has been invalidated */
#define BH_Touched 4 /* 1 if the buffer has been touched (aging) */
#define BH_Has_aged 5 /* 1 if the buffer has been aged (aging) */
#define BH_Protected 6 /* 1 if the buffer is protected */
#define BH_FreeOnIO 7 /* 1 to discard the buffer_head after IO */
struct buffer_head {
/* First cache line: */
unsigned long b_blocknr; /* block number */
kdev_t b_dev; /* device (B_FREE = free) */
kdev_t b_rdev; /* Real device */
unsigned long b_rsector; /* Real buffer location on disk */
struct buffer_head *b_next; /* Hash queue list */
struct buffer_head *b_this_page; /* circular list of buffers in one
page */
/* Second cache line: */
unsigned long b_state; /* buffer state bitmap (above) */
struct buffer_head *b_next_free;
unsigned int b_count; /* users using this block */
unsigned long b_size; /* block size */
/* Non-performance-critical data follows. */
char *b_data; /* pointer to data block */
unsigned int b_list; /* List that this buffer appears */
unsigned long b_flushtime; /* Time when this (dirty) buffer
* should be written */
unsigned long b_lru_time; /* Time when this buffer was
* last used. */
struct wait_queue *b_wait;
struct buffer_head *b_prev; /* doubly linked hash list */
struct buffer_head *b_prev_free; /* doubly linked list of buffers */
struct buffer_head *b_reqnext; /* request queue */
};
device
系统中每个网络设备都用一个设备数据结构来表示。
struct device
{
/*
* This is the first field of the "visible" part of this structure
* (i.e. as seen by users in the "Space.c" file). It is the name
* the interface.
*/
char *name;
/* I/O specific fields */
unsigned long rmem_end; /* shmem "recv" end */
unsigned long rmem_start; /* shmem "recv" start */
unsigned long mem_end; /* shared mem end */
unsigned long mem_start; /* shared mem start */
unsigned long base_addr; /* device I/O address */
unsigned char irq; /* device IRQ number */
/* Low-level status flags. */
volatile unsigned char start, /* start an operation */
interrupt; /* interrupt arrived */
unsigned long tbusy; /* transmitter busy */
struct device *next;
/* The device initialization function. Called only once. */
int (*init)(struct device *dev);
/* Some hardware also needs these fields, but they are not part of
the usual set specified in Space.c. */
unsigned char if_port; /* Selectable AUI,TP, */
unsigned char dma; /* DMA channel */
struct enet_statistics* (*get_stats)(struct device *dev);
/*
* This marks the end of the "visible" part of the structure. All
* fields hereafter are internal to the system, and may change at
* will (read: may be cleaned up at will).
*/
/* These may be needed for future network-power-down code. */
unsigned long trans_start; /* Time (jiffies) of
last transmit */
unsigned long last_rx; /* Time of last Rx */
unsigned short flags; /* interface flags (BSD)*/
unsigned short family; /* address family ID */
unsigned short metric; /* routing metric */
unsigned short mtu; /* MTU value */
unsigned short type; /* hardware type */
unsigned short hard_header_len; /* hardware hdr len */
void *priv; /* private data */
/* Interface address info. */
unsigned char broadcast[MAX_ADDR_LEN];
unsigned char pad;
unsigned char dev_addr[MAX_ADDR_LEN];
unsigned char addr_len; /* hardware addr len */
unsigned long pa_addr; /* protocol address */
unsigned long pa_brdaddr; /* protocol broadcast addr*/
unsigned long pa_dstaddr; /* protocol P-P other addr*/
unsigned long pa_mask; /* protocol netmask */
unsigned short pa_alen; /* protocol address len */
struct dev_mc_list *mc_list; /* M'cast mac addrs */
int mc_count; /* No installed mcasts */
struct ip_mc_list *ip_mc_list; /* IP m'cast filter chain */
__u32 tx_queue_len; /* Max frames per queue */
/* For load balancing driver pair support */
unsigned long pkt_queue; /* Packets queued */
struct device *slave; /* Slave device */
struct net_alias_info *alias_info; /* main dev alias info */
struct net_alias *my_alias; /* alias devs */
/* Pointer to the interface buffers. */
struct sk_buff_head buffs[DEV_NUMBUFFS];
/* Pointers to interface service routines. */
int (*open)(struct device *dev);
int (*stop)(struct device *dev);
int (*hard_start_xmit) (struct sk_buff *skb,
struct device *dev);
int (*hard_header) (struct sk_buff *skb,
struct device *dev,
unsigned short type,
void *daddr,
void *saddr,
unsigned len);
int (*rebuild_header)(void *eth,
struct device *dev,
unsigned long raddr,
struct sk_buff *skb);
void (*set_multicast_list)(struct device *dev);
int (*set_mac_address)(struct device *dev,
void *addr);
int (*do_ioctl)(struct device *dev,
struct ifreq *ifr,
int cmd);
int (*set_config)(struct device *dev,
struct ifmap *map);
void (*header_cache_bind)(struct hh_cache **hhp,
struct device *dev,
unsigned short htype,
__u32 daddr);
void (*header_cache_update)(struct hh_cache *hh,
struct device *dev,
unsigned char * haddr);
int (*change_mtu)(struct device *dev,
int new_mtu);
struct iw_statistics* (*get_wireless_stats)(struct device *dev);
};
device_struct
此结构被块设备和字符设备用来向核心登记(包含设备名称以及可对此设备进行的
文件操作)。chrdevs和blkdevs中的每个有效分别表示一个字符设备和块设备。
struct device_struct {
const char * name;
struct file_operations * fops;
};
file
每个打开的文件、套接口都用此结构表示。
struct file {
mode_t f_mode;
loff_t f_pos;
unsigned short f_flags;
unsigned short f_count;
unsigned long f_reada, f_ramax, f_raend, f_ralen, f_rawin;
struct file *f_next, *f_prev;
int f_owner; /* pid or -pgrp where SIGIO should be sent */
struct inode * f_inode;
struct file_operations * f_op;
unsigned long f_version;
void *private_data; /* needed for tty driver, and maybe others */
};
files_struct
描叙被某进程打开的所有文件。
struct files_struct {
int count;
fd_set close_on_exec;
fd_set open_fds;
struct file * fd[NR_OPEN];
};
fs_struct
struct fs_struct {
int count;
unsigned short umask;
struct inode * root, * pwd;
};
gendisk
包含关于某个硬盘的信息。用于磁盘初始化与分区检查时。
struct hd_struct {
long start_sect;
long nr_sects;
};
struct gendisk {
int major; /* major number of driver */
const char *major_name; /* name of major driver */
int minor_shift; /* number of times minor is shifted to
get real minor */
int max_p; /* maximum partitions per device */
int max_nr; /* maximum number of real devices */
void (*init)(struct gendisk *);
/* Initialization called before we
do our thing */
struct hd_struct *part; /* partition table */
int *sizes; /* device size in blocks, copied to
blk_size[] */
int nr_real; /* number of real devices */
void *real_devices; /* internal use */
struct gendisk *next;
};
inode
此VFS inode结构描叙磁盘上一个文件或目录的信息。
struct inode {
kdev_t i_dev;
unsigned long i_ino;
umode_t i_mode;
nlink_t i_nlink;
uid_t i_uid;
gid_t i_gid;
kdev_t i_rdev;
off_t i_size;
time_t i_atime;
time_t i_mtime;
time_t i_ctime;
unsigned long i_blksize;
unsigned long i_blocks;
unsigned long i_version;
unsigned long i_nrpages;
struct semaphore i_sem;
struct inode_operations *i_op;
struct super_block *i_sb;
struct wait_queue *i_wait;
struct file_lock *i_flock;
struct vm_area_struct *i_mmap;
struct page *i_pages;
struct dquot *i_dquot[MAXQUOTAS];
struct inode *i_next, *i_prev;
struct inode *i_hash_next, *i_hash_prev;
struct inode *i_bound_to, *i_bound_by;
struct inode *i_mount;
unsigned short i_count;
unsigned short i_flags;
unsigned char i_lock;
unsigned char i_dirt;
unsigned char i_pipe;
unsigned char i_sock;
unsigned char i_seek;
unsigned char i_update;
unsigned short i_writecount;
union {
struct pipe_inode_info pipe_i;
struct minix_inode_info minix_i;
struct ext_inode_info ext_i;
struct ext2_inode_info ext2_i;
struct hpfs_inode_info hpfs_i;
struct msdos_inode_info msdos_i;
struct umsdos_inode_info umsdos_i;
struct iso_inode_info isofs_i;
struct nfs_inode_info nfs_i;
struct xiafs_inode_info xiafs_i;
struct sysv_inode_info sysv_i;
struct affs_inode_info affs_i;
struct ufs_inode_info ufs_i;
struct socket socket_i;
void *generic_ip;
} u;
};
ipc_perm
此结构描叙对一个系统V IPC对象的存取权限。
struct ipc_perm
{
key_t key;
ushort uid; /* owner euid and egid */
ushort gid;
ushort cuid; /* creator euid and egid */
ushort cgid;
ushort mode; /* access modes see mode flags below */
ushort seq; /* sequence number */
};
irqaction
用来描叙系统的中断处理过程。
struct irqaction {
void (*handler)(int, void *, struct pt_regs *);
unsigned long flags;
unsigned long mask;
const char *name;
void *dev_id;
struct irqaction *next;
};
linux_binfmt
用来表示可被Linux理解的二进制文件格式。
struct linux_binfmt {
struct linux_binfmt * next;
long *use_count;
int (*load_binary)(struct linux_binprm *, struct pt_regs * regs);
int (*load_shlib)(int fd);
int (*core_dump)(long signr, struct pt_regs * regs);
};
mem_map_t
用来保存每个物理页面的信息。
typedef struct page {
/* these must be first (free area handling) */
struct page *next;
struct page *prev;
struct inode *inode;
unsigned long offset;
struct page *next_hash;
atomic_t count;
unsigned flags; /* atomic flags, some possibly
updated asynchronously */
unsigned dirty:16,
age:8;
struct wait_queue *wait;
struct page *prev_hash;
struct buffer_head *buffers;
unsigned long swap_unlock_entry;
unsigned long map_nr; /* page->;map_nr == page - mem_map */
} mem_map_t;
mm_struct
用来描叙某任务或进程的虚拟内存。
struct mm_struct {
int count;
pgd_t * pgd;
unsigned long context;
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack, start_mmap;
unsigned long arg_start, arg_end, env_start, env_end;
unsigned long rss, total_vm, locked_vm;
unsigned long def_flags;
struct vm_area_struct * mmap;
struct vm_area_struct * mmap_avl;
struct semaphore mmap_sem;
};
pci_bus
表示系统中的一个PCI总线。
struct pci_bus {
struct pci_bus *parent; /* parent bus this bridge is on */
struct pci_bus *children; /* chain of P2P bridges on this bus */
struct pci_bus *next; /* chain of all PCI buses */
struct pci_dev *self; /* bridge device as seen by parent */
struct pci_dev *devices; /* devices behind this bridge */
void *sysdata; /* hook for sys-specific extension */
unsigned char number; /* bus number */
unsigned char primary; /* number of primary bridge */
unsigned char secondary; /* number of secondary bridge */
unsigned char subordinate; /* max number of subordinate buses */
};
pci_dev
表示系统中的每个PCI设备,包括PCI-PCI和PCI-PCI桥接器。
/*
* There is one pci_dev structure for each slot-number/function-number
* combination:
*/
struct pci_dev {
struct pci_bus *bus; /* bus this device is on */
struct pci_dev *sibling; /* next device on this bus */
struct pci_dev *next; /* chain of all devices */
void *sysdata; /* hook for sys-specific extension */
unsigned int devfn; /* encoded device & function index */
unsigned short vendor;
unsigned short device;
unsigned int class; /* 3 bytes: (base,sub,prog-if) */
unsigned int master : 1; /* set if device is master capable */
/*
* In theory, the irq level can be read from configuration
* space and all would be fine. However, old PCI chips don't
* support these registers and return 0 instead. For example,
* the Vision864-P rev 0 chip can uses INTA, but returns 0 in
* the interrupt line and pin registers. pci_init()
* initializes this field with the value at PCI_INTERRUPT_LINE
* and it is the job of pcibios_fixup() to change it if
* necessary. The field must not be 0 unless the device
* cannot generate interrupts at all.
*/
unsigned char irq; /* irq generated by this device */
};
request
被用来向系统的块设备发送请求。它总是向buffer cache读出或写入数据块。
struct request {
volatile int rq_status;
#define RQ_INACTIVE (-1)
#define RQ_ACTIVE 1
#define RQ_SCSI_BUSY 0xffff
#define RQ_SCSI_DONE 0xfffe
#define RQ_SCSI_DISCONNECTING 0xffe0
kdev_t rq_dev;
int cmd; /* READ or WRITE */
int errors;
unsigned long sector;
unsigned long nr_sectors;
unsigned long current_nr_sectors;
char * buffer;
struct semaphore * sem;
struct buffer_head * bh;
struct buffer_head * bhtail;
struct request * next;
};
rtable
用来描叙向某个IP主机发送包的路由信息。此结构在IP路由cache内部实用。
struct rtable
{
struct rtable *rt_next;
__u32 rt_dst;
__u32 rt_src;
__u32 rt_gateway;
atomic_t rt_refcnt;
atomic_t rt_use;
unsigned long rt_window;
atomic_t rt_lastuse;
struct hh_cache *rt_hh;
struct device *rt_dev;
unsigned short rt_flags;
unsigned short rt_mtu;
unsigned short rt_irtt;
unsigned char rt_tos;
};
semaphore
保护临界区数据结构和代码信号灯。
struct semaphore {
int count;
int waking;
int lock ; /* to make waking testing atomic */
struct wait_queue *wait;
};
sk_buff
用来描叙在协议层之间交换的网络数据。
struct sk_buff
{
struct sk_buff *next; /* Next buffer in list
*/
struct sk_buff *prev; /* Previous buffer in list
*/
struct sk_buff_head *list; /* List we are on
*/
int magic_debug_cookie;
struct sk_buff *link3; /* Link for IP protocol level buffer chai
ns *
struct sock *sk; /* Socket we are owned by
*/
unsigned long when; /* used to compute rtt's
*/
struct timeval stamp; /* Time we arrived
*/
struct device *dev; /* Device we arrived on/are leaving by
*/
union
{
struct tcphdr *th;
struct ethhdr *eth;
struct iphdr *iph;
struct udphdr *uh;
unsigned char *raw;
/* for passing file handles in a unix domain socket */
void *filp;
} h;
union
{
/* As yet incomplete physical layer views */
unsigned char *raw;
struct ethhdr *ethernet;
} mac;
struct iphdr *ip_hdr; /* For IPPROTO_RAW
*/
unsigned long len; /* Length of actual data
*/
unsigned long csum; /* Checksum
*/
__u32 saddr; /* IP source address
*/
__u32 daddr; /* IP target address
*/
__u32 raddr; /* IP next hop address
*/
__u32 seq; /* TCP sequence number
*/
__u32 end_seq; /* seq [+ fin] [+ syn] + datalen
*/
__u32 ack_seq; /* TCP ack sequence number
*/
unsigned char proto_priv[16];
volatile char acked, /* Are we acked ?
*/
used, /* Are we in use ?
*/
free, /* How to free this buffer
*/
arp; /* Has IP/ARP resolution finished
*/
unsigned char tries, /* Times tried
*/
lock, /* Are we locked ?
*/
localroute, /* Local routing asserted for this frame
*/
pkt_type, /* Packet class
*/
pkt_bridged, /* Tracker for bridging
*/
ip_summed; /* Driver fed us an IP checksum
*/
#define PACKET_HOST 0 /* To us
*/
#define PACKET_BROADCAST 1 /* To all
*/
#define PACKET_MULTICAST 2 /* To group
*/
#define PACKET_OTHERHOST 3 /* To someone else
*/
unsigned short users; /* User count - see datagram.c,tcp.c
*/
unsigned short protocol; /* Packet protocol from driver.
*/
unsigned int truesize; /* Buffer size
*/
atomic_t count; /* reference count
*/
struct sk_buff *data_skb; /* Link to the actual data skb
*/
unsigned char *head; /* Head of buffer
*/
unsigned char *data; /* Data head pointer
*/
unsigned char *tail; /* Tail pointer
*/
unsigned char *end; /* End pointer
*/
void (*destructor)(struct sk_buff *); /* Destruct function
*/
__u16 redirport; /* Redirect port
*/
};
sock
包含BSD套接口的协议相关信息。例如对于一个INET(Internet AddressDomain)套接口
此数据结构 包含TCP/IP和UDP/IP信息。
struct sock
{
/* This must be first. */
struct sock *sklist_next;
struct sock *sklist_prev;
struct options *opt;
atomic_t wmem_alloc;
atomic_t rmem_alloc;
unsigned long allocation; /* Allocation mode */
__u32 write_seq;
__u32 sent_seq;
__u32 acked_seq;
__u32 copied_seq;
__u32 rcv_ack_seq;
unsigned short rcv_ack_cnt; /* count of same ack */
__u32 window_seq;
__u32 fin_seq;
__u32 urg_seq;
__u32 urg_data;
__u32 syn_seq;
int users; /* user count */
/*
* Not all are volatile, but some are, so we
* might as well say they all are.
*/
volatile char dead,
urginline,
intr,
blog,
done,
reuse,
keepopen,
linger,
delay_acks,
destroy,
ack_timed,
no_check,
zapped,
broadcast,
nonagle,
bsdism;
unsigned long lingertime;
int proc;
struct sock *next;
struct sock **pprev;
struct sock *bind_next;
struct sock **bind_pprev;
struct sock *pair;
int hashent;
struct sock *prev;
struct sk_buff *volatile send_head;
struct sk_buff *volatile send_next;
struct sk_buff *volatile send_tail;
struct sk_buff_head back_log;
struct sk_buff *partial;
struct timer_list partial_timer;
long retransmits;
struct sk_buff_head write_queue,
receive_queue;
struct proto *prot;
struct wait_queue **sleep;
__u32 daddr;
__u32 saddr; /* Sending source */
__u32 rcv_saddr; /* Bound address */
unsigned short max_unacked;
unsigned short window;
__u32 lastwin_seq; /* sequence number when we las
t
updated the window we offer
*/
__u32 high_seq; /* sequence number when we did
current fast retransmit */
volatile unsigned long ato; /* ack timeout */
volatile unsigned long lrcvtime; /* jiffies at last data rcv */
[目录]
--------------------------------------------------------------------------------
重新编译
1.核心的源程序:
我现在在用TLC, REDHAT也用过, SLACKWARE也用过. 无论哪一种, 都是把核心源程
序放到 /usr/src/linux 下, 因为有些别的应用程序在编译时好像也会从这个路径来引
用一些头文件之类. 一般来说这个 linux 目录都只是个符号连接, 有一点点像WIN下的
Shortcut, 而实际上它对应的目录可能是 /usr/src/linux-2.0.35 之类. RedHat的缺省
安装好像并不装源程序, 只有些头文件.
以现在的2.2.5 核心为例, 我装的时候就是这样(其实什么版本都一样
cd /usr/src
rm linux
# 这个linux只是个符号连接, 删掉它没事的. 可以 ls -l 看看, 如果看到这个:
# linux ->; linux-XXXXX, 就表示它是个连接而已. 原来的源程序在箭头后的目录.
tar zxvf XXXXXXX/linux-2.2.5.tar.gz
# 这个包解开后, 新核心的源程序就放在了新建立的linux目录下, 这可是个货真价
# 实的目录.
mv linux linux-2.2.5
ln -s linux-2.2.5 linux
# 按照惯例, 还是把目录另命个名, 再重新做个linux的符号连接
2.准备编译:
现在要做一些准备工作. 对于新释放出来的核心源程序也没啥好做的, 就打一个:
cd /usr/src/linux
make menuconfig
然后就会看到一个很友好的界面(在LINUX下...已经是很友好的了), 大致上有点像
WIN 9X安装时的选择安装项目. 这就是在配置核心, 选择哪些内容要, 哪些不要.
然后选EXIT退出来, 问是否保存修改时答YES. 然后会有一些提示. 如果看到了有叫你
"make dep", 就要打"make dep"先. 完了后就打 make bzImage. 如果提示信息中没有
叫你"make dep", 只有叫你 "make zImage", "make zdisk" 或 "make zlilo" 的,
就直接打 make bzImage 就行了.
一点说明: make dep 是作一些准备工作, make bzImage 则是开始编译生成核心. 而
make bzImage与make zImage的区别在于, 作成bzImage的核心压缩率比zImage
高, 核心就更小一些. make zdisk 与 make zlilo 是做别的用处的核心的.
然后就等吧(有得你等的). 一般从5分钟到半个钟头不等, 看你的机器了. 第一次编
译会 比较慢. 以后再改了配置后make就会快很多了.
等这个完了后一定还要 make modules 和 make modules_install.
make bzImage 完后会显示核心放在什么地方, 一般是/usr/src/linux/arch/i386/boot/
下. 把bzImage拷到根下. 然后修改 /etc/lilo.conf, 照着原来的image = XXXXX来加上
image = /bzImage
root = /dev/hda1 (这里视你的LINUX安装而定, 照你原有的改)
label = linux
read-only
把原来的 label = linux 改一下, 如 label = oldlinux.
把image = /bzImage 这一节加在原来的前面, 这样会自动作为缺省的核心. 你也可以在
LILO时打linux或oldlinux来启动不同的核心. 关于这一段, 也可以参考俺前面的"ALS007
发声经过". 最后, 切记切记, 一定要打个lilo来重新生成LILO程序.
好了, 重启...
[目录]
--------------------------------------------------------------------------------
重建内核选项
prompt for development and/or incomplete code/drivers
很多参考书上说这是那些开发人员认为还不是很稳定的功能,但
是根据我的经验,这个是应该选的一个选项,因为现代的LINUX是
建立在这些基础上的,所以应该可以回答Y,除非你只是想使用
LINUX中已经完全定型的东西,但性能肯定不会好到哪,而且对系
统特性的支持也不会好。
processor family (386,486/cx486,586/k5/5x86/6x86,pentinum/k6/tsc,ppro/6x86)
这应该没有太多可说的吧,选择你的CPU的种类,BIOS可以自检得
到,注意系统的启动信息。需要注意的是不能选择比你的CPU类型
还高级的CPU,否则可能不能正常工作。
math emulation
模拟数学协处理器,如果你的机器没有数学协处理器,那就选上
以提高性能,但486以后数学协处理器就集成到CPU内部了,应该是
用不上的,所以一般的选择是N。当然选上也不会有什么问题,除
了内 松陨 变大外。
mttr(memory type range register) support
这个选项是用来启动pentinum pro和pentinum II 的特殊功能,如果你用
的不是这类CPU就选N,否则也仅仅是使内核变大而已。
symmetric multi-processing support
同步处理器支持,如果你有多个CPU就选上吧。
enable loadable module support
这会启动动态载入额外模块的功能,所以一定选上。
set version information on all symbols for modules
这个选项可以为某个版本的内核而编译的模块在另一个版本的内
核下使用,但一般用不上。
kernel module loader
如果你启用这个选项,你可以通过kerneld程序的帮助在需要的时候
自动载入或卸载那些可载入式的模块。建议选上。
networking support
如果你用到任何网络就应该选上
pci bios support
这个一般是应该选上的,除非你用没有任何PCI设备的机器。PCI
BIOS是用来侦测并启用PCI设备的。
pci bridge optimization(v1.3)
当这个选项被启动时,操作系统会对从CPU和系统内存在PCI总线
来往的数据做最佳化,这个功能已经完成实验阶段,使用起来应
该很安全,而且还可增进系统的效率。
system v ipc
起用这个选项可以使内核支持System V 的进程间通信的功能
(IPC),有些从System V转移过来的程序会需要这个功能,建议启
用该功能。
sysctl support
除非你的内存少的可怜,否则你应该启动这个功能,启用该选项
后内核会大8K,但能让你直接改变内核的参数而不必重新开机。
kernel support for elf binaries
该选项让你的系统得以执行用ELF格式存储的可执行文件,而ELF
是现代LINUX的可执行文件、目标文件和系统函数库的标准格式。
当操作系统要和编译器以及连接器合作时会需要这些标准,所以
应该回答Y。
compile kernel as elf
这选项让你的内核本身以ELF的格式编译,如果你的系统上的过程
gcc默认产生ELF格式的可执行文件,那么你就应该启动这个选项。
先看看你的编译器的版本再决定。
parallel port support
如果你有任何并行口的设备并且想让LINUX使用,那么就可以启用
这个选项。LINUX不仅可以使用并口的打印机,还可以支持PLIP
(一种为并行口而设计的网络通讯协定),ZIP磁盘驱动器、扫描
仪等。在大多情况下,你需要额外的驱动程序才能使用外接的并
口设备。
plug and play support
支持PNP设备并非Microsoft的专利,如果你要让LINUX也支持PNP设
备,只要启用该选项就可以,但有些情况下会和其他设备产生冲
突(I/O,DMA,IRQ等)。这个选项对PCI设备没有影响,因为他们
天生就是PNP设备。
normal floppy disk support
除非你不想在LINUX下使用软盘,否则就应该回答Y。但对于一些
不需要支持软盘驱动器的系统而言,这个选项可以节省一些内
存。
enhanced ide/mfm/dll disk support
除非你不需要MFM/DLL/IDE硬盘的的支持,否则就应该回答Y,但如
果你只有SCSI的硬盘,关掉这个选项会比较安全。
enhanced ide/mfm/dll cdrom support
和上面的类似,只不过是对CDROM的支持而已。
enhanced ide/mfm/dll tape support
一般没有多少人在用磁带机吧,所以回答N是比较好的答案。
enhanced ide/mfm/dll floppy support
这个设备好象一般也没有人用,所以也可以回答N。
xt harddisk support
如果你有这种石器时代的XT硬盘,那么恭喜你你可以用上他了。
parallel port ide device support
LINUX是支持这种很新的并口的IDE设备的,如果你有的话就用上
吧。
networking options
如果你在前面选了支持网络的话,在这里会回答很多问题。除非
你有特别的需求,否则使用默认的选项应该就足够了。
scsi support
如果你有任何一种SCSI控制卡,这个选项就应该回答Y。事先搞清
楚你的硬件的类型,因为这些问题都是针对特定的SCSI控制芯片和
控制卡的,如果你不确定自己使用的是哪一种,查看你的硬件的
说明文件或者LINUX的HOWTO文档。同样也会让你回答很多SCSI设
备的支持(硬盘、CDROM、Tape、floppy等),依据你的情况选择。
如果你没有SCSI设备的话,建议不要支持,因为这会节约很多内核
空间。
network device support
这里面有很多关于网络控制卡的问题,如果你无法确定如何正确
选择,查看硬件文档或LINUX HOWTO文档。
amateur radio support
这个选项可以用来启动无线网络的基本支持,目前的无线网络可
以通过公众频率传输数据,如果你有此类设备就可以启用,具体
请参考AX25和HAM HOWTO 文档。
isdn subsystem
如果你有ISDN硬件就应该启用该选项并安装合适的硬件驱动程
序,你应该还会需要启用Support synchronous PPP选项(参考PPP over
ISDN)。
old cd-rom drivers
这是针对一些特殊光盘驱动器程序的问题,如果你有IDE或SCSI的
CDROM控制卡,那么就不用启用该选项了。