《Linux系统编程》笔记 第三章(二)

系列文章目录:http://blog.csdn.net/wylblq/article/details/51841684

3.6 定位流

标准库提供了与系统调用lseek()类似的函数来定位流中的读写位置。

#include <stdio.h>
int fseek (FILE *stream, long offset, int whence);
long ftell(FILE *stream);

与lseek()用法类似,whence提供了如下选择:
SEEK_CUR-将流的读写位置设置为当前位置加上pos个字节,pos可以是正数或负数。
SEEK_END-将流的读写位置设置为文件尾加上pos个字节,pos可以是正数或负数。
SEEK_SET-将流的读写位置设置为pos对应的位置,pos为0时代表设置为文件起始位置。
函数调用成功后返回0并取消之前的ungetc()操作,错误时返回-1。

上述的函数中偏移量类型是long,若处理的文件大小超过long变量的范围时,可以使用

#include <stdio.h>
int fseeko(FILE *stream, off_t offset, int whence);
off_t ftello(FILE *stream);

off_t在64位系统上被实现为64位大小,在32位系统上与long大小相同。

类似功能的函数有

#include <stdio.h>
int fsetpos (FILE *stream, fpos_t *pos);
int fgetpos(FILE *stream, fpos_t *pos);

除非为了源码兼容性,一般不使用这个函数。

#include <stdio.h>
void rewind (FILE *stream);

该调用将stream的读写位置设置为流初始,与fseek (stream, 0, SEEK_SET);功能一致。由于该函数没有返回值,因此需要验证是否正确的话调用之前应该将errno置0,调用知乎检查errno是否为0。

格式化I/O

格式化I/O是指将内容按照规定的格式整理后读取或输出。
格式化输出主要通过printf()系列函数:

#include <stdio.h>
int printf(const char *format, ...);//格式化到标准输出
int fprintf(FILE *stream, const char *format, ...);//格式化到流
int sprintf(char *str, const char *format, ...);//格式化到str中
int snprintf(char *str, size_t size, const char *format, ...);//与sprintf类似,更安全,其提供了可写缓冲区的长度
int dprintf(int fd, const char *format, ...);//格式化到文件描述符fd对应的文件中

上述函数的返回值均是真正格式化的长度,不包括字符串结束符\0
我们一般见到的printf()调用是printf("%d", i);的形式,其实printf()系列函数的完整格式是:

% [flags] [fldwidth] [precision] [lenmodifier] convtype

%-是格式化字符串的起始,必须要有
flags-是控制格式化样式的标志,有如下取值:

标志 说明
将整数按千分位组字符
- 在字段内左对齐输出
+ 总是显示带符号转换的正负号
(空格) 如果第一个字符不是正负号,则在其前面加上一个空格
# 指定另一种转换形式(例如,对于十六进制格式,加0x前缀)
0 添加前导0(默认是空格)进行填充

fldwidth-控制被格式化内容的宽度,若宽度不够则用空格或0补齐。可以指定非负数也可以指定*来默认处理
precision-数字的位数或字符串的字节数,以.开头,后面跟非负数或*
lenmodifier-用来修饰被格式化变量的长度:

取值 说明
hh 将相应的参数按signed或unsigned char类型输出
h 将相应的参数按signed或unsigned short类型输出
l 将相应的参数按signed或unsigned long或宽字符类型输出
ll 将相应的参数按signed或unsigned long long类型输出
j intmax_t或uintmax_t
z size_t
t ptrdiff_t
L long double

convtype-被格式化的变量类型:

取值 说明
d、i 有符号十进制
o 无符号八进制
u 无符号十进制
x,X 无符号十六进制
f, F 双精度浮点数
e, E 指数格式双精度浮点数
g, G 根据转换后的值解释为f、F、e或E
a, A 十六进制指数格式双进度浮点数
c 字符
s 字符串
p 指向void的指针
n 到目前为止,此printf调用输出的字符的数目将被写入到指针所指向的带符号整型中
% 一个%字符
C 宽字符
S 宽字符串

下面是各个参数的效果:

#include <stdio.h>
int main(void)
{
   printf("%+0.1lf\n", 1.23456);    //+1.2
   printf("%+0.1lf\n", -1.23456);   //-1.2
   printf("%+8.2lf\n", -1.23456);   // -1.23
   printf("%8.6d\n", 123);          // 000123
   return 0;
}

标准库还提供了使用可变长参数的版本,功能与对应版本类似:

#include <stdio.h>
#include <stdarg.h>
int vprintf(const char *format, va_list ap);
int vfprintf(FILE *stream, const char *format, va_list ap);
int vsprintf(char *str, const char *format, va_list ap);
int vsnprintf(char *str, size_t size, const char *format, va_list ap);
int vdprintf(int fd, const char *format, va_list ap);

格式化输入用于分析字符串并转换成对应类型变量保存起来,主要通过scanf()系列函数:

#include <stdio.h>
int scanf(const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);
int sscanf(const char *str, const char *format, ...);

完整的参数为:

%[*] [fldwidth] [m] [lenmodifier] convtype

fldwidth-最大字符宽度
m-当要输入的是字符串时,该参数指定提供的缓冲区的大小
lenmodifier-转换后要赋值的参数大小
convtype-要转化的参数类型,与printf()系列函数级别一致。当该标志代表无符号变量且输入的数据是负数时,将转换为二进制相同的正数,例如-1转为4294967295。
标准库同样提供了变长参数的版本,不再赘述。

3.7 清洗一个流

在向一个流写入数据后数据并没有真正交给内核,而是在用户空间的缓冲区内保存,等待数据累积到合适大小后再请求内核。标准库提供了立即将缓冲区数据提交内核的函数。

#include <stdio.h>
int fflush (FILE *stream);

该函数调用后,stream中的数据会被flush到内核缓冲区,此时与直接调用write()的效果是一样的。如果需要确保数据被提交给硬盘,需要使用fysnc()或相同功能的系统调用。一般fflush()后都要调用fsync()来确保数据从用户缓冲区到内核缓冲区再到硬盘。

3.8 错误和文件结束

fread()函数的返回值不能区分发生错误还是遇到了EOF,标准库提供了错误检查函数:

#include <stdio.h>
int ferror (FILE *stream);

用于检测stream上是否有错误标志。错误标志由标准I/O相关函数设置,如果存在错误标志,该函数返回非0值,否则返回0。

#include <stdio.h>
int feof (FILE *stream);

用来检测stream是否到了文件结尾。若到文件结尾,返回非0,否则返回0。

#include <stdio.h>
void clearerr (FILE *stream);

用于清理stream的errno和EOF标志。

3.9 获得关联的文件描述符

与fdopen()相对,fileno()用于获取与流关联的文件描述符。但是不建议读写文件时将文件描述符和流混用。

#include <stdio.h>
int fileno (FILE *stream);

失败时返回-1并设置errno。

3.10 控制缓冲

标准I/O库提供了三种缓冲类型,分别为:
不缓冲
不执行用户空间缓冲,数据直接提交给内核。这种情况下使用标准I/O没有什么优势。标准错误就是这种缓冲模式。
行缓冲
遇到换行符时将缓冲区提交到内核。标准输出是这种缓冲模式,也叫全缓冲。
块缓冲
默认的缓冲模式,缓冲效果最好。

#include <stdio.h>
int setvbuf (FILE *stream, char *buf, int mode, size_t size);

控制缓冲类型,mode可能是:
_IONBF-不缓冲
_IOLBF-行缓冲
_IOFBF-块缓冲

在_IONBF模式下,buf和size参数被忽略。其他模式下标准I/O会使用buf作为缓冲区,其大小是size。当buf是NULL时,缓冲区被自动分配。默认的缓冲区大小为BUFSIZ宏定义的,是块大小的整数倍。setbuf()必须在打开流后,做任何其他操作之前被调用,失败时返回非0并设置errno。
还要注意缓冲区是局部变量时,一定要在局部变量失效前关闭流,错误的使用例如

#include <stdio.h>
int main(void)
{
   char buf[BUFSIZ];
   setbuf(stdin, buf);
   printf("Hello, world!\n");
   return 0;
}

内存流

标准I/O库提供了fmemopen()函数来打开位于内存的流,而不与底层文件相关联,其用用户指定的缓冲区单做文件读写的位置,返回一个FILE*。

#include <stdio.h>
FILE *fmemopen(void *buf, size_t size, const char *mode);

buf-缓冲区的起始地址,若该参数是NULL,库函数会帮助分配一个size大小的缓冲区,在关闭流的时候被释放。
size-缓冲区大小。
mode-读写模式,与fopen()参数类似。
注意事项:
1 当以追加方式打开内存流时,当前的文件读写位置是缓冲区中的第一个字符串结束符位置(‘\0’)。缓冲区中无字符串结束符时,文件位置是缓冲区结尾的后一个字节。
2 当内存流不是以追加方式打开时,当前文件位置是缓冲区开始的位置
3 buf是NULL,以只读或只写方式打开内存流没有意义。因为我们没办法知道分配的缓冲区的地址,因此只能读取我们无法写入的数据或者写入我们无法读取的数据
4 增加内存流中数据或者调用fclose()、fflush()、fseek()、fseeko()和fsetpos()时都会在当前位置增加一个字符串结束符
下面代码测试上述内容:

#include <stdio.h>
#include <string.h>
#include <iostream>
using namespace std;
int main(void)
{
    //============
    //追加模式下文件位置是第一个'\0'处,非追加模式下是缓冲区开始位置
    FILE* fp =NULL;
    char buffer[256] = "this is a buffer.";
    fp = fmemopen(buffer, 256, "r+");
    cout << ftell(fp) << endl;//0
    fclose(fp);

    fp = fmemopen(buffer, 256, "a+");
    cout << ftell(fp) << " " << strlen(buffer) << endl;//17 17
    fclose(fp);
    //============

    //============缓冲区内容增加,会自动写入'\0'
    fp = fmemopen(buffer, 256, "w+");
    fputc('a', fp);
    fflush(fp);
    cout << buffer << endl;//a
    cout << &buffer[2] << endl;//is a buffer. 因为'th'变成了a'\0'
    fclose(fp);
    //============
    return 0;
}

由于内存流依赖字符串结束符,因此以二进制的形式读写文件流并不合适,因为二进制数据中’\0’出现的位置有可能是一条数据的中间而不是结尾,使用内存流来读写二进制数据很可能会破坏数据。
类似的函数还有

#include <stdio.h>
FILE *open_memstream(char **ptr, size_t *sizeloc);//对char类型的字符串做操作
#include <wchar.h>
FILE *open_wmemstream(wchar_t **ptr, size_t *sizeloc);//对宽字节的字符串做操作

与fmemopen()区别在于:
* 创建的流无法指定读写模式,只能写打开
* 不能自行指定缓冲区,函数返回时指针指向标准库分配的缓冲区。由于缓冲区可能会被重新分配(例如一开始分配的缓冲区不够扩展了),因此*ptr指向的地址可能会变
* 关闭流后需要自行释放缓冲区
* 缓冲区会随着流数据的增多而变大,每次fflush()或fclose()后sizeloc指向的值可能会被改变

内存流的作用

最直观的作用是其提供了一个处于内存中的文件指针,使我们可以像读写文件一样操作一块内存,方便的使用标准I/O提供的函数调用而没有真正读写文件的性能损失。在一些特殊的第三方API中,可能需要一个FILE*类型的参数,但是此时都在内存中,这时将内存写入文件再传到第三方API中显然是不划算的,因此可以将对应的内存映射为打开的文件。另外对于open_memstream()相关的函数来说,其内部管理了缓冲区,使我们不需要担心缓冲区溢出的问题,此时可以方便的格式化或者拼接字符串,例如格式化一段sql语句等。

3.11 线程安全

多线程程序中线程共享进程的资源,因此需要对共享资源做线程同步操作,避免产生非预期的结构,标准I/O默认是线程安全的(即在多线程并发读写同一个文件时,读写操作在任意时刻只能运行一个,且上一个请求结束前不会被其他读写请求抢占CPU)。
下面的代码验证线程安全:

//编译时要-lpthread,链接pthread库
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
void *thrd_func(void *arg);
FILE *stream;
int main()
{
    pthread_t tid;
    int *thread_ret = NULL;
    stream = fopen("1.txt", "a");
    if (!stream)
    {
        perror("fopen");
        return -1;
    }
    if (pthread_create(&tid,NULL,thrd_func,NULL)!=0)//创建一个线程,使其与主线程同时向一个流输出内容。
    {
        printf("Create thread error!\n");
        exit(1);
    }

    for(int i = 0; i<10000; ++i)
    {
       if (fputs("This is a test line,it should not be broken1\r\n", stream) == EOF)
        {
            printf("err!");
        }
    }
    pthread_join(tid, (void**)&thread_ret );
    return 0;
}

void *thrd_func(void *arg)
{
    for(int i = 0; i<10000; ++i)
    {
       if (fputs("This is a test line,it should not be broken2\r\n", stream) == EOF)
        {
            printf("err!");
        }
    }
    pthread_exit(NULL);
}

在多核的主机上运行上述代码,可以看到”This is a test line,it should not be broken1”和”This is a test line,it should not be broken2”交替出现,但是每行都是完整的。交替出现的原因是不同的CPU核心会同时向文件写入字符串。
上面的代码也暴露了一些问题,假如现在有一个多线程的服务器,要在日志中打印出来内部一个map中的数据,那么在实际情况中很可能打印出的内容被其他线程的日志输出穿插,读日志时带来一些困难,这就需要线程同步来进行,标准I/O库提供了针对流的加锁功能。

3.11.1 手动文件加锁

用flockfile()给对应流加锁,用funlockfile()解锁。

#include <stdio.h>
void flockfile (FILE *stream);
void funlockfile (FILE *stream);

标准I/O库中的锁是递归锁(可重入锁),即一个线程可以多次获得该锁而不被锁死或断言错误。该锁使用计数,当flockfile()时,计数器+1;funlockfile()时,计数器-1,因此调用funlockfile()的次数一定要与flockfile()次数一致,尤其是错误处理提前返回时更要小心。当计数器为0时,代表线程不再保持锁,此时其他线程对同一个流加锁的话能够无阻塞的获得锁。
当第一次加锁成功时flockfile()返回0,当前线程获得锁;已经获得锁时本线程再次调用flockfile(),返回非0。
在输出map元素之前加锁,输出完成后释放锁,这样就可以保证map的数据在一起而不被其他信息穿插了。

3.11.2 不加锁流操作

既然开发人员选择手动控制锁的范围,那么就没必要在读写文件时再次加锁了。标准库提供了一系列不加锁的库函数。

#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);
void clearerr_unlocked (FILE *stream);

这些函数除了不再加锁外,行为与加锁版本一致。
感兴趣的同学可以做一下小练习,将之前校验标准I/O线程安全的代码改用非加锁的调用,试试看输出文件有什么变化。
另外还可以用系统调用write()来测试一下write()是否是线程安全的(事实上系统调用基本都是原子操作,即线程安全的,但是write()比较特殊,其内部是两个调用:定位和写入。不使用O_APPEND模式的话,可能会因为偏移量没有增加而导致写入内容被覆盖,这里有资料,因此在多线程读写时,如果不打算自己做线程同步的话,使用系统调用write()时一定要加上O_APPEND标志)

3.12 对标准I/O的批评

首先需要明确的是标准I/O提供了非常方便的用户空间缓冲机制,使开发人员无需关注系统的块大小而提高文件I/O效率;其次库函数提供了便利的操作,能够按行读写文本;另外由于是标准库,其代码可移植性非常高,使用也广泛。
但标准I/O库也有一些缺点,其中一个就是双副本问题。双副本问题是指,标准I/O库内部维护了一个缓冲区,从内核读取到的数据拷贝到该缓冲区中维护,在用户需要数据时,要再次拷贝到用户指定的地址中:一段数据在用户空间中有两个副本,同时也有两次拷贝操作,写入时也是类似情况。
对于读取操作,一个改善方式是返回一个指向标准I/O缓冲区的指针,用户程序只有在修改读取内容或在缓冲区被清空之前拷贝数据即可。另外setvbuf()函数设置的用户缓冲区是不是也能减少一次拷贝?
对于写操作,可以使用[分散输入和集中输出]的I/O模式,见后面章节内容。
另一些函数库也提供了相关解决方案,例如快速I/O库(fio、sfio)、映射文件(mmap函数)。

你可能感兴趣的:(linux,笔记)