【Linux】文件IO

# 前置知识

  • Linux文件I/O分为系统IO和标准IO,常用于系统编程

  • 系统I/O通过文件描述符 fd 来操作文件

  • 标准I/O通过文件流 FILE* 来操作文件

  • Linux下可以使用man命令来查看使用手册

man指令 

  • 通过man man指令可以查看man指令的帮助手册,其中man 2 xxx是查看系统IO,man 3 xxx是查看标准IO

【Linux】文件IO_第1张图片

# 内部调用机制 

【Linux】文件IO_第2张图片

  •  以 open 函数为例,用户态调用 API 触发异常进入内核;
  •  内核识别异常后,取出异常值,old ABI从参数中获取,EABI从R7中获取,ARN64从R8中获取,即__NR_open,调用sys_call_table[__NR_open],sys_call_table就是一个数组,其值为typedef void (*sys_call_ptr_t)(void)类型的函数指针,内容大致如下:
__SYSCALL_COMMON(0, sys_read, sys_read)
__SYSCALL_COMMON(1, sys_write, sys_write)
__SYSCALL_COMMON(2, sys_open, sys_open)
__SYSCALL_COMMON(3, sys_close, sys_close)
__SYSCALL_COMMON(5, sys_newfstat, sys_newfstat)
...
...
...
  •  最后调用sys_open函数,完成用户态到内核的切入,进入内核后, sys_read/open 会首先根据参数判断文件的类型,然后根据不同的文件类型去找不同的设备驱动,继而进行读写或者输入输出控制。

# 文件句柄fd

        在调用 open 函数时,在成功打开文件后,会返回一个文件句柄fd,在后续的 write 、read 等操作都可以通过文件句柄fd完成对文件的操作,那么是如何通过一个文件句柄fd就可以完成对文件各式各样的操作

  •  在上述成功调用sys_open函数后,sys_open函数定义如下:
SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, int, mode)
{
	long ret;
 
	if (force_o_largefile())
		flags |= O_LARGEFILE;
 
	ret = do_sys_open(AT_FDCWD, filename, flags, mode);
	/* avoid REGPARM breakage on x86: */
	asmlinkage_protect(3, ret, filename, flags, mode);
	return ret;
}
  •  在sys_open函数里面,又调用了 de_sys_open 函数,定义如下:

long do_sys_open(int dfd, const char __user *filename, int flags, int mode)
{
	char *tmp = getname(filename);
	int fd = PTR_ERR(tmp);
 
	if (!IS_ERR(tmp)) {
		fd = get_unused_fd_flags(flags);        /* 获取未使用的函数句柄 */
		if (fd >= 0) {
			struct file *f = do_filp_open(dfd, tmp, flags, mode, 0);    /* 打开文件,得到file结构体 */
			if (IS_ERR(f)) {
				put_unused_fd(fd);
				fd = PTR_ERR(f);
			} else {
				fsnotify_open(f);
				fd_install(fd, f);    /* 在当前进程里面记录下来 */
			}
		}
		putname(tmp);
	}
	return fd;
}
  •  通过以上过程,就绑定了文件句柄fd与file结构体,后续操作文件句柄fd,实际上就是操作文件结构体file,文件结构体file如下,在结构体中,有f_op,里面包含了读写等对文件的操作;有f_flags,这就是使用open函数时的打开模式;有f_pos,这就是使用读写函数时,指向文件的位置
struct file {
        /*
         * fu_list becomes invalid after file_free is called and queued via
         * fu_rcuhead for RCU freeing
         */
        union {
                struct list_head        fu_list;
                struct rcu_head         fu_rcuhead;
        } f_u;
        struct path             f_path;
#define f_dentry        f_path.dentry
#define f_vfsmnt        f_path.mnt
        const struct file_operations    *f_op;
        atomic_t                f_count;
        unsigned int            f_flags;
        mode_t                  f_mode;
        loff_t                  f_pos;
        struct fown_struct      f_owner;
        unsigned int            f_uid, f_gid;
        struct file_ra_state    f_ra;
        unsigned long           f_version;
#ifdef CONFIG_SECURITY
        void                    *f_security;
#endif
        /* needed for tty driver, and maybe others */
        void                    *private_data;
#ifdef CONFIG_EPOLL
        /* Used by fs/eventpoll.c to link all the hooks to this file */
        struct list_head        f_ep_links;
        spinlock_t              f_ep_lock;
#endif /* #ifdef CONFIG_EPOLL */
        struct address_space    *f_mapping;
};

         通过上述内容,我们得到了文件句柄fd与操作文件结构体file的绑定流程,下述将看看在一个进程里面,是如何管理文件句柄fd的

  •  以下程序是一个非常简单的代码,在程序运行时,将传进一个文件名参数,程序将打开这个文件,打印出此文件的文件句柄fd,然后进入一个死循环
#include 
#include 
#include 

int main(int argc, char *argv[])
{
        int fd;

        if(argc != 2)
        {
                printf("file name error\r\n");
                return -1;
        }

        fd = open(argv[1], O_RDONLY);
        if(fd < 0)
        {
                perror("open:");
                return -1;
        }

        printf("%s file fd is %d\r\n", argv[1], fd);

        while(1)
        {
                sleep(100);
        }

        return 0;
}
  •  在运行上述程序后,我们将会得到1.txt的文件句柄为3,且此进程号为4297

【Linux】文件IO_第3张图片

  • 在使用 ls -l 指令查看 open 程序的进程后,我们可以发现,此进程一共打开了4个文件,0为标准输入,1为标准输出,2为标准错误,3为1.txt文件句柄

【Linux】文件IO_第4张图片

  •  在 do_sys_open 函数执行过程中,有一个 fd_install 函数,他只要执行的工作就是将参数的文件句柄与当前的进程进行绑定,换句话说,每一个进程的文件句柄都是独立的,在不同的进程里面,同一个数字的文件句柄也可能指向不同的文件
long do_sys_open(int dfd, const char __user *filename, int flags, int mode)
{
	char *tmp = getname(filename);
	int fd = PTR_ERR(tmp);
 
	if (!IS_ERR(tmp)) {
		fd = get_unused_fd_flags(flags);        /* 获取未使用的函数句柄 */
		if (fd >= 0) {
			struct file *f = do_filp_open(dfd, tmp, flags, mode, 0);    /* 打开文件,得到file结构体 */
			if (IS_ERR(f)) {
				put_unused_fd(fd);
				fd = PTR_ERR(f);
			} else {
				fsnotify_open(f);
				fd_install(fd, f);    /* 在当前进程里面记录下来 */
			}
		}
		putname(tmp);
	}
	return fd;
}
void fd_install(unsigned int fd, struct file *file)//将文件对象 file 安装到进程的文件描述符表中
{
	__fd_install(current->files, fd, file);
	/*current->files 是一个指向当前进程的文件描述符表的指针。
		在操作系统中,每个进程都有一个与之关联的文件描述符表,用于跟踪进程打开的文件和文件描述符的状态。
	current 表示当前正在执行的进程,而 current->files 则是获取该进程的文件描述符表的方式之一。
		它是进程控制块(Process Control Block,PCB)中的一个成员变量。
	通过 current->files,可以访问当前进程的文件描述符表,并对其中的文件对象进行操作,如打开、关闭、读取、写入等。
		文件描述符表是一个数组或链表的形式,在操作系统内核中维护着每个文件描述符对应的文件对象信息。*/
}
  •  在上述 fd_install 函数中,current 代表的是一个task_struct的结构体,task_struct是一个很庞大的结构体,现在我们只关心它很小的一部分,其中current->files是一个指向当前进程的文件描述符表的指针,从以下关系可以看出,current->files里面有一个 struct fdtable 类型的指针,struct fdtable 里面呢具有一个 struct file 类型的数组,这个数组就存放了不同文件的文件描述结构体

【Linux】文件IO_第5张图片

  • 从下述图片中我们可以理解更清晰一点,当使用 open 函数打开 1.txt 时,得到的文件句柄 fd 为3,实际上代表的是当前进程中的 fdtable 结构体中 fd 数组中的第四个元素是1.txt 的文件描述符

【Linux】文件IO_第6张图片

# 系统I/O

1. open()

使用man 2 open查看使用手册

函数原型:

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

函数参数:

mode:

【Linux】文件IO_第7张图片

返回值: 

  • 返回值类型:int
  • 调用成功时返回一个文件描述符fd
  • 调用失败时返回-1,并设置errno

2. read()

使用man 2 read查看使用手册

函数原型:

ssize_t read(int fd, void buf[], size_t count);

函数参数:

参数 意义
fd 即将读取文件的文件描述符
buf 存储读入数据的缓冲区
count 将要读入的数据的个数

返回值: 

  • 返回值类型是:ssize_t,32位机上等同于int
  • 成功时返回读取的字节数
  • 出错时返回EOF,读到文件末返回0

3. write

使用man 2 write查看使用手册

函数原型:

ssize_t write(int fd, const void buf[.count], size_t count)

函数参数: 

参数 意义
fd 即将读取文件的文件描述符
buf 要写入的数据缓冲区
count 写入数据的个数,大小不应该大于buf大小

返回值: 

  • 返回值类型为:ssize_t
  • 成功时返回写入的字节数
  • 出错时返回EOF,读到文件末返回0

4. close

使用 man 2 close查看使用手册

函数原型:

int close(int fd);

 函数参数: 

  • fd: 要关闭的文件描述符。

返回值: 

  • 关闭成功时返回0,出错时返回EOF.。

5. lseek

使用 man 2 lseek查看使用手册

函数原型:

off_t lseek(int fd, off_t offset, int whence);

  函数参数: 

参数 意义
fd 文件描述符
offset 偏移量,可正可负
whence 指定一个基准点,基准点+偏移量等于当前位置

 whence:

【Linux】文件IO_第8张图片

 返回值: 

  • 返回值类型为:off_t, 32位机等同于int
  • 成功时则返回目前的读写位置, 也就是距离文件开头多少个字节
  • 出错时返回-1, errno 会存放错误代码.

6. dup

使用 man 2 dup查看使用手册

函数原型:

int dup(int oldfd)

  函数参数: 

  返回值: 

  • 一个新的文件描述符,失败为-1

函数说明:

用来打开一个新的文件描述符,指向和oldfd同一个文件,共享文件偏移量和文件状态。

当我们调用dup(3)的时候,会打开新的最小描述符,也就是4,这个4指向了3所指向的文件,后续操作这两个中任意一个fd都有一样的效果。

 

【Linux】文件IO_第9张图片

 函数原型:

int dup2(int oldfd, int newfd)

函数参数: 

 返回值:

  • 如果出错就返回-1,否则返回的就是newfd

函数说明: 

        dup2是直接让传入的参数newfd与参数oldfd指向同一文件表项,如果newfd已经被open过,那么就会先将newfd关闭,然后让newfd指向oldfd所指向的文件表项,如果newfd本身就等于oldfd,那么就直接返回newfd

        假设我们使用 dup2(3, 4) 则呈现以下效果:

【Linux】文件IO_第10张图片

你可能感兴趣的:(linux,运维,服务器)