最全Linux应用开发八股文(一)——文件IO
你好,我是拉依达。
这是我的Linux应用开发八股文详细解析系列。
本系列最开始是我在csdn上更新的文章全文总字数超3w字,现重新对内容进行整理,希望可以帮助到更多学习嵌入式的同学。
【下面是拉依达推荐学习相关专栏:】
一、Linux驱动学习专栏:拉依达的Linux驱动八股文 - 牛客网
二、Linux应用学习专栏:拉依达的Linux应用八股文 - 牛客网
【我的嵌入式学习和校招经验】 拉依达的嵌入式学习和秋招经验-CSDN博客
嵌入式学习规划/就业经验指导,可私信咨询
———————————————————————————————————————————————————
一、文件IO
1.1 文件描述符
在 Linux 操作系统中的一切都被抽象成了文件,那么一个打开的文件是如何与应用程序进行对应呢?
解决方案是使用文件描述符(file descriptor,简称fd),当在进程中打开一个现有文件或者创建一个新文件时,内核向该进程返回一个文件描述符,用于对应这个打开/新建的文件。这些文件描述符都存储在内核为每个进程维护的一个文件描述符表中。
- Linux 系统中一切皆文件,系统中一切都被抽象成了文件。对这些文件的读写都需要通过文件描述符来完成。
- 标准 C 库的文件 IO 函数使用的文件指针 FILE* 在 Linux 中也需要通过文件描述符的辅助才能完成读写操作。
- FILE 其实是一个结构体,其内部有一个成员就是文件描述符。
启动一个进程就会得到一个对应的虚拟地址空间,这个虚拟地址空间分为两大部分,在内核区有专门用于进程管理的模块。 Linux 的进程控制块 PCB(process control block)本质是一个叫做 task_struct 的结构体,里边包括管理进程所需的各种信息,其中有一个结构体叫做 file ,我们将它叫做文件描述符表,里边有一个整形索引表,用于存储文件描述符。
内核为每一个进程维护了一个文件描述符表,索引表中的值都是从 0 开始的,所以在不同的进程中你会看到相同的文件描述符,但是它们指向的不一定是同一个磁盘文件。
- 每个进程对应的文件描述符表默认支持打开的最大文件数为 1024,可以修改
- 每个进程的文件描述符表中都已经默认分配了三个文件描述符,对应的都是当前终端文件(/dev/tty)
- STDIN_FILENO:标准输入,可以通过这个文件描述符将数据输入到终端文件中,宏值为 0。
- STDOUT_FILENO:标准输出,可以通过这个文件描述符将数据通过终端输出出来,宏值为 1。
- STDERR_FILENO:标准错误,可以通过这个文件描述符将错误信息通过终端输出出来,宏值为 2。
- 这三个默认分配的文件描述符是可以通过 close() 函数关闭掉,但是关闭之后当前进程也就不能和当前终端进行输入或者输出的信息交互了。
- 每打开新的文件,内核会从进程的文件描述符表中找到一个空闲的没有别占用的文件描述符与其进行关联,(从3开始分配)
- 文件描述符表中不同的文件描述符可以对应同一个磁盘文件
- 每个进程文件描述符表中的文件描述符值是唯一的,不会重复。不同进程间会重复
1.2 系统IO
每个系统都有自己的专属函数,我们习惯称其为系统函数。系统函数并不是内核函数,因为内核函数是不允许用户使用的,系统函数就充当了二者之间的桥梁,这样用户就可以间接的完成某些内核操作了。
open
open是一个系统函数, 只能在linux系统中使用, windows不支持 fopen 是标准c库函数, 一般都可以跨平台使用, 可以这样理解:
- 在linux中 fopen底层封装了Linux的系统API open
- 在window中, fopen底层封装的是 window 的 api
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
// 打开一个已经存在的磁盘文件
int open(const char *pathname, int flags);
// 打开磁盘文件, 如果文件不存在, 就会自动创建
int open(const char *pathname, int flags, mode_t mode);
参数介绍:
-
pathname: 被打开的文件的文件名
-
flags: 使用什么方式打开指定的文件,这个参数对应一些宏值,需要根据实际需求指定必须要指定的属性 , 以下三个属性不能同时使用,只能任选其一
O_RDONLY: 以只读方式打开文件 O_WRONLY: 以只写方式打开文件 O_RDWR: 以读写方式打开文件 可选属性 , 和上边的属性一起使用 O_APPEND: 新数据追加到文件尾部,不会覆盖文件的原来内容 O_CREAT: 如果文件不存在,创建该文件,如果文件存在什么也不做 O_EXCL: 检测文件是否存在,必须要和 O_CREAT 一起使用,不能单独使用: O_CREAT | O_EXCL 检测到文件不存在,创建新文件 检测到文件已经存在,创建失败,函数直接返回 - 1(如果不添加这个属性,不会返回 - 1)
-
mode: 在创建新文件的时候才需要指定这个参数的值,用于指定新文件的权限,这是一个八进制的整数,这个参数的最大值为:0777
close
如果需要释放这个文件描述符就需要关闭文件。对应的这个系统函数叫做 close,函数原型如下:
#include <unistd.h>
int close(int fd);
- 函数参数: fd 是文件描述符,是 open () 函数的返回值
- 函数返回值:函数调用成功返回值 0, 调用失败返回 -1
read
read 函数用于读取文件内部数据,在通过 open 打开文件的时候需要指定读权限,函数原型如下:
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
参数:
- fd: 文件描述符,open () 函数的返回值,通过这个参数定位打开的磁盘文件
- buf: 是一个传出参数,指向一块有效的内存,用于存储从文件中读出的数据
- count: buf 指针指向的内存的大小,指定可以存储的最大字节数
返回值:
- 大于 0: 从文件中读出的字节数,读文件成功
- 等于 0: 代表文件读完了,读文件成功
- -1: 读文件失败了
write
write 函数用于将数据写入到文件内部,在通过 open 打开文件的时候需要指定写权限,函数原型如下:
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
参数: fd: 文件描述符,open () 函数的返回值,通过这个参数定位打开的磁盘文件 buf: 指向一块有效的内存地址,里边有要写入到磁盘文件中的数据 count: 要往磁盘文件中写入的字节数,一般情况下就是 buf 字符串的长度,strlen (buf)
返回值: 大于 0: 成功写入到磁盘文件中的字节数 -1: 写文件失败了
lseek
系统函数 lseek 的功能是比较强大的,我们既可以通过这个函数移动文件指针,也可以通过这个函数进行文件的拓展。这个函数的原型如下:
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
参数:
- fd: 文件描述符,open () 函数的返回值,通过这个参数定位打开的磁盘文件
- offset: 偏移量,需要和第三个参数配合使用
- whence: 通过这个参数指定函数实现什么样的功能
- SEEK_SET: 从文件头部开始偏移 offset 个字节
- SEEK_CUR: 从当前文件指针的位置向后偏移 offset 个字节
- SEEK_END: 从文件尾部向后偏移 offset 个字节
返回值:
- 成功:文件指针从头部开始计算总的偏移量
- 失败: -1
移动文件指针的使用
- 文件指针移动到文件头部 : lseek(fd, 0, SEEK_SET);
- 得到当前文件指针的位置 : lseek(fd, 0, SEEK_CUR);
- 得到文件总大小 : lseek(fd, 0, SEEK_END);
truncate/ftruncate
truncate/ftruncate 这两个函数的功能是一样的,可以对文件进行拓展也可以截断文件。使用这两个函数拓展文件比使用 lseek 要简单。这两个函数的函数原型如下:
// 拓展文件或截断文件
#include <unistd.h>
#include <sys/types.h>
int truncate(const char *path, off_t length);-
int ftruncate(int fd, off_t length);
参数:
- path: 要拓展 / 截断的文件的文件名
- fd: 文件描述符,open () 得到的
- length: 文件的最终大小
- 文件原来 size > length,文件被截断,尾部多余的部分被删除,文件最终长度为 length
- 文件原来 size < length,文件被拓展,文件最终长度为 length
- 返回值:成功返回 0; 失败返回值 - 1
truncate () 和 ftruncate () 两个函数的区别在于一个使用文件名一个使用文件描述符操作文件,功能相同。 不管是使用这两个函数还是使用 lseek () 函数拓展文件,文件尾部填充的字符都是 0。
perror
通过 perror 函数将错误号对应的描述信息打印出来
#include <stdio.h>
// 参数, 自己指定这个字符串的值就可以, 指定什么就会原样输出, 除此之外还会输出错误号对应的描述信息
void perror(const char *s);
1.3 文件的属性信息
Linux 是一个基于文件的操作系统,因此作为文件本身也就有很多属性。 如果想要查看某一个文件的属性有两种方式:命令和函数。虽然有两种方式但是它们对应的名字是相同的,叫做 stat。另外使用 file 命令也可以查看文件的一些属性信息。
file命令
$ file 文件名 [参数]
stat命令
stat 命令显示文件或目录的详细属性信息包括文件系统状态,比 ls 命令输出的信息更详细。语法格式如下:
$ stat [参数] 文件或者目录名
stat/lstat 函数
stat/lstat 函数的功能和 stat 命令的功能是一样的,只不过是应用场景不同。这两个函数的区别在于处理软链接文件的方式上:
- lstat (): 得到的是软连接文件本身的属性信息
- stat (): 得到的是软链接文件关联的文件的属性信息
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int stat(const char *pathname, struct stat *buf);
int lstat(const char *pathname, struct stat *buf);
参数:
- pathname: 文件名,要获取这个文件的属性信息
- buf: 传出参数,文件的信息被写入到了这块内存中
- 返回值:函数调用成功返回 0,调用失败返回 -1 这个函数的第二个参数是一个结构体类型,这个结构体相对复杂,通过这个结构体可以存储得到的文件的所有属性信息
1.4 文件描述符复制和重定向
Linux 系统还提供了一些其他的 API 用于文件描述符的分配
dup
dup 函数的作用是复制文件描述符,这样就有多个文件描述符可以指向同一个文件了。函数原型如下:
#include <unistd.h>
int dup(int oldfd);
- 参数: oldfd 是要被复制的文件描述符
- 返回值:函数调用成功返回被复制出的文件描述符,调用失败返回 -1
dup2
dup2 () 函数是 dup () 函数的加强版,基于 dup2 () 既可以进行文件描述符的复制,也可以进行文件描述符的重定向。文件描述符重定向就是改变已经分配的文件描述符关联的磁盘文件。
- 文件描述符的复制, 和dup是一样的
- 能够重定向文件描述符
重定向: 改变文件描述符和文件的关联关系, 和新的文件建立关联关系, 和原来的文件断开关联关系
- 首先通过open()打开文件 a.txt , 得到文件描述符 fd
- 然后通过open()打开文件 b.txt , 得到文件描述符 fd1
- 将fd1从定向 到fd上: fd1和b.txt这磁盘文件断开关联, 关联到a.txt上, 以后fd和fd1都对用同一个磁盘文件 a.txt
#include <unistd.h>
int dup2(int oldfd, int newfd);
- 参数: oldfd 和 newfd 都是文件描述符
- 返回值:函数调用成功返回新的文件描述符,调用失败返回 -1
使用场景
- 假设参数 oldfd 对应磁盘文件 a.txt, newfd 对应磁盘文件 b.txt。在这种情况下调用 dup2 函数,是给 newfd 做了重定向,newfd 和文件 b.txt 断开关联, 相当于关闭了这个文件, 同时 newfd 指向了磁盘上的a.txt文件,最终 oldfd 和 newfd 都指向了磁盘文件 a.txt。
- 假设参数 oldfd 对应磁盘文件 a.txt, newfd 不对应任何的磁盘文件(newfd 必须是一个大于等于 0 的整数)。在这种情况下调用 dup2 函数,在这种情况下会进行文件描述符的复制,newfd 指向了磁盘上的a.txt文件,最终 oldfd 和 newfd 都指向了磁盘文件 a.txt。
- 假设参数 oldfd 和 newfd 两个文件描述符对应的是同一个磁盘文件 a.txt, 在这种情况下调用 dup2 函数,相当于啥也没发生,不会有任何改变。
fcntl函数
fcntl () 是一个变参函数,并且是多功能函数,在这里只介绍如何通过这个函数实现文件描述符的复制和获取/设置已打开的文件属性。该函数的函数原型如下:
#include <unistd.h>
#include <fcntl.h> // 主要的头文件
int fcntl(int fd, int cmd, ... /* arg */ );
参数:
- fd: 要操作的文件描述符
- cmd: 通过该参数控制函数要实现什么功能
返回值 :函数调用失败返回 -1,调用成功,返回正确的值:
- 参数 cmd = F_DUPFD:返回新的被分配的文件描述符
- 参数 cmd = F_GETFL:返回文件的 flag 属性信息
文件的状态标志指的是在使用 open () 函数打开文件的时候指定的 flags 属性,也就是第二个参数
使用场景
- 使用 fcntl () 函数进行文件描述符复制,第二个参数 cmd 需要指定为 F_DUPFD(这是个变参函数其他参数不需要指定)。
int newfd = fcntl(fd, F_DUPFD);
- 通过 open() 函数打开文件之后,文件的 flag 属性就已经被确定下来了,如果想要在打开状态下修改这些属性,可以使用 fcntl() 函数实现,但是有一点需要注意,不是所有的 flag 属性都能被动态修改,只能修改如下状态标志: O_APPEND, O_NONBLOCK, O_SYNC, O_ASYNC, O_RSYNC 等。
// 得到文件的flag属性
int flag = fcntl(fd, F_GETFL);
// 添加新的flag 标志
flag = flag | O_APPEND;
// 将更新后的falg设置给文件
fcntl(fd, F_SETFL, flag);
#嵌入式##校招##八股文##Linux##Linux应用开发#你好,我是拉依达。 这是我的Linux应用开发八股文详细解析系列。 本系列最开始是我在csdn上更新的文章全文总字数超3w字,现重新对内容进行整理,希望可以帮助到更多学习嵌入式的同学。