结合源码的操作系统学习记录(3)--系统调用

  • 系统调用过程

    当应用程序经过库函数向内核发出一个中断调用 int 0x80 时,就开始执行一个系统调用,eax 存放调用号,ebx,,ecx,edx依次存放携带的参数(最多 3 个)。根据传入参数的不同,有对应的 __syscall0,__syscall3 等宏定义

  • __syscall0

    位于 include/unistd.h 中

    #define _syscall0(type,name) \
    type name(void) \
    { \
    long __res; \
    __asm__ volatile ("int $0x80" \	// 调用系统中断
    	: "=a" (__res) \	// 返回值
    	: "0" (__NR_##name)); \	// 输入的是系统中断调用号
    if (__res >= 0) \
    	return (type) __res; \
    errno = -__res; \
    return -1; \
    }
    

    在系统调用之前需要保存现场,在用户态想内核态转换之前,将寄存器的值压入内核栈中,

    image-20220414165945886

    在 sched.c 中定义了中断调用的函数 system_call:

    set_system_gate(0x80,&system_call);
    

    跟进 system_call 的定义,这一部分是汇编实现的

    reschedule:
    	push ret_from_sys_call # 将ret_from_sys_call 的地址入栈
    	jmp _schedule
    
    _system_call:
    	cmpl $nr_system_calls-1,%eax	# 如果调用号超出范围的话就置 eax 为 -1 并退出 
    	ja bad_sys_call
    	push %ds	# 保存原段寄存器值
    	push %es	
    	push %fs
    	pushl %edx
    	pushl %ecx		# %ebx,%ecx,%edx 中存放着传入的参数
    	pushl %ebx		
    	movl $0x10,%edx		# ds,es 指向内核数据段(全局描述符表中数据段描述符)
    	mov %dx,%ds		
    	mov %dx,%es
    	movl $0x17,%edx		# fs 指向局部数据段(局部描述符表中数据段描述符)
    	mov %dx,%fs
    	call _sys_call_table(,%eax,4)		# 调用地址 = _sys_call_table + %eax * 4
    	pushl %eax		# 系统调用号入栈
    	movl _current,%eax		# 取当前任务(进程)pcb 地址
    	# 如果当前任务不在就绪状态,或在就绪状态但时间片用完,则重新执行调度程序
    	cmpl $0,state(%eax)		# state
    	jne reschedule
    	cmpl $0,counter(%eax)		# counter
    	je reschedule
    # 当从系统调用 c 函数返回后,对信号量进行识别处理
    ret_from_sys_call:
    	movl _current,%eax		# 判断进程是不是 task0,如果是则不必进行信号量方面的处理
    	cmpl _task,%eax
    	je 3f		# 向前跳到标签 3f
    	# 通过对原调用程序代码选择符的检查来判断调用程序是否是超级用户
    	# 如果是超级用户则退出中断,否则进行信号量处理
    	# 比较选择符是否为普通用户代码段的选择符 0x000f (RPL=3,局部表,第1 个段(代码段))
    	cmpw $0x0f,CS(%esp)		# was old code segment supervisor ?
    	jne 3f
    	# 如果原堆栈段选择符不为 0x17(也即原堆栈不在用户数据段中),则退出
    	cmpw $0x17,OLDSS(%esp)		# was stack segment = 0x17 ?
    	jne 3f
    	# 首先取当前任务结构中的信号位图( 32 位,每位代表 1 种信号)
    	# 然后用任务结构中的信号阻塞(屏蔽)码,阻塞不允许的信号位,取得数值最小的信号值,再把原始号位图中该信号对应的为复位
    	# 最后将该信号值作为参数之一调用 do_signal
    	movl signal(%eax),%ebx		# 取信号位图
    	movl blocked(%eax),%ecx		# 取阻塞
    	notl %ecx		# 每位取反
    	andl %ebx,%ecx		# 获得许可的信号位图
    	bsfl %ecx,%ecx		# 从低位(位0)开始扫描位图,看是否有 1 的位,若有,则 ecx 保留该位的偏移值
    	je 3f
    	btrl %ecx,%ebx		# 复位该信号(ebx 含有原 signal 位图)
    	movl %ebx,signal(%eax)		# 重新保存signal 位图信息
    	incl %ecx		#  将信号调整为从1 开始的数(1-32)。
    	pushl %ecx		# 信号值入栈作为调用 do_signal 的参数之一
    	call _do_signal
    	popl %eax		# 弹出信号值
    3:	popl %eax
    	popl %ebx
    	popl %ecx
    	popl %edx
    	pop %fs
    	pop %es
    	pop %ds
    	iret
    

    调用 call _sys_call_table(,%eax,4) 时的内核栈

    image-20220414210442520

  • read

    函数实现在 fs/read_write.c 的 sys_read 中,fd 是文件句柄,buf 是缓冲区,count 是要读的字节数

    int sys_read(unsigned int fd,char * buf,int count)
    {
    	struct file * file;
    	struct m_inode * inode;
    
        // 如果 fd 的值大于程序最多打开文件数(NR_OPEN)或者 count < 0 或者这个 fd 的文件结构指针为空,则返回出错码并退出
    	if (fd>=NR_OPEN || count<0 || !(file=current->filp[fd]))
    		return -EINVAL;
    	if (!count)
    		return 0;
        // 验证存放数据的缓冲区的内存限制
    	verify_area(buf,count);
    	inode = file->f_inode;
        // 取文件对应的 i 结点,如果是读管道文件模式,则进行读管道操作,返回读取的字节数
    	if (inode->i_pipe)
    		return (file->f_mode&1)?read_pipe(inode,buf,count):-EIO;
        // 如果是字符型文件,则执行读字符设备操作,返回读取字节数
    	if (S_ISCHR(inode->i_mode))
    		return rw_char(READ,inode->i_zone[0],buf,count,&file->f_pos);
        // 如果是块设备文件,则执行块设备读操作,并返回读取的字节数
    	if (S_ISBLK(inode->i_mode))
    		return block_read(inode->i_zone[0],&file->f_pos,buf,count);
        // 如果是目录文件或者常规文件,则首先验证 count 并进行调整
        // (若 count + 文件当前读写指针值 > 文件大小,则重新设置 count  = 文件大小 - 当前读写的指针值)
        // 成功后执行文件读操作,返回读取字节数并退出
    	if (S_ISDIR(inode->i_mode) || S_ISREG(inode->i_mode)) {
    		if (count+file->f_pos > inode->i_size)
    			count = inode->i_size - file->f_pos;
    		if (count<=0)
    			return 0;
    		return file_read(inode,file,buf,count);
    	}
        // 否则打印结点的文件属性,并返回错误码并退出
    	printk("(Read)inode->i_mode=%06o\n\r",inode->i_mode);
    	return -EINVAL;
    }
    

    以 file_read 为例,位于 fd/file_dev.c 中。i 结点确定设备号,filp 结构确定文件中当前读写指针的位置,buf 指定缓冲区地址,count 为读取字节数,最后的返回值为实际读取的字节数或出错号。

    int file_read(struct m_inode * inode, struct file * filp, char * buf, int count)
    {
    	int left,chars,nr;
    	struct buffer_head * bh;
    
        // 读取字节数小于等于 0 则直接返回
    	if ((left=count)<=0)
    		return 0;
    	while (left) {
            // bmap 根据 i 结点和文件结构信息,读数据块文件当前读写位置在设备上对应的逻辑块号(nr)
    		if (nr = bmap(inode,(filp->f_pos)/BLOCK_SIZE)) {
    			if (!(bh=bread(inode->i_dev,nr)))
    				break;
    		} else
    			bh = NULL;
            // 计算文件读写指针在数据块中的偏移值 nr
    		nr = filp->f_pos % BLOCK_SIZE;
            // 可读数据和要读数据取 min
    		chars = MIN( BLOCK_SIZE-nr , left );
            // 调整读写文件指针,指针前移此次读取字节数 chars
    		filp->f_pos += chars;
            // 剩余字节数减去 chars
    		left -= chars;
            // 若从设备上读到了数据,则将 p 指向数据块缓冲区中开始读的位置,并且复制 chars 字节到 buf 中,否则向 buf 中填入 char 个 0
    		if (bh) {
    			char * p = nr + bh->b_data;
    			while (chars-->0)
    				put_fs_byte(*(p++),buf++);
    			brelse(bh);
    		} else {
    			while (chars-->0)
    				put_fs_byte(0,buf++);
    		}
    	}
        // 修改该 i 结点的访问时间为当前时间
    	inode->i_atime = CURRENT_TIME;
        // 返回读取的字节数
    	return (count-left)?(count-left):-ERROR;
    }
    

    接着看从底层读取文件数据的函数 bread,位于 fs/buffer.c 中

    struct buffer_head * bread(int dev,int block)
    {
    	struct buffer_head * bh;
    	
        // 在高速缓冲中申请一块缓冲区,如果返回 NULL,直接死机
    	if (!(bh=getblk(dev,block)))
    		panic("bread: getblk returned NULL\n");
        // 如果数据有效(已更新),也就是之前读过的,则可以直接使用
    	if (bh->b_uptodate)
    		return bh;
        // 产生读设备块请求,从硬盘中读取数据到缓冲区
    	ll_rw_block(READ,bh);
        // ll_rw_block 会锁住 bh,在这里等待唤醒
    	wait_on_buffer(bh);
        // 读取成功后该缓冲区会更新,该字段会置 1
    	if (bh->b_uptodate)
    		return bh;
        // 否则表明读取失败,释放该缓冲区
    	brelse(bh);
    	return NULL;
    }
    

    继续往下跟 ll_rw_block 函数,位于 kernel/blk_drv/ll_rw_blk.c 中。ll_rw_block 实际上调用了 make_request 函数读取

    void ll_rw_block(int rw, struct buffer_head * bh)
    {
    	unsigned int major;
    
        // 如果设备的主设备号不存在,或者该设备的读写操作函数不存在,则报错并返回
    	if ((major=MAJOR(bh->b_dev)) >= NR_BLK_DEV ||
    	!(blk_dev[major].request_fn)) {
    		printk("Trying to read nonexistent block-device\n\r");
    		return;
    	}
        // 创建请求项并插入请求队列
    	make_request(major,rw,bh);
    }
    

    make_request:

    static void make_request(int major,int rw, struct buffer_head * bh)
    {
    	struct request * req;
    	int rw_ahead;
    
    // WRITEA / READA 是特殊情况,如果缓冲区已经上锁,就直接退出,否则就执行一般的读写操作
    // READ 和 WRITE 后面的 A 表示 Ahead,提前预读/写,如果缓冲区正在使用,也就是被上了锁,就放弃预读/写
    	if (rw_ahead = (rw == READA || rw == WRITEA)) {
    		if (bh->b_lock)
    			return;
    		if (rw == READA)
    			rw = READ;
    		else
    			rw = WRITE;
    	}
    	if (rw!=READ && rw!=WRITE)
    		panic("Bad block dev command, must be R/W/RA/WA");
    	// 缓冲区上锁
        lock_buffer(bh);
        // 如果缓冲区是写并且数据不脏,或者命令是读并且数据更新过,则不需添加请求,直接 unlock 并退出
    	if ((rw == WRITE && !bh->b_dirt) || (rw == READ && bh->b_uptodate)) {
    		unlock_buffer(bh);
    		return;
    	}
    repeat:
    // 不能让队列中全都是写请求项,需要为读请求保留一些空间
    // 读操作是优先的,请求队列的后三分之一是为读准备的
        
    // 请求项是从请求数组末尾开始搜索空项并填入的,读请求可以从队列末尾开始操作,写请求只能从队列 2/3 处填入
    	if (rw == READ)
    		req = request+NR_REQUEST;
    	else
    		req = request+((NR_REQUEST*2)/3);
    // 搜索空请求项,request 结构的 dev 字段为 -1 时,表示该项未被占用
    	while (--req >= request)
    		if (req->dev<0)
    			break;
    
    // 如果没有找到空闲项,则查看此次请求是否是提前读/写,如果是则放弃此次请求,否则让本次请求睡眠(等待请求队列腾出空项)
    	if (req < request) {
    		if (rw_ahead) {
    			unlock_buffer(bh);
    			return;
    		}
    		sleep_on(&wait_for_request);
    		goto repeat;
    	}
    // 向空闲请求项中填写信息,并将其加入队列
    	req->dev = bh->b_dev;	// 设备号
    	req->cmd = rw;	// 命令(READ/WRITE)
    	req->errors=0;	// 操作时产生的错误次数
    	req->sector = bh->b_blocknr<<1;	// 起始扇区(1块=2扇区)
    	req->nr_sectors = 2;	// 读写扇区数
    	req->buffer = bh->b_data;	// 数据缓冲区
    	req->waiting = NULL;	// 任务等待操作执行完成的地方
    	req->bh = bh;	// 缓冲区头指针
    	req->next = NULL;	// 指向下一请求项
    	add_request(major+blk_dev,req);	// 将此次请求项加入请求队列中
    }
    

    继续跟进 add_request,看一下加入请求队列的过程,dev 时指定的块设备,req 时请求项的信息

    static void add_request(struct blk_dev_struct * dev, struct request * req)
    {
    	struct request * tmp;
    
    	req->next = NULL;
    	cli();	// 关中断
    	if (req->bh)
    		req->bh->b_dirt = 0;	// 清缓冲区的脏标记
        // 如果 dev 的当前请求字段为空,则表示目前该设备没有请求项,本次时第一个请求项
        // 因此可将块设备当前的请求指针直接指向请求项,并立刻执行响应设备的请求函数
    	if (!(tmp = dev->current_request)) {
    		dev->current_request = req;
    		sti();	// 开中断
    		(dev->request_fn)();	// 执行设备请求函数,实际执行了 do_hd_request
    		return;
    	}
        // 如果目前该设备已经有请求项在等待,则首先利用电梯算法搜索最佳位置,然后将当前请求插入请求链表中
    	for ( ; tmp->next ; tmp=tmp->next)
    		if ((IN_ORDER(tmp,req) ||
    		    !IN_ORDER(tmp,tmp->next)) &&
    		    IN_ORDER(req,tmp->next))
    			break;
    	req->next=tmp->next;
    	tmp->next=req;
    	sti();
    }
    

    插入队列之后,看一下最终执行操作的 do_hd_request,读操作的分支

    	else if (CURRENT->cmd == READ)
    	{
    		hd_out (dev, nsect, sec, head, cyl, WIN_READ, &read_intr);
    	}
    

    继续跟进 hd_out ,该函数的作用是向硬盘控制器发送命令块。其中,drive 时硬盘号(0-1),nsect 是读写扇区数,sect 是起始扇区,head 是磁头号,cyl 是柱面号,cmd 是命令码,*intr_addr() 是硬盘中断处理程序中将调用的 c 处理函数。

    static void hd_out(unsigned int drive,unsigned int nsect,unsigned int sect,
    		unsigned int head,unsigned int cyl,unsigned int cmd,
    		void (*intr_addr)(void))
    {
    	register int port asm("dx");	// port 变量对应寄存器dx。
    
        // 不支持的读写
    	if (drive>1 || head>15)
    		panic("Trying to write bad sector");
        // 等待一段时间后仍未就绪则出错死机
    	if (!controller_ready())
    		panic("HD controller not ready");
    	do_hd = intr_addr;		// do_hd 函数指针将在硬盘中断程序中被调用。
    	outb_p (hd_info[drive].ctl, HD_CMD);	// 向控制寄存器(0x3f6)输出控制字节。
    	port = HD_DATA;		// 置dx 为数据寄存器端口(0x1f0)。
    	outb_p (hd_info[drive].wpcom >> 2, ++port);	// 参数:写预补偿柱面号(需除4)。
    	outb_p (nsect, ++port);	// 参数:读/写扇区总数。
    	outb_p (sect, ++port);	// 参数:起始扇区。
    	outb_p (cyl, ++port);		// 参数:柱面号低8 位。
    	outb_p (cyl >> 8, ++port);	// 参数:柱面号高8 位。
    	outb_p (0xA0 | (drive << 4) | head, ++port);	// 参数:驱动器号+磁头号。
    	outb (cmd, ++port);		// 命令:硬盘控制命令。
    }
    

    到现在,从驱动到硬盘控制器的读处理就完成了,再回去看 ll_rw_block 之后的处理,接着是 wait_on_buffer。

    static inline void wait_on_buffer(struct buffer_head * bh)
    {
    	cli();		// 关中断。
    	while (bh->b_lock)	// 如果已被上锁,则进程进入睡眠,等待其解锁。
    		sleep_on(&bh->b_wait);
    	sti();		// 开中断。
    }
    

    具体分析一下 sleep_on,这里主要是一些进程调度的处理了,将当前进程挂载到睡眠队列 p 中(此时任务置为不可中断的等待状态),并让睡眠队列头的指针指向当前任务。

    void sleep_on(struct task_struct **p)
    {
    	struct task_struct *tmp;
    
    	if (!p)
    		return;
        // 如果当前任务是 0 则死机
    	if (current == &(init_task.task))
    		panic("task[0] trying to sleep");
        // tmp 指向已经在等待队列上的任务,等待队列头指针指向当前任务
    	tmp = *p;
    	*p = current;
        // // 将当前任务置为不可中断的等待状态。
    	current->state = TASK_UNINTERRUPTIBLE;
        // 重新调度
    	schedule();
        // 只有当这个等待任务呗唤醒时,调度程序才会回到这里
        // 若还存在等待的任务,则也将其置为就绪状态(唤醒)
    	if (tmp)
    		tmp->state=0;
    }
    

    当硬盘读好数据之后,给系统发送中断,执行 hd_interrupt

    _hd_interrupt:
    	push eax
    	push ecx
    	push edx
    	push ds
    	push es
    	push fs
    	mov eax,10h ; ds,es 置为内核数据段。
    	mov ds,ax
    	mov es,ax
    	mov eax,17h ; fs 置为调用程序的局部数据段。
    	mov fs,ax
    ; 由于初始化中断控制芯片时没有采用自动EOI,所以这里需要发指令结束该硬件中断。
    	mov al,20h
    	out 0A0h,al ; EOI to interrupt controller ;//1 ;// 送从8259A。
    	jmp l3 ; give port chance to breathe
    l3: jmp l4 ; 延时作用。
    l4: xor edx,edx
    	xchg edx,dword ptr _do_hd ; do_hd 定义为一个函数指针,将被赋值read_intr()或 write_intr()函数地址。(kernel/blk_drv/hd.c)
    ; 放到edx 寄存器后就将do_hd 指针变量置为NULL。
    	test edx,edx ; 测试函数指针是否为Null。
    	jne l5 ; 若空,则使指针指向C 函数unexpected_hd_interrupt()。
    	mov edx,dword ptr _unexpected_hd_interrupt ; (kernel/blk_drv/hdc,237)。
    l5: out 20h,al ; 送主8259A 中断控制器EOI 指令(结束硬件中断)。
    	call edx ; "interesting" way of handling intr.
    	pop fs ; 上句调用do_hd 指向的C 函数。
    	pop es
    	pop ds
    	pop edx
    	pop ecx
    	pop eax
    	iretd
    

    显然在读操作中最终会调用 read_intr 函数,

    static void read_intr(void)
    {
        // 如果硬盘执行命令后出错
    	if (win_result()) {
    		bad_rw_intr();	// 读写硬盘失败处理
    		do_hd_request();	// 再次请求硬盘作相应处理
    		return;
    	}
    	port_read(HD_DATA,CURRENT->buffer,256);	// // 将数据从数据寄存器口读到请求结构缓冲区。
    	CURRENT->errors = 0;		// 清出错次数。
    	CURRENT->buffer += 512;	// 调整缓冲区指针,指向新的空区。
    	CURRENT->sector++;		// 起始扇区号加1,
        // 如果需要读出的扇区数还没读完,则再次置硬盘调用 c 函数指针为  read_intr()
    	if (--CURRENT->nr_sectors) {
    		do_hd = &read_intr;
    		return;
    	}
    	end_request (1);		// 若全部扇区数据已经读完,则处理请求结束事宜,
    	do_hd_request ();		// 执行其它硬盘请求操作。
    }
    

    终于结束了,最后再看一下 end_request

    extern inline void end_request(int uptodate)
    {
    	DEVICE_OFF(CURRENT->dev);	// 关闭设备
        // 读写成功,b_uptodate 置 1,解锁缓冲区
    	if (CURRENT->bh) {
    		CURRENT->bh->b_uptodate = uptodate;
    		unlock_buffer(CURRENT->bh);
    	}
        // 如果 b_uptodate 标志为 0,则显示设备错误信息
    	if (!uptodate) {
    		printk(DEVICE_NAME " I/O error\n\r");
    		printk("dev %04x, block %d\n\r",CURRENT->dev,
    			CURRENT->bh->b_blocknr);
    	}
    	wake_up(&CURRENT->waiting);	// 唤醒等待该请求项的进程
    	wake_up(&wait_for_request);	// 唤醒等待请求的进程
    	CURRENT->dev = -1; // 释放该请求项
    	CURRENT = CURRENT->next; // 从请求链表中删除该请求项
    }
    
  • write

    write 函数的部分逻辑和 read 相似,首先看入口的 sys_write 函数,对比可以看出,流程基本相同,就是将对应的处理函数换了一下

    int sys_write (unsigned int fd, char *buf, int count)
    {
    	struct file *file;
    	struct m_inode *inode;
    
    // 如果文件句柄值大于程序最多打开文件数NR_OPEN,或者需要写入的字节计数小于0,或者该句柄的文件结构指针为空,则返回出错码并退出。
    	if (fd >= NR_OPEN || count < 0 || !(file = current->filp[fd]))
    		return -EINVAL;
    // 若需读取的字节数count 等于0,则返回0,退出
    	if (!count)
    		return 0;
    // 取文件对应的i 节点。若是管道文件,并且是写管道文件模式,则进行写管道操作,若成功则返回
    // 写入的字节数,否则返回出错码,退出。
    	inode = file->f_inode;
    	if (inode->i_pipe)
    		return (file->f_mode & 2) ? write_pipe (inode, buf, count) : -EIO;
    // 如果是字符型文件,则进行写字符设备操作,返回写入的字符数,退出。
    	if (S_ISCHR (inode->i_mode))
    		return rw_char (WRITE, inode->i_zone[0], buf, count, &file->f_pos);
    // 如果是块设备文件,则进行块设备写操作,并返回写入的字节数,退出。
    	if (S_ISBLK (inode->i_mode))
    		return block_write (inode->i_zone[0], &file->f_pos, buf, count);
    // 若是常规文件,则执行文件写操作,并返回写入的字节数,退出。
    	if (S_ISREG (inode->i_mode))
    		return file_write (inode, file, buf, count);
    // 否则,显示对应节点的文件模式,返回出错码,退出。
    	printk ("(Write)inode->i_mode=%06o\n\r", inode->i_mode);
    	return -EINVAL;
    }
    

    同样以 file_write 为例,跟进查看一下

    int file_write(struct m_inode * inode, struct file * filp, char * buf, int count)
    {
    	off_t pos;
    	int block,c;
    	struct buffer_head * bh;
    	char * p;
    	int i=0;
    
        // 如果是要向文件后添加数据,则将文件读写指针移到文件尾部,否则就在文件读写指针写入
    	if (filp->f_flags & O_APPEND)
    		pos = inode->i_size;
    	else
    		pos = filp->f_pos;
        // 若已写入字节数 i 小于需要写入的字节数 count
    	while (i<count) {
            // 创建数据块号(pos/BLOCK_SIZE)在设备上对应的逻辑块,并返回在设备上的逻辑块号
    		if (!(block = create_block(inode,pos/BLOCK_SIZE)))
    			break;
            // 根据该逻辑块号读取设备上的相应数据块
    		if (!(bh=bread(inode->i_dev,block)))
    			break;
            // 求出文件读写指针在数据块中的偏移值c,
    		c = pos % BLOCK_SIZE;
            // 将 p 指向读出数据块缓冲区中开始读取的位置
    		p = c + bh->b_data;
            // 脏标记,表示缓冲区已修改
    		bh->b_dirt = 1;
            // 可写的长度
    		c = BLOCK_SIZE-c;
            // 在可写长度和能写长度中取小的
    		if (c > count-i) c = count-i;
            // 文件读写指针前移此次需要写入的字节数,如果当前文件读写指针位置超过了文件大小,就修改 i 结点中文件大小字段,并标记 i 已修改
    		pos += c;
    		if (pos > inode->i_size) {
    			inode->i_size = pos;
    			inode->i_dirt = 1;
    		}
            // 已写入的字节数累加到此次写入的字节数c
    		i += c;
            // 从用户缓冲区 buf 中复制 c 个字节到高速缓冲区中
          // p 指向开始位置处,然后释放该缓冲区
    		while (c-->0)
    			*(p++) = get_fs_byte(buf++);
    		brelse(bh);
    	}
        // 更新文件修改时间
    	inode->i_mtime = CURRENT_TIME;、
        // 如果此次操作不是在文件尾添加数据,则把文件读写指针调整到当前读写位置,并更改 i 节点修改时间为当前时间。
    	if (!(filp->f_flags & O_APPEND)) {
    		filp->f_pos = pos;
    		inode->i_ctime = CURRENT_TIME;
    	}
        // 返回写入的字节数
    	return (i?i:-1);
    }
    

    其中创建逻辑块的 create_block 实际上是调用的 _bmap 函数,

    inode :文件的 i 结点,block :文件中的数据块号,create:创建标志

    // 创建文件数据块block 在设备上对应的逻辑块,并返回设备上对应的逻辑块号。
    int create_block(struct m_inode * inode, int block)
    {
    	return _bmap(inode,block,1);
    }
    
    static int _bmap(struct m_inode * inode,int block,int create)
    {
    	struct buffer_head * bh;
    	int i;
    
    	if (block<0)
    		panic("_bmap: block<0");
        // 如果块号大于 直接块数 + 间接块数 + 二次间接块数,超出文件系统表示范围,则死机。
    	if (block >= 7+512+512*512)
    		panic("_bmap: block>big");
        // 如果该块号小于 7,则使用直接块表示
    	if (block<7) {
            // 如果传入的创建标志位为1,并且 i 结点中对应改块的逻辑块字段为 0
    		if (create && !inode->i_zone[block])
                // 则向相应设备申请一磁盘块,并将盘上的逻辑块号填入逻辑块字段中
    			if (inode->i_zone[block]=new_block(inode->i_dev)) {
                    // 更新 i 结点时间和脏标记
    				inode->i_ctime=CURRENT_TIME;
    				inode->i_dirt=1;
    			}
    		return inode->i_zone[block];
    	}
        // 如果该块号在 7 ~ 7+512 中,说明这是一次间接块
    	block -= 7;
    	if (block<512) {
            同上
    		if (create && !inode->i_zone[7])
    			if (inode->i_zone[7]=new_block(inode->i_dev)) {
    				inode->i_dirt=1;
    				inode->i_ctime=CURRENT_TIME;
    			}
    		if (!inode->i_zone[7])
    			return 0;
            // 读取设备上的一次间接块
    		if (!(bh = bread(inode->i_dev,inode->i_zone[7])))
    			return 0;
            // 取该简介快上第 block 项中的逻辑块号(盘块号)
    		i = ((unsigned short *) (bh->b_data))[block];
    		if (create && !i)
                // 申请一磁盘块
    			if (i=new_block(inode->i_dev)) {
                    // 简介快中的第 block 项等于新逻辑块块号
    				((unsigned short *) (bh->b_data))[block]=i;
    				bh->b_dirt=1;
    			}
            // 释放该简介块
    		brelse(bh);
    		return i;
    	}
    	block -= 512;
        // 申请一磁盘块用于存放二次简接块
    	if (create && !inode->i_zone[8])
    		if (inode->i_zone[8]=new_block(inode->i_dev)) {
    			inode->i_dirt=1;
    			inode->i_ctime=CURRENT_TIME;
    		}
        // 若此时 i 结点二次间接块字段为 0,表明申请磁盘块失败
    	if (!inode->i_zone[8])
    		return 0;
        // 读取该二次间接块的一级块
    	if (!(bh=bread(inode->i_dev,inode->i_zone[8])))
    		return 0;
        // 取该二次间接块的一级块上第(block/512)项中的逻辑块号
    	i = ((unsigned short *)bh->b_data)[block>>9];
        // 如果二次间接块的一级块上第(block/512)项中的逻辑块号为 0 的话,则需申请一磁盘块作为二次间接块的二级块
    	if (create && !i)
    		if (i=new_block(inode->i_dev)) {
                // 让二次间接块的一级块中第(block/512)项等于该二级块的块号
    			((unsigned short *) (bh->b_data))[block>>9]=i;
    			bh->b_dirt=1;
    		}
    	brelse(bh);
    	if (!i)
    		return 0;
        // (读取二次间接块的二级块)获取二级索引对应的数据
    	if (!(bh=bread(inode->i_dev,i)))
    		return 0;
        // 取该二级块上第 block 项中的逻辑块号
    	i = ((unsigned short *)bh->b_data)[block&511];
    	if (create && !i)
    		if (i=new_block(inode->i_dev)) {
    			((unsigned short *) (bh->b_data))[block&511]=i;
    			bh->b_dirt=1;
    		}
    	brelse(bh);
    	return i;
    }
    

    跟进创建块的 new_block 操作,根据当前块的使用情况申请一个新的块并标记已使用,然后把超级块的信息写回到硬盘,并返回新建的块号

    int new_block(int dev)
    {
    	struct buffer_head * bh;
    	struct super_block * sb;
    	int i,j;
    
        // 从设备 dev 中取超级块
    	if (!(sb = get_super(dev)))
    		panic("trying to get new block from nonexistant device");
        // 扫描逻辑块位图,寻找空闲逻辑块,获取放置该逻辑块的块号
    	j = 8192;
    	for (i=0 ; i<8 ; i++)
            // s_zmap[i]为数据块位图的缓存 
    		if (bh=sb->s_zmap[i])
    			if ((j=find_first_zero(bh->b_data))<8192)
    				break;
        // 如果全部扫描完还没找到,或者位图所在缓冲块无效,则退出
    	if (i>=8 || !bh || j>=8192)
    		return 0;
        // 设置新逻辑块对应逻辑块位图中的 bit 位,若对应 bit 位已置为,则死机
        // 
    	if (set_bit(j,bh->b_data))
    		panic("new_block: bit already set");
        // 更新脏标记,该位图对应的 buffer 需要回写
    	bh->b_dirt = 1;
        // 如果新逻辑块大于该设备上的总逻辑块数,则说明指定逻辑块在对应设备上不存在,申请失败
        // 位图存在多个块中,i 位第 i 个块,每个块对应的位图管理者 8192 个数据块
    	j += i*8192 + sb->s_firstdatazone-1;
    	if (j >= sb->s_nzones)
    		return 0;
        // 读取设备上的该新逻辑块数据,失败则死机
    	if (!(bh=getblk(dev,j)))
    		panic("new_block: cannot get block");
        // 新块的引用计数应为1。否则死机
    	if (bh->b_count != 1)
    		panic("new block: count is != 1");
        // 将该新逻辑块清零
    	clear_block(bh->b_data);
        // 更新已更新标记和脏标记,并释放对应缓冲区
    	bh->b_uptodate = 1;
    	bh->b_dirt = 1;
    	brelse(bh);
    	return j;
    }
    

    在 create_block 之后,需要用 bread 把块的内容读进来存到 buffer 中(新块内容都是 0),接着就可以把数据写到 buffer 中。

    在写文件的时候数据不是直接到硬盘的,只是放在缓存里,系统会有线程定期更新缓存到硬盘。

  • sync

    可以实时将数据同步到硬盘

    int sys_sync(void)
    {
    	int i;
    	struct buffer_head * bh;
    
        // 将所有 i 节点写入高速缓冲
    	sync_inodes();		
        // 扫描所有高速缓冲区,对于已被修改的缓冲块产生的写请求,将缓冲区中的数据与设备中同步
    	bh = start_buffer;
    	for (i=0 ; i<NR_BUFFERS ; i++,bh++) {
            // 等待缓冲区解锁
    		wait_on_buffer(bh);
    		if (bh->b_dirt)
                // 写设备块请求,等待底层驱动写回到硬盘,不一定立刻写入        
    			ll_rw_block(WRITE,bh);
    	}
    	return 0;
    }
    

    先看 sync_inodes 函数,该函数把 inode_table 中(进程打开文件对应的 inode 节点)写入到 buffer 中。

    void sync_inodes(void)
    {
    	int i;
    	struct m_inode * inode;
    
        // 指针首先指向 i 节点表指针数组首项 
    	inode = 0+inode_table;
        // 扫描 i 节点表数组指针
    	for(i=0 ; i<NR_INODE ; i++,inode++) {
            // 等待该 i 节点可用
    		wait_on_inode(inode);
            // i 结点已修改
            // 管道内容直接存在在内存中,所以不需要写
    		if (inode->i_dirt && !inode->i_pipe)
                // 写盘
    			write_inode(inode);
    	}
    }
    

    继续跟进写盘操作

    static void write_inode(struct m_inode * inode)
    {
    	struct super_block * sb;
    	struct buffer_head * bh;
    	int block;
    
        // 先对该节点加锁,如果改节点没在修改中活着该节点的设备号为0,则解锁并退出
    	lock_inode(inode);
    	if (!inode->i_dirt || !inode->i_dev) {
    		unlock_inode(inode);
    		return;
    	}
        // 获取该 i 节点的超级快
    	if (!(sb=get_super(inode->i_dev)))
    		panic("trying to write inode without device");
        // 该节点的逻辑块号 = 2(启动块+超级块) + inode 位图占用的块数 + 逻辑块位图占用的块数 + inode 的相对偏移(i 节点号-1)/每块含有的i 节点数
    	block = 2 + sb->s_imap_blocks + sb->s_zmap_blocks +
    		(inode->i_num-1)/INODES_PER_BLOCK;
        // 读取该 i 节点所在的逻辑块
    	if (!(bh=bread(inode->i_dev,block)))
    		panic("unable to read i-node block");
        // 将该 i 节点信息复制到逻辑块对应该 i 节点的项中
    	((struct d_inode *)bh->b_data)
    		[(inode->i_num-1)%INODES_PER_BLOCK] =
    			*(struct d_inode *)inode;
        // 更新缓冲区的脏标记和 i 节点修改标记
    	bh->b_dirt=1;
    	inode->i_dirt=0;
        // 释放并解锁
    	brelse(bh);
    	unlock_inode(inode);
    }
    

    现在 i 节点的内容以及在 buffer 中了,然后遍历 buffer,通过 ll_rw_block 写回硬盘。

  • 用于在文件系统中创建硬链接(需要一些文件系统的知识)

    int sys_link(const char * oldname, const char * newname)
    {
    	struct dir_entry * de;
    	struct m_inode * oldinode, * dir;
    	struct buffer_head * bh;
    	const char * basename;
    	int namelen;
    
        // 取源文件路径名对应的 i 节点
    	oldinode=namei(oldname);
    	if (!oldinode)
    		return -ENOENT;
        // 如果原路径名对应的是一个目录名,则释放该 i 节点
    	if (S_ISDIR(oldinode->i_mode)) {
    		iput(oldinode);
    		return -EPERM;
    	}
        // 查找新路径名的最顶层目录的 i 节点,并返回最后的文件名及其长度
        // 如果没找到,则释放原路径名的 i 节点
    	dir = dir_namei(newname,&namelen,&basename);
    	if (!dir) {
    		iput(oldinode);
    		return -EACCES;
    	}
        // 如果新路径名中不包括文件名,则释放原路径名 i 节点和新路径名目录的 i 节点
    	if (!namelen) {
    		iput(oldinode);
    		iput(dir);
    		return -EPERM;
    	}
        // 如果新路径名目录的设备号与原路径名的设备号不一样,则也不能建立连接
    	if (dir->i_dev != oldinode->i_dev) {
    		iput(dir);
    		iput(oldinode);
    		return -EXDEV;
    	}
        // 如果用户没有在新目录中写的权限,则也不能建立连接
    	if (!permission(dir,MAY_WRITE)) {
    		iput(dir);
    		iput(oldinode);
    		return -EACCES;
    	}
        // 查询该新路径名是否已经存在,如果存在,则也不能建立连接
    	bh = find_entry(&dir,basename,namelen,&de);
    	if (bh) {
    		brelse(bh);
    		iput(dir);
    		iput(oldinode);
    		return -EEXIST;
    	}
        // 在新目录中添加一个目录项,若失败则释放该目录的 i 节点和原路径名的i 节点
    	bh = add_entry(dir,basename,namelen,&de);
    	if (!bh) {
    		iput(dir);
    		iput(oldinode);
    		return -ENOSPC;
    	}
        // 如果上面的判断都过了,就开始创建硬链接
        // 设置该目录项的 i 节点号等于原路径的 i 节点号,并更新包含该新添目录项的高速缓冲区的脏标记,然后释放目录的 i 节点
    	de->inode = oldinode->i_num;
    	bh->b_dirt = 1;
    	brelse(bh);
    	iput(dir);
        // 原 i 节点的应用计数+1,更新修改时间和脏标记
    	oldinode->i_nlinks++;
    	oldinode->i_ctime = CURRENT_TIME;
    	oldinode->i_dirt = 1;
    	iput(oldinode);
    	return 0;
    }
    

    image-20220413194113065

  • 对应就是删除硬链接的系统调用

    int sys_unlink(const char * name)
    {
    	const char * basename;
    	int namelen;
    	struct m_inode * dir, * inode;
    	struct buffer_head * bh;
    	struct dir_entry * de;
    
    // 如果找不到对应路径名目录的 i 节点,则返回出错码。
    	if (!(dir = dir_namei(name,&namelen,&basename)))
    		return -ENOENT;
    // 如果最顶端的文件名长度为 0,则说明给出的路径名最后没有指定文件名,释放该目录 i 节点,
    // 返回出错码,退出。
    	if (!namelen) {
    		iput(dir);
    		return -ENOENT;
    	}
    // 如果在该目录中没有写的权限,则释放该目录的 i 节点,返回访问许可出错码,退出。
    	if (!permission(dir,MAY_WRITE)) {
    		iput(dir);
    		return -EPERM;
    	}
    // 如果对应路径名上最后的文件名的目录项不存在,则释放包含该目录项的高速缓冲区,释放目录的 i 节点,返回文件已经存在出错码,退出。否则 dir 是包含要被删除目录名的目录 i 节点,de 是要被删除目录的目录项结构。
    	bh = find_entry(&dir,basename,namelen,&de);
    	if (!bh) {
    		iput(dir);
    		return -ENOENT;
    	}
    // 取该目录项指明的 i 节点。若出错则释放目录的 i 节点,并释放含有目录项的高速缓冲区,
    // 返回出错号。
    	if (!(inode = iget(dir->i_dev, de->inode))) {
    		iput(dir);
    		brelse(bh);
    		return -ENOENT;
    	}
    // 如果该目录设置了受限删除标志并且用户不是超级用户,并且进程的有效用户 id 不等于被删除文件名 i 节点的用户id,并且进程的有效用户 id 也不等于目录 i 节点的用户 id,则没有权限删除该文件名。则释放该目录 i 节点和该文件名目录项的 i 节点,释放包含该目录项的缓冲区,返回出错号。
    	if ((dir->i_mode & S_ISVTX) && !suser() &&
    	    current->euid != inode->i_uid &&
    	    current->euid != dir->i_uid) {
    		iput(dir);
    		iput(inode);
    		brelse(bh);
    		return -EPERM;
    	}
    // 如果该指定文件名是一个目录,则也不能删除,释放该目录 i 节点和该文件名目录项的 i 节点,
    // 释放包含该目录项的缓冲区,返回出错号。
    	if (S_ISDIR(inode->i_mode)) {
    		iput(inode);
    		iput(dir);
    		brelse(bh);
    		return -EPERM;
    	}
    // 如果该 i 节点的连接数已经为0,则显示警告信息,修正其为 1。
    	if (!inode->i_nlinks) {
    		printk("Deleting nonexistent file (%04x:%d), %d\n",
    			inode->i_dev,inode->i_num,inode->i_nlinks);
    		inode->i_nlinks=1;
    	}
    // 将该文件名的目录项中的 i 节点号字段置为 0,表示释放该目录项,并设置包含该目录项的缓冲区已修改标志,释放该高速缓冲区。
    	de->inode = 0;
    	bh->b_dirt = 1;
    	brelse(bh);
    // 该 i 节点的连接数减 1,置已修改标志,更新改变时间为当前时间。最后释放该 i 节点和目录的 i 节点,返回0(成功)。
    	inode->i_nlinks--;
    	inode->i_dirt = 1;
    	inode->i_ctime = CURRENT_TIME;
    	iput(inode);
    	iput(dir);
    	return 0;
    }
    
  • sys_close

    负责关闭一个文件,主要步骤:

    1. 根据文件描述符,把指针数组的对应项置空
    2. 如果指向的 file 结构体没有其他进程在使用,则这个 file 结构体可以重用,但他指向的 i 节点需要写回到硬盘。
    int sys_close(unsigned int fd)
    {	
    	struct file * filp;
    
        // 若文件句柄值大于程序同时能打开的文件数,则返回出错码。
    	if (fd >= NR_OPEN)
    		return -EINVAL;
        // 清除 close_on_exec 标记,该标记表示 fork+exec 时关闭该文件
    	current->close_on_exec &= ~(1<<fd);
        // 若该文件句柄的文件结构指针为 NULL,报错
    	if (!(filp = current->filp[fd]))
    		return -EINVAL;
        // 置该文件句柄的文件结构指针为 NULL
    	current->filp[fd] = NULL;
        // 若在关闭文件之前,对应文件结构中的句柄引用计数已经为 0,说明内核出错
    	if (filp->f_count == 0)
    		panic("Close: file count is 0");
        // 否则将对应文件结构的句柄引用计数减 1,如果还不为 0,则返回 0(成功)。
    	if (--filp->f_count)
    		return (0);
        // 若已等于 0,说明该文件已经没有句柄引用,则释放该文件 i 节点,返回0。
    	iput(filp->f_inode);
    	return (0);
    }
    

    跟进 inode 的释放操作 iput

    void iput(struct m_inode * inode)
    {
    	if (!inode)
    		return;
        // 用进程在使用这个 i 节点则等待
    	wait_on_inode(inode);
    	if (!inode->i_count)
    		panic("iput: trying to free free inode");
        // 如果是管道 i 节点,则唤醒等待该管道的进程,引用次数减 1
    	if (inode->i_pipe) {
    		wake_up(&inode->i_wait);
            // 如果还有引用则返回
    		if (--inode->i_count)
    			return;
            // 对于 pipe 节点,其 i_size 存放着物理内存页地址
    		free_page(inode->i_size);
    		inode->i_count=0;
    		inode->i_dirt=0;
    		inode->i_pipe=0;
    		return;
    	}
        // 如果 i 节点对应的设备号为 0,则将此节点的引用计数递减 1,返回。
    	if (!inode->i_dev) {
    		inode->i_count--;
    		return;
    	}
        // 如果是块设备文件的 i 节点,此时逻辑块字段 0 中是设备号,则刷新该设备。并等待 i 节点解锁。
    	if (S_ISBLK(inode->i_mode)) {
    		sync_dev(inode->i_zone[0]);
    		wait_on_inode(inode);
    	}
    repeat:
    	if (inode->i_count>1) {
    		inode->i_count--;
    		return;
    	}
        // 如果该 i 节点的链接数为 0,则释放该 i 节点的所有逻辑块,并释放该 i 节点
    	if (!inode->i_nlinks) {
    		truncate(inode);
    		free_inode(inode);
    		return;
    	}
        // 如果该 i 节点已作过修改,则先更新该 i 节点,并等待该 i 节点解锁
    	if (inode->i_dirt) {
    		write_inode(inode);	/* we can sleep - so do again */
    		wait_on_inode(inode);
    		goto repeat;
    	}
    	inode->i_count--;
    	return;
    }
    
  • sys_exit

    用于进程退出,实际上调用了 do_exit 函数

    int sys_exit(int error_code)
    {
         return do_exit((error_code&0xff)<<8);
    }
    

    do_exit 的流程在进程调用中分析过,这里重点看 free_page_tables 的实现过程。 free_page_tables 的作用是根据指定的线性地址和页表个数,释放对应内存页表所指定的内存块,并置表项空闲。其中:

    • 页目录位于物理地址 0 开始处,共 1024 项,占 4k 字节,每个目录项指定一个页表
    • 页表从物理地址 0x1000 处开始(紧接着目录空间),每个页表有 1024 项,也占 4k 内存
    • 每个页表项对应一页物理内存(4K),目录项和页表项的大小均为 4 个字节

    from 是其实地址,size 是释放长度

    int free_page_tables(unsigned long from,unsigned long size)
    {
    	unsigned long *pg_table;
    	unsigned long * dir, nr;
    
        // 要释放的内存块的地址需以 4M 为边界
    	if (from & 0x3fffff)
    		panic("free_page_tables called with wrong alignment");
    	if (!from)
    		panic("Trying to free up swapper memory space");
        // 计算所占页目录项数(4M 的整数倍),也即所占页数
    	size = (size + 0x3fffff) >> 22;
        // 计算起始目录项,对应的目录项号=from>>22
        // 因每项占 4 字节,并且由于页目录是从物理地址 0 开始,因此实际的目录项指针 = 目录项号 << 2,也即 from >> 20
        // 与上 0xffc 确保指针范围有效
    	dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */
        // size 现在是需要被释放内存的目录项数
    	for ( ; size-->0 ; dir++) {
            // 如果该目录项无效( P 位 = 0),则继续
    		if (!(1 & *dir))
    			continue;
            // 取目录项中页表地址
    		pg_table = (unsigned long *) (0xfffff000 & *dir);
            // 每个页表有 1024 个页项
    		for (nr=0 ; nr<1024 ; nr++) {
                // 若该页表项有效( P 位 = 1),则释放对应内存页
    			if (1 & *pg_table)
    				free_page(0xfffff000 & *pg_table);
    			*pg_table = 0;// 该页表项内容清零。
    			pg_table++;// 指向页表中下一项。
    		}
            // 释放该页表所占内存页面
    		free_page(0xfffff000 & *dir);
    		*dir = 0;
    	}
    	invalidate();	// 刷新页变换高速缓冲
    	return 0;
    }
    
posted @ 2022-04-14 21:11  moon_flower  阅读(113)  评论(0)    收藏  举报