> 文档中心 > Linux基础IO

Linux基础IO

提示:文章容较长,请参考目录阅读

文章目录

  • 一、C语言中的文件接口
    • 1.1 fopen
    • 1.2 fwrite
    • 1.3 fseek
    • 1.4 fread
    • 1.5 fclose
    • 1.6 stdin&stdout&stderr
  • 二、系统文件IO
    • 2.1 open
    • 2.2 write
    • 2.3 lseek
    • 2.4 read
    • 2.5 close
  • 三、文件描述符fd
    • 3.1 概念
    • 3.2 文件描述符的分配规则
    • 3.3 一个进程可以打开多少个文件描述符
    • 3.4 深入理解文件描述符与文件流指针
    • 3.5 重定向
  • 四、动态库和静态库
    • 4.1 概念
    • 4.2动态库
    • 4.3 静态库
  • 五、简单文件系统
    • 文件元数据
    • 理解inode
    • 软链接与硬链接

一、C语言中的文件接口

在学习Linux的基础IO之前,先回顾一下C语言中的文件操作

1.1 fopen

FILE *fopen(const char *path, const char *mode);
  • 功能:打开文件
  • 返回值:打开成功返回文件流指针,失败则返回NULL
  • 参数:
    path,要打开文件的路径,若不指定则默认打开可执行程序所在路径下文件
    mode,文件的打开方式

fopen打开文件的方式

  • r 只读方式打开文件
  • r+ 可读可写方式打开文件
  • w 以只写方式打开文件,若文件存在则清空内容,不存在则创建文件
  • w+ 可读可写方式打开文件,若文件存在则清空其内容,不存在则创建文件
  • a 追加写,若文件不存在则创建文件,文件存在,文件流指针指向文件末尾进行写
  • a+ 可以读,可追加写,若文件不存在则创建文件,读的位置初始化到文件头,写的位置从文件末尾开始
    在实际应用中,若文件读取或写入不成功,要先查看打开方式是否正确

程序:
在这里插入图片描述
运行结果:
在这里插入图片描述

1.2 fwrite

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE*stream);
  • 功能:向文件写入数据
  • 返回值:成功写入的文件快的个数
  • 参数
    ptr:要写入文件的内容
    size:写入时,一个块的大小,单位字节(通常定义1个字节)
    nmemb:写入多少块
    stream:文件流指针

程序
在这里插入图片描述

1.3 fseek

int fseek(FILE *stream, long offset, int whence)
  • 函数功能:移动文件流指针位置
  • 返回值:成功返回0,失败返回-1
  • 参数
    stream:文件流指针
    offset:偏移量
    whence:将文件流指针移动到什么位置

whence常用的几个参数

  1. SEEK_SET 文件头部
  2. SEEK_CUR 当前文件流指针位置
  3. SEEK_END 文件末尾

1.4 fread

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)
  • 函数功能:从文件中读取数据
  • 返回值:成功读入的文件块的个数
  • 参数
    ptr:要写入的位置
    size:读取文件时,一个块的大小
    nmeb:读取多少块
    stream:文件流指针

程序
在这里插入图片描述
所以正确的做法是,先移动文件流指针到文件开头位置,再读取,如图:
在这里插入图片描述

1.5 fclose

int fclose(FILE *fp)

关闭文件流指针,在写入或读取结束后,要在程序中关闭文件流指针,否则会造成文件句柄泄漏,持续泄露,最终会导致进程不能打开新的文件。
下面是一个完整程序,包括文件的打开,写入,移动文件流指针,读取,移动文件流指针:

#include#includeint main(){    //1.文件打开    FILE*fp=fopen("1.txt","w+");    if(fp==NULL)//文件打开失败    { perror("fopen");    }    else//文件打开成功    { printf("Open success...\n");    }    //2.写入    const char*w_word={"Never say never."};    size_t w_size=fwrite(w_word,1,strlen(w_word),fp);    printf("write size:%ld\n",w_size);    //3.移动文件流指针位置    fseek(fp,0,SEEK_SET); //4.读    char r_word[100]={0};    size_t r_size=fread(r_word,1,sizeof(r_word)-1,fp);    printf("read size:%ld\n",r_size);    printf("%s\n",r_word);    //5.关闭文件流指针    fclose(fp);    return 0;}

程序运行结果:
在这里插入图片描述

1.6 stdin&stdout&stderr

C进程会打开三个输入输出流,分别是stdin,stdout,stderr
这里先写一个程序,让其一直处于运行状态,并通过getpid接口获取到进程号,如图:
在这里插入图片描述
根据拿到的进程号查看该进程在proc目录中的fd文件,如图:
在这里插入图片描述
fd实际上就是文件描述符,0、1、2三个描述符对应stdin,stdout,stderr。
可以查看"stdio.h"头文件中的定义:
Linux基础IO

二、系统文件IO

2.1 open

int open(const char *pathname, int flags);int open(const char *pathname, int flags, mode_t mode);
  • 头文件
#include #include #include 
  • 函数功能:打开文件
  • 返回值:打开成功返回文件描述符,打开失败返回-1
  • 参数
    pathname:文件的路径
    flags:打开方式
    mode:创建的文件权限,传一个8进制数字(如:0664)

open的打开方式

  1. O_RDONLY:只读打开
  2. O_WRONLY:只写打开
  3. O_RDWR:可读可写
    前三个选项只能指定一个
  4. O_CREAT:
  5. O_APPEND

这些方式若选取多个,使用“或”运算连接,如:"O_RDWR | O_CREAT"表示以可读可写方式打开,若文件不存在则创建文件。
程序
在这里插入图片描述
上面和程序打开失败,因为当前路径没有该文件,应该先创建文件,或者改变打开方式,如:
在这里插入图片描述

2.2 write

ssize_t write(int fd, const void *buf, size_t count)
  • 函数功能:向文件写入内容
  • 返回值:成功写入文件的字节数
  • 参数
    fd:文件描述符
    buf:将buf指向的内容写入文件
    count:期望写入多少字节

程序
在这里插入图片描述

2.3 lseek

 off_t lseek(int fd, off_t offset, int whence)
  • 函数功能:重新定位读/写文件偏移量
  • 返回值:移动成功,返回偏移的位置,单位字节;调用失败返回-1
  • 参数
    fd:文件描述符
    offset:偏移量,单位字节
    whence:要便宜到什么位置

wehence常用参数

  • SEEK_SET 文件头部
  • SEEK_CUR 当前文件读取或写入位置
  • SEEK_END 文件尾部

2.4 read

 ssize_t read(int fd, void *buf, size_t count)
  • 函数功能:读取文件内容
  • 返回值:从文件中读取的字节数
  • 参数
    fd:文件描述符
    buf:读取的内容保存到buf中
    count:期望读多少字节

程序
在这里插入图片描述

2.5 close

int close(int fd)

关闭文件描述符
包括文件打开、写入、读写位置偏移、读取、关闭文件描述符的程序:

#include#include#include#includeint main(){    //1.打开文件    int fd =open("1.txt",O_RDWR | O_CREAT,0664);    if(fd<0)    { perror("open"); return 0;    }    printf("Open success...\n");    //2.写入    const char* w_word="hello linux!";    write(fd,w_word,strlen(w_word));    //3.移动读取位置    lseek(fd,0,SEEK_SET);    //4.读取    char r_word[100]={0};    read(fd,r_word,sizeof(r_word)-1);    printf("%s\n",r_word);    //关闭文件描述符    close(fd);    return 0;}

三、文件描述符fd

3.1 概念

通过一个程序观察文件描述符
先获取进程的pid
在这里插入图片描述
在"/proc"路径下根据进程号查看对应进程的fd文件,可以查看一个进程打开的文件描述符:
在这里插入图片描述
可以看到,该进程打开了4个文件描述符,前0、1、2依次是标准输入、标准输出、标准错误, 3号是进程中使用open接口打开的文件。
我们知道,每个进程都有一个task_struct结构体用于保存进程的信息,task_struct结构体中有一个指针*files,这个指针指向一张表files_struct,该表最重要的部分就是包含一个指针数组,每个元素都是一个指向打开文件的指针,所以,本质上,文件描述符就是该数组的下标
其关系如图:
在这里插入图片描述
Linux内核源码sched.h中有对files_struct的声明,如图:
Linux基础IO
该结构体是保存进程打开的文件信息的。

3.2 文件描述符的分配规则

文件描述符不是固定分配的,而是按照最小未使用原则分配,看下面这个程序:
在这里插入图片描述
从这个程序我们可以看出,文件描述符并不是固定分配的,即使是0、1、2也是如此,文件描述符按照最小未使用原则进行分配。

3.3 一个进程可以打开多少个文件描述符

  • 系统当中一个进程打开文件描述符的数量是由限制的,可以通过"ulimit -a"指令查看,如图:
    在这里插入图片描述
    这里的"open files"的大小就是一个进程可以打开文件描述符的数量
  • "open files"的大小可以通过ulimit -n [num]修改,eg:ulimit -n 10000,将一个进程可以打开的文件描述符数量限制为10000

3.4 深入理解文件描述符与文件流指针

查看文件流指针FILE的定义
Linux基础IO

在"stdio.h"头文件中的搜索"FILE",查看结果如下:
Linux基础IO
从这里可以看出,文件流指针FILE本质上就是_IO_FILE,即C标准库中的一个结构体
再查找"struct _IO_FILE"结构体的定义:
在这里插入图片描述
最终在libio.h头文件中找到_IO_FILE结构体的定义:

在这里插入图片描述
这里=="_IO_FILE"结构体的成员变量"_fileno"保存的正是文件描述符的数值==
通过程序也可以验证这一点:
在这里插入图片描述
用一句话概括文件流指针与文件描述符的关系就是:文件流指针指向的结构体_IO_FILE内部的成员变量_fileno保存了文件描述符的数值。

在这里插入图片描述
在这里插入图片描述

3.5 重定向

符号

  • >> 追加重定型
  • > 清空重定向

使用dup2系统调用进行重定向

int dup2(int oldfd, int newfd);

dup2的两个参数均是文件描述符,含义是将newfd的值重定向为oldfd,即即newfd对应的文件指针指向原来oldfd指向的文件。
程序:
在这里插入图片描述

四、动态库和静态库

4.1 概念

静态库与动态库都是二进制的程序代码的集合。将程序编写成库提供给第三方使用,这样做的好处是不会造成源码泄漏,而且调用者不用关心内部实现。

  • 静态库(.a):程序在编译链接的时候把库的代码连接到可执行文件当中,运行时不再需要静态库。Linux中:前缀为"lib",后缀".a";Windows中:后缀".lib"。
  • 动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。Linux中:前缀为"lib",后缀".so";Windows中:后缀".lib。

4.2动态库

生成动态库

  • shared:表示生成共享格式
  • fPIC:产生位置无关码
  • 动态库名称规则:libxxx.so

在这里插入图片描述

使用动态库——编译
先写一个程序,调用库中的接口
在这里插入图片描述
如果直接编译会有这样的报错
Linux基础IO
正确的编译指令
L:链接库所在路径
l:链接动态库,后面紧跟库名,没有空格
Linux基础IO

此时main_test依赖的库中已经有我们自己生成的库。

使用动态库——运行
如果直接运行编译生成的可执行程序,程序运行失败
Linux基础IO
为什么会这样呢?
通过ldd指令查看可执行程序依赖的库,发现是因为无法找到程序所链接的库的位置。如图:
在这里插入图片描述
那么有什么办法能让程序找到动态库呢?

  1. 将动态库拷贝到可执行程序的目录下(不推荐)

  2. 更改"LD_LIBRARY_PATH"环境变量,将我们生成的动态库的路径添加到该环境变量中。
    在这里插入图片描述
    这个环境变量可以在"~/.bshrc"文件中修改,完成以后别忘记source一下,让修改生效。

  3. 将生成的库放到系统库路径下:/lib64(极不推荐)

我们采取修改环境变量的方法,修改之后再通过ldd查看
在这里插入图片描述
运行程序,打印成功:
Linux基础IO

4.3 静态库

生成
静态库的生成有两个阶段

  1. 用gcc -c 指令把源文件编译成.o的二进制文件
    在这里插入图片描述
    Linux基础IO
  2. 通过ar -rc 指令将将.o文件编译成静态库
    ar是gnu归档工具,rc表示(replace and creat) Linux基础IO
    此时,可以通过ar -tv指令查看静态库中的目录列表,即包含了哪些二进制文件
    t:列出静态库中的文件
    v:verbose 详细信息
    Linux基础IO

使用
在这里插入图片描述
在编译可执行程序的时候,如果使用了静态库,静态库会编译到可执行程序中,没有可执行程序依赖动态库的问题。

五、简单文件系统

文件元数据

使用ls -l或ll查看文件详细信息内容如下

在这里插入图片描述
每行有7项内容

  1. 模式
  2. 硬链接数
  3. 文件所有者
  4. 文件所属组
  5. 文件大小(单位:字节)
  6. 最后修改时间
  7. 文件名

还可以使用stat指令查看更详细的文件信息
在这里插入图片描述

理解inode

Linux ext2文件系统,按块儿划分,硬盘分区被划分为一个一个的block,如图
在这里插入图片描述
文件在磁盘中是离散式存储在Data blocks(数据区)中的,所以用Block Bitmap中的一个比特位来记录Data blocks中哪个数据块已经被占用,那个数据块没有被占用,分别用0、1表示。
即inod节点表中的一个节点就保存了一个文件的存储位置等信息,所以一个inode节点可以代表一个文件。

创建一个文件的4个操作:

  1. 存储属性
    内核先找到一个空闲的i节点,如:10001。内核把文件信息记录到其中。
  2. 存储数据
    假设该文件需要存储在三个磁盘块,内核找到了三个空闲块:100,300,500。将内核缓冲区的数据块分开拷贝到三个数据块
  3. 记录分配情况
    文件内容按顺序100,300,500存放。内核在inode上的磁盘分布区记录了上述块列表。
  4. 添加文件名到目录
    设文件名为xxx。内核将入口(10001,xxx)添加到目录文件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来。

软链接与硬链接

软链接
软链接可以看成源文件的快捷方式,生成方法:
Linux基础IO

  1. 修改源文件,软链接文件也会被修改:修改软链接文件,源文件也会被修改
  2. 删除源文件,软链接文件还在,但其内容已经不存在,若此时再修改软链接文件,又会生成新的源文件,但此时源文件内容已经发生改变。所以一般删除源文件时要同时删除软链接文件。

硬链接
硬链接可以当作源文件的替身
Linux基础IO
查看文件信息
Linux基础IO
可以看出,硬链接与源文件的节点号相同,状态信息也完全相同。
事实上,我们在删除一个文件的时候做了两件事

  1. 在目录中删除文件记录
  2. 硬链接数-1,如果链接数为0,则释放该文件对应的磁盘
    Linux基础IO
    如上图所示,删除test的硬链接文件后,文件的硬链接数-1。