不积硅步无以至千里,不积小流无以成江河。继续。。。
文件I/O被称为不带缓冲的I/O,指的是每个read和write都是调用内核中的一个系统调用。这些不带缓冲的文件I/O函数不是ISO C的组成部分。它们是POSIX.1和Single UNIX Specification的组成部分。
不带缓冲的文件I/O函数常用的有:open、read、write、lseek、close。
1、关于系统调用
系统调用,英文名system call,每个操作系统都在内核里有一些内建的函数库,这些函数可以用来完成一些系统系统调用,把应用程序的请求传给内核,调用相应的的内核函数完成所需的处理,将处理结果返回给应用程序,如果没有系统调用和内核函数,用户将不能编写大型应用程序,及别的功能,这些函数集合起来就叫做程序接口或应用编程接口(Application Programming Interface,API),我们要在这个系统上编写各种应用程序,就是通过这个API接口来调用系统内核里面的函数。如果没有系统调用,那么应用程序就失去内核的支持。
2、 关于缓冲区
linix对IO文件的操作分为不带缓存的IO操作和标准IO操作(即带缓存)要明确以下几点:
1)不带缓存,不是直接对磁盘文件进行读取操作,像read()和write()函数,它们都属于系统调用,只不过在用户层没有缓存,所以叫做无缓存IO,但对于内核来说,还是进行了缓存,只是用户层看不到罢了。
2)带不带缓存是相对来说的,如果你要写入数据到文件上时(就是写入磁盘上),内核先将数据写入到内核中所设的缓冲储存器,假如这个缓冲储存器的长度是100个字节,调用系统函数:
ssize_t write (int fd,const void * buf,size_t count);
写操作时,设每次写入长度count=10个字节,那么你几要调用10次这个函数才能把这个缓冲区写满,此时数据还是在缓冲区,并没有写入到磁盘,缓冲区满时才进行实际上的IO操作,把数据写入到磁盘上。
那么,既然不带缓存的操作实际在内核是有缓存器的,那带缓存的IO操作又是怎么回事呢?
带缓存IO也叫标准IO,符合ANSI C 的标准IO处理,不依赖系统内核,所以移植性强,我们使用标准IO操作很多时候是为了减少对read()和write()的系统调用次数,带缓存IO其实就是在用户层再建立一个缓存区,这个缓存区的分配和优化长度等细节都是标准IO库代你处理好了,不用去操心,还是用上面那个例子说明这个操作过程:
上面说要写数据到文件上,内核缓存(注意这个不是用户层缓存区)区长度是100字节,我们调用不带缓存的IO函数write()就要调用10次,这样系统效率低,现在我们在用户层建立另一个缓存区(用户层缓存区或者叫流缓存),假设流缓存的长度是50字节,我们用标准C库函数的fwrite()将数据写入到这个流缓存区里面,流缓存区满50字节后在进入内核缓存区,此时再调用系统函数write()将数据写入到文件(实质是磁盘)上,看到这里,你应该明白一点,标准IO操作fwrite()最后还是要掉用无缓存IO操作write,这里进行了两次调用fwrite()写100字节也就是进行两次系统调用write()。
两条总结:
无缓存IO操作数据流向路径:数据——内核缓存区——磁盘
标准IO操作数据流向路径:数据——流缓存区——内核缓存区——磁盘
(以上参考:https://blog.csdn.net/scottly1/article/details/24186719)
3、文件描述符:
对于内核文件而言,所有打开的文件都通过文件描述符引用。文件描述符是一个非负整数。当打开一个现有文件或创建一个新文件时,内核向进程返回一个文件描述符。当读或写一个文件时,使用open或creat返回的文件描述符标识该文件,将其作为参数传送给read或write。按照惯例,UNIX系统shell使用文件描述符0与进程的标准输入相关联,文件描述符1与标准输出相关联,文件描述符2与标准错误相关联。在依从POSIX的应用程序中,幻数0、1、2应当替换成符号常量STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO。这些常量定义在头文件
1、open函数:打开或创建一个文件。
1)头文件:#include
2)函数原型:int open(const char *pathname, int oflag, ... /*mode_t mode*/);
3)返回值:若成功则返回文件描述符,若出错则返回-1;
4)参数:
const char *pathname:要打开或创建文件的名字;
int oflag:用来说明此函数的多个选项;
O_RDONLY 只读打开
O_WRONLY 只写打开
O_RDWR 读、写打开
以上三个常量中必须指定一个且只能指定一个,以下常量则是可选择的:
O_APPEND 每次写时都追加到文件的尾端。
O_CREAT 若此文件不存在,则创建它。使用此文件时需要第三个参数mode,用其指定该新文件的访问权限。
O_EXCL 如果同时指定了O_CREAT,而文件已经存在,则会出错。用此可以测试一个文件是否存在,如果不存在,则创建此文件,这使测试和创建两者成为一个原子操作
O_TRUNC 如果此文件存在,而且为只写或读写成功打开,则将其长度截短为0;
O_NOCTTY 如果pathname指的是终端设备,则不将该设备分配作为此进程的控制终端。
O_NONBLOCK 如果pathname指的是一个FIFO、一个块特殊设备或一个字符特殊文件,则此选项为文件的本次打开操作和后续的I/O操作设置非阻塞模式。
第三个参数mode仅当创建新文件时才使用;
5)mode:
mode代表文件权限标志也可以使用加权数字表示,这组数字被称为umask变量,它的类型是mode_t,是一个无符号八进制数。umask变量的定义方法下表所示。umask变量由3位数字组成,数字的每一位代表一类权限。用户所获得的权限是加权数值的总和。例如764表示所有者拥有读、写和执行权限,群组拥有读和写权限,其他用户拥有读权限。
加 权 数 值 |
第1位 |
第2位 |
第3位 |
4 |
所有者拥有 读权限 |
群组拥有读权限 |
其他用户拥 有读权限 |
2 |
所有者拥有 写权限 |
群组拥有写权限 |
其他用户拥 有写权限 |
1 |
所有者拥有 执行权限 |
群组拥有执行权限 |
其他用户拥 有执行权限 |
mode 设置:
文件权限 = 给定对的文件权限 & 本地掩码(取反)
例如:设定权限 0777
umask 出来的本地掩码是 0002
777 ----------------------------二进制 111 111 111
002 ----------------------------二进制 00 000 010 取反后得 111 111 101
& (按位与)
实际权限 111 111 101
即实际权限为 0775
使用open 函数创建文件范例:open(pathname, ORDWR | 0_CREAT |O_TRUNC, 0777);
2、creat函数:创建一个新文件。
1)头文件:#include
2)函数原型:int creat(const char *pathname, mode_t mode);
3)返回值:若成功则返回为只写打开的文件描述符,若出错则返回-1;
注意:creat是以只写方式打开所创建的文件。如果要创建一个临时文件,并要先写该文件再读该文件,则必须先调用creat、close然后再调用open。可以用以下调用open的方式替代:
open(pathname, ORDWR | 0_CREAT |O_TRUNC, mode);
3、lseek函数:显式地为一个打开的文件设置其偏移量。
每个文件都有一个与其相关联的“当前文件偏移量”(current file offset)。它通常是一个非负整数,用以度量从文件开始处计算的字节数。通常,读写操作都是从当前文件偏移量处开始,并使偏移量增加所读写的字节数。按系统默认情况,当打开一个文件时,除非指定O_APPEND选项,否则该偏移量被设置为0;
1)头文件:#include
2)函数原型:off_t lseek(int filedes, off_t offset, int whence);
3)返回值:若成功返回新的文件偏移量,若出错则返回-1;
4)参数:
int filedes:文件描述符;
int whence:若whence是SEEK_SET,则将该文件的偏移量设置为距文件开始处offset个字节。
若whence是SEEK_CUR,则将该文件的偏移量设置为其当前值加offset,offset可为正或负。
若whence是SEEK_END,则将该文件的偏移量设置为文件长度加offset,offset可为正或负。
若lseek成功执行,则返回新的文件偏移量,为此可以用下列方式确定打开文件的当前偏移量:
off_t currpos;
currpos = lseek(fd, 0, SEEK_CUR);
4、close函数:关闭一个打开的文件。
1)头文件:#include
2)函数原型:int close(int filedes);
3)返回值:若成功返回0,若出错则返回-1;
关闭一个文件时还会释放该进程加在该文件上的所有纪录锁。
当一个进程终止时,内核会自动关闭它所有打开的文件。很多程序都利用了这一功能而不显式地用close关闭打开的文件;
4)参数:
int filedes:文件描述符;
5、read函数:
1)头文件:#include
2)函数原型:ssize_t read(int filedes, void *buf, size_t nbytes);
3)返回值:若成功则返回读到的字节数,若已到文件结尾则返回0,若出错则返回-1;
有多种情况可使实际读到的字节数少于要求读的字节数:
读普通文件时,在读到要求字节数之前已到达了文件尾端。例如,若在到达文件尾端之前还有30个字节,而要求读100个字 节,则read返回30,下一次再调用read时,他将返回0(文件尾端);
当从终端设备读时,通常一次最多读一行。
当从网络读时,网络中的缓冲机构可能造成返回值小于所要求读的字节数。
当从管道或FIFO读时,如若管道包含的字节少于所需的数量,那么read将只返回实际可用的字节数。
当从某些面向记录的设备读时,一次最多返回一个记录。
4)参数:
int filedes:文件描述符;
void *buf:用于存储从文件中读到的内容;
size_t nbytes:要求读取的字节数;
6、write函数:
1)头文件:#include
2)函数原型:ssize_t write(int filedes, const void *buf, size_t nbytes);
3)返回值:若成功则返回已写的字节数,若出错则返回-1;
其返回值通常与nbytes的值相同,否则表示出错。write出错的原因通常是磁盘已写满,或者超过了一个给定进程的文件长度限制。
对于普通文件,写操作从文件的当前偏移量处开始。如果在打开文件时,指定了O_APPEND选项,则在每次写操作之前,将文件的偏移量设置在文件的当前结尾处。在一次成功写之后,该文件偏移量增加实际写的字节数。
4)参数:
int filedes:文件描述符;
void *buf:用于存储待写入的数据;
size_t nbytes:要求写入的字节数;
7、size_t与ssize_t
1)size_t是一些C/C++标准在stddef.h中定义的。这个类型足以用来表示对象的大小。size_t的真实类型与操作系统有关,在32位架构中被普遍定义为:
typedef unsigned int size_t;
而在64位架构中被定义为:
typedef unsigned long size_t;
size_t在32位架构上是4字节,在64位架构上是8字节,在不同架构上进行编译时需要注意这个问题。
2)ssize_t是有符号整型,在32位机器上等同与int,在64位机器上等同与long int32 位和 64 位C数据类型
3)32和64位C语言内置数据类型,如下表所示:
从源文件中读取指定长度的字节数,将读到的数据写入到目的文件中。
#include
#include
#include
#include
#define READ_MAX_SIZE 2048 //要求从源文件中读的字节数
#define WRITE_SIZE 100 //每次调用write时写入的字节数
int copy_info(char *str, char *dstr)
{
int fp = -1;
int fd = -1;
char *buf = NULL;
char *bufp = NULL;
size_t max_bytes = READ_MAX_SIZE; //要求读的字节数;max_bytes要做自减操作,必须定义为无符号整型;
size_t total_read_bytes = 0; //实际读到的字节数
size_t total_write_bytes = 0; //实际写入的字节数
ssize_t read_bytes = 0;
ssize_t write_bytes = 0;
int count = 0;
int i = 0;
buf = (char *) malloc(sizeof(char) * (READ_MAX_SIZE + 1));
memset(buf, '\0', sizeof(char) * (READ_MAX_SIZE + 1));
bufp = buf;
if((str == NULL) || (dstr == NULL))
{
printf("input parameter is NULL!\n");
return -1;
}
fp = open(str, O_RDONLY);
if ( 0 > fp )
{
printf("[%s]%s %d open %s file failed!\n", __FILE__, __func__, __LINE__, str);
return -1;
}
while ( (0 != max_bytes) && ( read_bytes = read(fp, bufp, max_bytes)) != 0)
{
if ( -1 == read_bytes )
{
if ( EINTR == errno )
{
continue;
}
else
{
printf("[%s]%s %d Read %s file failed!\n", __FILE__, __func__, __LINE__, str);
close(fp);
return -1;
}
}
max_bytes -= read_bytes;
bufp += read_bytes;
total_read_bytes += read_bytes;
}
errno = 0;
fd = open(dstr, O_WRONLY | O_CREAT |O_TRUNC, 0777);
if(fd == -1)
{
printf("open file %s failed!\n", dstr);
return -1;
}
bufp = buf;
count = total_read_bytes / WRITE_SIZE;
printf("[%s]%s %d, total_read_bytes = %u ,count = %d\n", __FILE__, __func__, __LINE__, total_read_bytes, count);
for(i = 0; i < count; i++)
{
write_bytes = write(fd, bufp, WRITE_SIZE);
if(WRITE_SIZE != write_bytes)
{
printf("[%s]%s %d write %s file failed!\n", __FILE__, __func__, __LINE__, dstr);
close(fp);
close(fd);
return -1;
}
bufp += write_bytes;
total_write_bytes += write_bytes;
}
if(total_read_bytes % WRITE_SIZE != 0)
{
write_bytes = write(fd, bufp, total_read_bytes % WRITE_SIZE);
if(total_read_bytes % WRITE_SIZE != write_bytes)
{
printf("[%s]%s %d write %s file failed!\n", __FILE__, __func__, __LINE__, dstr);
close(fp);
close(fd);
return -1;
}
bufp += write_bytes;
total_write_bytes += write_bytes;
printf("[%s]%s %d remainder = %d, total_write_bytes = %u\n", __FILE__, __func__, __LINE__, write_bytes, total_write_bytes);
}
close(fp);
close(fd);
return 0;
}
/* 从file1 READ_MAX_SIZE个字节的字符,将其写入写到file2中 */
int main(int argc, char *argv[])
{
int ret = 0;
printf("%s %d argc:%d\r\n", __FUNCTION__, __LINE__, argc );
if((argv[1] == NULL) || (argv[2] == NULL) || (argc < 3))
{
printf("input parameter is NULL!\n");
return -1;
}
printf("argv0 = %s\r\n", argv[0]);
printf("argv1 = %s\r\n", argv[1]);
printf("argv1 = %s\r\n", argv[2]);
ret = copy_info( argv[1], argv[2]);
if(ret != 0)
{
printf("copy_info error!\n");
}
return 0;
}
1)源文件为二进制文件,且文件长度大于2048字节。输出结果如下:
$ ./read_info test.bin result.bin
main 120 argc:3
argv0 = ./read_info
argv1 = test.bin
argv1 = result.bin
[read_info.c]copy_info 73, total_read_bytes = 2048 ,count = 20
[read_info.c]copy_info 103 remainder = 48, total_write_bytes = 2048
$
将源文件与目的文件做二进制对比查看是否符合要求。
按照要求拷贝了2048个字节的数据。前2048字节完全一致。
2) 源文件为文本文件,且文件长度小于2048字节。输出结果如下:
$ ./read_info ifconfig ifconfig.bin
main 120 argc:3
argv0 = ./read_info
argv1 = ifconfig
argv1 = ifconfig.bin
[read_info.c]copy_info 73, total_read_bytes = 952 ,count = 9
[read_info.c]copy_info 103 remainder = 52, total_write_bytes = 952
$
将源文件与目的文件做文本文件对比查看是否符合要求。
要求读2048字节的数据,实际只读到了952字节的数据,实际写入到目标文件的字节数也是952字节。读出与写入的数据完全一致。
再次回顾一下read、write函数的定义:
1、read函数
1)头文件:#include
2)函数原型:ssize_t read(int filedes, void *buf, size_t nbytes);
3)返回值:若成功则返回读到的字节数,若已到文件结尾则返回0,若出错则返回-1;
有多种情况可使实际读到的字节数少于要求读的字节数:读普通文件时,在读到要求字节数之前已到达了文件尾端。例如,若在到达文件尾端之前还有30个字节,而要求读100个字节,则read返回30,下一次再调用read时,他将返回0(文件尾端);
4)参数:
int filedes:文件描述符;
void *buf:用于存储从文件中读到的内容;
size_t nbytes:要求读取的字节数;
read函数可以根据函数的返回值来判断文件是否结束。即使nbytes大于filedes文件的实际长度,最后实际读到的数据长度还是文件的实际长度,不会出现越界读取数据的问题。
2、write函数
1)头文件:#include
2)函数原型:ssize_t write(int filedes, const void *buf, size_t nbytes);
3)返回值:若成功则返回已写的字节数,若出错则返回-1;
其返回值通常与nbytes的值相同,否则表示出错。write出错的原因通常是磁盘已写满,或者超过了一个给定进程的文件长度限制。
对于普通文件,写操作从文件的当前偏移量处开始。如果在打开文件时,指定了O_APPEND选项,则在每次写操作之前,将文件的偏移量设置在文件的当前结尾处。在一次成功写之后,该文件偏移量增加实际写的字节数。
4)参数:
int filedes:文件描述符;
void *buf:用于存储待写入的数据;
size_t nbytes:要求写入的字节数;
注意writr函数的返回值描述。write函数无法根据函数的返回值来判断文件是否已经读取结束。如果写入的长度nbytes大于buf的长度,write函数还是会往filedes文件中写入nbytes个字节的数据。所以使用write函数时,要注意控制写入数据长度nbytes,防止将Buf以外的数据写入到文件中。
以下例子中,对于write和fwrite函数,要求写入的字节数nbyte大于buf长度。
#include
#include
#include
#include
int main()
{
int fd = -1;
FILE *sp = NULL;
ssize_t write_bytes = 0;
char bufp[10] = {0};
#if 1
fd = open("111", O_WRONLY | O_CREAT |O_TRUNC, 0777);
if(fd == -1)
{
printf("open file 111 failed!\n");
return -1;
}
write_bytes = write(fd, bufp, 100);
printf("[%s]%s %d remainder = %d\n", __FILE__, __func__, __LINE__, write_bytes);
close(fd);
#else
sp = fopen("112", "r");
fwrite(bufp, sizeof(char), 100, sp);
fclose(sp);
#endif
return 0;
}
编译没有报错,执行过程中也没有报错,但是输出的结果以二进制方式查看,write写入到目标文件111中的前0个字节的数据是buf中的数据,后90个数据应该是buf后面存储空间中的数据。这显然是有问题的。
上图中,使用fwrite函数写112文件,从二进制文件来看,好像fwrite很安全,并没有写入越界的字符。但是只要写入的数据不符合预期就是有问题的,至少在使用上是有问题的。代码和结果如下:
#include
#include
#include
#include
#define READ_MAX_SIZE 2048
#define WRITE_SIZE 100
int copy_info(char *str, char *dstr)
{
int fp = -1;
int fd = -1;
char *buf = NULL;
char *bufp = NULL;
size_t max_bytes = READ_MAX_SIZE; //max_bytes要做自减操作,必须定义为无符号整型
ssize_t read_bytes = 0;
ssize_t write_bytes = 0;
int count = 0;
int i = 0;
buf = (char *) malloc(sizeof(char) * (READ_MAX_SIZE + 1));
memset(buf, '\0', sizeof(char) * (READ_MAX_SIZE + 1));
bufp = buf;
if((str == NULL) || (dstr == NULL))
{
printf("input parameter is NULL!\n");
return -1;
}
fp = open(str, O_RDONLY);
if ( 0 > fp )
{
printf("[%s]%s %d open %s file failed!\n", __FILE__, __func__, __LINE__, str);
return -1;
}
while ( (0 != max_bytes) && ( read_bytes = read(fp, bufp, max_bytes)) != 0)
{
if ( -1 == read_bytes )
{
if ( EINTR == errno )
{
continue;
}
else
{
printf("[%s]%s %d Read %s file failed!\n", __FILE__, __func__, __LINE__, str);
close(fp);
return -1;
}
}
max_bytes -= read_bytes;
bufp += read_bytes;
}
errno = 0;
//测试fwrite是否安全。
FILE *sp = NULL;
sp = fopen("113", "a");
fwrite(buf, sizeof(char), 1024, sp);
close(fp);
fclose(sp);
return 0;
}
/* 从file1 READ_MAX_SIZE个字节的字符,将其写入写到file2中 */
int main(int argc, char *argv[])
{
int ret = 0;
printf("%s %d argc:%d\r\n", __FUNCTION__, __LINE__, argc );
if((argv[1] == NULL) || (argv[2] == NULL) || (argc < 3))
{
printf("input parameter is NULL!\n");
return -1;
}
printf("argv0 = %s\r\n", argv[0]);
printf("argv1 = %s\r\n", argv[1]);
printf("argv1 = %s\r\n", argv[2]);
ret = copy_info( argv[1], argv[2]);
if(ret != 0)
{
printf("file_copy error!\n");
}
return 0;
}
要求读2048字节的数据,实际只读到了952字节的数据;要求写入实际写入到目标文件的字节数1024字节,实际写入1024字节。其中952字节是实际有用的数据,后72字节是buf中的初始化数据。后72字节的数据虽然不是越界读取,但也不是我们想要的数据。所以,在使用fwrite时也要注意nbytes的长度。
引用以下资料,衷心感谢大家的分享:
https://blog.csdn.net/scottly1/article/details/24186719
UNIX环境高级编程