文章目录
- Linux文件编程
- 标注C的IO缓存类型
- 代码示例--缓存区的存在
- 文件I/O系统调用
- 标准C库关于文件的输入输出函数
- FILE结构体
- 文件描述符
- 文件描述符与文件指针的相互转换
- 系统调用常用函数
- open函数(打开或者创建文件)
- creat函数(创建一个现有文件)
- close函数(关闭文件)
- read函数(从打开文件中读取数据)
- write函数(向打开的文件中写入数据)
- lseek函数(偏移文件当前的位置)
- 代码示例:实现cp操作
- 空洞文件
- 代码示例:空洞文件
- 缓存buffer大小设置
Linux文件编程
对于Linux的输入输出而言,它并不是直接输出到屏幕上或者直接从键盘中获取输入。这中间经过了一个缓存。
标注C的IO缓存类型
全缓存
- 要求填满整个缓存区后才进行I/O系统调用操作。对于磁盘文件通常要求使用全缓存访问。
行缓存
-
涉及一个终端时(例如标准输入和标准输出),使用行缓存。
行缓存的特点:
- 行缓存满自动输出
- 碰到换行符自动输出
无缓存
- 标准错误流
stderr
通常是不带缓存区的,这使得错误信息能够尽快的显示出来。
当写入的字节数不能够满足要填满的最大缓存区这一条件,可以调用fflush
函数强制清空缓存,将缓存中的数据输出到指定的流中。
当程序结束之前会自动将缓存里边的内容全部清空。
代码示例–缓存区的存在
#include <stdio.h>
int main()
{
printf("haha");
while(1);
return 0;
}
上边的这行代码由于因为有缓存的缘故,所以当去调用printf
函数去向屏幕输出东西不加换行的时候,没有任何东西输出,这就说明在代码和屏幕之间确实是存在一个缓存区。此时若想要输出,可以在printf
函数后边加一个换行符或者使用fflush
函数强制清空缓存
文件I/O系统调用
下列有关文件的函数都是内核提供的系统调用,它们都是不带缓存功能的。
标准C库关于文件的输入输出函数
char *fgets(char *restrict s, int n, FILE *restrict stream);
int fputs(const char *restrict s, FILE *restrict stream);
int fprintf(FILE *restrict stream, const char *restrict format, ...);
int fscanf(FILE *restrict stream, const char *restrict format, ...);
size_t fread(void *restrict ptr, size_t size, size_t nitems,FILE *restrict stream);
size_t fwrite(const void *restrict ptr, size_t size, size_t nitems,FILE *restrict stream);
int scanf(const char *restrict format, ...);
int printf(const char *restrict format, ...);
FILE结构体
typedef struct iobuf
{
int cnt; //剩余的字节数
char *ptr; //下一个字符的位置
char *base; //缓冲区的位置
int flag; //文件访问模式
int fd; //文件描述符
}FILE;
由上边的结构体可知,在标准C库里边定义一个FILE
类型的指针,实际上是定义了一个结构体指针。
实际上在标准C库中定义的FILE
类型的结构体,它内部也是通过系统调用来是实现它的文件的打开、关闭、读写等操作。系统调用的最重要的是通过一个文件描述符来进行一系列的操作,在FILE
类型的结构体中也能看到这个成员变量,说明fopen
底层确实是通过调用open
函数来实现它的功能,其他C库函数也是一样。
关于标准C库和系统调用函数的使用场景的话:由于标准C库是面向应用层的,所以它的级别会更高一点,而对于系统调用函数它由于更靠近内核,所以它的级别更低一点。所以如果在实际应用中要求响应更快,可以使用系统调用函数,因为它没有缓存,而且更靠近内核,所以它的实现速度会更快。
文件描述符
-
在系统调用中,每一个文件都对应一个唯一的文件描述符,文件描述符是一个非负整数。当打开一个现有文件或者创建一个新的文件时,内核向进程返回一个文件描述符。后续的读写、关闭等操作都是基于这个文件描述符。
-
在
POSIX
应用程序中,整数0、1、2被宏定义为STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO
,这些宏定义被定义在unistd.h
头文件中 -
文件描述符的范围是
0-OPEN_MAX
,不同的操作系统所对应的OPEN_MAX
的值不相同。在终端输入getconf OPEN_MAX
可以获取OPEN_MAX的值 。也就是说一个进程最多能够打开或者创建1024个文件。
文件描述符与文件指针的相互转换
文件描述符–>文件指针(fdopen)
FILE *fdopen(int fd, const char *mode);
//参数1:文件描述符
//参数2:权限(r 只读 w 只写)
//返回值:文件指针
文件指针–>文件描述符
int fileno(FILE *stream);
//参数:文件流指针
//返回值:文件描述符
系统调用常用函数
open函数(打开或者创建文件)
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *path, int oflag);
int open(const char *path, int oflag, mode_t mode);
//功能:打开或者创建一个文件
//参数1:要打开或者创建文件的路径;
//参数2:打开文件的选项,例如(O_RDONLY 只读) (O_WRONLY 只写) (O_RDWR 读写)
//第二个函数是只有文件不存在,需要创建新的文件的时候才使用
//参数1:要打开或者创建文件的路径;
//参数2:打开文件的选项,例如(O_RDONLY 只读) (O_WRONLY 只写) (O_RDWR 读写)
//参数3:创建文件要赋予的权限
//返回值是一个文件描述符
open
函数的flag
参数
O_RDONLY
:以只读的方式打开文件
O_WRONLY
:以只写的方式打开文件
O_RDWR
:以读写方式打开文件
O_APPEND
:以追加模式打开文件,每次写时都加到文件的尾端
O_CREAT
:如果指定的文件不存在,则按mode
参数指定的权限来创建文件
O_EXCL
:如果同时指定了O_CREAT
,而文件已经存在,则出错。可以用来测试一个文件是否存在。
O_DIRECTORY
:如果参数pathname
不是一个目录,则open
出错。
O_TRUNC
:如果此文件存在,而且为只读或者只写成功打开,则将其长度截断为0
o_NONBLOCK
:如果pathname
指的是一个FIFO
、一个块特殊文件或一个字符特殊文件,则此选项为此文件的本粗打开操作和后续的I/O操作设置非阻塞方式。
creat函数(创建一个现有文件)
#include <sys/stat.h>
#include <fcntl.h>
int creat(const char *path, mode_t mode);
//参数1:要创建文件的路径
//参数2:创建文件富裕的权限
//返回值:若创建成功返回一个为只写打开的文件描述符,若出错返回-1
-
此函数等价于
open(pathname, O_WRONLY | O_CREAT | O_TRUNC, mode)
; -
ceate
的一个不足之处时它打开的文件是为只写打开的
close函数(关闭文件)
#include <unistd.h>
int close(int fildes);
//参数:一个已经打开的文件描述符
//返回:若成功为0,若出错为-1
read函数(从打开文件中读取数据)
#include <unist.h>
ssize_t read(int fildes, void *buf, size_t nbyte);
//参数1:一个已经打开的文件描述符
//参数2:用来存放读取到的一个缓存
//参数3:要求一次读取的字节数
//返回值:读取到的字节数,若已到达文件尾为0,若出错为-1
注意:
read
函数的返回值可能会少于要求读取的字节数的原因:
- 读取普通文件时,再读到要求字节数之前已经达到了文件尾端;
- 当从终端设备读时,通常一次最多读一行;
- 当从网络读时,网络中的缓冲机构可能造成返回值小于所要求读的字节数;
- 某些面向记录的设备,例如磁带,一次最多返回一个记录;
- 进程由于信号造成中断。
read
函数从文件的当前的位移量处开始,在成功返回之前,该位移量增加实际读得的字节数。
write函数(向打开的文件中写入数据)
#include <unistd.h>
ssize_t write(int fildes, const void *buf, size_t nbyte);
//参数1:一个已经打开的文件描述符
//参数2:存放要写入文件的数据的一个缓存
//参数3:要求一次写入文件的字节数
//返回值:若成功为已写入的字节数,若出错为-1
需要注意的是:当使用write
函数的时候需要特别注意的是要清楚是追加写入还是从头写入,如果想要从头写入那么就直接使用O_TRUNC
标志位将文件清空,如果想要追加写入那么就使用O_APPEND
标志位在文件的末尾追加写入。
lseek函数(偏移文件当前的位置)
#include <unistd.h>
off_t lseek(int fildes, off_t offset, int whence);
//参数1:已经打开的文件描述符
//参数2:位移量
//参数3:定位的位置
//返回值:若成功则返回新的文件位移量(绝对偏移量(距文件开头的字节数)),若出错返回-1
参数3:whence
SEEK_SET
:将该文件的位移量设置为距文件开始处offset
个字节SEEK_CUR
:将该文件的位移量设置为其当前值加offset
,offset
可正可负SEEK_END
:将该文件的位移量设置为文件长度(文件末尾)加offset
,offset
可正可负
lseek
函数的注意事项
lseek
也可以用来确定所涉及的文件是否可以设置偏移量。如果文件描述符引用的是一个管道或FIFO,则lseek
返回-1,并将errno
设置为EPIPE
。- 每个打开文件都有一个与其相关联的“当前文件偏移量”。它是一个非负整数,用以度量从文件开始处计算的字节数。通常,读写操作都是从当前文件偏移量处开始,并使用偏移量增加所读或所写的字节数。按系统默认,当打开一个文件时,除非指定
O_APPEND
追加写入或者使用lseek
函数进行偏移,否则该位移量被设置为0。
代码示例:实现cp操作
#include "io.h"
#define BUFFER_SIZE 1024
void copy_function(int fdin, int fdout)
{
ssize_t nbytes = 0;
char buffer[BUFFER_SIZE];
memset(buffer,'\0',BUFFER_SIZE);
//从源文件中读取到buffer中
while((nbytes = read(fdin, buffer, BUFFER_SIZE)) > 0)
{
printf("the size of read bytes is %ld\n",lseek(fdin,0,SEEK_CUR));
//将buffer里边的数据写入到目标文件中去
if(write(fdout, buffer, nbytes) != nbytes)
{
fprintf(stderr,"write error: %s\n",strerror(errno));
exit(EXIT_FAILURE);
}
}
if(nbytes < 0)
{
fprintf(stderr,"read error: %s\n",strerror(errno));
exit(EXIT_FAILURE);
}
}
//cp.c
#include "io.h"
int main(int argc, char **argv)
{
long size;
if(argc != 3)
{
fprintf(stderr,"usage: %s srcfile destfile\n",argv[0]);
exit(EXIT_FAILURE);
}
int fdin, fdout;
fdin = open(argv[1],O_RDONLY);
if(fdin < 0)
{
fprintf(stderr,"open file error: %s\n",strerror(errno));
exit(EXIT_FAILURE);
}
size = lseek(fdin,0,SEEK_END);
printf("the size of file is %ld\n",size);
fdout = open(argv[2],O_WRONLY | O_CREAT | O_TRUNC, 0777);
if(fdout < 0)
{
fprintf(stderr,"open file error: %s\n",strerror(errno));
exit(EXIT_FAILURE);
}
lseek(fdin,-size,SEEK_END);
copy_function(fdin,fdout);
close(fdin);
close(fdout);
return 0;
}
//io.h
#ifndef _IO_H
#define _IO_H
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
void copy_function(int fdin, int fdout);
#endif
需要注意的是lseek
函数定位的位置如果设置为SEEK_END
标志位,它的偏移量是可正可负的。负方向就是从文件末尾开始向前偏移offset
个字节,但如果向正方向偏移就从文件尾开始向后偏移offset
个字节。如果在偏移若干个文件后再次写入数据,那么这就是一个空洞文件。
空洞文件
空洞文件是指文件中未写入数据的部分,不占用磁盘空间,直到写入数据时才会分配磁盘块。空洞文件的存在意味着文件名义上的大小可能比其占用的磁盘存储总量要大。在UNIX文件操作中,文件位移量可以大于文件的当前长度,这种情况下,对该文件的下一次写将延长该文件,并在文件中构成一个空洞。创建空洞文件可以让多线程在不同的seek上面开始写入文件,如果不是空洞文件就只能串行写入了。例如常常在网络传输中会使用到空洞文件。当从网络上下载的时候会首先创建一个和目标文件相同大小的空洞文件,然后使用多线程在不同的地址上进行写入文件,这样能大大加快传输速度。
代码示例:空洞文件
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <errno.h>
char *buffer = "1234567890";
int main(int argc, char **argv)
{
if(argc != 2)
{
fprintf(stderr,"usage: %s [file]\n",argv[0]);
exit(EXIT_FAILURE);
}
int fd;
fd = open(argv[1],O_WRONLY | O_CREAT | O_TRUNC, 0777);
if(fd < 0)
{
perror("open error");
exit(EXIT_FAILURE);
}
if(write(fd, buffer, strlen(buffer)) != strlen(buffer))
{
fprintf(stderr,"write error: %s\n",strerror(errno));
exit(EXIT_FAILURE);
}
lseek(fd, 10, SEEK_END);
if(write(fd, buffer, strlen(buffer)) != strlen(buffer))
{
fprintf(stderr,"write error: %s\n",strerror(errno));
exit(EXIT_FAILURE);
}
close(fd);
return 0;
}
缓存buffer大小设置
在进行读写操作的时候,必然会有一个缓存来存放临时的数据。实际上这个缓存的大小也是有讲究的。在数据读写的过程中,一般使用块作为一个单位进行存放,如果定义缓存大小位一个块的大小,会大大地增加传输的效率。不同系统的块大小是不一样的,可以使用以下指令查看块大小
首先使用df -h
指令查看磁盘情况
然后使用指令tune2fs
查看文件系统参数(以超级用户权限)
sudo tune2fs -l /dev/sda3
上边的Block siez
就是当前系统的块大小,将这个定义为缓存的大小有利用系统进行文件的读写操作。