【Linux系统编程】|【03】文件I/O操作

文章目录

    • 1、概述
        • 1.1 I/O4个主要系统调用
    • 2、打开文件:open
        • 2.1 flags参数
        • 2.2 函数错误
        • 2.3 creat系统调用
    • 4、读取文件内容read
    • 5、数据写入文件write
    • 6、关闭文件close
    • 7、改变文件偏移量lseek
    • 8、原子操作和竞争条件
    • 9、文件控制操作fcntl
    • 10、文件描述符和打开文件之间的关系
    • 11、复制文件描述符
    • 12、文件特定偏移量处的I/O pread和pwrite
    • 13、分散输入和集中输出readv和writev
    • 14、截断文件truncate和ftruncate
    • 15、非阻塞I/O
    • 16、大文件I/O
    • 16、/dev/fd
    • 17、创建临时文件

1、概述

文件描述符:所有执行I/O操作的系统调用都以文件描述符来指代打开的文件;、
	- 管道、FIFO、socket、终端、设备和普通文件;
【标准文件描述符】:
	【0】:标准输入(stdin);
	【1】:标准输出(stdout);
	【2】:标准错误(stderr);

1.1 I/O4个主要系统调用

- fd = open(pathname, flags, mode)打开pathname,返回文件描述符,若文件不存在,可设置位掩码进行创建;
	flags可指定文件的打开方式,只读、只写、读写;
	mode可指定文件的访问权限,若open未创建文件,可忽略或省略mode参数;
- numread = read(fd, buffer, count)调用fd指代的打开文件读取至多count字节的数据,存于buffer;并返回实际读取的字节数;
- numwritten = write(fd, buffer, count)调用从buffer中读取count字节的数据写入fd指代的文件,write返回实际写入文件的字节数;
- status = close(fd)操作后,释放文件描述符及与之相关的内核资源;

copy.cpp

#include 
#include 
#include "../Jxiepc/tlpi_hdr.h"

#define BUF_SIZE 1024

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

    int iFd, oFd, openFlags;    // 文件描述符
    mode_t filePerms;
    ssize_t numRead;
    char buf[BUF_SIZE];

    /* 判断命令行输入 */
    if(argc != 3 || strcmp(argv[1], "--help") == 0)
        usageErr("%s old-file new-file\n", argv[0]);

    /* 只读打开文件 */
    iFd = open(argv[1], O_RDONLY);
    if(iFd == -1)
        errExit("opening file %s", argv[1]);

    /* 设置文件打开方式 */
    openFlags = O_CREAT | O_WRONLY | O_TRUNC;
    filePerms = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP|
            S_IROTH | S_IWOTH;
    oFd = open(argv[2], openFlags, filePerms);
    if(oFd == -1)
        errExit("opening file %s", argv[2]);
    
    /* 从文件1中读取并写入到文件2中 */
    while ((numRead = read(iFd, buf, BUF_SIZE)) > 0)
        if(write(oFd, buf, numRead) != numRead)
            fatal("couldn't write whole buffer");
    /* 关闭文件 */
    if(numRead == -1)
        errExit("read");
        errExit("close input");
    if(close(oFd) == -1)
        errExit("close output");

    exit(EXIT_SUCCESS);
    return 0;
}

2、打开文件:open

#include 
#include 

int open(const char *path, int oflag, ... );

/**
@func: 返回当前未打开的最低文件描述符;
@param path: 文件路径;
@param flags: O_RDONLY(只读), O_WRONLY(只写),O_RDWR(读写)【O_APPEND、O_CREAT、O_EXCL、O_TRUNC、O_NONBLOCK】
@param mode: 指定文件的权限【0xxx】,xxx按照二进制【421 - rwx】
return: 错误返回-1,并设置
*/

【eg】:
openFlags = O_CREAT | O_WRONLY | O_TRUNC;
filePerms = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP|
        S_IROTH | S_IWOTH;
oFd = open(argv[2], openFlags, filePerms);
if(oFd == -1)
    errExit("opening file %s", argv[2]);

2.1 flags参数

【Linux系统编程】|【03】文件I/O操作_第1张图片
【Linux系统编程】|【03】文件I/O操作_第2张图片

2.2 函数错误

errno错误号:
【EACCES】:无法访问文件,原因目录权限的限制、文件不存在并且也无法创建该文件;
【EISDIR】:所指定的文件属于目录,而调用者企图打开该文件进行写操作;
【EMFILE】:进程已打开的文件描述符数量达到了进程资源限制所设定的上限(在36.3节将描述RLIMIT_NOFILE参数)。
【ENFILE】:文件打开数量已经达到系统允许的上限;
【ENOENT】:要么文件不存在=;
【EROFS】:所指定的文件隶属于只读文件系统,调用者想执行写操作;
【ETXTBSY】:该文件为可执行文件,且正在运行;

2.3 creat系统调用

#include 
#include 

int creat(const char *path, mode_t mode);

/**
@func: 创建并打开一个新文件,若有文件存在,则文件内容清0;
	等价于open(path, O_WRONLY|O_CREAT|O_TRUNC, mode);
@param path: 文件路径;
@param mode: 指定文件的权限【0xxx】,xxx按照二进制【421 - rwx】
return: 返回文件描述符;
*/

4、读取文件内容read

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

/*
read:采用预读入缓输出机制,先将读取到的数据缓存在内核,达到一定的数据量在往磁盘写入
-------
@param fd: 文件描述符;
@param buf:数据缓冲区;
@param count:最多能读取的字节数。
return:成功返回读取到的字节数,失败返回-1,文件末尾返回0 
【注意】:read没有字符串尾部需要添加一个表示终止的空字符;
*/

【eg】:
char buf[MAX_READ+1];
ssize_t numRead;

numRead = read(STDIN_FILENO, buf, MAX_RAED);
if(numRead == -1)
	errExit("read");
buf[numRead] = '\0';		// 添加终止空字符
printf("The input data was: %s\n", buf);

5、数据写入文件write

ssize_t write(int fd, const void *buf, size_t count);

/*  
write
-------
@param fd: 文件描述符;
@param buf:数据缓冲区;
@param count:实际内容的大小。
return:成功返回实际写入文件的字节数,该值可能小于count参数值(磁盘满、文件大小的限制),失败返回-1;
*/

【eg】:
if(write(oFd, buf, numRead) != numRead)
    fatal("couldn't write whole buffer");

6、关闭文件close

int close(int fd);
/**
@func: 关闭文件描述符,释放回调进程,
*/

7、改变文件偏移量lseek

文件偏移量即读写偏移量或指针,指执行下一个read或write的文件起始位置;
	- 文件的第一个字节为偏移量0;
	- 文件打开时,将文件偏移量设置为文件开始,后每次调用read或write将会自动对其进行调整,指向已读/写的下一个字节;
off_t lseek(int fd, off_t offset, int whence);
> - 获取文件大小;
> - 移动文件指针;
> - 文件拓展;
/* 文件拓展
* @param fd: 文件;
* @param offset: 文件字节数;
* @param whence: 
* 	- SEEK_SET: 设置偏移字节,此时offset的值必须为非负数;
* 	- SEEK_CUR: 文件偏移量被设置为当前位置加上偏移量字节;
* 	- SEEK_END: 文件偏移量被设置为文件的大小加上偏移量字节;
* 	- SEEK_DATA: 将文件的偏移量调整到文件中大于或等于包含偏移量的下一个位置数据;
* 	- SEEK_HOLE: 
* 使用文件拓展,则需要在最后做一次写操作。
* return:返回新的文件偏移量,但并没有修改(只是调整与文件描述符相关的文件偏移量记录);
【注意】:不允许用于管道、FIFO、socket、终端;
*/

【Linux系统编程】|【03】文件I/O操作_第3张图片
文件空洞

若文件偏移量跨越文件结尾,在执行I/O操作将会发生什么?
- read返回0,但write可以在文件结尾后的任意位置写入数据;
- 【文件空洞】:从文件结尾到新写入数据的该空间,不占用任何磁盘空间;
	- 为实际需要的空字节分配磁盘块相比,稀疏填充文件会占用较少的磁盘空间;

8、原子操作和竞争条件

所有系统调用都以原子操作执行(避免执行过程中被其他进程中断);

以独占方式创建文件

避免在试图读取文件中,判断是否存在从而在去创建文件时,中间出现其他进程对该文件进行创建;
为了防止该方式的出现,从而使用O_CREAT和O_EXCL一次性调用防止此类情况;

【Linux系统编程】|【03】文件I/O操作_第4张图片
向文件尾部追加数据

多进程中向同一个文件追加数据,应使用O_APPEND标志,而不是使用lseek和write避免在写入过程中被其他进程中断;

9、文件控制操作fcntl

#include 

int fcntl(int fd, int cmd, ... /* arg */ );
/*
* 以下根据cmd参数设置:
* @fun1: 复制一个现有的描述符 - F_DUPFD;
	dup(oldfd) == fcntl(oldfd, F_DUPFD, 0);
	dup2(oldfd, newfd) = close(oldfd); fcntl(oldfd, F_DUPFD, newfd);
* @fun2: 获取/设置文件描述符 - F_GETFD/F_SETFD;
* @fun3: 获取/设置文件状态标记
* 	- F_GETFL:
* 		O_RDONLY:只读打开
* 		O_WRONLY:只写打开
* 		O_RDWR:读写打开
* 		O_EXEC:执行打开
* 		O_SEARCH:搜索打开目录
* 		O_APPEND:追加写
* 		O_NONBLOCK:非阻塞模式
* 	- F_SETFL:更改标识 - O_AOOEND/O_NONBLOCK
* @fun4:获得/设置异步I/O所有权 - F_GETOWN/F_SETOWN
* @fun5:获得/设置记录锁 - F_GETLK/F_SETLK/F_SETKW
*/
【eg】:获取/修改访问模式和文件状态标志
#include
#include
#include
#include 
#include 

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

    // 先用只写打开文件
    int fd = open("test.txt", O_WRONLY);

    if(fd == -1){
        std::cout << "open error..." << std::endl;
        exit(1);
    }

    // 写入内容,此时的文件指针在开头,且覆盖原来开头的内容
    if(write(fd, "test one", 8) == -1){
        std::cout << "write error..." << std::endl;
        exit(1);
    }

    // 获取文件状态标志,并修改该文件状态标志
    int flag = fcntl(fd, F_GETFL, 0);

    if(flag == -1){
        std::cout << "error..." << std::endl;
        exit(1);
    }
    // 修改文件权限为追加模式,文件指针指向末尾
    flag |= O_APPEND;

    if(fcntl(fd, F_SETFL, flag) == -1){
        std::cout << "set error..." << std::endl;
        exit(1);
    }
    // 
    if(write(fd, "test two", 8) == -1){
        std::cout << "write error..." << std::endl;
        exit(1);
    }
    close(fd);
    return 0;
}

10、文件描述符和打开文件之间的关系

内核中维护了3个数据结构:
	- 进程级的文件描述符表:记录控制文件描述符操作的一组标志、对打开句柄的引用;
	- 系统级的打开文件表:存储打开文件相关的全部信息;
		- 当前文件偏移量;
		- 打开文件时所使用的状态标志;
		- 文件访问模式;
		- 与信号驱动I/O相关的设置;
		- 对该文件i-node对象的引用;
	- 文件系统的i-node表;
		- 文件类型和访问权限;
		- 一个指针,指向该文件所只有的锁的列表;
		- 文件的各种属性,包括文件大小及与不同类型操作相关的时间戳;
		- 当访问一个文件时,会在内存中为i-node创建一个副本;

【Linux系统编程】|【03】文件I/O操作_第5张图片

11、复制文件描述符

dup

#include

int dup(int oldfd);
/**
@func: 系统调用分配一个相同的打开文件描述符,新的描述符保证为最低编号进程中方未使用的;
*/
【eg】:
void test() {
     char buf[5] = "asd\n";
     write(1, buf, 5);	// 1为stdout
     int fd = dup(1);
     cout << "dup 后...." << endl;
     write(fd, buf, 5);
}
【out】:即fd的文件描述符和1相同
asd
dup 后....
asd

dup2

#include 

int dup2(int oldfd, int newfd)/**
@func: oldfd会为文件描述符创建副本,若newfd参数指定描述符已经打开则dup2会将其向关闭;
	- 会忽略关闭文件出现的错误,为了安全,我们应该手动close;
	- 等同于newfd = fcntl(oldfd, F_DUPFD, startfd);
		为oldfd创建一个副本,且使用>=startfd的最小未用值作为描述符编号。该调用还能保证新描述符(newfd)编号落在特定的区间范围内;
	

	
return: 成功返回newfd;
*/
【eg】:
void test_dup2() {
    char buf[5] = "asd\n";
    write(1, buf, 5);
    close(2);
    int fd = dup2(1, 2);
    cout << fd << endl;
    cout << "dup after...." << endl;
    write(2, buf, 5);
}
【out】:
asd
2
dup after....
asd

dup3

#include 

int dup3(int oldfd, int newfd, int flags)/**
与dup2相同,新增了flags参数,修改系统调用行为的位掩码;
但只支持O_CLOEXEC;
*/

12、文件特定偏移量处的I/O pread和pwrite

pread

#include 

ssize_t pread(int fd, void *buf, size_t count, off_t offset);
/**
@func: 从fd中的偏移量读取最多count个字节从buf开始的缓冲区中,文件偏移量没有改变;
return: 返回读取的字节数,返回0则表示到文件末尾;
*/

【eg】:指定偏移量读取文件
int fd = open("./test.txt", O_RDWR);
char buf[255];
/* 指定从偏移量1开始读取 */
ssize_t readNum = pread(fd, buf, 255, 1);
buf[readNum] = '\0';
cout << readNum << ":" << buf << endl;
// test.txt的内容为qwertyuiopasdfghjklzxcvbnm

【out】:26:wertyuiopasdfghjklzxcvbnm

pwrite

#include 

ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);
/**
@func: 将缓冲区从buf开始的count字节写入便宜来拿offset处的fd,文件偏移量没有改变;
return:返回学入的字节数;
*/

【eg】:
void test_pwrite() {
    int fd = open("./test.txt", O_RDWR);
    /* 指定从偏移量为2的地方写入 */
    ssize_t writeNum = pwrite(fd, "aaaaaaaa", 8, 2);
    cout << "writeNum: "  << writeNum << endl;
    char buf[255];
    /* 指定 */
    ssize_t readNum = read(fd, buf, 255);
    buf[readNum] = '\0';
    cout << readNum << ":" << buf << endl;
}
// test.txt的内容为qwertyuiopasdfghjklzxcvbnm
【out】:
writeNum: 8
27:qwaaaaaaaaasdfghjklzxcvbnm

注意

上述两个函数为原子操作相比于使用sleek和read避免出现竞争状态;

13、分散输入和集中输出readv和writev

#include 

ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
/**
@func: 分散输入,能够一次即可传输多个缓冲区的数据;
使用结构体:通过IOV_MAX设置成员格式为1024
	struct iovec{
		void *iov_base;	// 缓冲区起始地址
		size_t iov_len;	// 字节数
	};
return: 返回读取的字节数;
*/

【eg】:将test.txt的文件内容分别读入到myStruct、x、str中;
void test() {
    int fd;
    struct iovec iov[3];
    struct stat myStruct;
    int x;
    const int STR_SIZE  = 100;
    char str[STR_SIZE];
    ssize_t numRead, totRequired;

    fd = open("test.txt", O_RDONLY);
    if(fd == -1) {
         perror("open");
         exit(-1);
     }
    totRequired = 0;
    iov[0].iov_base = &myStruct;
    iov[0].iov_len = sizeof(struct stat);
    totRequired += iov[0].iov_len;

    iov[1].iov_base = &x;
    iov[1].iov_len = sizeof(x);
    totRequired += iov[1].iov_len;

    iov[2].iov_base = str;
    iov[2].iov_len = STR_SIZE;
    totRequired += iov[2].iov_len;

    numRead = readv(fd, iov, 3);
    if(numRead == -1){
        perror("read");
        exit(-1);
    }

    cout << str << x << endl;

    printf("total bytes requested: %ld; bytes read %ld\n",
           (long)totRequired, (long)numRead);
}

【Linux系统编程】|【03】文件I/O操作_第6张图片

#include 

ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
/**
@func: 集中输出,将数据一次性从用户内存传入到fd中,不被其他进程影响;
return: 返回写入的字节数;
*/

也提供在偏移量处执行上述操作

ssize_t preadv(int fd, const struct iovec *iov, int iovcnt,
                       off_t offset);
ssize_t pwritev(int fd, const struct iovec *iov, int iovcnt,
                off_t offset);

14、截断文件truncate和ftruncate

#include 

int truncate(const char *path, off_t length);
int ftruncate(int fd, off_t length);
/**
@func: 将文件大小设置为length长度;
	若文件长度大于length,将丢弃超出部分;
	若小于长度length,则在尾部添加一系列空字节或是文件空洞;	
	要求文件具有写权限,不会修改文件偏移量;
*/

15、非阻塞I/O

打开文件时指定O_NONBLOCK非阻塞,其作用:
- 若open调用未能立即打开的文件,则返回错误,而非陷入阻塞;
- 调用成功后,后续的I/O操作也是非阻塞,若I/O调用未能及时完成,则可能只传输部分数据或调用失败;
- 管道、FIFO、套接字、设备都支持非阻塞模式,管道无法使用open,故只能通过fcntl设置非阻塞;

16、大文件I/O

过渡型LFS API

必须在编译程序时定义_LARGEFILE64_SOURCE功能测试宏,上述函数后加上64即是;
	能够处理64位大小和文件偏移量;
或可直接将_FILE_OFFSET_BITS设置为64,即可直接调用open即是调用open64;

16、/dev/fd

内核提供虚拟目录/dev/fd,该目录下记录进程中打开的文件描述符对应的编号;
	打开该目录下的一个文件等同于复制相应的文件描述符;

17、创建临时文件

#include 

int mkstemp(char *template);
/**
@func: template生成一个唯一的临时文件名,创建并打开该文件,并返回一个打开的文件的描述符;
	打开临时文件后,将会调用unlink系统调用将在close调用后被删除;
*/
【eg】:
void test_template() {
   char tmp[] = "./someXXXXXX";
   int fd = mkstemp(tmp);
   if(fd == -1){
        perror("mkstemp");
        exit(-1);
    }
    cout << "the temp name: " << tmp << endl;
    unlink(tmp);
}

tmpfile

#include 

FILE *tmpfile(void);
/**
@func: 返回一个文件流供stdio库函数使用,文件流关闭后将自动删除临时文件;
	打开文件后,内部会立即调用unlink删除文件名;
	该文件打开使用O_EXCL标志,防止文件名通知;
*/

你可能感兴趣的:(Linux系统编程,linux,服务器,unix,文件I/O)