1.引言
从事Linux环境工作2年有余,一直懵懵懂懂,1年前拜读了《莱昂氏UNIX源代码分析》一书,感觉自己的学习道路漫漫且修远。最近受chinaunix的精华文帖启发,拟将近来的部分内核调用分析笔记拿出来与各前辈先进共同探讨学习,以壮个人学习之路。
本部分主要讲述的是文件I/O操作的2.6.11内核版本实现,包括了主要的数据结构、宏定义和函数流程。以下分别讲述open,create,close,read,write,lseek系统调用。
2.主要参考
《莱昂氏UNIX源代码分析》
《UNIX环境高级编程》
3.主要数据结构
3.1.FD
对于内核而言,所有打开文件都由文件描述符引用。文件描述符是一个非负整数。当打开一个现存文件或创建一个新文件时,内核向进程返回一个文件描述符。
当读、写一个文件时,用open或creat返回的文件描述符标识该文件,将其作为参数传送给read或write。在POSIX.1应用程序中,文件描述符为常数0、1和2分别代表STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO,意即标准输入,标准输出和标准出错输出,这些常数都定义在头文件;中。
文件描述符的范围是0~OPEN_MAX,在目前常用的linux系统中,是32位整形所能表示的整数,即65535,64位机上则更多。
3.2.File
struct file { struct list_head f_list; //文件链表指针 struct dentry *f_dentry; // 文件对应的目录结构 struct vfsmount *f_vfsmnt; // 虚拟文件系统挂载点 struct file_operations *f_op; // 文件操作函数指针 atomic_t f_count; unsigned int f_flags; mode_t f_mode; // 文件模式 int f_error; loff_t f_pos; // 文件offset struct fown_struct f_owner; //文件owner 结构 unsigned int f_uid, f_gid; struct file_ra_state f_ra; // 跟踪上次文件操作状态的结构指针 size_t f_maxcount; // 文件大小 unsigned long f_version; void *f_security; // hook 文件操作的security结构指针 void *private_data; // tty 驱动器所需数据 #ifdef CONFIG_EPOLL struct list_head f_ep_links; // EPOLL 机制检测所需链表结构 spinlock_t f_ep_lock; // 兼容早期gcc bug 的标志 #endif /* #ifdef CONFIG_EPOLL */ struct address_space *f_mapping; // 地址映射表 } |
3.3.File_struct
File_struct结构保存了进程打开的所有文件表数据。
struct files_struct {
atomic_t count; // 自动增量
spinlock_t file_lock; // 低位成员保护标识
int max_fds; // 最大文件句柄数目
int max_fdset; // 最大的fd集合容量
int next_fd; // 下一个空闲fd
struct file ** fd; // 当前fd对应的文件结构指针列表
fd_set *close_on_exec; // 可执行close的fd集合
fd_set *open_fds; // 打开的fd集合
fd_set close_on_exec_init; //
fd_set open_fds_init;
struct file * fd_array[NR_OPEN_DEFAULT]; // 默认打开的fd队列
}; |
4.2.3.sys_open子函数getname
getname函数主要功能是在使用文件名之前将其拷贝到内核数据区,正常结束时返回内核分配的空间首地址,出错时返回错误代码。其调用了函数do_getname来实现。
static inline int do_getname(const char __user *filename, char *page){
int retval;
unsigned long len = PATH_MAX; // 内核允许的最大路径长度
// 如果进程的地址限制是否和KERNEL_DS相等,则检查文件名是否小于用户进程空间
if (!segment_eq(get_fs(), KERNEL_DS)) {
// 文件名地址大于用户进程空间,则返回错误-EFAULT
if ((unsigned long) filename >;= TASK_SIZE)
return -EFAULT;
// 获取较小的地址长度
if (TASK_SIZE - (unsigned long) filename < PATH_MAX)
len = TASK_SIZE - (unsigned long) filename;
}
// 将filename拷贝len长度到page中,返回实际拷贝长度
retval = strncpy_from_user(page, filename, len);
if (retval >; 0) {
// 如果retval大于等于len,则返回-ENAMETOOLONG
if (retval < len)
return 0;
return -ENAMETOOLONG;
} else if (!retval)
// filename 为空,则返回-ENOENT
retval = -ENOENT;
return retval;
}
char * getname(const char __user * filename){
char *tmp, *result;
result = ERR_PTR(-ENOMEM);
// 从内核缓存中分配空间,如果成功,则执行do_getname
tmp = __getname();
if (tmp) {
// 执行文件名拷贝操作
int retval = do_getname(filename, tmp);
result = tmp;
if (retval < 0) {
// do_getname出错,则释放空间,并返回错误代码
__putname(tmp);
result = ERR_PTR(retval);
}
}
// 如果前面操作成功,且audit_context不为空,则将当前文件名添加到audit列表中
if (unlikely(current->;audit_context) && !IS_ERR(result) && result)
audit_getname(result);
// 返回处理结果
return result;
} |
4.2.4.sys_open子函数filp_open
这后面的函数使用了一个nameidata的数据结构来描述文件相关的操作数据。
struct nameidata {
struct dentry *dentry; // 目录数据
struct vfsmount *mnt; // 虚拟文件挂载点数据
struct qstr last; // hash值
unsigned int flags; // 文件操作标识
int last_type; // 类型
unsigned depth;
char *saved_names[MAX_NESTED_LINKS + 1];
union {
struct open_intent open;
} intent; // 专用数据
};
struct file *filp_open(const char * filename, int flags, int mode){
int namei_flags, error;
struct nameidata nd;
namei_flags = flags;
if ((namei_flags+1) & O_ACCMODE)
namei_flags++; // 如果flags有O_WRONLY,则增加O_RDONLY
if (namei_flags & O_TRUNC)
namei_flags |= 2; // 如果有O_TRUNC,则增加O_RDWR
error = open_namei(filename, namei_flags, mode, &nd); // 如3.2.3.1 描述
if (!error)
return dentry_open(nd.dentry, nd.mnt, flags); // 如3.2.3.2描述
return ERR_PTR(error); // 返回错误代码
} |
4.2.4.1.filp_open子函数open_namei
open_namei函数主要执行文件操作的inode部分的打开等操作。
int open_namei(const char * pathname, int flag, int mode, struct nameidata *nd){
int acc_mode, error = 0;
struct dentry *dentry;
struct dentry *dir;
int count = 0;
acc_mode = ACC_MODE(flag); // 取出低2位操作标识
if (flag & O_APPEND) // 取出O_APPEND操作标识
acc_mode |= MAY_APPEND;
//赋值open函数的专用数据
nd->;intent.open.flags = flag;
nd->;intent.open.create_mode = mode;
// 如果不需要创建文件,则在进程目录文件表搜索已有文件,并把结果拷贝到nd中
if (!(flag & O_CREAT)) {
error = path_lookup(pathname, lookup_flags(flag)|LOOKUP_OPEN, nd);
if (error) // 错误代码有ENOENT,ENOTDIR,EAGAIN,ESTALE,
return error;
goto ok; // 否则执行打开函数,更新inode数据
}
// 在进程文件表中搜索该文件,如果不存在,则创建,结果由nd保存
error = path_lookup(pathname, LOOKUP_PARENT|LOOKUP_OPEN|LOOKUP_CREATE, nd);
if (error)
return error;
// 检测nd的结果是否是一个目录文件,是则返回
error = -EISDIR;
if (nd->;last_type != LAST_NORM || nd->;last.name[nd->;last.len])
goto exit;
// 获取文件的相关目录数据,结果返回到dentry中。
dir = nd->;dentry;
nd->;flags &= ~LOOKUP_PARENT;
down(&dir->;d_inode->;i_sem);
dentry = __lookup_hash(&nd->;last, nd->;dentry, nd);
do_last:
// 如果dentry是一个错误值,则返回
error = PTR_ERR(dentry);
if (IS_ERR(dentry)) {
up(&dir->;d_inode->;i_sem);
goto exit;
}
// 如果dentry不存在,则创建他
if (!dentry->;d_inode) {
if (!IS_POSIXACL(dir->;d_inode))
mode &= ~current->;fs->;umask;
error = vfs_create(dir->;d_inode, dentry, mode, nd); // 创建inode
up(&dir->;d_inode->;i_sem);
dput(nd->;dentry);
nd->;dentry = dentry;
if (error)
goto exit;
acc_mode = 0;
flag &= ~O_TRUNC;
goto ok;
}
up(&dir->;d_inode->;i_sem);
error = -EEXIST; // 如果指定了O_EXCL和O_CREAT,文件存在时,出错
if (flag & O_EXCL)
goto exit_dput;
if (d_mountpoint(dentry)) { // 检测文件是否是连接文件
error = -ELOOP;
if (flag & O_NOFOLLOW) // 如果指定不遍历连接文件,则返回
goto exit_dput;
// 检测dentry挂载点
while (__follow_down(&nd->;mnt,&dentry) && d_mountpoint(dentry));
}
error = -ENOENT;
if (!dentry->;d_inode) // inode 不存在,则返回
goto exit_dput;
if (dentry->;d_inode->;i_op && dentry->;d_inode->;i_op->;follow_link)
goto do_link; // 允许遍历连接文件,则手工找到连接文件对应的文件
// 将处理后的dentry复制到nd结构中,并判断其是否是目录,是则返回错误
dput(nd->;dentry);
nd->;dentry = dentry;
error = -EISDIR;
if (dentry->;d_inode && S_ISDIR(dentry->;d_inode->;i_mode))
goto exit;
ok:
error = may_open(nd, acc_mode, flag); // 打开文件,返回处理结果代码。如3.2.3.1.1描述
if (error)
goto exit;
return 0;
exit_dput:
dput(dentry); // 释放dentry
exit:
path_release(nd); // 释放nd结构
return error; // 返回错误代码
do_link:
error = -ELOOP;
if (flag & O_NOFOLLOW)
goto exit_dput; // 不允许遍历连接文件,则返回错误
// 以下代码是手工找到连接文件对应的文件dentry数据
nd->;flags |= LOOKUP_PARENT;
error = security_inode_follow_link(dentry, nd);
if (error)
goto exit_dput;
error = __do_follow_link(dentry, nd);
dput(dentry);
if (error)
return error;
nd->;flags &= ~LOOKUP_PARENT;
if (nd->;last_type == LAST_BIND) {
dentry = nd->;dentry;
goto ok;
}
error = -EISDIR;
if (nd->;last_type != LAST_NORM)
goto exit;
if (nd->;last.name[nd->;last.len]) {
putname(nd->;last.name);
goto exit;
}
error = -ELOOP;
if (count++==32) {
putname(nd->;last.name);
goto exit;
}
dir = nd->;dentry;
down(&dir->;d_inode->;i_sem);
dentry = __lookup_hash(&nd->;last, nd->;dentry, nd);
putname(nd->;last.name);
goto do_last;
} |
4.2.4.1.1.filp_open子函数may_open
may_open执行权限检测和文件打开,和truncate的操作。
int may_open(struct nameidata *nd, int acc_mode, int flag){
struct dentry *dentry = nd->;dentry;
struct inode *inode = dentry->;d_inode;
int error;
if (!inode) return -ENOENT; // inode为空,则返回错误
if (S_ISLNK(inode->;i_mode)) // 连接文件,返回错误
return -ELOOP;
if (S_ISDIR(inode->;i_mode) && (flag & FMODE_WRITE))
return -EISDIR; // 是目录且仅有写权限,返回错误
error = permission(inode, acc_mode, nd); // 见擦inode的accmode
if (error)
return error;
if (S_ISFIFO(inode->;i_mode) || S_ISSOCK(inode->;i_mode)) {
flag &= ~O_TRUNC; // 如果是FIFO文件,则不允许truncate
} else if (S_ISBLK(inode->;i_mode) || S_ISCHR(inode->;i_mode)) {
if (nd->;mnt->;mnt_flags & MNT_NODEV)
return -EACCES; // 如果是设备,则不允许truncate,否则返回错误
flag &= ~O_TRUNC;
} else if (IS_RDONLY(inode) && (flag & FMODE_WRITE))
return -EROFS; 如果flag标识和inode权限冲突,则返回错误
// 如果inode只允许append方式写入,则不允许truncate和非append写入方式。
if (IS_APPEND(inode)) {
if ((flag & FMODE_WRITE) && !(flag & O_APPEND))
return -EPERM;
if (flag & O_TRUNC)
return -EPERM;
}
// O_NOATIME方式仅在inode用户是文件拥有者或者超级用户情况下才被允许
if (flag & O_NOATIME)
if (current->;fsuid != inode->;i_uid && !capable(CAP_FOWNER))
return -EPERM;
// 检查是否有其他进程在使用该文件
error = break_lease(inode, flag);
if (error)
return error;
if (flag & O_TRUNC) {
error = get_write_access(inode); // 获取一次inode写操作权限
if (error)
return error;
// 锁定inode
error = locks_verify_locked(inode);
if (!error) {
DQUOT_INIT(inode); // 对inode执行配额初始化
error = do_truncate(dentry, 0); // truncate dentry
}
put_write_access(inode); // 释放当前写操作权限
if (error)
return error;
} else
if (flag & FMODE_WRITE) // 如果有写标识,则对inode执行配额初始化
DQUOT_INIT(inode);
return 0;
} |
4.2.4.2.open_namei子函数dentry_open
dentry_open函数主要实现文件表的对应打开等操作,返回文件指针。
struct file *dentry_open(struct dentry *dentry, struct vfsmount *mnt, int flags){
struct file * f;
struct inode *inode;
int error;
error = -ENFILE;
f = get_empty_filp(); // 从进程文件表中获取一个未使用的文件结构指针,空则出错返回
if (!f)
goto cleanup_dentry;
// 设置文件的flags和mode标识
f->;f_flags = flags;
f->;f_mode = ((flags+1) & O_ACCMODE) | FMODE_LSEEK | FMODE_PREAD | FMODE_PWRITE;
inode = dentry->;d_inode;
if (f->;f_mode & FMODE_WRITE) {
error = get_write_access(inode); // 获取一次inode写操作权限
if (error)
goto cleanup_file;
}
// 初始化文件结构
f->;f_mapping = inode->;i_mapping;
f->;f_dentry = dentry;
f->;f_vfsmnt = mnt;
f->;f_pos = 0;
f->;f_op = fops_get(inode->;i_fop);
file_move(f, &inode->;i_sb->;s_files);
// 调用文件驱动模块初始化物理磁盘
if (f->;f_op && f->;f_op->;open) {
error = f->;f_op->;open(inode,f);
if (error)
goto cleanup_all;
}
f->;f_flags &= ~(O_CREAT | O_EXCL | O_NOCTTY | O_TRUNC);
// 初始化上次读取状态
file_ra_state_init(&f->;f_ra, f->;f_mapping->;host->;i_mapping);
// 如果设置了O_DIRECT,则检测文件结构中是否有驱动的操作函数指针
if (f->;f_flags & O_DIRECT) {
if (!f->;f_mapping->;a_ops || !f->;f_mapping->;a_ops->;direct_IO) {
fput(f);
f = ERR_PTR(-EINVAL);
}
}
return f; // 返回文件结构
cleanup_all: // 出错,则释放资源并返回
fops_put(f->;f_op);
if (f->;f_mode & FMODE_WRITE)
put_write_access(inode);
file_kill(f);
f->;f_dentry = NULL;
f->;f_vfsmnt = NULL;
cleanup_file:
put_filp(f);
cleanup_dentry:
dput(dentry);
mntput(mnt);
return ERR_PTR(error);
} |
4.3. 总结
open函数的主要操作就是为文件初始化inode,初始化文件结构,刷新进程文件链表。
5.creat 函数
asmlinkage long sys_creat(const char __user * pathname, int mode){
return sys_open(pathname, O_CREAT | O_WRONLY | O_TRUNC, mode);
}
从上面的简短的实现代码可以看出,creat的系统调用直接以O_CREAT|O_WRONLY|O_TRUNC标识调用open函数实现的。
6.read 函数
6.1.原型与参数
ssize_t read(unsigned int fd, char * buf, size_t count)
read函数是从打开的文件中读取数据。如read成功,则返回读到的字节数。如已到达文件的尾端,则返回0。如果失败,则返回-1。有多种情况可使实际读到的字节数少于要求读字节数:
• 读普通文件时,在读到要求字节数之前已到达了文件尾端。例如,若在到达文件尾端之前还有30个字节,而要求读100个字节,则read返回30,下一次再调用read时,它将返回0 (文件尾端)。
• 当从终端设备读时,通常一次最多读一行(第11章将介绍如何改变这一点)。
• 当从网络读时,网络中的缓冲机构可能造成返回值小于所要求读的字节数。
• 某些面向记录的设备,例如磁带,一次最多返回一个记录。
读操作从文件的当前位移量处开始,在成功返回之前,该位移量增加实际读得的字节数。
6.2.实现分析
6.2.1.主要函数调用关系图
sys_read (参见6.2.2 )
| ------------- vfs_read (参见6.2.3) |
6.2.2.主调用函数sys_read
asmlinkage ssize_t sys_read(unsigned int fd, char __user * buf, size_t count)
{
struct file *file;
ssize_t ret = -EBADF;
int fput_needed;
// 从进程文件链表中根据fd获取file文件表,如果没有这个文件表,则返回EBADF错误
file = fget_light(fd, &fput_needed); // fput_needed标识是否需要更新file结构的count
if (file) {
loff_t pos = file_pos_read(file); // 获取file结构的pos位移参数
ret = vfs_read(file, buf, count, &pos); // 读取文件内容,参见6.2.2
file_pos_write(file, pos); // 更新file结构的pos位移参数
fput_light(file, fput_needed); // 更新文件count标识
}
return ret;
} |
6.2.3.sys_read子函数vfs_read
vfs_read将调用文件驱动模块的read函数从磁盘中读入数据并返回。
ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{
ssize_t ret;
if (!(file->;f_mode & FMODE_READ))
return -EBADF;// 如果文件不允许读模式,则返回EBADF错误
if (!file->;f_op || (!file->;f_op->;read && !file->;f_op->;aio_read))
return -EINVAL;// 如果文件结构没有read函数指针,则返回EINVAL错误
if (unlikely(!access_ok(VERIFY_WRITE, buf, count)))
return -EFAULT;// 检查传入的参数是否超出了用户空间地址,如果是则返回EFAULT错误
// 通过inode结构lock当前要操作的区域,成功返回0
ret = rw_verify_area(READ, file, pos, count);
if (!ret) {
// 检查进程对文件是否有READ操作权限,成功,则返回0
ret = security_file_permission (file, MAY_READ);
if (!ret) {
if (file->;f_op->;read) // 调用文件驱动模块read函数读入数据
ret = file->;f_op->;read(file, buf, count, pos);
else // 否则调用直接磁盘读操作
ret = do_sync_read(file, buf, count, pos);
if (ret >; 0) {
// 通知目录,当前文件已经被访问
dnotify_parent(file->;f_dentry, DN_ACCESS);
current->;rchar += ret; // 刷新进程读数据计数器
}
current->;syscr++;
}
}
return ret;
} |
7.write函数
7.1.原型与参数
sys_write(unsigned int fd, const char * buf, size_t count)
sys_write函数将数据写入到文件中。其返回值通常与参数nbytes的值不同,-1表示出错。write出错的一个常见原因是:磁盘已写满,或者超过了对一个给定进程的文件长度限制。对于普通文件,写操作从文件的当前位移量处开始。如果在打开该文件时,指定了O_APPEND选择项,则在每次写操作之前,将文件位移量设置在文件的当前结尾处。在一次成功写之后,该文件位移量增加实际写的字节数。
7.2.实现分析
7.2.1.主要函数调用关系图
sys_write (参见7.2.2 )
| ------------- vfs_write (参见7.2.3)
7.2.2.主调用函数sys_write
asmlinkage ssize_t sys_write(unsigned int fd, const char __user * buf, size_t count)
{
struct file *file;
ssize_t ret = -EBADF;
int fput_needed;
// 从进程文件链表中根据fd获取file文件表,如果没有这个文件表,则返回EBADF错误
file = fget_light(fd, &fput_needed); // fput_needed标识是否需要更新file结构的count
if (file) {
loff_t pos = file_pos_read(file); // 获取file结构的pos位移参数
ret = vfs_write(file, buf, count, &pos); // 写入文件,参见7.2.3 描述
file_pos_write(file, pos); // 更新file结构的pos位移参数
fput_light(file, fput_needed); // 更新文件count标识
}
return ret;
} |
7.2.3.sys_write子函数vfs_write
vfs_write将调用文件驱动模块的write函数向磁盘中写入数据并返回。如果在打开该文件时,指定了O_APPEND选择项,则在每次写操作之前,将文件位移量设置在文件的当前结尾处再继续写入。
ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
{
ssize_t ret;
if (!(file->;f_mode & FMODE_WRITE))
return -EBADF; // 如果文件不允许写模式,则返回EBADF错误
if (!file->;f_op || (!file->;f_op->;write && !file->;f_op->;aio_write))
return -EINVAL; // 如果文件结构没有read函数指针,则返回EINVAL错误
if (unlikely(!access_ok(VERIFY_READ, buf, count)))
return -EFAULT; // 检查传入的参数是否超出了用户空间地址,如果是则返回EFAULT错误
// 通过inode结构lock当前要操作的区域,成功返回0
ret = rw_verify_area(WRITE, file, pos, count);
if (!ret) {
// 检查进程对文件是否有WRITE操作权限,成功,则返回0
ret = security_file_permission (file, MAY_WRITE);
if (!ret) {
if (file->;f_op->;write) // 调用文件驱动模块write函数写入数据
ret = file->;f_op->;write(file, buf, count, pos);
else // 否则调用直接磁盘写操作
ret = do_sync_write(file, buf, count, pos);
if (ret >; 0) {
// 通知目录,当前文件已经被改写
dnotify_parent(file->;f_dentry, DN_MODIFY);
current->;wchar += ret; // 刷新进程写数据计数器
}
current->;syscw++;
}
}
return ret;
} |