Linux 文件IO编程.之read/write,open/close,fcntl/lseek,阻塞和非阻塞,文件操作和文件系统调用(一)

本文详细介绍了Linux下的文件I/O编程,包括文件描述符的概念、文件结构体file的解析、缓冲区机制的工作原理,以及open、read、write等系统调用的使用方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Linux中的设备管理

Linux采用文件系统管理硬件设备,对于计算机/操作系统来说,屏幕网卡键盘鼠标都是硬件设备,也就是说Linux下有一个文件就可以代表一个硬件,所有的设备都看成是特殊的文件,从而将硬件设备的特性及管理细节对用户隐藏起来,实现设备无关性。
在这里插入图片描述
如/dev可以管理硬件设备,/mnt可以U盘挂载…
Linux系统要想管理硬件,就是通过文件来管理的,因此,文件怎么操作,非常重要!因为Linux下的文件的操作,就相当于是对设备的操作了。

在这里插入图片描述
pcb进程控制块本质是一个结构体:struct task_struct 结构体,结构体的其中一个成员是一个指向文件描述符表(如上图右边)的一个指针(file_struct *file)文件描述符表里面存的是文件描述符。

文件描述符是一个整数,本质上是一个指针(目前的理解)该指针指向一个文件结构体(struct file )将文件描述符表当成是一个数组,可以将文件描述符当成是数组的下标,数组里存的是文件结构体的指针。可以直接用文件描述符访问文件。

一个进程最多可以打开1024个文件,文件描述符表大小是1024
在这里插入图片描述

文件管理结构体file结构体
file结构体在,源码在linux/inlcude/linux/fs.h文件中

struct file {
    union {
        struct llist_node    fu_llist;
        struct rcu_head     fu_rcuhead;
    } f_u;
    struct path        f_path;
    struct inode        *f_inode;    /* cached value */
    const struct file_operations    *f_op;

    /*
     * Protects f_ep_links, f_flags.
     * Must not be taken from IRQ context.
     */
    spinlock_t        f_lock;
    atomic_long_t        f_count;
    unsigned int         f_flags;
    fmode_t            f_mode;
    struct mutex        f_pos_lock;
    loff_t            f_pos;
    struct fown_struct    f_owner;
    const struct cred    *f_cred;
    struct file_ra_state    f_ra;

    u64            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;
    struct list_head    f_tfile_llink;
#endif /* #ifdef CONFIG_EPOLL */
    struct address_space    *f_mapping;
} __attribute__((aligned(4)));    /* lest something weird decides that 2 is OK */

每个数据中文解释

struct file {

  union {

        struct list_head fu_list; //文件对象链表指针linux/include/linux/list.h

        struct rcu_head fu_rcuhead; //RCU(Read-Copy Update)是Linux 2.6内核中新的锁机制

   } f_u;

  struct path f_path; //包含dentry和mnt两个成员,用于确定文件路径

  #define f_dentry f_path.dentry //f_path的成员之一,当前文件的dentry结构

  #define f_vfsmnt f_path.mnt //表示当前文件所在文件系统的挂载根目录

  const struct file_operations *f_op; //与该文件相关联的操作函数

  atomic_t f_count; //文件的引用计数(有多少进程打开该文件)

  unsigned int f_flags; //对应于open时指定的flag

  mode_t f_mode; //读写模式:open的mod_t mode参数

    loff_t     f_pos;//当前文件指针位置

  off_t f_pos; //该文件在当前进程中的文件偏移量

  struct fown_struct f_owner; //该结构的作用是通过信号进行I/O时间通知的数据。

  unsigned int f_uid, f_gid;// 文件所有者id,所有者组id

  struct file_ra_state f_ra; //在linux/include/linux/fs.h中定义,文件预读相关

  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;

  };

在这里插入图片描述

每个进程在PCB(Process Control Block)即进程控制块中都保存着一份文件描述符表,文件描述符就是这个表的索引,文件描述表中每个表项都有一个指向已打开文件的指针,现在我们明确一下:已打开的文件在内核中用file结构体表示,文件描述符表中的指针指向file结构体。

struct file结构体定义在include/linux/fs.h中定义。文件结构体代表一个打开的文件,系统中的每个打开的文件在内核空间都有一个关联的 struct file。它由内核在打开文件时创建,并传递给在文件上进行操作的任何函数。在文件的所有实例都关闭后,内核释放这个数据结构。在内核创建和驱动源码中,struct file的指针通常被命名为file或filp。

其有两个非常重要的字段:文件描述符和缓冲区。
文件描述符fd:
fd只是一个小整数,在open时产生。起到一个索引的作用,进程通过PCB中的文件描述符表找到该fd所指向的文件指针filp。
文件描述符的操作(如: open)返回的是一个文件描述符,内核会在每个进程空间中维护一个文件描述符表, 所有打开的文件都将通过此表中的文件描述符来引用;
而流(如: fopen)返回的是一个FILE结构指针, FILE结构是包含有文件描述符的,FILE结构函数可以看作是对fd直接操作的系统调用的封装, 它的优点是带有I/O缓存。

缓冲区:

a、缓冲区机制

根据应用程序对文件的访问方式,即是否存在缓冲区,对文件的访问可以分为带缓冲区的操作和非缓冲区的文件操作:
1、带缓冲区文件操作:高级标准文件I/O操作,将会在用户空间中自动为正在使用的文件开辟内存缓冲区。
2、非缓冲区文件操作:低级文件I/O操作,读写文件时,不会开辟对文件操作的缓冲区,直接通过系统调用对磁盘进行操作(读、写等),当然用于可以在自己的程序中为每个文件设定缓冲区。

两种文件操作的解释和比较:
1、非缓冲的文件操作访问方式,每次对文件进行一次读写操作时,都需要使用读写系统调用来处理此操作,即需要执行一次系统调用,执行一次系统调用将涉及到CPU状态的切换,即从用户空间切换到内核空间,实现进程上下文的切换,这将损耗一定的CPU时间,频繁的磁盘访问对程序的执行效率造成很大的影响。
2、ANSI标准C库函数 是建立在底层的系统调用之上,即C函数库文件访问函数的实现中使用了低级文件I/O系统调用,ANSI标准C库中的文件处理函数为了减少使用系统调用的次数,提高效率,采用缓冲机制,这样,可以在磁盘文件进行操作时,可以一次从文件中读出大量的数据到缓冲区中,以后对这部分的访问就不需要再使用系统调用了,即需要少量的CPU状态切换,提高了效率。

b、缓冲类型
标准I/O提供了3种类型的缓冲区。

1、全缓冲区:这种缓冲方式要求填满整个缓冲区后才进行I/O系统调用操作。对于磁盘文件的操作通常使用全缓冲的方式访问。第一次执行I/O操作时,ANSI标准的文件管理函数通过调用malloc函数获得需要使用的缓冲区,默认大小为8192。
2、行缓冲区:在行缓冲情况下,当在输入和输出中遇到换行符时,标准I/O库函数将会执行系统调用操作。当所操作的流涉及一个终端时(例如标准输入和标准输出),使用行缓冲方式。因为标准I/O库每行的缓冲区长度是固定的,所以只要填满了缓冲区,即使还没有遇到换行符,也会执行I/O系统调用操作,默认行缓冲区的大小为1024。
3、无缓冲区:无缓冲区是指标准I/O库不对字符进行缓存,直接调用系统调用。标准出错流stderr通常是不带缓冲区的,这使得出错信息能够尽快地显示出来。

注:
1、标准输入和标准输出设备:当且仅当不涉及交互作用设备时,标准输入流和标准输出流才是全缓冲的。
2、标准错误输出设备:标准出错绝不会是全缓冲方式的。
3、对于任何一个给定的流,可以调用setbuf()和setvbuf()函数更改其缓冲区类型。

下面我们通过如下程序来进一步了解缓冲区:

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<errno.h>
#include<string.h>
#include<stdlib.h>

char* msg="hello bit\n";
char* msg1="hello world\n";

int main()
{
    printf("printf\n");
    fwrite(msg,1,strlen(msg),stdout);
    write(1,msg1,strlen(msg1));

    pid_t id=fork();
    if(id<0){
        printf("fork,%s\n",strerror(errno));
        exit(0);
    }else if(id==0) {
        printf("child:pid=%d,ppid=%d\n",getpid(),getppid());
    }else{
        printf("father:pid=%d,ppid=%d\n",getpid(),getppid());
        sleep(3);
    }
    return 0;
}

执行 ./a.out 输出打印如下
printf
hello bit
hello world
father:pid=19502,ppid=13555
child:pid=19503,ppid=19502

再来测试执行 ./a.out > testfile 将结果写到文件里

testfile内容如下所示:
hello world
printf
hello bit
child:pid=22995,ppid=22994
printf
hello bit
father:pid=22994,ppid=13555

那么为什么输出到屏幕只有5条输出命令而输出到文件有7条输出命令呢?根据输出结果我们可以看出printf和fwrite重复写了两次,没有重复打印的是write.。
printf和fwrite都是库函数,而write则是系统直接调用的:结合已有知识,我们了解到当使用库函数命令时,打印消息并没有直接写到输出位置上,而是先把数据写到输出缓冲区,在刷新至输出位置。
1、当输出目标位置为输出到显示器时,则刷新方式是行刷新;
2、当输出目标位置为输出到文件中时,刷新方式由行缓冲变为全缓冲,全缓冲是指当把缓冲区写满后才能刷新。(或者强制刷新)代码中printf和fwrite第一次打印在fork操作之前,第二次打印则在fork操作之后,原因是在fork操作前,printf和fwrite的输出命令将数据先写到缓冲区中,此时只执行了这两条命令,由于是全缓冲的刷新方式,所以这两条命令并不足以将缓存写满,所以数据暂存在缓冲区中;然后进行fork创建子进程,由于fork创建出的父子进程代码共享,而数据不共享,各自私有一份,缓冲区中的数据都属于数据,因为父进程的残留数据还在缓冲区中,所以fork完毕后,父子进程将缓存中的数据各自存一份有父进程残留数据的数据,所以当父子进程各自刷新时,父子进程会各自执行一次printf和fwrite的输出命令,所以命令就从原来的两条变为四条。

struct file 的其他重要成员有:.

1.mode_t f_mode;
文件模式确定文件是可读的或者是可写的(或者都是), 通过位 FMODE_READ 和FMODE_WRITE. 你可能想在你的 open 或者 ioctl 函数中检查这个成员的读写许可, 但是不需要检查读写许可, 因为内核在调用你的方法之前检查. 当文件还没有为那种存取而打开时读或写的企图被拒绝, 驱动甚至不知道这个情况.

2.loff_t f_pos;
当前读写位置. loff_t 在所有平台都是 64 位( 在 gcc 术语里是 long long ). 驱动可以读这个值,如果它需要知道文件中的当前位置, 但是正常地不应该改变它; 读和写应当使用它们作为最后参数而收到的指针来更新一个位置, 代替直接作用于 filp->f_pos. 这个规则的一个例外是在 llseek 方法中, 它的目的就是改变文件位置.

3.unsigned int f_flags;
这些是文件标志, 例如 O_RDONLY, O_NONBLOCK, 和 O_SYNC. 驱动应当检查O_NONBLOCK 标志来看是否是请求非阻塞操作; 其他标志很少使用. 特别地, 应当检查读/写许可, 使用 f_mode 而不是f_flags. 所有的标志在头文件<linux/fcntl.h> 中定义.

*4.struct file_operations f_op;
和文件关联的操作. 内核安排指针作为它的open 实现的一部分, 接着读取它当它需要分派任何的操作时. filp->f_op 中的值从不由内核保存为后面的引用; 这意味着你可改变你的文件关联的文件操作, 在你返回调用者之后新方法会起作用. 例如, 关联到主编号 1 (/dev/null, /dev/zero, 等等)的 open 代码根据打开的次编号来替代 filp->f_op 中的操作. 这个做法允许实现几种行为, 在同一个主编号下而不必在每个系统调用中引入开销. 替换文件操作的能力是面向对象编程的"方法重载"的内核对等体.

*5.void private_data;
open 系统调用设置这个指针为 NULL, 在为驱动调用 open 方法之前. 你可自由使用这个成员或者忽略它; 你可以使用这个成员来指向分配的数据, 但是接着你必须记住在内核销毁文件结构之前, 在 release 方法中释放那个内存. private_data 是一个有用的资源, 在系统调用间保留状态信息, 我们大部分例子模块都使用它.

*6.struct dentry f_dentry;
关联到文件的目录入口( dentry )结构. 设备驱动编写者正常地不需要关心 dentry 结构, 除了作为 filp->f_dentry->d_inode 存取 inode 结构.

三:系统调用和函数库调用的区别

系统调用比函数库调用效率更高 ,因为少了函数库这一过程,库函数需要调用系统调用函数。

缺点:系统调用(无法跨平台、Linux开发就只能Linux下使用,不能window下使用)
在这里插入图片描述
在这里插入图片描述
ile_operations结构体成员函数,这个结构体可以对文件进行打开,读写,定位,控制等操作;如下图所示:
在这里插入图片描述

一、c库函数
Linux 文件IO编程.之api一览表(read,write,close,fcntl,等)(一)

四、文件系统调用api

open系统调用
read系统调用
write系统调用
create系统调用
close系统调用
mkdir系统调用
lseek

open系统调用有两种,如下
有几种方法可以获得允许访问文件的文件描述符
最常用的是使用open()(打开)系统调用

函数原型

int open(const char *path, int flags);

参数
path :文件的名称,可以包含(绝对和相对)路径
flags:文件打开模式
返回值
打开成功,返回文件描述符;
打开失败,返回-1

函数原型

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

参数
path :文件的名称,可以包含(绝对和相对)路径
flags:文件打开模式
mode: 用来规定对该文件的所有者,文件的用户组及系统中其他用户的访问权限,则文件权限为:mode&(~umask)
返回值
打开成功,返回文件描述符;
打开失败,返回-1
打开文件的方式
在这里插入图片描述
访问权限
在这里插入图片描述

flags:文件打开方式
O_RDONLY|O_WRONLY|O_RDWR 只读、只写、读写
O_CREAT|O_APPEND|O_TRUNC|O_EXCL|O_NONBLOCK …
O_TRUNC:如果该文件已经存在并且是常规文件,并且访问模式允许写入(即,为O_RDWR或O_WRONLY),则它将被截断为长度0。如果文件是FIFO或终端设备文件,O_TRUNC标志将被忽略。

mode: 参数3使用的前提, 参2指定了 O_CREAT。 取值8进制数,用来描述文件的 访问权限。 rwx 0664

创建文件最终权限 = mode & ~umask (自己设定的mode 和 umask的非 相与)(与:全1为1,其他情况都为0。或:全0为0,其他情况都为1)一般mode设为0664
在Linux系统中umask值代表的是我们创建新文件/目录的默认权限设置,系统root账号默认的umask的值为0022 普通账号的umask的值为0002

write系统调用

用write()系统调用将数据写到一个文件中

函数原型:

int write(int fd,void *buf,size_t nbytes);

函数参数:
fd :要写入的文件的文件描述符
buf: 指向内存块的指针,从这个内存块中读取数据写入 到文件中
nbytes: 要写入文件的字节个数
返回值
如果出现错误,返回-1
如果写入成功,则返回写入到文件中的字节个数

read系统调用

一旦有了与一个打开文件描述相连的文件描述符,只要该文件是用O_RDONLY或O_RDWR标志打开的,就可以用read()系统调用从该文件中读取字节

函数原型:

int read(int fd, void *buf, size_t nbytes)

参数
fd :想要读的文件的文件描述符
buf: 指向内存块的指针,从文件中读取来的字节放到这个内存块中
nbytes: 从该文件复制到buf中的字节个数
返回值
如果出现错误,返回-1
返回从该文件复制到规定的缓冲区中的字节数,文件结束,返回0否则

注意点:文件的随机读写
到目前为止的所有文件访问都是顺序访问

这是因为所有的读和写都从当前文件的偏移位置开始,然后文件偏移值自动地增加到刚好超出读或写结束时的位置,使它为下一次访问作好准备

有个文件偏移这样的机制,在Linux系统中,随机访问就变得很简单,你所需做的只是将当前文件移值改变到有关的位置,它将迫使一次read()或write()发生在这一位置(除非文件被O_APPEND打开,在这种情况下,任何write调用仍将发生在文件结束处)

lseek系统调用
在这里插入图片描述
在这里插入图片描述
计算文件多大小int lenth = lseek(fd, 0, SEEK_END);

#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<errno.h>
#include<string.h>
 
 
//报错函数
void my_err(const char *err_string,int line)
{
    fprintf(stderr,"line:%d",line);
    perror(err_string);
    _exit(1);
}
 
//读数据函数
int my_read(int fd)
{
    int len;
    int ret;
    int i;
    char read_buf[64];
 
    //指针移到最后
    if(lseek(fd,0,SEEK_END)==-1)
    {
        my_err("lseek",__LINE__);
    }
 
    //最后位置到文件开始位置字节数
    if((len=lseek(fd,0,SEEK_CUR))==-1)
    {
        my_err("lseek",__LINE__);
    }
 
    //指针回到文件开始处,下一步方便读取数据
    if(lseek(fd,0,SEEK_SET)==-1)
    {
        my_err("lseek",__LINE__);
    }
 
    printf("len:%d\n",len);
 
    //读取数据,返回读取字节数
    if((ret=read(fd,read_buf,len))<0)
    {
 
        my_err("read",__LINE__);
    }
 
    //打印
    for(i=0;i<len;i++)
    {
        printf("%c",read_buf[i]);
 
    }
    printf("\n");
    return ret;
 
}
 
int main()
{
    int fd;
    char write_buf[32]="Hello World";
    char write_buf2[32]="Q";
 
    if((fd=open("test.txt",O_CREAT|O_RDWR|O_TRUNC,S_IRUSR|S_IWUSR))==-1)
    {
        my_err("open",__LINE__);
    }
    else
    {
        printf("creat the file success\n");
    }
 
    if(write(fd,write_buf,strlen(write_buf))!=strlen(write_buf))
    {
        my_err("write",__LINE__);
    }
    printf("---------------------------------\n");
    if(lseek(fd,10,SEEK_END)==-1)
    {
        my_err("lseek",__LINE__);
    }
    if(write(fd,write_buf2,strlen(write_buf2))!=strlen(write_buf2))
    {
        my_err("write",__LINE__);
    }
    my_read(fd);
    close(fd);
    return 0;
}

六、_exit,_Exit 函数
函数原型

#include <unistd.h>
void _exit(int status);
#include <stdlib.h>
void _Exit(int status);

函数作用
当程序在前面步骤失败,导致后面的操作不能继续进行时,应该在前面的错误监测中结束整个程序。结束程序有两种方法:在main用return(一般原则是程序正常终止return 0,如果程序异常终止则return -1);或者使用_exit、_Exi、exit(C库函数)三者之一。

注意,_exit、_Exit函数没有返回值。
参数说明
status表示当前进程的退出状态,这个值会被父进程收集,以作为某些判断之用。
补充说明
上面的_exit、_Exit是Linux系统的API函数,两者的功能是一样的。
另外还有一个C库函数exit,也能实现同样效果,函数原型如下所示。

#include <stdlib.h>
void exit(int status);

七、perror函数
函数原型

#include <stdio.h>
#include <errno.h> //之所以要包含这个文件,是因为errno的定义在这个文件中
 
void perror(const char *s);

参数说明
字符串s表示要打印显示的内容。

补充说明
Linux系统中对各种常见错误做了编号,当函数执行错误时,函数会返回一个特定的errno编号来告诉我们这个函数到底哪里错了。errno全称是error number,意思是“错误号码”,它是由OS来维护的一个全局变量,任何OS内部函数都可以通过设置errno来告诉上层调用者究竟刚才发生了一个什么错误。

errno本质是一个int类型的数字,每个数字编号对应一种错误。我们从errno这个变量里只能得知它的值是多少,不知道这个值所代表的错误信息。

因此Linux系统提供了一个perror函数(意思print error),perror函数内部会读取errno,然后将这个不好认的数字转换成对应的错误信息字符串,然后print打印出来。

它用命令“ man 2 perror ”显示不出来,要用” man 3 perror ”才行,这说明它是C库函数。另外从它的头文件stdio.h也可以看出它是C库函数(虽然不是API,但既然写在这里了,那就保留着吧)。

八、dup、dup2、dup3函数
函数原型

#include <unistd.h>
 
int dup(int oldfd);
int dup2(int oldfd, int newfd);
 
#define _GNU_SOURCE             /* See feature_test_macros(7) */
#include <fcntl.h>              /* Obtain O_* constant definitions */
#include <unistd.h>
int dup3(int oldfd, int newfd, int flags);

函数作用
接下来都是关于dup函数的描述,dup2、dup3函数略。
dup函数对旧的 fd 进行复制,返回一个新的文件描述符,它们都指向同一个动态文件。相当于两个不同的指针指向同一个文件。

参数说明
oldfd是要进行复制的文件描述符。

补充说明
(1)dup函数不能指定复制得到的fd的值,而是由操作系统内部自动分配。分配的原则遵守fd分配的原则,即分配没被占用的且数值最小的fd。
(2)dup函数返回的 fd 和原来的 oldfd,都表示原来oldfd所表示的那个动态文件,操作这两个fd实际操作的是 oldfd 表示的那个文件,这就构成了文件共享(这是文件共享的方式之一,其他另种方式包括:同一进程中多次使用open函数打开同一文件,不同进程中打开同一文件)。
(3)利用dup返回的fd和原来的oldfd同时向一个文件写入时,结果应该是分别写。
(4)dup函数的应用场景:标准输出的重定位

利用上面3点的补充说明,可以实现标准输出的重定位。

我们已知fd=0,1,2默认分别被标准输入文件、标准输出文件、错误通道文件占用,但我们可以使用close函数关闭这三个文件。比如我们可以用close(1)来关闭标准输出,关闭后我们使用printf函数时,屏幕上不再显示内容输出的内容。close(1)之后,fd=1这个就空出来了,没有被占用。

由于dup函数分配fd时,分配原则是分配没被占用且数值最小的fd,那么此时我们使用dup函数去复制文件 test.txt 的fd时,返回的fd就是之前被释放不再被占用的fd=1。这样一来,fd=1就指向了文件test.txt。由于fd=1所表示的文件被默认是标准输出,所以使用echo命令、ls命令、printf函数等,都会将显示的内容输出到文件test.txt里面。这就实现了标准输出的重定位。

也就说,可以利用“open文本文件得到文件描述符+close标准输出的文件描述符+dup文本文件的描述符”的形式,实现标准输出的重定位。

Linux 中的重定位符“>”,其实也是利用open+close+dup来实现的。open打开一个文件2.txt,然后close关闭stdout,然后使用 dup 函数将 fd=1 和 2.txt 文件关联起来即可。

九、fcntl函数
函数原型

#include <unistd.h>
#include <fcntl.h>
 
int fcntl(int fd, int cmd, ... /* arg */ );

函数作用
fcntl函数是一个多功能的、用于文件管理的工具箱。

参数说明
(1)fd表示此函数要操作的文件的文件描述符。

(2)cmd表示此函数要进行的操作。

(3)变参用来传递参数,配合cmd使用。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值