4.6 linux文件系统-虚拟文件系统VFS

1:信息

版本:Linux-0.11

2:VFS和FS

在这里插入图片描述

3:普通文件

file_dev.c 主要操作数据:逻辑块号

该文件包括file_read和file_write两个函数,也供系统调用函数read()和write()调用,是对普通文件进行读写操作。用于访问数据文件数据。
函数通过指定路径名的方式进行操作。函数参数给出的i节点 和文件结构信息信息,通过i节点信息获取相应的设备号,由file结构获取文件当前读写指针位置

3.1 结构体

struct file {
	unsigned short f_mode; //文件的类型和属性
	unsigned short f_flags;//读写,执行
	unsigned short f_count;//引用次数
	struct m_inode * f_inode;//对应的inode节点
	off_t f_pos;//本文件当前读写位置,lseek可以修改的
};

fd
file_table
file
node

3.2 file_read 文件读函数

/*
 *  linux/fs/file_dev.c
 *
 *  (C) 1991  Linus Torvalds
 */

#include <errno.h>
#include <fcntl.h>

#include <linux/sched.h>
#include <linux/kernel.h>
#include <asm/segment.h>

#define MIN(a,b) (((a)<(b))?(a):(b))
#define MAX(a,b) (((a)>(b))?(a):(b))
//根据i节点和文件结构体读取文件中的数据
//i节点可以得到设备号,
//filep可以得到文件当前读写指针的位置
//buf用来指定用户缓冲区的位置
//count是需要读取的字节数
//返回值是实际读取的字节数或者出错号(小于0)
int file_read(struct m_inode * inode, struct file * filp, char * buf, int count)
{
	int left,chars,nr;
	struct buffer_head * bh;
//参数有效性判断, 读取的字节数count 赋值给left 如果count小于等于 0 则返回0,
	if ((left=count)<=0)
		return 0;
	while (left) {//需要读取字节数left不为0,则循环执行下面 
		//	(filp->f_pos)/BLOCK_SIZE)用来计算当前指针位置所在的数据块号
		if (nr = bmap(inode,(filp->f_pos)/BLOCK_SIZE)) {//通过i节点和文件结构体信息,利用bmap获取文件当前位置在设备逻辑块中对应的块号nr
			if (!(bh=bread(inode->i_dev,nr)))//nr不为0,则从设备上读取该逻辑块到高速缓冲区
				break;
		} else
			bh = NULL;//nr为0,表述数据块不存在,返回NULL
		//	(filp->f_pos)%BLOCK_SIZE用来计算,当前指针位置相对于本块开始位置的偏移大小
		nr = filp->f_pos % BLOCK_SIZE;//获取在数据块中的偏移nr
		//通过比较min(本数据块中剩余的字节数,需要读取的字节数)获取本次要读取的长度chars 
		//BLOCK_SIZE-nr > left 本快的数据已经满足需求,说明需要读取的数据是最后一块。反之则说明还需要读取下一块的数据
		chars = MIN( BLOCK_SIZE-nr , left ); 
		filp->f_pos += chars;//指针前移次数将读取的数据char
		left -= chars;//计数剩余还要读取的字节数left相应减去chars
		if (bh) {//如果从设备中读到了数据
			char * p = nr + bh->b_data;//得到在高速缓冲区中要读取的位置
			while (chars-->0)//本次要读取的字数char > 0 就循环读, 赋值chars个数据到用户缓冲区
				put_fs_byte(*(p++),buf++);
			brelse(bh);
		} else {//如果从设备中没有读到数据
			while (chars-->0)
				put_fs_byte(0,buf++);//向用户缓冲区填入chars个0
		}
	}
	inode->i_atime = CURRENT_TIME;
	return (count-left)?(count-left):-ERROR;
}

重要参数 file->f_pos;逻辑块号
过程:

  1. 根据inode找到当前文件读写位置的逻辑块号
  2. 依次取其逻辑块号的高速缓冲区
  3. 把逻辑块中的数据读到指定用户指定的buf中

3.2 file_write 文件写函数

//i节点可以得到设备号,
//filep可以得到文件当前读写指针的位置
//buf用来指定用户缓冲区的位置
//count是需要写入的字节数
//返回值是实际写入字节数或者出错号(小于0)

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;

/*
 * ok, append may not work when many processes are writing at the same time
 * but so what. That way leads to madness anyway.
 *  ok,当许多进程同时写时,append操作可能不行,但那又怎么样。不管怎样 那样做会导致混乱一团
 * 
 */
  //首先确定数据要写入文件的位置。
	if (filp->f_flags & O_APPEND)
		pos = inode->i_size;//如果要向文件后添加数据,则将pos指向到文件尾部
	else
		pos = filp->f_pos;//否则pos指向当前文件读写指针
	//i : 已经写入的数据的字节数 
	//count : 要写入的数据的字节数 
	while (i<count) { //数据写入未完成,执行循环写入数据
		if (!(block = create_block(inode,pos/BLOCK_SIZE)))//获取要写入数据的逻辑块号
			break;
		if (!(bh=bread(inode->i_dev,block)))//获得逻辑块号对应的高速缓冲区
			break;
		c = pos % BLOCK_SIZE;// c:要读写指针位置相对于数据块中的偏移值
		p = c + bh->b_data;// p : 在缓冲区中开始写入数据的位置
		bh->b_dirt = 1;//设置缓冲区修改标志位
		c = BLOCK_SIZE-c; // c:从开始写到块末供能写入的字节数
		if (c > count-i) c = count-i;//如果(c  > 还需要写入的字节数)则次数写入 c = count-i个字节数即可
		pos += c; //更写的位置,下一次循环
		if (pos > inode->i_size) {//如果pos的位置超过文件当前的长度
			inode->i_size = pos;//修改i节点中文件长度字段
			inode->i_dirt = 1;//节点修改标志位置位
		}
		i += c;//把要写入的字节数c累加到已写入字节计数值i中,供循环判断使用
		while (c-->0)//从用户缓冲区buf中复制c个字节到高速缓冲块中p指向的位置
			*(p++) = get_fs_byte(buf++);
		brelse(bh);//复制完后释放缓冲块
	}
	//当全部数据已经写入文件或者在写操作发生问题时就会退出循环
	inode->i_mtime = CURRENT_TIME;//更改文件的修改时间为当前时间
	if (!(filp->f_flags & O_APPEND)) {;//如果此时操作不是在文件尾添加数据,刚进函数是有这方面的判断
		filp->f_pos = pos;//则把文件读写指针调整到当前读写位置pos处
		inode->i_ctime = CURRENT_TIME;;//更改i节点修改时间为当前时间
	}
	return (i?i:-1);;//返回写入字节数,若是0则返回出错号-1
}

3:块设备文件

block_dev.c : 块设备文件数据访问操作类程序,block_write和block_read直接读写设备原始数据,这两个函数供read()和write()来调用
主要操作数据: 块号,偏移
块设备每次对设备读写以盘块为单位(与缓冲区缓冲块长度相同)
因此,

  • block_write首先把参数中文件指针pos位置映射成数据块号和块中的偏移值量,然后使用块读函数bread或块预读函数breada将文件指针位置所在的数据块读入缓冲区的一个缓冲块。
  • 然后根据本块中需要写的数据长度chars,从用户数据缓冲中将数据复制到当前缓冲块的偏移位置开始处。
  • 如果还要写数据,则将下一个块读入缓冲块中,并将用户数据复制到该缓冲块中,在第二次以及以后写数据时,偏移量offset均为0
    在这里插入图片描述
    用户缓冲区是用户程序在开始执行时由系统分配的,或者是在执行过程中动态申请的。用户缓冲区虚拟线程地址,在调用本函数之前,会将虚拟线性地址映射到主内存区中相应的内存中

3.1 block_write 数据块写入函数

参数:

  • dev - 设备号
  • pos - 设备文件中偏移量指针
  • buf - 用户空间缓冲区地址
  • count - 要传入的字节数

返回:

  • 成功-返回已写入的字节数
  • 没有写入任何字节或者出错,则返回错误号

对于内核来说,写操作是向高速缓冲区写入数据。什么时候数据写入最终设备是由高速缓冲管理程序决定并处理的。
另外,应为设备是以块为单位进行读写的,因此对于写起始地址不在,块开始地址时,需要先将开始字节所在的整个块读取,然后将需要写的数据从写开始处填写满该块,然后将完整的一块数据写盘(即交给高速缓冲程序区处理)

int block_write(int dev, long * pos, char * buf, int count)
{
	
	int block = *pos >> BLOCK_SIZE_BITS;//首先将文件位置pos换算成开始写盘块的块序号block
	int offset = *pos & (BLOCK_SIZE-1);//获取开始位置在该块中的偏移位置offset
	int chars;
	int written = 0;
	struct buffer_head * bh;
	register char * p;//局部寄存器变量,存放在寄存器中
	//针对要写入的数据count,循环执行一下操作,知道数据全部写入
	while (count>0) {
		chars = BLOCK_SIZE - offset;//计算本块可以写入的字节数
		//剩余的字节数,大于要写的字节数count,设置要读的字节数chars 为count。否则char还是等于上一句获得的该块中剩余的大小
		if (chars > count)//要写入的字节数count填不满一块
			chars=count;//就直接写入count字节
			
		if (chars == BLOCK_SIZE)//正好写一块数据内容
			bh = getblk(dev,block);//申请一块高速缓冲块
		else//否者
			bh = breada(dev,block,block+1,block+2,-1);//预读取两块数据
		block++;//块号递增1,为下次操作做好准备
		if (!bh)//缓冲块操作失败
			return written?written:-EIO;//返回已写数据,没有写入就返回错误号
		p = offset + bh->b_data;//p指向读出数据缓冲块开始写入数据的位置
		offset = 0;//若是最后一次循环写入的数据不足一块,则需从块地址开始处填写所需的字节,因此这里要预先设置为0
		*pos += chars;//将文件中的偏移指针pos前移此次要写入的字节数chars
		written += chars;//累加写入的字节数
		count -= chars;//计算还要写的字节数
		while (chars-->0)//从用户缓冲区复制chars个字符到p指向的高速缓冲块中开始写入的位置
			*(p++) = get_fs_byte(buf++);
		bh->b_dirt = 1;//设置缓冲块的修改标志位
		brelse(bh);//释放缓冲区
	}
	return written;//返回写入的字节数
}

3.1 block_read数据块读函数

操作方式与block_write相同,只是把数据从缓冲区复制用户指定的地方
从指定设备和位置读入指定长度数据到用户缓冲区
参数:

  • dev - 设备号
  • pos - 设备文件中偏移量指针
  • buf - 用户缓冲区地址
  • count - 要传入的字节数

返回:已读入的字节数。若没有读入数据或者出错,则返回出错号

int block_read(int dev, unsigned long * pos, char * buf, int count)
{
	int block = *pos >> BLOCK_SIZE_BITS;//首先将文件位置pos换算成开始写盘块的块序号block
	int offset = *pos & (BLOCK_SIZE-1);//获取开始位置在该块中的偏移位置offset
	int chars;
	int read = 0;
	struct buffer_head * bh;
	register char * p;
	//针对要写读入的数据count,循环执行操作
	while (count>0) {
		chars = BLOCK_SIZE-offset;//获得块中剩余的字节数chars 
		//剩余的字节数,大于要读的字节数count,设置要读的字节数chars 为count。否则char还是等于上一句获得的该块中剩余的大小
		if (chars > count)
			chars = count;//
		//读入需要需要的数据块,并预读剩下的两块数据,到缓冲区
		if (!(bh = breada(dev,block,block+1,block+2,-1)))
			return read?read:-EIO;//读取失败,则返回已读数据,没有读到数据就返回错误
		block++;//块号加1,为下次读取做准备
		p = offset + bh->b_data;//p指向该块要写入的起始位置
		offset = 0;//后续块中,需要从块起始地址读出字节,因此这里设置为0
		*pos += chars;//文件偏移指针移动前移chars(要读的字节数)
		read += chars;//记录读出的数据数read
		count -= chars;//计算剩余要读的数据
		//从高速缓冲区中p指向的位置处复制chars个字节到用户缓冲区中
		while (chars-->0)
			put_fs_byte(*(p++),buf++);
		brelse(bh);//释放缓冲块
	}
	return read;//返回已读的字节
}

4:pipe文件

文件:pipe.c
主要操作数据:
pipe缓冲区的指针 ,inode-size对应缓冲区指针 pipe的头(inode->i_zone0])和尾(inode->i_zone[1]

管道时进程间通信的最基本方式。本程序包括读写函数,同时实现了管道系统调用sys_pipe()。
读写函数也时系统调用read和write的底层实现函数,尽在write_read.c中调用

  • 在初始化管道时,程序会专门申请一个管道i节点,并为管道分配一页缓存(4KB)。
  • 管道i节点的i_size字段中被设置为指向管道缓冲区的指针,管道数据头部指针存在在i_zone[0]字段,而管道数据尾部指针存放在i_zone[1] 字段。
  • 对于读管道操作,数据从管道尾部读出,并使用管道尾指针前移读字节数个位置
  • 对于往管道写操作,数据向管道头写入,并使管道头指针前移写入的字节数个位置
    在这里插入图片描述

4.1 read_pipe

用于读管道中的数据。若管道中没有数据,就唤醒写管道的进程。则自己进入睡眠状态。
若读到了数据,就相应的调用管道的尾指针,并把数据传到用户缓冲区中。
当把管道中的所有的数据都读走后,也要唤醒等待写的管道的进程,并返回已读的字节数
当管道写进程已退出管道操作时,函数就立刻退出,并返回已读的字节数

//管道读操作函数
//参数:inode-管道对应的i节点。buf-用户数据缓冲区指针。count-读取得字节数
int read_pipe(struct m_inode * inode, char * buf, int count)
{
	int chars, size, read = 0;
	//需要读取得字节数大于0,我们就循环执行一下操作
	while (count>0) {
		while (!(size=PIPE_SIZE(*inode))) {//当前管道中没有数据(size = 0)
			wake_up(&inode->i_wait);//唤醒等待该节点的进程,通常时写管道进程
			if (inode->i_count != 2) /* are there any writers? *///如果没有写管道进程,表现为节点的引用计数小于2
				return read;//返回已读取的字节数,退出
			sleep_on(&inode->i_wait);//否则在该节点上睡眠
		}
		//此时说明管道(缓冲区)中有数据
		chars = PAGE_SIZE-PIPE_TAIL(*inode);//去管道尾指针到缓冲区末端的字节数chars
		if (chars > count)//如果其大于要读取得字节数count,则令其等于count
			chars = count;
		if (chars > size)//如果chars大于当前管道中含有得数据长度size,则令其等于size
			chars = size;
		//上面会让chars等于可以读取的数据
		count -= chars;//计算剩余要读取得数据
		read += chars;//累加已读取得数据
		size = PIPE_TAIL(*inode);//size指向管道得尾指针处
		PIPE_TAIL(*inode) += chars;//调整尾指针,前移chars字节
		PIPE_TAIL(*inode) &= (PAGE_SIZE-1);//尾指针超过管道末端则绕回
		while (chars-->0)//将管道中得数据复制到用户缓冲区中
			put_fs_byte(((char *)inode->i_size)[size++],buf++);//i_size是管道缓冲块指针
	}
	wake_up(&inode->i_wait);//读取完毕后,唤醒等待还管道得进程,并返回读取得字节数
	return read;
}

4.2 write_pipe

该函数与读管道类似

//管道写函数
//参数:inode-管道对应得i节点,buf-数据缓冲区指针,count-将写入管道得字节数
int write_pipe(struct m_inode * inode, char * buf, int count)
{
	int chars, size, written = 0;
	//如果写入的字节数大于0,则循环执行下面的操作
	while (count>0) {
		while (!(size=(PAGE_SIZE-1)-PIPE_SIZE(*inode))) {//管道已经写满了,size = 0
			wake_up(&inode->i_wait);//唤醒等待该节点的进程,通常是读管道进程
			if (inode->i_count != 2) { /* no readers *///如果已经没有读管道,即i节点引用计数小于2
				current->signal |= (1<<(SIGPIPE-1));//向当前进程发送SIGPIPE信号
				return written?written:-1;//返回已读字节,已读为0则返回-1
			}
			//如果有读管道进程
			sleep_on(&inode->i_wait);//则让当前进程在该i节点上睡眠,等待读管道进程读出数据,从而让管道腾出空间
		}
		//程序执行到这里说明管道缓冲区中有可写空间size。
		chars = PAGE_SIZE-PIPE_HEAD(*inode);//计算剩余可写的空间chars
		if (chars > count)//chars 比要写的count大,则chars 就等于count
			chars = count;
		if (chars > size)//char大于当前管道中空闲的长度size,则chars 就等于size
			chars = size;
		//上面会让chars等于可以写入的数据	
		count -= chars;//计算剩余要写的数据
		written += chars;//累加已读写的数据
		size = PIPE_HEAD(*inode);;//size指向管道的数据头指针处
		PIPE_HEAD(*inode) += chars;//调整头指针,前移chars字节
		PIPE_HEAD(*inode) &= (PAGE_SIZE-1);//头指针超过管道末端则绕回
		while (chars-->0)//从用户缓冲区复制chars个字节到管道头指针开始处。
			((char *)inode->i_size)[size++]=get_fs_byte(buf++);
	}
	//写操作结束,则唤醒等待管道的进程,返回已写入的字节数,退出
	wake_up(&inode->i_wait);
	return written;
}

对pipe的读写可以参考下图

在这里插入图片描述

在这里插入图片描述

4.3 sys_pipe

系统调用函数,用于创建无名管道,它首先在系统文件表中取得两个表项,然后在当前进程的文件描述符表中也同样寻找两个未使用的描述符表项,用来保存相应的文件结构体指针。
接着在系统中申请一个空闲的节点,同时获得管道使用的一个缓冲块。然后对相应的文件结构进行初始化,将文件结构设置为只读模式,最后将两个文件描述符传给用户

//创建管道系统调用
//在filedes所指的数据中创建一对文件句柄(描述符)。这对文件句柄指向一管道i节点。
//参数:filedes - 文件句柄数据。filed[0]用于读管道数据,fileds[1]向管道写入数据
//成功返回0,出错返回-1

#define NR_OPEN 20
#define NR_INODE 32
#define NR_FILE 64
int sys_pipe(unsigned long * fildes)
{
	struct m_inode * inode;
	struct file * f[2];//文件结构体数组
	int fd[2];//文件句柄数组
	int i,j;
	//首先从系统文件表中取出两个空闲项(引用计数字段为0的项),并分别设置引用计数为1
	j=0;
	for(i=0;j<2 && i<NR_FILE;i++)
		if (!file_table[i].f_count)
			(f[j++]=i+file_table)->f_count++;
	if (j==1)//若只有一个空闲项
		f[0]->f_count=0;//释放该项
	if (j<2)//若没有找到两个空闲项,则返回-1
		return -1;
	//针对上面取得的两个文件表结构项,分别分配一句柄文件号,并使进程文件结构指针数组的两项分别指向这两个文件结构
	//而文件句柄即是该数组的索引号,类似的,如果只有一个空闲文件句柄,则释放该句柄(置空相应数组项)
	//如果没有找到两个空闲句柄,则释放上面获得的两个文件结构项(复用引用计数值),并返回-1
	j=0;
	for(i=0;j<2 && i<NR_OPEN;i++)
		if (!current->filp[i]) {
			current->filp[ fd[j]=i ] = f[j];
			j++;
		}
	if (j==1)
		current->filp[fd[0]]=NULL;
	if (j<2) {
		f[0]->f_count=f[1]->f_count=0;
		return -1;
	}
	//利用get_pipe_inode申请一个管道使用的i节点,并为管道分配一页内存作为缓冲区。
	//如果不成功,则相应释放连个文件句柄和结构体项,并返回-1
	if (!(inode=get_pipe_inode())) {
		current->filp[fd[0]] =
			current->filp[fd[1]] = NULL;
		f[0]->f_count = f[1]->f_count = 0;
		return -1;
	}
	//如果管道i节点申请成功,则对两个文件结构进行初始化操作,让他们都指向同一个管道i节点
	//并把读写指针都置0
	//第一个文件结构体的文件模式置为读
	//第二个文件结构体的文件模式置为写
	f[0]->f_inode = f[1]->f_inode = inode;
	f[0]->f_pos = f[1]->f_pos = 0;
	f[0]->f_mode = 1;		/* read */
	f[1]->f_mode = 2;		/* write */
	//将文件句柄数组赋值到对应的用户空间数组中
	put_fs_long(fd[0],0+fildes);
	put_fs_long(fd[1],1+fildes);
	return 0;//成供返回0
}

其中寻找空闲的文件表结构项
在这里插入图片描述

5:字符设备文件

char_dev.c
文件包括字符设备文件访问函数。另外还有个一个设备读写函数指针表。该表的项号代表主设备号

rw_ttyx()是串口中断设备读写函数,其主设备号是4,通过调用tty驱动程序实现了对串口终端的读写操作
rw_tty()是控制台终端读写函数,主设备号是5,实现原理与rw_ttyx()相同,只是对进程能否进行控制台操作有所限制
rw_memory()是内存设备文件读写函数,主设备号是1。实现对内存映像的字节操作。但是liunux0.11版内核对次设备号是0,1,2的操作还没有实现。到0.96才开始实现

rw_char() 是对字符设备读写操作的接口函数。其他字符设备通过该函数读字符设备读写函数指针表进行相应的字符设备的操作。文件系统的操作函数open,read都是通过它对所有字符设备文件进行操作

extern int tty_read(unsigned minor,char * buf,int count);//终端读
extern int tty_write(unsigned minor,char * buf,int count);//终端写
//定义字符设备读写函数指着类型
typedef (*crw_ptr)(int rw,unsigned minor,char * buf,int count,off_t * pos);
//串口终端读写操作函数
//参数:rw - 读写命令;minor - 终端设备子设备号;buf - 缓冲区;count -读写字节数
//pos - 读写操作当前指针,对于终端操作,该指针无用
//返回:实际读写的字节数。失败返回错误码
static int rw_ttyx(int rw,unsigned minor,char * buf,int count,off_t * pos)
{
	return ((rw==READ)?tty_read(minor,buf,count):
		tty_write(minor,buf,count));
}
//终端读写函数操作
//同上,只是增加了对进程是否有控制终端的检车
static int rw_tty(int rw,unsigned minor,char * buf,int count, off_t * pos)
{
	if (current->tty<0)//若进程没有对应的控制终端,则返回错误号
		return -EPERM;
	return rw_ttyx(rw,current->tty,buf,count,pos);
}
//内存数据读写,未实现
static int rw_ram(int rw,char * buf, int count, off_t *pos)
{
	return -EIO;
}
//物理内存数据读写操作函数,未实现
static int rw_mem(int rw,char * buf, int count, off_t * pos)
{
	return -EIO;
}
//内核虚拟内存数据读写函数,未实现
static int rw_kmem(int rw,char * buf, int count, off_t * pos)
{
	return -EIO;
}
//端口读写操作函数
//参数:rw - 读写命令;buf -缓冲区; count-读写字节数; pos -端口地址
//返回:实际读写的字节数
static int rw_port(int rw,char * buf, int count, off_t * pos)
{
	int i=*pos;
	//对于所要求读写的字节数,并且端口地址小于64k时,循环执行单个字节的读写操作
	while (count-->0 && i<65536) {
		if (rw==READ)//若是读命令,则从端口i中读取一字节内容并放到用户缓冲区中
			put_fs_byte(inb(i),buf++);
		else//若是写命令
			outb(get_fs_byte(buf++),i);//从用户缓冲区读取一字节输出到端口i
		i++;//前移一个端口。??
	}
	i -= *pos;//计算读写的字节数
	*pos += i;//调整相应的读写指针
	return i;//返回肚子饿字节数
}
//内存读写操作函数
static int rw_memory(int rw, unsigned minor, char * buf, int count, off_t * pos)
{
//根据内存设备子设备号,分别调用不同的内存读写函数
	switch(minor) {
		case 0:
			return rw_ram(rw,buf,count,pos);
		case 1:
			return rw_mem(rw,buf,count,pos);
		case 2:
			return rw_kmem(rw,buf,count,pos);
		case 3:
			return (rw==READ)?0:count;	/* rw_null */
		case 4:
			return rw_port(rw,buf,count,pos);
		default:
			return -EIO;
	}
}
//定义系统中的设备种数
#define NRDEVS ((sizeof (crw_table))/(sizeof (crw_ptr)))
//字符设备读写函数指针表
static crw_ptr crw_table[]={
	NULL,		/* nodev *///空设备
	rw_memory,	/* /dev/mem etc */
	NULL,		/* /dev/fd 软驱*/
	NULL,		/* /dev/hd 硬盘*/
	rw_ttyx,	/* /dev/ttyx 串口终端*/
	rw_tty,		/* /dev/tty 终端*/
	NULL,		/* /dev/lp 打印机*/
	NULL};		/* unnamed pipes 未命令管道*/
//字符设备读写操作函数
//参数:rw - 读写命令; dev - 设备号; buf - 缓冲区; count - 读写字节数; pos - 读写指针
//返回:实际读写字节数
int rw_char(int rw,int dev, char * buf, int count, off_t * pos)
{
	crw_ptr call_addr;
//如果设备号超过系统设备数,则返回出错码
	if (MAJOR(dev)>=NRDEVS)
		return -ENODEV;
	//如果设备没有对应的读写函数,返回出错码	
	if (!(call_addr=crw_table[MAJOR(dev)]))
		return -ENODEV;
	//调用对应设备的读写操作函数,并返回实际读写的字节数	
	return call_addr(rw,MINOR(dev),buf,count,pos);
}

6:最上层的读写函数

read_write.c
该文件实现了系统操作调用的read,write和lseek。read和write将根据不同的文件类型,分别调用前面4个文件实现的相应函数。因此本文件是上层接口的实现。lseek用于设置读写文件读写指针

read系统调用首先判断所给参数的有效性,然后根据文件i节点信息判断文件的类型。

  • 若是管道文件则调用程序pipe.c中的读函数
  • 若是字符设备文件,则调用char_dev.c中的rw_char()字符读函数
  • 若是块设备文件,则执行block_dev.c程序的块设备读操作,并返回读取的字节数
  • 若是目录文件或者一般正规文件,在调用file_dev.c中的文件读函数file_read()

write()系统调用实现和read类似

lseek()系统调用将文件句柄对应文件结构中当前读写指针进行修改,对于读写指针不能移动的文件和管道文件,将给出错误号,并立即返回。

6.1 sys_lseek 重定位文件读写指针系统调用

参数:

  • fd - 文件句柄
  • offset - 新的文件读写指针偏移值
  • origin - 偏移的起始位置 (SEEK_SET (0,文件开始处),SEEK_CUR(1,从文件当前位置), SEEK_END(2,从文件尾处))
int sys_lseek(unsigned int fd,off_t offset, int origin)
{
	struct file * file;
	int tmp;
	//判断参数有效性
	//如果(句柄值大于程序最多打开的文件数NR_OPEN(20) || 该句柄文件结构体指针为空 || 文件结构体的i节点字段为空)
	// || 指定设备文件指针是不可定位的)
	if (fd >= NR_OPEN || !(file=current->filp[fd]) || !(file->f_inode)
	   || !IS_SEEKABLE(MAJOR(file->f_inode->i_dev)))
		return -EBADF;//则返回错误码
	if (file->f_inode->i_pipe)//文件对应的i节点是管道节点,则但会出错码,管道头尾指针不可随意移动
		return -ESPIPE;
	switch (origin) {//根据设置的定位标志重新定位文件读写指针
		case 0://origin = SEEK_SET,要求以文件开始处作为原点设置文件读写指针。
			if (offset<0) return -EINVAL;//若偏移值小于0,返回出错码
			file->f_pos=offset;//否则文件读写指针等于offset
			break;
		case 1:://origin = SEEK_CUR,要求以文当前读写指针处作为原点设置文件读写指针。
			if (file->f_pos+offset<0) return -EINVAL;//如若文件当前指针加上偏移小于0,返回出错码
			file->f_pos += offset;//否则在当前读写指针上加上偏移值
			break;
		case 2://origin = SEEK_END,要求以文末尾处作为原点设置文件读写指针。
			if ((tmp=file->f_inode->i_size+offset) < 0)//如若文件的大小加上偏移小于0,返回出错码
				return -EINVAL;
			file->f_pos = tmp;//否则重定位文件读写指针
			break;
		default:
			return -EINVAL;
	}
	return file->f_pos;//返回重定位后的文件读写指针
}

6.2 sys_read 读文件系统调用

参数:

  • fd - 文件句柄
  • buf - 缓冲区
  • count - 要读的字节数
int sys_read(unsigned int fd,char * buf,int count)
{
	struct file * file;
	struct m_inode * inode;
	//参数有效性判断
	//if(文件句柄大于最多打开文件数 || 要读的字节数小于0 || 还句柄文件接结构体指针为空)
	if (fd>=NR_OPEN || count<0 || !(file=current->filp[fd]))
		return -EINVAL;
	if (!count)//要读取的字节数等于0,则返回0
		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);
	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)//如果读取的字节数小于等于0,则返回0
			return 0;
		return file_read(inode,file,buf,count);//执行文件读操作,返回读取的字节数
	}
	//执行到这里,说明无法判断文件的属性。则打印节点属性,并返回出错码退出
	printk("(Read)inode->i_mode=%06o\n\r",inode->i_mode);
	return -EINVAL;
}

6.3 sys_write写统统调用

参数:

  • fd - 文件句柄
  • buf - 缓冲区
  • count - 要读的字节数

与读函数一致

int sys_write(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;
	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;
}

7 应用程序与系统调用

通常应用程序不直接调用Linux的系统调用(system calls),而是通过调用函数库(例如,lib.a)中的子程序进行操作的。但是若为了提高一些效率。当然也是是可以直接进行调用的。

对于一个基本函数库来讲,通常提供下面的基本函数和子程序集合:

  • 系统调用接口函数
  • 内存分配管理函数
  • 信号处理函数集
  • 字符串处理函数
  • 标准数据输入函数
  • 其他函数;如bsd,加密,算法,终端操作,网络套接字

这些函数集中,系统调用函数是操作系统的底层接口函数。
许多涉及到系统调用的函数都会调用系统调用接口函数中具有标准名称的系统函数,而不是直接使用linux的系统调用接口。
这样做可以很大程度上让一个函数库与所在的系统操作系统无关,让函数库具有较高的可移植性
对于一个新的函数库源码,只要讲其中涉及系统调用的部分替换成新操作系统的系统调用,就基本上完成了该函数库的移植工作

从程序执行read函数到进入内核中进行实际操作的整个过程如下图
在这里插入图片描述

8.1:读操作示例

  1. 当在代码中调用了write
#define __LIBRARY__
#include <unistd.h>
#include <time.h>
...
...
static int printf(const char *fmt, ...)
{
	va_list args;
	int i;

	va_start(args, fmt);
	write(1,printbuf,i=vsprintf(printbuf, fmt, args));
	va_end(args);
	return i;
}
...
...
  1. 在文件unistd.h声明了write函数
...
...

int write(int fildes, const char * buf, off_t count);


...
...
  1. 并在write.c中通过_syscall3的宏来实现函数的实体
/*
 *  linux/lib/write.c
 *
 *  (C) 1991  Linus Torvalds
 */

#define __LIBRARY__
#include <unistd.h>

_syscall3(int,write,int,fd,const char *,buf,off_t,count)

_syscall3这个宏也在 unistd.h中定义

//_syscall3:代表有3个参数的的系统调用
// type是返回值的类型,name是函数的名字
//参数分别是a,b,c 对于的类型是atype,btype,ctype
//此外还有_syscall0, _syscall1,_syscall2等
#define _syscall3(type,name,atype,a,btype,b,ctype,c) \
type name(atype a,btype b,ctype c) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
	: "=a" (__res) \
	: "0" (__NR_##name),"b" ((long)(a)),"c" ((long)(b)),"d" ((long)(c))); \
if (__res>=0) \
	return (type) __res; \
errno=-__res; \
return -1; \
}

通过上面的宏把write.c中定义的函数展开

_syscall3(int,write,int,fd,const char *,buf,off_t,count)
int write(int,fd,const char *,buf,off_t,count)
{ \
long __res; \
__asm__ volatile ("int $0x80" \
	: "=a" (__res) \
	: "0" (__NR_write),"b" ((long)(a)),"c" ((long)(b)),"d" ((long)(c))); \
if (__res>=0) \
	return (int) __res; \
errno=-__res; \
return -1; \
}
  1. 所以wirte函数,具体的功能就是,触发int80中断,进行系统调用,调用号是__NR_write(#define __NR_write 4),最终会调用数组sys_call_table[4], 也就是sys_write
fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,
sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link,
sys_unlink, sys_execve, sys_chdir, sys_time, sys_mknod, sys_chmod,
sys_chown, sys_break, sys_stat, sys_lseek, sys_getpid, sys_mount,
sys_umount, sys_setuid, sys_getuid, sys_stime, sys_ptrace, sys_alarm,
sys_fstat, sys_pause, sys_utime, sys_stty, sys_gtty, sys_access,
sys_nice, sys_ftime, sys_sync, sys_kill, sys_rename, sys_mkdir,
sys_rmdir, sys_dup, sys_pipe, sys_times, sys_prof, sys_brk, sys_setgid,
sys_getgid, sys_signal, sys_geteuid, sys_getegid, sys_acct, sys_phys,
sys_lock, sys_ioctl, sys_fcntl, sys_mpx, sys_setpgid, sys_ulimit,
sys_uname, sys_umask, sys_chroot, sys_ustat, sys_dup2, sys_getppid,
sys_getpgrp, sys_setsid, sys_sigaction, sys_sgetmask, sys_ssetmask,
sys_setreuid,sys_setregid };

  1. sys_write会得以执行
int sys_write(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;
	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;
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值