目录
1 文件 IO 简介
Linux文件I/O(输入/输出)是操作系统中处理文件读写操作的基本机制。在Linux系统中,文件I/O操作是通过系统调用实现的,这些系统调用允许用户空间的程序与内核空间的文件系统进行交互。
2 文件 IO 示例
一个通用的 IO 模型通常包括打开文件、读写文件、关闭文件这些基本操作, 主要涉及到 4 个函数: open()、 read()、 write()以及 close()。我们先来看一个简单地文件读写示例,应用程序代码如下所示:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(void)
{
char buff[1024];
int fd1, fd2;
int ret;
/* 打开源文件 src_file(只读方式) */
fd1 = open("./src_file", O_RDONLY);
if (-1 == fd1)
return fd1;
/* 打开目标文件 dest_file(只写方式) */
fd2 = open("./dest_file", O_WRONLY);
if (-1 == fd2) {
ret = fd2;
goto out1;
}
/* 读取源文件 1KB 数据到 buff 中 */
ret = read(fd1, buff, sizeof(buff));
if (-1 == ret)
goto out2;
/* 将 buff 中的数据写入目标文件 */
ret = write(fd2, buff, sizeof(buff));
if (-1 == ret)
goto out2;
ret = 0;
out2:
/* 关闭目标文件 */
close(fd2);
out1:
/* 关闭源文件 */
close(fd1);
return ret;
}
这段代码从源文件 src_file 中读取 1KB 数据,然后将其写入到目标文件 dest_file 中(这里假设当前目录下这两个文件都是存在的);在进行读写操作之前,首先调用 open 函数将源文件和目标文件打开,成功打开之后再调用 read 函数从源文件中读取 1KB 数据,然后再调用 write 函数将这 1KB 数据写入到目标文件中,至此,文件读写操作就完成了,读写操作完成之后,最后调用 close 函数关闭源文件和目标文件。
3 文件描述符
3.1 文件描述符简介
文件描述符(File Descriptor)是Linux和UNIX系统编程中的一个重要概念,它是一个用于标识打开文件或其他输入/输出资源的非负整数,文件描述符允许程序通过一个抽象的数字来引用文件和其他输入输出资源,而不是直接使用文件名或设备名。
从上面的示例代码中可以看到,调用 open 函数会有一个返回值, 譬如示例代码中的 fd1 和 fd2, 这是一个 int 类型的数据,在 open函数执行成功的情况下, 会返回一个非负整数, 该返回值就是一个文件描述符(file descriptor)。
3.2 文件描述符特点
-
唯一性:每个打开的文件或设备在进程中都有一个唯一的文件描述符。文件描述符是从 3 开始分配的,譬如说进程中第一个被打开的文件对应的文件描述符是 3、第二个文件是 4……以此类推。为什么是从3开始?因为0、 1、 2 这三个文件描述符已经默认被系统占用了,分别分配给了系统标准输入(0)、 标准输出(1)以及标准错误(2)。
-
抽象性:文件描述符提供了一个抽象层,使得对各种类型的输入输出设备的操作看起来像是对文件的操作。
-
有限性:文件描述符的数量是有限的,通常由系统设置决定,例如Linux系统中可以通过
ulimit -a
或ulimit -a
命令查看文件描述符的限制。
该最大值默认情况下是 1024,也就意味着一个进程最多可以打开 1024 个文件,当然这个限制数其实是可以设置的。
3.3 标准文件描述符
在Linux中,每个进程启动时都会自动打开三个标准文件描述符:
- 标准输入(stdin):文件描述符为0,通常用于从键盘接收输入。
- 标准输出(stdout):文件描述符为1,通常用于向屏幕输出文本。
- 标准错误(stderr):文件描述符为2,通常用于输出错误信息。
3.4 文件描述符的生命周期
-
打开:使用
open()
或openat()
等系统调用打开文件时,系统会分配一个新的文件描述符。 -
使用:通过文件描述符,可以使用
read()
、write()
、lseek()
等系统调用来读写数据或移动文件指针。 -
关闭:使用
close()
系统调用关闭文件描述符,释放与该文件描述符关联的资源。 -
重定向:可以将文件描述符重定向到其他文件描述符或文件,例如通过
dup()
或dup2()
。
4 文件操作函数
4.1 文件执行权限
在Linux系统中,文件执行权限是控制用户可以对文件执行哪些操作的重要机制,正确设置文件权限对于系统的安全性至关重要,不恰当的权限设置可能会导致安全漏洞。Linux文件权限分为三类:读(r)、写(w)和执行(x)。这些权限可以被分配给文件的所有者(owner)、文件所属组(group)以及其他用户(others)。权限表示方法如下所示:
mode 权限表示方法(参考正点原子教程)
我们从低位从上看,每 3 个 bit 位分为一组,分别表示:
- O---这 3 个 bit 位用于表示其他用户的权限;
- G---这 3 个 bit 位用于表示同组用户(group)的权限,即与文件所有者有相同组 ID 的所有用户;
- U---这 3 个 bit 位用于表示文件所属用户的权限,即文件或目录的所属者;
- S---这 3 个 bit 位用于表示文件的特殊权限,文件特殊权限一般用的比较少。
权限的设置:
- 执行权限可以通过
chmod
命令设置。例如,chmod u+x file
会给文件所有者添加执行权限。 - 权限可以用数字表示,3 个 bit 位中,按照 rwx 顺序来分配权限位(特殊权限除外) ,最高位(权值为 4)表示读权限,为 1 时表示具有读权限,为 0 时没有读权限;中间位(权值为 2)表示写权限,为 1 时表示具有写权限,为 0 时没有写权限;最低位(权值为 1)表示执行权限,为 1 时表示具有可执行权限,为 0 时没有执行权限。
其中4代表读(r),2代表写(w),1代表执行(x)。例如,chmod 755 file
会给所有者读、写和执行权限(7=4+2+1),给组和其他用户读和执行权限(5=4+1)。
特殊权限:
- 粘滞位(Sticky Bit):当对目录设置执行权限时,粘滞位(通常是对目录的执行权限)允许文件只能被其所有者删除或重命名。
- SUID(Set User ID):当对可执行文件设置SUID位时,运行该文件的用户将以文件所有者的权限执行它,而不是以执行者自己的权限。
- SGID(Set Group ID):类似于SUID,但以文件所属组的权限执行。
查看权限:
- 使用
ls -l
命令可以查看文件的权限。例如,-rwxr-xr-x
表示所有者有读、写和执行权限,组和其他用户有读和执行权限。
权限的组合:
- 权限可以组合使用,以控制不同用户对文件的访问。例如,
chmod g+w file
会给文件所属组添加写权限。
文件类型与执行权限:
- 只有可执行文件(二进制程序、脚本等)才需要执行权限。对于文本文件或数据文件,通常不需要执行权限。
4.2 open()打开文件
在 Linux 系统中要操作一个文件,需要先打开该文件,得到文件描述符,然后再对文件进行相应的读写操作(或其他操作),最后在关闭该文件; open 函数用于打开文件,当然除了打开已经存在的文件之外,还可以创建一个新的文件,函数原型如下所示:
#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
(读写)等。flags
其他常用参数介绍如下:
标志 | 用途 |
O_RDONLY | 以只读方式打开文件 |
O_WRONLY | 以只写方式打开文件 |
O_RDWR | 以可读可写方式打开文件 |
O_CREAT | 如果 pathname 参数指向的文件不存在则创建此文件 |
O_DIRECTORY | 如果 pathname 参数指向的不是一个目录,则调用 open 失败 |
O_EXCL | 此标志一般结合 O_CREAT 标志一起使用,用于专门创建文件。在 flags 参数同时使用到了 O_CREAT 和O_EXCL 标志的情况下,如果 pathname 参数指向的文件已经存在,则 open 函数返回错误。 |
O_NOFOLLOW | 如果 pathname 参数指向的是一个符号链接,将不对其进行解引用,直接返回错误。 |
O_APPEND | 当以 |
O_TRUNC | O_TRUNC 标志用于打开一个已存在的文件,并在打开时截断该文件,使其长度变为0。这会导致文件中现有的所有数据被删除,文件指针被定位到文件的开始位置。如果文件不存在,O_TRUNC 标志则没有效果,文件将被创建。 |
flags 参数时既可以单独使用某一个标志,也可以通过位或运算(|)将多个标志进行组合,例如:
open("./src_file", O_RDONLY) //单独使用某一个标志
open("./src_file", O_RDONLY | O_NOFOLLOW) //多个标志组合
mode
: (可选参数)当创建文件时设置文件的权限。如果flags
参数包含O_CREAT
标志,则需要这个参数。
我们可以直接使用 Linux 中已经定义好的宏,不同的宏定义表示不同的权限,如下所示:
宏定义 | 说明 |
S_IRUSR | 允许文件所属者读文件 |
S_IWUSR | 允许文件所属者写文件 |
S_IXUSR | 允许文件所属者执行文件 |
S_IRWXU | 允许文件所属者读、写、执行文件 |
S_IRGRP | 允许同组用户读文件 |
S_IWGRP | 允许同组用户写文件 |
S_IXGRP | 允许同组用户执行文件 |
S_IRWXG | 允许同组用户读、写、执行文件 |
S_IROTH | 允许其他用户读文件 |
S_IWOTH | 允许其他用户写文件 |
S_IXOTH | 允许其他用户执行文件 |
S_IRWXO | 允许其他用户读、写、执行文件 |
S_ISUID S_ISGID S_ISVTX | set-user-ID(特殊权限) set-group-ID(特殊权限) sticky(特殊权限) |
同样,这些权限宏既可以单独使用,也可以通过位或运算将多个宏组合在一起,例如:
S_IRUSR | S_IWUSR | S_IROTH
返回值:成功时返回一个非负的文件描述符;失败时返回-1,并设置errno
以指示错误。
4.3 read()读文件函数
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
参数说明:
fd
: 要读取数据的文件描述符。buf
: 指向一个缓冲区的指针,用于存储读取的数据。count
: 要读取的字节数。
返回值: 成功时返回实际读取的字节数;如果到达文件末尾,则返回0;失败时返回-1,并设置errno
以指示错误。
4.4 write() 写文件函数
write()
函数用于向指定的文件描述符写入数据。其原型如下:
ssize_t write(int fd, const void *buf, size_t count);
参数说明:
fd
:文件描述符,是一个非负整数,代表要写入的文件。buf
:指向要写入数据的缓冲区的指针。count
:要写入的字节数。
返回值:成功时,返回写入的字节数;失败时,返回-1,并设置全局变量 errno
以指示错误。
4.5 close() 函数
close()
函数用于关闭一个打开的文件描述符,一旦文件使用完毕,就应该用close()
系统调用来关闭文件。其原型如下:
int close(int fd);
参数说明:
fd
:要关闭的文件描述符。
返回值:成功时,返回0;失败时,返回-1,并设置全局变量 errno
以指示错误。
4.6 lseek() 函数
lseek()
函数是Linux系统中的一个系统调用,用于重新定位文件描述符的读写位置。这个函数特别有用,因为它允许你以不同的方式对文件进行读写操作,比如随机访问文件。
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
参数说明:
fd
:文件描述符,是一个非负整数,代表要操作的文件。offset
:偏移量,可以是正数、负数或零。这个值表示相对于whence
参数指定的位置的偏移。whence
:指定offset
参数的参考点,可以是以下宏之一:SEEK_SET
:文件的开头(这是默认值,如果省略whence
参数)。SEEK_CUR
:当前文件位置。SEEK_END
:文件的末尾。
返回值:成功时,返回新的文件位置偏移量(从文件开头到当前位置的字节数)。失败时,返回(off_t)-1,并设置全局变量 errno
以指示错误。
使用示例:将读写位置移动到偏移文件开头 100 个字节处:
off_t off = lseek(fd, 100, SEEK_SET);
if (-1 == off)
return -1;
4.7 creat() 函数
O WRONLY | O CREAT | O TRUNC 组合经常被使用,以至于专门有个系统调用来实现。使用creat()
打开一个已存在的文件,如果文件不存在,则会创建一个新文件。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int creat (const char *name, mode_t mode);
参数
name
:指向一个以 null 结尾的字符串的指针,该字符串指定了要创建或打开的文件的路径。mode
:指定文件的访问权限和属性。这个参数是一个mode_t
类型的值,通常使用open()
函数的返回值来设置。
返回值:如果成功,creat()
函数返回新分配的文件标识符;如果失败,返回-1,并设置errno
以指示错误类型。