linux系统调用之read源码解析(基于linux0.11)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/THEANARKH/article/details/89789776

进程通过系统调用,从而进入中断处理,中断处理从系统调用表里找到sys_read函数执行。

int sys_read(unsigned int fd,char * buf,int count)
{
	struct file * file;
	struct m_inode * inode;

	if (fd>=NR_OPEN || count<0 || !(file=current->filp[fd]))
		return -EINVAL;
	if (!count)
		return 0;
	verify_area(buf,count);
	inode = file->f_inode;
	// 该文件描述符对应的是一个管道文件,并且是读端则读管道
	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);
	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函数。

int file_read(struct m_inode * inode, struct file * filp, char * buf, int count)
{
	int left,chars,nr;
	struct buffer_head * bh;

	if ((left=count)<=0)
		return 0;
	while (left) {
		// bmap取得该文件偏移对应的硬盘块号,然后读进来
		if (nr = bmap(inode,(filp->f_pos)/BLOCK_SIZE)) {
			if (!(bh=bread(inode->i_dev,nr)))
				break;
		} else
			bh = NULL;
		// 偏移
		nr = filp->f_pos % BLOCK_SIZE;
		// 读进来的数据中,可读的长度和还需要读的长度,取小的,如果还没读完继续把块从硬盘读进来
		chars = MIN( BLOCK_SIZE-nr , left );
		filp->f_pos += chars; // 更新偏移指针
		left -= chars; // 更新还需药读取的长度
		if (bh) {
			char * p = nr + bh->b_data;
			while (chars-->0)
				put_fs_byte(*(p++),buf++); //复制到buf里 
			brelse(bh);
		} else {
			while (chars-->0)
				put_fs_byte(0,buf++);
		}
	}
	// 更新访问时间
	inode->i_atime = CURRENT_TIME;
	// 返回读取的长度,如果一个都没读则返回错误
	return (count-left)?(count-left):-ERROR;
}

这个函数主要的操作是从底层读取对应文件的对应数据。这里的底层首先是buffer缓存,如果没有的话需要去硬盘读。我们看bread函数。

struct buffer_head * bread(int dev,int block)
{
	struct buffer_head * bh;
	// 先从buffer链表中获取一个buffer
	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;
}

bread函数首先从缓存里读取需要的数据,如果有并且是有效的即最新的数据。则直接返回。如果没有的话,就调用ll_rw_block函数到硬盘读。我们看一下ll_rw_block及相关函数。

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);
}

static void make_request(int major,int rw, struct buffer_head * bh)
{
	struct request * req;
	int rw_ahead;

/* WRITEA/READA is special case - it is not really needed, so if the */
/* buffer is locked, we just forget about it, else it's a normal read */
	if (rw_ahead = (rw == READA || rw == WRITEA)) {
		// 预读写的时候,buffer被锁则直接返回,因为预读本身不是必须的
		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");
	// 锁住buffer导致bread阻塞
	lock_buffer(bh);
	/*
		写但数据块装载后还没有被修改过
		读但内容和硬盘的内容是一致的
	*/
	if ((rw == WRITE && !bh->b_dirt) || (rw == READ && bh->b_uptodate)) {
		unlock_buffer(bh);
		return;
	}
repeat:
/* we don't allow the write-requests to fill up the queue completely:
 * we want some room for reads: they take precedence. The last third
 * of the requests are only for reads.
 */
	// 请求队列1/3用于读,2/3用于写
	if (rw == READ)
		req = request+NR_REQUEST;
	else
		req = request+((NR_REQUEST*2)/3);
/* find an empty request */
	while (--req >= request)
		// 小于0说明该结构没有被使用
		if (req->dev<0)
			break;
/* if none found, sleep on new requests: check for rw_ahead */
	// 没有找到可用的请求结构
	if (req < request) {
		// 预读写则直接返回
		if (rw_ahead) {
			unlock_buffer(bh);
			return;
		}
		// 阻塞等待可用的请求结构
		sleep_on(&wait_for_request);
		// 被唤醒后重新查找
		goto repeat;
	}
/* fill up the request-info, and add it to the queue */
	req->dev = bh->b_dev;
	req->cmd = rw;
	req->errors=0;
	req->sector = bh->b_blocknr<<1; // 一块等于两个扇区所以乘以2,即左移1位,比如要读地10块,则读取第二十个扇区
	req->nr_sectors = 2;// 一块等于两个扇区,即读取的扇区是2
	req->buffer = bh->b_data;
	req->waiting = NULL;
	req->bh = bh;
	req->next = NULL;
	// 插入请求队列
	add_request(major+blk_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;
	// 当前没有请求项,开始处理请求
	if (!(tmp = dev->current_request)) {
		dev->current_request = req;
		sti();
		(dev->request_fn)();
		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();
}

我们看到,这里是给一个队列插入了一个请求节点。那么这个队列是啥呢?继续看驱动程序的代码。系统有一张表,保存了驱动程序需要处理的请求和处理函数。

struct blk_dev_struct {
	void (*request_fn)(void);
	struct request * current_request;
};
struct blk_dev_struct blk_dev[NR_BLK_DEV] = {
	{ NULL, NULL },		/* no_dev */
	{ NULL, NULL },		/* dev mem */
	{ NULL, NULL },		/* dev fd */
	{ NULL, NULL },		/* dev hd */
	{ NULL, NULL },		/* dev ttyx */
	{ NULL, NULL },		/* dev tty */
	{ NULL, NULL }		/* dev lp */
};
struct request {
	int dev;		/* -1 if no request */
	int cmd;		/* READ or WRITE */
	int errors;
	unsigned long sector;
	unsigned long nr_sectors;
	char * buffer;
	struct task_struct * waiting;
	struct buffer_head * bh;
	struct request * next;
};

我们看硬盘驱动的初始化代码。

void hd_init(void)
{
	blk_dev[MAJOR_NR].request_fn = DEVICE_REQUEST;
	set_intr_gate(0x2E,&hd_interrupt);
	outb_p(inb_p(0x21)&0xfb,0x21);
	outb(inb_p(0xA1)&0xbf,0xA1);
}
#define DEVICE_REQUEST do_hd_reques

do_hd_reques函数就是摘取待处理的请求队列中摘下一个节点然后进行处理。我们这里是读取的操作,所以只看相关代码。把命令和参数写入硬盘控制器。

hd_out(dev,nsect,sec,head,cyl,WIN_READ,&read_intr);
=>
      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");

	if (drive>1 || head>15)
		panic("Trying to write bad sector");
	if (!controller_ready())
		panic("HD controller not ready");
	// 数据准备好触发中断时执行的回调,在blk.h定义,每个驱动都维护了自己的do_hd
	do_hd = intr_addr;
	outb_p(hd_info[drive].ctl,HD_CMD);
	port=HD_DATA;
	outb_p(hd_info[drive].wpcom>>2,++port);
	outb_p(nsect,++port);
	outb_p(sect,++port);
	outb_p(cyl,++port);
	outb_p(cyl>>8,++port);
	outb_p(0xA0|(drive<<4)|head,++port);
	outb(cmd,++port);
}

至此,驱动到硬盘控制器的处理完成。我们回到ll_rw_block函数处理,继续往下看,发现执行了
wait_on_buffer(bh);
我们看wait_on_buffer的代码

// 加锁,互斥访问
static inline void wait_on_buffer(struct buffer_head * bh)
{
	cli();
	while (bh->b_lock)
		sleep_on(&bh->b_wait);
	sti();
}
// 当前进程挂载到睡眠队列p中,p指向队列头指针的地址
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");
	/*
		*p为第一个睡眠节点的地址,即tmp指向第一个睡眠节点
		头指针指向当前进程,这个版本的实现没有采用真正链表的形式,
		他通过每个进程在栈中的临时变量形成一个链表,每个睡眠的进程,
		在栈里有一个变量指向后面一个睡眠节点,然后把链表的头指针指向当前进程,
		然后切换到其他进程执行,当被wake_up唤醒的时候,wake_up会唤醒链表的第一个
		睡眠节点,因为第一个节点里保存了后面一个节点的地址,所以他唤醒后面一个节点,
		后面一个节点以此类推,从而把整个链表的节点唤醒,这里的实现类似nginx的filter,
		即每个模块保存后面一个节点的地址,然后把全局指针指向自己。
	*/
	tmp = *p;
	*p = current;
	// 不可中断睡眠只能通过wake_up唤醒,即使有信号也无法唤醒
	current->state = TASK_UNINTERRUPTIBLE;
	schedule();
	// 唤醒后面一个节点
	if (tmp)
		tmp->state=0;
}

因为bh在ll_rw_block中被加锁了,所以进程被阻塞在这。系统调度其他进程执行。
时间过了很久…
硬盘读好了数据,给系统发了中断。从硬盘驱动的初始化函数中(参考上面的hd_init)我们发现。硬盘中断的处理函数是hd_interrupt。该函数是用汇编定义的。

_hd_interrupt:
	pushl %eax
	pushl %ecx
	pushl %edx
	push %ds
	push %es
	push %fs
	movl $0x10,%eax
	mov %ax,%ds
	mov %ax,%es
	movl $0x17,%eax
	mov %ax,%fs
	movb $0x20,%al
	outb %al,$0xA0		# EOI to interrupt controller #1
	jmp 1f			# give port chance to breathe
1:	jmp 1f
1:	xorl %edx,%edx
	// 把do_hd的内容和edx的交换
	xchgl _do_hd,%edx
	// 判断do_hd是否有效
	testl %edx,%edx
	jne 1f
	movl $_unexpected_hd_interrupt,%edx
1:	outb %al,$0x20
	// 执行注册的回调
	call *%edx		# "interesting" way of handling intr.
	pop %fs
	pop %es
	pop %ds
	popl %edx
	popl %ecx
	popl %eax
	iret

该函数执行了do_hd执行的函数。该函数就是在执行do_hd_request时注册的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++;
	// 还有数据要读,继续注册该函数,等待中断回调
	if (--CURRENT->nr_sectors) {
		do_hd = &read_intr;
		return;
	}
	// 结束该request,通知上层进程
	end_req
uest(1);
	// 处理下一个request
	do_hd_request();
}
// 数据读写完后执行该函数
extern inline void end_request(int uptodate)
{
	DEVICE_OFF(CURRENT->dev);
	// 读写数据成功,数据有效位置1
	if (CURRENT->bh) {
		CURRENT->bh->b_uptodate = uptodate;
                // 唤醒进程
		unlock_buffer(CURRENT->bh);
	}
	if (!uptodate) {
		printk(DEVICE_NAME " I/O error\n\r");
		printk("dev %04x, block %d\n\r",CURRENT->dev,
			CURRENT->bh->b_blocknr);
	}
	// 唤醒等待该request的请求,貌似暂时没有使用这个字段
	wake_up(&CURRENT->waiting);
	// 有request可用了 
	wake_up(&wait_for_request);
	CURRENT->dev = -1;
	// 更新请求队列,移除当前处理完的节点
	CURRENT = CURRENT->next;
}
static inline void unlock_buffer(struct buffer_head * bh)
{
	if (!bh->b_lock)
		printk("ll_rw_block.c: buffer not locked\n\r");
	bh->b_lock = 0;
        // 唤醒进程
	wake_up(&bh->b_wait);
}

至此,数据读取的过程差不多就结束了,等系统调度时选择该进程执行,然后进程从buffer里就获取了需要的数据,再返回到应用层。

猜你喜欢

转载自blog.csdn.net/THEANARKH/article/details/89789776
今日推荐