系统编程——缓冲I/O

需要对普通文件执行很多轻量级I/O请求的程序通常会采用用户缓冲 I/O ,用户缓冲 I/O 是在用户空间而不是在内核中完成的,它可以在应用程序中设置,也可以调用标准库,对用户而言透明执行。
块:
块是文件系统中最小存储单元的抽象,在内核中所有的文件系统操作都是基于块来执行的。
块是I/O中的基本概念。所有的I/O操作都是在块大小或者块的整数倍上来执行。也就是说即便你只想读取一个字节,但实际上你需要读取一整个块。
 
所以说,读取一个字节相比读取一整个块的数据开销反而更大。因为前者需要删除更新(删除)后半部分内容,然后再把整个块写出去,后者是直接把整个块写出去。
大部分用户空间的应用在实现时并不会考虑块的概念。绝大多数的应用都是在更高层抽象上执行的,比如成员变量和字符串,其大小变化和块大小无关。
最糟糕的是,用户空间可能每次只读写一个字节,这会带来很多不必要的开销。
 
在实际应用中,块大小一般是512字节,1024字节、2048字节或者是4098字节。
把每次操作的数据设置成块大小的整数倍或者约数,可以实现大规模的效率提升。
 
内核和硬件之间的交互单元是块,因此,使用块大小或者块大小的约数可以保证I/O请求是块对齐的,可以避免内核内其他的冗余操作。
通过系统调用 stat() 或者 stat(1) 可以轻松为给定设备指定块大小。
 
为提高效率,又考虑到大部分用户空间的应用在实现时并不会考虑块的概念,所以最简单的方式就是设置一个较大的缓冲区,缓冲区的大小是块的倍数。应用使用自己的抽象结构,通过用户缓冲 I/O 实现块操作。
工作原理如下:
1、数据写入时,先被存储在程序地址空间的缓冲区中。
2、当缓冲区数据达到给定大小时,整个缓冲区通过一次写操作全部写出。
读数据一样:
1、缓冲区先一次读入整个缓冲区大小的数据
2、应用通过各种需求大小的读请求从缓冲区一块一块的读取
3、缓冲区数据为空时,缓冲区再读一个大的块对齐的数据

用户缓冲I/O:
 需要对普通文件执行很多轻量级的 I/O 请求的程序通常会采用用户缓冲I/O。
用户缓冲 I/O 是在用户空间而不是内核中完成的,它可以在应用程序中设置,也可以调用标准库,对用户而言是“透明”执行。
dd bs=1 count=2097152 if=/dev/zero of=pirate
dd命令会从设备/dev/zero(提供值全为0的文件流的虚拟设备)中拷贝共2M的数据到文件 pirate 中,每次操作拷贝1个字节,共执行2097152次操作共2M

文件指针:
标准 I/O 程序集并不是直接操作文件 I/O,相反它们通过唯一标识符,即文件指针来操作。
在C标准库中,文件指针和文件描述符一 一映射。
文件指针是由指向类型为 FILE 的指针来表示,FILE定义在<stdio.h>
在标准I/O中,打开的文件称之为流“”。流可以打开用来读(输入流),写(输出流),二者兼有(输入/输出流)

打开文件:
#include <stdio.h>
FILE * fopen (const char *path , const char *mode);
该函数,根据参数mode,按照指定的方式打开文件,并给他关联上新的流。
mode:
r:以只读模式打开文件。流指向文件的开始。
r+:以可读写模式打开文件。流指向文件的开始。
w:以只写模式打开文件。如果文件存在文件会先被清空,如果文件不存在则会创建,流指向文件的开始。
w+:以可读写模式打开。如果文件存在,文件会被清空,如果文件不存在则会被穿件,流指向文件的开始。
a:以追加写模式打开文件。如果文件不存在文件会被创建,流指向文件的末尾。
a+:以追加读写模式打开文件。如果文件不存在机会被创建。流指向文件的末尾。
返回值:
成功——返回一个合法的 FILE 指针
失败——返回 NULL

通过文件描述符打开流:
函数fdopen()会把一个已经打开的文件描述符(fd)转换为流
#include <stdio.h>
FILE * fdopen(int fd , const char *mode)
****************************************************注:******************************************************
fdopen() 的 mode 值和fopen() 相同,但是必须要和初始打开文件描述符的模式匹配。
一旦文件描述符被转换为流,则在该文件描述符上不应该直接执行I/O操作,虽然这么做是合法的。
期间,文件描述符并没有被复制,只是关联了一个新的流,关闭流也会关闭这个文件描述符。
****************************************************************************************************************
返回值:
成功——返回一个合法的 FILE 指针
失败——返回 NULL

关闭流:
 fclose()函数会关闭给定的流
#include <stdio.h>
int fclose (FILE *stream);
在关闭之前,所有被缓冲但还没有写出的数据都会被写出。
返回值:
成功——返回0
失败——返回EOF,并设置相应的errno值
关闭所有的流:
 fcloseall()函数会关闭和当前进程相关联的所有流,包括标准输入、标准输出、标准错误
#define _GNU_SOURCE
#include <stdio.h>
int fcloseall (void);
在关闭前,所有流都会被写出。
返回值:
 这个函数始终返回 0 ,为Linux特有。

 从流中读取数据:
 前面讲述了如何打开和关闭流,这里主要讲:从一个流中读数据,再把数据写入到另一个流中。
C标准库实现了多种从流中读取数据的方法,有很常见的也有不常见的。
这里主要讲述三种:每次读取一个字节、每次读取一行、读取二进制数据。
为了从流中读取数据,该流必须以适当的模式打开成输入流,也就是除了w,a以外的模式都可以。
每次读取一个字节:
#include <stdio.h>
int fgetc (FILE *stream);
该函数从流stream中读取一个字符,并把该字符强制准换为 unsigned int 返回。
返回值:
成功——返回读取的强制转换后的字符
失败——文件结束,或者错误返回EOF
例:
#include <stdio.h>
int c;
c = fgetc(stream);
if(c == EOF)
  /*error*/
else
  printf("%c\n", (char)c)
每次写入一个字节:
#include <stdio.h>
int ungetc (int c , FILE *stream);
每次调用都会把参数 c 强制转换为 char 类型并放回到流 stream 中。
返回值:
成功——返回c
失败——返回EOF
****************************************************注:******************************************************
如果有多个字符返回到流中,读取的时候会以相反的顺序返回——也就是说,先放回的字符后返回。
如果在调用ungetc()之后,但在发起下一个读请求之前,发起了一次seek函数调用,会导致所有返回stream中的数据丢弃。
数据丢弃情况常见于单进程的多线程场景,因为所有线程共享一个缓冲区。
****************************************************************************************************************
每次读取一行:
#include <stdio.h>
char * fgets (char *str , int size , FILE *stream);
该函数从 stream 中读取 size-1 个字节的数据,并把结果保存到 str 中。
返回值:
成功——返回str
失败——返回NULL
****************************************************注:******************************************************
读完最后一个字节后,缓冲区会写入空字符(\0)。
当读到 EOF 或换行符时会结束读操作,并把 \n 写入到str中。
****************************************************************************************************************
读取二进制文件:
#include <stdio.h>
size_t fread (void *buf , size_t size , size_t nr , FILE *stream);
调用 fread() 会从 stream 中读取 nr 项数据,每项 size 个字节,并将数据保存到 buf 所指向的缓冲区。
文件指针向前移动读出数据的字节数。
返回值:
成功——返回读取到的数据项个数(不是字节数个数)
失败——读取失败或者文件结束,返回一个比 nr 小的数
****************************************************注:******************************************************
必须要用 ferror() 和 feof() 函数,才能确定失败还是文件结束。
由于变量大小、对齐、填充、字节序这些因素的不同,由一个应用程序写入的二进制文件,另一个应用程序可能无法读取,甚至同一个应用程序在另一台机械上也无法读取。
****************************************************************************************************************
例:
#include <stdio.h>
char buf[64];
size_t nr;
nr = fread (buf , sizeof(buf) , 1 , stream);
if(nr == 0)
  /* error */

向流中写数据:
写入单个字符:
#include <stdio.h>
int fputc (FILE *stream);
fputc() 函数将参数 c 所表示的字节(强制类型转换为 unsigned char)写到指针 stream 所指向的流。
返回值:
成功——返回c
失败——返回 EOF ,并设置相的 error 值
写入字符串:
#include <stdio.h>
char * fputs (char *str , int size , FILE *stream);
fputs()调用会把 str 指向的所有字符串写入到 stream 所指向的流中,不会写入结束标记符。
返回值:
成功——返回非负整数
失败——返回 EOF
写入二进制数据:
#include <stdio.h>
size_t fwrite (void *buf , size_t size , size_t nr , FILE *stream);
调用 fwrite() 函数会把 buf 指向的 nr 个数据项写入到 stream 所指向的流中,每个数据项长为 size 个字节。
文件指针向前移动写入的所有字节的长度。
返回值:
成功——返回写入的数据项·个数
失败——返回值小于nr

缓冲I/O示例程序:
#include <stdio.h>
int mian()
{
    FILE *in, *out ;
    struct pirate{
        char            name[50];
        unsigned long   booty;
        unsigned int    beard_len;
    }p , blackbeard = { "Edward-Teach" , 950 , 48};

    out = fopen("date" , "w");
    if(!out){
        perror("fopen");
        return -1;
    }

    if(!fwrite (&blackbeard , sizeof(blackbeard) , 1 , out)){
        perror("fwrite");
        return -1;
    }

    if(fclose(out)){
        perror("fclose");
        return -1;
    }
    in = fopen("data" , "r");
    if(!in){
        perror("fopen");
        return -1;
    }

    if(!fread (&p , sizeof(struct pirate) , 1 , in)){
        perror("fread");
        return -1;
    }
   
  if(fclose(in)){
        perror("fclose");
        return -1;
    }
    printf("name=\"%s\" booty=%lu beard_len=%u\n, p.name, p.booty, p.beard_len);
    return 0;
}
结果输出:name = name=Edward-Teach booty=950 beard_len=48
定位流:
通常控制当前流的位置是很有用的。
可能应用程序正在读取复杂的、基于记录的文件,而且需要跳跃式的读取,或者把流重新设置成指向初始位置。
在任何情况下,标准 I/O 都提供了一系列功能等价与系统调用 lseek() 的函数。
fseek() 函数是标准I/O最常用的定位函数,控制 stream 指向文件中由参数 offset 和 whence 确定的位置:
#include <stdio.h>
int fseek (FILE *stream , long offset , int whence);
参数:
whence:
SEEK_CUR:将文件位置设置成当前值再加上offset 个偏移值,offset 可以是负值、0、正值,如果offset 为0则返回当前文件位置。
SEEK_END:将文件位置设置成文件长度再加上offset 个偏移值,offset 可以是负值、0、正值,如果offset 为0则设置为文件末尾。
SEEK_SET:将文件位置设置成offset 值,offset 可以是负值、0、正值,如果offset 为0则设置为文件开始。
返回值:
成功——返回0,并清空文件结束标志符 EOF,取消(如果有的话)ungetc() 操作。
错误——返回-1,并设置errno值(EBADF:流非法    EINVAL:参数非法)
 
此外,标准I/O还提供了 fsetpos() 函数:
#include <stdio.h>
int fsetpos (FILE *stream , fpos_t *pos);
fsetpos() 函数会把 stream 的流指针指向 pos。
它的功能和把 fseek() 函数的 whence 设置为 SEEK_SET 时一致。
返回值:
成功——返回0
失败——返回-1,并相应的设置errno值
****************************************************注:******************************************************
该函数只是为了给其他通过复杂数据类型表示流位置的平台使用,非UNIX
Linux上编程一般不使用这个接口,除非希望能够在所有的平台上可移植
****************************************************************************************************************
标准I/O也提供了 rewind() 函数:
#include <stdio.h>
void rewind (FILE *stream);
该调用会把位置重新设置成流的初始位置。功能等价于:
fseek(stream , 0 , SEEK_SET);
唯一的区别在于 rewind 会清空错误标识。
****************************************************注:******************************************************
rewind() 函数没有返回值。
调用该函数如果希望获取错误,需要在调用之前清空 errno 值,并检查该变量在调用之后是否非零。
errno = 0;
rewind (stream);
if(errno)
  /* error */
****************************************************************************************************************
获取当前流位置:
和 lseek() 不同,fseek() 不会返回更新后的流位置。
另一个接口提供了该功能,ftell() 函数返回 stream 的当前位置:
#include <stdio.h>
long ftell(FILE *stream);
返回值:
成功——返回流位置
失败——返回-1,并设置相应的errno值
 
获取当前流位置另一个接口:
#include <stdio.h>
long fgetpos (FILE *stream , fpos_t *pos);
返回值:
成功——返回0,并把当前 stream 的流位置设置为 pos。
失败——返回-1,并设置相应的errno值
****************************************************注:******************************************************
该函数只是为了给其他通过复杂数据类型表示流位置的平台使用,非UNIX
Linux上编程一般不使用这个接口,除非希望能够在所有的平台上可移植
****************************************************************************************************************

 Flush(刷新输出) 流:
 标准 I/O 库提供了一个接口,可以将用户缓冲写入内核,并且保证写到流中的所有数据都通过 write() 函数 flush 输出。
fflush() 函数提供了这一功能:
#include <stdio.h>
int fflush (FILE *stream);
调用该函数时,stream 指流中所有未写入的数据都会被 flush 到内核中。(用户缓冲区写入到内核缓冲区,并不保证数据直接写入到物理介质上,要写入到物理介质,后续还应该调用 fsync() 函数)
如果 stream 是空的 (NULL) ,进程中所有打开的流都会被flush。
返回值:
成功——返回0
失败——返回EOF,并设置相应的errno值
错误和文件结束:
对于某些标准 I/O 接口,如 fread() 函数,把失败信息返回给调用方做的很不友善,因为他们没有提供机制可以区分错误和文件结束(EOF)。
对于这些调用,在某些场合需要检查流的状态,从而区分是出现错误还是到达文件结尾。
标准 I/O 针对上面问题给出了两个接口:
ferror():
#include <stdio.h>
int ferror (FILE *streamj);
该函数数用于判断给定的 stream 是否有错误标志
标志错误由其他标准 I/O 在相应某种情况时设置
返回值:
错误标志被设置——返回非0
错误标志没有设置——返回0
feof():
#include <stdio.h>
int feof (FILE *stream);
该函数用于判断给定的 stream 是否设置了文件结束标志
当到达文件结尾时,其他标准的 I/O 函数会设置 EOF 标志。
返回值:
设置了EOF标志——返回非0
没有设置EOF标志——返回0
clearer():
#include <stdio.h>
void clearer (FILE *stream);
该函数会清空指定的 stream 的错误标志和 EOF 标志
该函数没有返回值,且不会失败。
获取关联的文件描述符:
当不存在和流关联的标准 I/O 函数时,可以通过其他文件描述对该流执行系统调用。
为了获得和流关联的文件描述符,可以使用 fileno() 函数:
#include <stdio.h>
int fileno(FILE *stream)
返回值:
成功——返回和指定 stream 关联的文件描述符
失败——返回-1,这里值得一提的是,该函数只有指定流非法的时候才会失败,滨州、设置 errno 值为 EBADF
****************************************************注:******************************************************
通常,不建议混合使用标准 I/O 调用和系统调用,当使用 fileno() 函数时,必须谨慎,确保基于文件描述符的操作与用户缓冲没有冲突。
所以,在操作和流相关的文件描述符之前,最好先对流刷新。
****************************************************************************************************************
控制缓冲:
 标准 I/O 实现了三种类型的用户缓冲,并未开发者提供了接口,可以控制缓冲区的类型和大小。
无缓冲:
不执行用户缓冲。数据直接提交给内核。因为这种无缓冲模式不支持用户缓冲(而用户缓冲一般会带来很多好处),通常很少使用。
只有一个例外:标准错误,它默认的就是采用无缓冲模式。
行缓冲:
缓冲是以行为单位执行的。没遇到换行符,缓冲就会被提交到内核。
行缓冲把流输出到屏幕的时候很好用,因为输出到屏幕的消息也是通过换行符分隔的。
行缓冲是终端的默认缓冲模式,比如标准输出。
块缓冲:
缓冲是以块为单位执行,每个块是固定的字节数。
上面所讨论的缓冲模式即块缓冲,它很适用于处理文件。
默认情况下看,和文件相关的所有的流都是采用块缓冲模式。而,标准 I/O 称块缓冲为“完全缓冲”。
 
大部分情况下系统默认的缓冲模式对于特定场景就是最高效的。但是标准 I/O 还提供了一个接口,可以修改使用的缓冲模式:
#include <stdio.h>
int setvbuf (FILE *stream , char *buf , int mod , size_t size);
参数:
setbuf()函数把指定的 stream 的缓冲模式设置成 mode 模式,mode 值必须是以下之一:
_IONBF 无缓冲:此模式下会忽略参数 buf 和 size
_IOLBF 行缓冲
_IOFBF 块缓冲
buf是指向一个size字节大小的缓冲空间。如果为NULL ,glibc 会自动分配指定 size 的缓冲区。
返回值:
成功——返回0
失败——返回非0
****************************************************注:******************************************************
setvbuf() 函数必须在打开流后,并在执行任何的操作之前调用。
在关闭流时,其缓冲区必须存在。
不要再main() 函数中定义一个局部缓冲区,且没有显示关闭流。
****************************************************************************************************************

线程安全:
线程是指在一个进程中的执行单元。绝大多数的进程只有一个线程。但一个进程也可以持有多个线程,每个线程执行自己的代码逻辑,称之为多线程。
线程共享同一片地址空间,如果没有显示的协调,线程会在任意时刻,以任意方式运行。
在多处理器系统中,通、同一个进程的两个或是多个线程可能会并发执行。
在访问共享数据时,有两种方式可以避免修改它:
  1、采用数据同步访问机制(通过加锁实现)
  2、把数据存储在线程的局部变量中(称之为线程封闭)
 
标准 I/O 在本质上是线程安全的。
在函数的内部实现中,都关联了一把锁、一个锁计数器,以及持有该锁并打开一个流的线程。
每个线程在执行任何 I/O 请求之前,必须先获得锁且持有该锁。
两个或多个运行在同一个流上的线程不会交叉执行标准 I/O 操作。
因此,在单个函数调用中,标准 I/O 操作是原子操作。
 
在实际应用中,很多应用程序需要比独立函数调用级别更高的原子性。
例如:一个进程中有多个线程发起写请求,由于标准 I/O 函数是线程安全的,各个写请求不会交叉执行导致输出混乱
   也就是同一进程中多个线程同时发起写请求时,加锁可以保证执行完一个写请求之后再执行下一个。
   但是,如果进程连续发起很多写请求,希望不同线程和同一线程的写请求不会交叉,这就需要下面几个函数了。
 
手动文件加锁:
#include <stdio.h>
void flockfile (FILE *stream);
函数flockfile() 会等待指定的 stream 解锁,然后增加自己的锁计数,获得锁,该函数的执行线程成为流的持有者。
#include <stdio.h>
void funlockfile (FILE *stream);
如果锁的计数值为0,当前线程会放弃对该流的持有权,另一个线程可以获得该锁。
这些调用可以嵌套,也就是说单个线程可以执行多次 flockfile() 调用 ,直到该线程执行相同数量的 funlockfile() 调用后,该流才会被解锁。
#include <stdio.h>
int ftrylockfile (FILE *stream);
ftrylockfile() 函数是 flockfile() 函数的非阻塞版
如果指定 stream 流当前已经加锁,ftrylockfile() 函数不做任何处理,会立即返回一个非 0 值。
如果指定 stream 流当前没有加锁,ftrylockfile() 的线程会增加锁计数,获得锁,成为流的持有者,并返回0
对流的操作解锁:手动给流加锁有另外一个原因:只有应用者才能提供更精细的锁控制,这样才可以将加锁的1代价降到最小,并提高效率。
为此 Linux 提供了一系列函数,类似于常见的I/O接口,但是不执行任何锁操作,因为它们并不是加锁的标准 I/O
#define _GNU_SOURCE
#include <stdio.h>
int fgetc_unlocked (FILE *stream)
char *fgets_unlocked(char *str , int size , FILE *stream);
size_t fread_unlocked(void *buf , size_t size , size_t nr , FILE *stream);
int fputc_unlocked(int c , FILE *stream);
int fputs_unlocked(const char *str , FILE *stream);
size_t fwrite_unlocked(void *buf , size_t size , size_t nr , FILE *stream);
int fflush_unlocked (FILE *stream);
int feof_unlocked (FILE *stream);
int ferror_unlocked (FILE *stream);
int fileno_unlocked (FILE *stream);
int clearerr_unlocked (FILE *stream);
除了不检查或获取指定 stream 上的锁以外,这些函数和其对应的加锁函数功能完全相同。
如需加锁,程序员必须确保手工获得并释放锁。
 
 
 
 
 
posted @   Programming-novices  阅读(362)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示