国内最全IT社区平台 联系我们 | 收藏本站
华晨云阿里云优惠2
您当前位置:首页 > php开源 > php教程 > 《Linux系统编程》笔记 第三章(二)

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

来源:程序员人生   发布时间:2016-08-06 09:13:49 阅读次数:2365次

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()操作,毛病时返回⑴。

上述的函数中偏移量类型是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);

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

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

该调用将stream的读写位置设置为流初始,与fseek (stream, 0, SEEK_SET);功能1致。由于该函数没有返回值,因此需要验证是不是正确的话调用之前应当将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
我们1般见到的printf()调用是printf("%d", i);的情势,其实printf()系列函数的完全格式是:

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

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

标志 说明
将整数按千分位组字符
- 在字段内左对齐输出
+ 总是显示带符号转换的正负号
(空格) 如果第1个字符不是正负号,则在其前面加上1个空格
# 指定另外一种转换情势(例如,对106进制格式,加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 有符号10进制
o 无符号8进制
u 无符号10进制
x,X 无符号106进制
f, F 双精度浮点数
e, E 指数格式双精度浮点数
g, G 根据转换后的值解释为f、F、e或E
a, A 106进制指数格式双进度浮点数
c 字符
s 字符串
p 指向void的指针
n 到目前为止,此printf调用输出的字符的数目将被写入到指针所指向的带符号整型中
% 1个%字符
C 宽字符
S 宽字符串

下面是各个参数的效果:

#include <stdio.h> int main(void) { printf("%+0.1lf\n", 1.23456); //+1.2 printf("%+0.1lf\n", -1.23456); //⑴.2 printf("%+8.2lf\n", -1.23456); // ⑴.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致。当该标志代表无符号变量且输入的数据是负数时,将转换为2进制相同的正数,例如⑴转为4294967295。
标准库一样提供了变长参数的版本,不再赘述。

3.7 清洗1个流

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

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

该函数调用后,stream中的数据会被flush到内核缓冲区,此时与直接调用write()的效果是1样的。如果需要确保数据被提交给硬盘,需要使用fysnc()或相同功能的系统调用。1般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);

失败时返回⑴并设置errno。

3.10 控制缓冲

标准I/O库提供了3种缓冲类型,分别为:
不缓冲
不履行用户空间缓冲,数据直接提交给内核。这类情况下使用标准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。
还要注意缓冲区是局部变量时,1定要在局部变量失效前关闭流,毛病的使用例如

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

内存流

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

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

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

#include <stdio.h> #include <string.h> #include <iostream> using namespace std; int main(void) { //============ //追加模式下文件位置是第1个'\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; }

由于内存流依赖字符串结束符,因此以2进制的情势读写文件流其实不适合,由于2进制数据中’\0’出现的位置有多是1条数据的中间而不是结尾,使用内存流来读写2进制数据极可能会破坏数据。
类似的函数还有

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

内存流的作用

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

3.11 线程安全

多线程程序中线程同享进程的资源,因此需要对同享资源做线程同步操作,避免产生非预期的结构,标准I/O默许是线程安全的(即在多线程并发读写同1个文件时,读写操作在任意时刻只能运行1个,且上1个要求结束前不会被其他读写要求抢占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)//创建1个线程,使其与主线程同时向1个流输出内容。 { 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核心会同时向文件写入字符串。
上面的代码也暴露了1些问题,假设现在有1个多线程的服务器,要在日志中打印出来内部1个map中的数据,那末在实际情况中极可能打印出的内容被其他线程的日志输出穿插,读日志时带来1些困难,这就需要线程同步来进行,标准I/O库提供了针对流的加锁功能。

3.11.1 手动文件加锁

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

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

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

3.11.2 不加锁流操作

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

#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);

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

3.12 对标准I/O的批评

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

生活不易,码农辛苦
如果您觉得本网站对您的学习有所帮助,可以手机扫描二维码进行捐赠
程序员人生
------分隔线----------------------------
分享到:
------分隔线----------------------------
关闭
程序员人生