首页 > 代码库 > Linu基础 文件IO(读写操作)

Linu基础 文件IO(读写操作)

前言

本章讨论普通文件的读写、读写效率、简单介绍文件描述符、IO效率、文件共享和原子操作、dup、文件映射、临时文件。

文件描述符

在Linux系统中,打开的文件是用一个整数来表示的,表示打开文件的整数,称之为文件描述符。当需要往写数据/读数据时,读写函数都需要文件描述符作为参数,以便系统知道用户操作的时哪个文件。

文件基本操作

open/creat

mode选项解释
O_RDONLY 读方式打开
O_WRONLY 写方式打开
O_RDWR 读写方式打开
O_CREAT 创建文件,如果文件存在,会被截断
O_TRUNC 截断
O_APPEND 追加
O_EXCL 和O_CREAT一起用,如果文件存在则失败
int open(const char* path, int flag, ...);

open函数的flag值得是mode选项(注意互斥问题)。第三个参数指示新建的文件的属性。文件真实的权限,受umask的影响。影响方法

真实mode = 指定的mode & ~umask
 

 close

关闭文件。

在dup时,有更多讨论。

 read/write

读写文件,会导致文件指针移动。

文件指针和lseek

文件指针是一个整数,描述当前读写位置,可以使用lseek移动文件指针。

 

文件读写效率

当读写文件时,缓冲区设置为1024到4096是一个比较合适的尺寸。

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

int main1()
{
int fdr = open("a.out", O_RDONLY);
if(fdr < 0)
{
perror("open read");
return -1; 
}

int fdw = open("b.out", O_WRONLY|O_CREAT, 0777);
if(fdw < 0)
{
perror("open write");
return -1;
}

// 1. 如果文件很大怎么办
int filelen = lseek(fdr, 0, SEEK_END);//读取文件的长度
lseek(fdr, 0, SEEK_SET);//将文件读的指针。制回开始位置

char* buf = malloc(filelen);
read(fdr, buf, filelen);
write(fdw, buf, filelen);

close(fdr);
close(fdw);
}


int main2()
{
int fdr = open("a.out", O_RDONLY);
if(fdr < 0)
{
perror("open read");
return -1; 
}

int fdw = open("b.out", O_WRONLY|O_CREAT, 0777);
if(fdw < 0)
{
perror("open write");
return -1;
}

// 一个一个字节拷贝,效率很低下
char buf;
while(1)
{
if(read(fdr, &buf, 1) <= 0)
{
break;
}
write(fdw, &buf, 1);
}


close(fdr);
close(fdw);

}

int main()
{
int fdr = open("a.out", O_RDONLY);
if(fdr < 0)
{
perror("open read");
return -1; 
}

int fdw = open("b.out", O_WRONLY|O_CREAT, 0777);
if(fdw < 0)
{
perror("open write");
return -1;
}

// buf尺寸多少最合适?
char buf[1024];
int ret;
while(1)
{
ret = read(fdr, &buf, sizeof(buf));
if(ret <= 0)
{
break;
}
write(fdw, &buf, ret);//避免最后一次读取数据,没有1024个字节
}


close(fdr);
close(fdw);

}

 


技术分享

 文件共享

两个进程可以打开同一个文件进行操作,实现数据的共享。但是当两个进程打开同一个文件进行写操作时,会相互覆盖。当文件被打开两次时,两个文件描述符有各自的文件指针

内核保存一个全局的文件描述结构体,而一个文件打开两次之后,两个结构体各自有各自的文件指针。

技术分享

 dup

dup函数可以复制文件描述符,让两个文件描述符指向同一个文件结构,通过dup复制文件描述符和两次打开文件描述符不同,所以两个文件描述符共享一个文件指针。

当一个文件描述符被关闭时,关闭的是内核的文件描述结构,但是如果文件描述结构体中,引用计数器不为1,那么close函数就只是减少了引用计数器而已。

 技术分享
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>

// 0, 1, 2
// 0 标准输入
// 1 标准输出
// 2 标准错误

int main()
{
printf("this is output to terminate\n");

// 保存1号文件描述符
int fd_save1 = dup(1);

// 打开了一个新文件
int fd_file = open("new_output.txt", O_CREAT|O_RDWR, 0777);

// 将新文件拷贝到1的位置
dup2(fd_file, 1);

// 打印调试信息到文件
printf("this is output to file\n");

// 将保存的1号文件恢复到1号位置上
dup2(fd_save1, 1);

// 此时再输出,则又输出到终端
printf("output to terminate again\n");

return 0;
}

 

文件原子操作

原子操作是指一个操作一旦启动,则无法被能破坏它的其它操作打断

  • 写文件
    无论是两次打开还是dup,同时操作一个文件都可能引起混乱,解决这个问题的方法是,可以通过O_APPEND解决这个问题。O_APPEND选项可以使得当一个写操作正在进行时,另外一个对该文件的写操作会阻塞等待。这意味着有O_APPEND选项的文件描述符,写操作无法被打断。

应用场景,多进程写Log文件

  • 创建文件
    除了写操作有原子性问题,创建文件也有,如果两个进程同时调用creat或者带O_CREAT的open,创建同一个文件时,可能会出现这种情况,第一个操作创建成功之后,写入数据,而第二个操作的O_CREAT把数据抹去了。
    但是如果在O_CREAT之后,加上O_EXCL,那么可以避免这种情况。

 fcntl和ioctl

fcntl可以用来设置文件描述符属性、文件状态属性、文件描述符复制、设置文件锁、设置文件通知等功能,这里只表示学习通过fcntl修改文件描述符属性。

如果一个文件描述符没有O_APPEND属性,但是后来又需要这个属性,那么可以通过fcntl来设置。

 

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

// uint32_t status; // 位操作

int main()
{
    int fd = open("t11.txt", O_CREAT|O_RDWR, 0777);
    if(fd < 0)
    {
        perror("open");
        return -1;
    }

    // 通过fcntl修改文件属性,增加O_APPEND属性
    int flags = fcntl(fd, F_GETFL);
    flags |= O_APPEND;
    fcntl(fd, F_SETFL, flags);

    lseek(fd, 0, SEEK_SET);
    write(fd, "hello", 5);

    lseek(fd, 0, SEEK_SET);
    write(fd, "world", 5);

    close(fd);
    return 0;
}

 

 

ioctl是一个杂项函数,一般用于文件底层属性设置。

文件映射

文件映射能将硬盘映射到进程的地址,这样可以像操作内存一样操作文件,而且效率很高,但是有一定限制:

  • 文件长度必须大于等于映射长度

  • 映射的offset必须是页的整数倍

页的尺寸获取方式:
命令行getconf -a | grep PAGE_SIZE
函数sysconf(_SC_PAGE_SIZE)

//运用mmap实现文件的映射
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <string.h> #include <stdio.h> #include <stdlib.h> #include <sys/mman.h> int main() { int fd = open("a.map", O_RDWR); if(fd < 0) { perror("open"); return -1; } void* ptr = mmap(NULL, /* 指定映射的地址,如果为空,那么内核自动选择一个地址 */ 4096, /* 映射长度 */ PROT_READ|PROT_WRITE, /* 访问方式,要和打开文件使的flag一致 */ MAP_SHARED, /* 修改映射地址的数据,反应到硬盘,如果是MAP_PRIVITED,那么修改数据,不会刷新到硬盘 */ fd, /* 文件描述符 */ 0 /*从什么地方开始映射*/); if(ptr == MAP_FAILED) { perror("mmap"); return -1; } // 像访问内存一样的访问硬盘,虚拟内存 // 通过这种方式访问大文件效率更高 // 进程之间共享数据 strcpy((char*)ptr, "hello"); munmap(ptr, 4096); close(fd); return 0; }

 

 临时文件

可以通过mktemp(3)来获取一个临时文件路径,但是该文件不一定在/tmp目录下,在哪个目录下需要程序员指定。

Linux还有更多的创建临时文件的函数,学有余力的同学可以通过man 3 mktemp,查看相关函数。

#include <stdio.h>
#include <stdlib.h>
int main()
{
    char buf[] = "./hello-XXXXXX";

    char* p = mktemp(buf);
    printf("p=%s\n", p);
    printf("buf=%s\n", buf);
}

 

 缓存

为了提高IO效率,系统为应用程序提供了缓存服务。当应用程序写数据到硬盘时,内核只是将数据写入内核缓存,然后返回成功。

缓存的存在隐藏风险,如果缓存数据未写入硬盘时,发生断电故障,那么会导致数据的不完整性。

关键数据的不完整,可能会导致系统崩溃。

使用O_SYNC选项打开文件时,那么写入操作将保证数据写入到硬盘再返回,当然这个选项导致IO效率降低。

也可以使用syncfsyncfsyncdata之类的函数,将数据写入硬盘。

fwrite和write都有缓存,不过fwrite在用户空间和内核空间都有缓存,而write只有在内核空间有缓存。

补充

标准输入/输出/错误

每一个进程都默认打开三个文件,三个文件描述符分别是0,1,2。printf其实是调用write(1)实现的。

一般不直接使用0,1,2来表示三个文件,而是用宏STDIN_FILENO,STDOUT_FILENO, STDERR_FILENO来表示输入、输出、错误。

open返回值

返回可用的最小的文件描述符。

小于0,表示对文件操作失败

文件描述符

进程范围内唯一。

 dup2

也是用来赋值文件描述符,第二个参数指示复制的位置。

dup2(int oldfd, int newfd);

使用的命令和函数总结

函数

open/creat:打开文件/创建文件
read:读文件
write:写文件
close:关闭文件
lseek:定位文件读写位置
fcntl:修改文件属性
sysconf:读取系统配置
dup/dup2:复制文件描述符
sync/fsync/fsyncdata:同步文件数据
mmap/munmap:文件映射
mkstemp:得到临时文件路径

 命令

touch:修改文件的访问时间,创建文件
cat:访问文件内容
vim:编辑
ulimit:显示一些限制信息(文件描述符最大值、栈的空间尺寸)
umask:文件创建的权限掩码
getconf:对应sysconf

Linu基础 文件IO(读写操作)