(一) 一块儿学 Unix 环境高级编程 (APUE) 之 标准IO

.html

.linux

.面试

.shell

.编程

目录vim

(一) 一块儿学 Unix 环境高级编程 (APUE) 之 标准IO数组

(二) 一块儿学 Unix 环境高级编程 (APUE) 之 文件 IO安全

(三) 一块儿学 Unix 环境高级编程 (APUE) 之 文件和目录网络

(四) 一块儿学 Unix 环境高级编程 (APUE) 之 系统数据文件和信息多线程

(五) 一块儿学 Unix 环境高级编程 (APUE) 之 进程环境

(六) 一块儿学 Unix 环境高级编程 (APUE) 之 进程控制

(七) 一块儿学 Unix 环境高级编程 (APUE) 之 进程关系 和 守护进程

(八) 一块儿学 Unix 环境高级编程 (APUE) 之 信号

(九) 一块儿学 Unix 环境高级编程 (APUE) 之 线程

(十) 一块儿学 Unix 环境高级编程 (APUE) 之 线程控制

(十一) 一块儿学 Unix 环境高级编程 (APUE) 之 高级 IO

(十二) 一块儿学 Unix 环境高级编程 (APUE) 之 进程间通讯(IPC)

(十三) [终篇] 一块儿学 Unix 环境高级编程 (APUE) 之 网络 IPC:套接字

 

 

最近在学习 APUE,因此顺便将每日所学记录下来,一方面为了巩固学习的知识,另外一方面也为一样在学习APUE的童鞋们提供一份参考。

本系列博文均根据学习《UNIX环境高级编程》一书总结而来,若有错误请多多指教。

APUE主要讨论了三部份内容:文件IO、并发、进程间通讯。

文件IO:

  标准IO:优势是可移植性高,缺点是性能比系统 IO 差,且功能没有系统 IO 丰富。

  系统IO:由于是内核直接提供的系统调用函数,因此性能比标准 IO 高,可是可移植性比标准 IO 差。

并发:

  信号 + 多进程;

  多线程;

进程间通讯:

  FIFO:管道;

  System V:又称为 XSI,支持如下三种方式:

    msg:消息队列;

    sem:信号量;

    shm:共享存储;

  Socket:套接字(网络通讯);

本系列博文就是围绕着这些内容进行学习和总结出来的,可是APUE一书讲述的主要是 Unix 环境,而 LZ 用的是 Linux 环境,因此本系列博文的全部内容都是基于 Linux 环境的,仅供各位童鞋参考。LZ 尽可能多讲原理,少讲函数的具体使用,在使用 LZ 提到的函数的时候,如与各位开发环境的 man 手册冲突,则以 man 手册为准。

你们在学习的过程当中必定要使用 Linux 系统,每学习到一个新的函数或知识点的时候要本身运行起来。

从 LZ 的经验来看,仅仅看 LZ 写的文字和你本身手动作实验得出的结果是不一样的。只看不练的话跟没看过同样,什么都是看着简单,写起来才知道哪里有坑。

若是你由于工做等关系实在没法在电脑上安装 Linux 系统,那么在虚拟机里安装一个 Linux 也是很必要的。

若是你真的想学好 APUE,那么最好使用真正的 Linux。APUE 就是让你在 Linux 下面写程序用的,只有使用它才能体会到你平时使用的各类程序和命令是如何实现的,只有是使用它你才知道你须要什么程序,你本身能够写什么程序。边使用边学习你才能体会到 Linux 的种种设计思想,才能融入到整个 *nix 你们庭来。

废话先写到这里。预祝你们学习成功,记住,坚持就是胜利。

 

==============================华丽的分割线==============================

 

好了,经过简单的介绍相信你们对 APUE 的结构已经有了一个大体的了解了,接下来就开始今天的正题:文件 IO。

前面提到了标准 IO(STDIO)和系统 IO(SYSIO),那么这里整理一下它们的差异。

类型 可移植性 实时性 吞吐量 功能
STDIO 受限
SYSIO 自由

这里我一个一个的解释表格中的每一项,表格中的每一项都是二者之间相对而言,使用哪一种 IO 并无绝对的好坏之分,要根据实际的需求来决定应该使用哪一个。

可移植性:

  标准 IO 是 C89 支持的函数,因此使用了标准 IO 的程序不管在 Linux 平台仍是换成了 Windows 平台,不用修改代码是能够直接编译运行的。

  而系统 IO 是由内核直接提供的函数库实现的,不一样的操做系统平台上提供的 IO 操做接口是不一样的,因此想要移植使用了系统 IO 的程序,必须按照目标平台的 IO 库修改程序并从新调试。

  因此你写的程序未来可能在不一样的平台上运行,那么最好使用标准 IO 库;若是你的程序是专门针对于某个平台而开发的,那么使用系统 IO 库可以获得咱们下面说的其它优点。

实时性和吞吐量:

  讲这两个概念以前我先给你们看一段代码:

 1 #include <stdio.h>
 2 #include <unistd.h>
 3 
 4 int main (void)
 5 {
 6         putchar('a');
 7         write(1, "b", 1);
 8 
 9         putchar('a');
10         write(1, "b", 1);
11 
12         putchar('a');
13         write(1, "b", 1);
14 
15         printf("\n");
16 
17         return 0;
18 }

 

1 >$ gcc -Wall 1.c 
2 >$ ./a.out 
3 bbbaaa

 

  输出的结果为何不是 ababab 呢,这就是由于标准 IO 具备合并系统调用的功能,putchar(3) 将本应该执行屡次的 write(2) 动做合并成了一步来完成,因此 aaa 是做为一个字符串打印的,这一点咱们能够经过 strace(1) 命令跟踪系统调用来得出结论(下方第7行)。另外因为 stdout 默认使用的是行缓冲模式(下面会讲缓冲),因此对 putchar(3) 的调用并无当即打印出来。

 1 >$ strace ./a.out
 2 # ... 此处省略 n 行不相关内容
 3 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f077a35f000
 4 write(1, "b", 1b)                        = 1
 5 write(1, "b", 1b)                        = 1
 6 write(1, "b", 1b)                        = 1
 7 write(1, "aaa\n", 4aaa
 8 )                    = 4
 9 exit_group(0) 
10 >$ 

 

  到这里咱们仍是没有说明白为何标准 IO 吞吐量高,而系统 IO 实时性高。我再举个简单的栗子:门卫老大爷负责送信到邮局,他去一次邮局要花费 10 分钟的时间,而每次最多能送 20 封信,每当信件累计到 20 封的时候他就要动身去邮局了。可是当他收到一封加急的邮件时,就会当即去一趟邮局。系统 IO 就比如每收到一封信时都要去一趟邮局,因此实时性高。而标准 IO 就比如要攒够 20 封信才去一趟邮局,因此吞吐量高,由于用户把信件交到老大爷的手上时就会当即返回,响应速度快,用户体验更好。而咱们使用 fflush(3) 之类的函数强制刷新缓冲的时候,就至关因而老大爷收到了一封加急信件须要当即去一趟邮局送信。

至于这里所说的标准 IO 功能受限,是由于标准 IO 在各个平台上都是使用系统 IO 封装的,为了使它具备通用性,又要考虑底层操做系统各自平台在实现上的差别,不免在功能上就要做出让步。

 

今天讲的全部的 IO 操做都是标准 IO,若是是方言我会单独标识出来。

首先要了解的一个概念是文件位置指针。

当咱们打开一个文件要对它进行读写的时候,咱们怎么能知道要从哪里开始读(写)文件呢?其实标准库准备了一个工具辅助咱们读写文件,它就是文件位置指针。当咱们使用标准库函数操做文件的时候,它会自动根据文件位置指针找到咱们要操做的位置,也会随着咱们的读写操做而自动修改指向,而不用咱们本身手动记录和修改文件的操做位置。它使用起来很是方便,以致于你彻底感受不到它的存在,可是为了更好的理解文件 IO,你必须知道它的做用。

1.fopen(3)

1 fopen - stream open functions
2 
3 #include <stdio.h>
4 
5 FILE *fopen(const char *path, const char *mode);

这是今天要学习的第一个函数,在操做文件以前,咱们须要经过 fopen() 函数将文件打开,经过这个函数咱们能够告诉操做系统咱们要操做的是哪一个文件,以及用什么样的方式操做这个文件。

参数列表:

path:要操做的文件路径。

mode:文件的打开方式,这个打开方式一共分为6种。

  r:以只读的方式打开文件,而且文件位置指针会被定位到文件首。若是要打开的文件不存在则报错。

  r+:以读写的方式打开文件,而且文件位置指针会被定位到文件首。若是要打开的文件不存在则报错。

  w:以只写的方式打开文件,若是文件不存在则建立,若是文件已存在则被截断为 0 字节,而且文件位置指针会被定位到文件首。

  w+:以读写的方式打开文件,若是文件不存在则建立,若是文件已存在则被截断为 0 字节,而且文件位置指针会被定位到文件首。

  a:以追加的方式打开文件,若是文件不存在则建立,且文件位置指针会被定位到文件最后一个有效字符的后面(EOF,end of the file)。

  a+:以读和追加的方式打开文件,若是文件不存在则建立,且读文件位置指针会被初始化到文件首,可是老是写入到最后一个有效字符的后面(EOF,end of the file)。

返回值:

  FILE 是一个由标准库定义的结构体,各位童鞋不要企图经过手动修改结构体里的内容来实现文件的操做,必定要经过标准库函数来操做文件。

  这个函数返回一个 FILE 类型的指针,它做为咱们打开文件的凭据,后面全部对这个文件的操做都须要使用这个指针,并且使用以后必定不要忘记调用 fclose(3) 函数释放资源。

  若是该函数返回了一个指向 NULL 的指针,则表示文件打开失败了,能够经过 errno 获取到具体失败的缘由。

 

error 是什么呢?它是标准 C 中定义的一个整形值,用来表示上次发生的错误。你们能够在头文件中看看 errno 都定义了哪些值:

>$ vim /usr/include/asm-generic/errno.h
>$ vim /usr/include/asm-generic/errno-base.h

 

一般系统调用会给咱们我返回一个整形值来表示是否出现了错误,当出现了错误的时候会设置 errno,经过 errno 咱们就能够得知出现了什么错误了。

固然,直接给咱们一个数字,咱们本身再从头文件中查找这个数字表示的意义,而后再打印出来给用户看,彷佛态麻烦了,没有什么简便的办法吗?

别担忧,其实标准库已经为咱们准备好专门的转换函数了:perror(3) 和 strerror(3)

perror(3) 会自动读取 errno 帮咱们转换成对应的文字描述,而且将它们输出到标准错误流中。它的参数是一个字符串,用来让咱们自定义一些错误消息给用户看,它的输出格式就是 咱们给传递的参数:errno 转换的描述文字。

strerror(3) 函数也会将 errno 转换为文字,不过它不会自动读取 errno 当前的值,须要咱们把 errno 传递给它。它也不会帮咱们输出到标准输出中,而是将转换完的字符串返回给咱们。

若是你们是开发一个前台应用,通常可使用 perror(3) 函数直接将错误输出给用户。

若是你们开发的是后台应用(如守护进程等),那么通常先使用 strerrno(3) 函数将 errno 转换为字符串,而后再把这个字符串传给日志系统记录下来。

你们在使用 errno 这个全局变量的时候要导入 errno.h 头文件:

#include <errno.h>

 

在使用 strerror(3) 函数时不要忘记导入 string.h 头文件,不然会报段错误!

#include <string.h>

 

其实如今的不少 *nix 系统中,errno 早已不是全局变量了,为了线程安全它已经变成了一个宏定义,这个咱们在后面的博文中介绍线程的时候会讨论它。

 

2.fclose(3)

1 fclose - close a stream
2 
3 #include <stdio.h>
4 
5 int fclose(FILE *fp);

这个函数是与 fopen(3) 函数对应的,当咱们使用完一个文件以后,须要调用 fclose(3) 函数释放相关的资源,不然会形成内存泄漏。当一个 FILE 指针被 fclose(3) 函数成功释放后,这个指针所指向的内容将不能再次被使用,若是须要再次打开文件还须要调用 fopen(3) 函数。

参数列表:

  fp:fopen(3) 函数的返回值做为参数传入便可。

 

3.fgets(3)

1 fgets - input of strings
2 
3 #include <stdio.h>
4 
5 int fgetc(FILE *stream);
6 
7 char *fgets(char *s, int size, FILE *stream);

从输入流 stream 中读取一个字符串回填到 s 所指向的空间。

这里出现了一个 stream 的概念,这个 stream 是什么呢,它被成为“流”,其实就是操做系统对于能够像文件同样操做的东西的一种抽象。它并不是像天然界的小河流水同样潺潺细流,而一般是要么没有数据,要么一会儿来一坨数据。固然 stream 也未必必定就是文件,好比系统为每一个进程默认打开的三个 stream:stdin、stdout、stderr,它们自己就不是文件,就是与文件有着相同的操做方式,因此一样被抽象成了“流”。

这个函数并无解决 gets(3) 函数可能会致使的数组越界问题,而是经过牺牲了获取数据的正确性来保证程序不会出现数组越界的错误,其实是掩盖了 gets(3) 的问题。

该函数遇到以下四种状况会返回:

  1.当读入的数据量达到 size - 1 时;

  2.当读取的字符遇到 \n 时;

  3.当读取的字符遇到 EOF 时;

  4.当读取遇到错误时;

而且它会在读取到的数据的最后面添加一个 \0 到 s 中。

返回值:

  成功时返回 s。

  返回 NULL 时表示出现了错误或者读到了 strem 的末尾(EOF)。

 

4.fread(3)、fwrite(3)

1 fread, fwrite - binary stream input/output
2 
3 #include <stdio.h>
4 
5 size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
6 
7 size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

 

这两个函数使用得最频繁,用来读写 stream,一般是用来读写文件。

参数列表:

  ptr:fread(3) 将从 stream 中读取出来的数据回填到 ptr 所指向的位置;fwrite(3) 则将从 ptr 所只想的位置读取数据写入到 stream 中;

  size:要读取的每一个对象所占用的字节数;

  nmemb:要读取出多少个对象;

  stream:数据来源或去向;

返回值:

  注意这两个函数的返回值表示的是成功读(写)的对象的个数,而不是字节数!

例如:

  read(buf, 1, 10, fp); // 读取 10 个对象,每一个对象 1 个字节

  read(buf, 10, 1, fp); // 读取 1 个对象,每一个对象 10 个字节

当数据量充足的时候,这两种方式是没有区别的。

可是!!当数据量少于 size 个字节的整倍数时,第二种方法的的最后一个对象会读取失败。好比数据只有 45 个字节,那么第二种方法的返回值为 4,由于它只能成功读取 4 个对象。

因此一般第一种方式读写数据使用得比较广泛。

 

5.atoi(3)

1 atoi, atol, atoll, atoq - convert a string to an integer
2 
3 #include <stdlib.h>
4 
5 int atoi(const char *nptr);
6 long atol(const char *nptr);
7 long long atoll(const char *nptr);
8 long long atoq(const char *nptr);

 

atoi(3) 函数族在这里提一下,主要是为了下面的 printf(3) 函数族作一个铺垫。

这些函数的做用是方便的将一个字符串形式的数字转换为对应的数字类型的数字。

上面这句话可能有点坳口,给你看个例子就懂了,下面是伪代码。

1 char *str = "123abc456";
2 int i = 0;
3 i = atoi(str);

 

i 的结果会变成 123。这些函数会转换一个字符串中地一个非有效数字前面的数字。若是很不幸这个字符串中的第一个字符就不是一个有效数字时,那么它们会返回 0。

 

6.printf(3)

1 printf,   fprintf,  sprintf,  snprintf - formatted output conversion
2 
3 #include <stdio.h>
4 
5 int printf(const char *format, ...);
6 int fprintf(FILE *stream, const char *format, ...);
7 int sprintf(char *str, const char *format, ...);
8 int snprintf(char *str, size_t size, const char *format, ...);

 

printf(3) 函数你们必定不会陌生了,应该从写 Hello World! 的时候就接触到了的吧,因此我也很少介绍了,主要介绍两个内容。

一个是面试常考的一个问题,用了这么久的 printf(3) 函数,你们有没有注意过它的返回值表示什么呢?

printf(3) 的返回值表示成功打印的有效字符数量,不包括 \0。

另外一个要说的就是刚才咱们提到了 atoi(3) 函数族,它们负责将字符串转换为数字,那么有没有什么函数能够将数字转换为字符串呢,其实经过 sprintf(3) 或 snprintf(3) 就能够了。

有了这两个函数,不只能够方便的将数字转换为字符串,还能够将多个字符串任意拼接为一个完整的字符串。

这里直接讲解一下 snprintf(3) 函数。

参数列表:

  str:拼接以后的结果会回填到这个指针所指向的位置;

  size:size - 1 为回填到 str 中的最大长度,数据超过这个长度的部分则会被舍弃,而后会在拼接的字符串的尾部追加 \0;

  format:格式化字符串,用法与 printf(3) 相同,这里再也不赘述;

  ...:格式化字符串的参数,用法与 printf(3) 相同;

这个函数与 fputs(3) 同样,只是掩盖了 sprintf(3) 可能会致使的数组越界问题,经过牺牲数据的正确性来保证程序不会出现数组越界的错误。

 

7.scanf(3)

1 scanf,  fscanf, sscanf - input format conversion
2 
3 #include <stdio.h>
4 
5 int scanf(const char *format, ...);
6 int fscanf(FILE *stream, const char *format, ...);
7 int sscanf(const char *str, const char *format, ...);

 

scanf(3) 函数族相信也不用过多的介绍了,这里惟一要强调的就是:scanf(3) 函数支持多种格式化参数,惟独 %s 是不能安全使用的,可能会致使数组越界,因此当须要接收用户输入的时候可使用 fgets(3) 等函数来替代。

 

8.fseek(3)

1 fgetpos, fseek, fsetpos, ftell, rewind - reposition a stream
2 
3 #include <stdio.h>
4 
5 int fseek(FILE *stream, long offset, int whence);
6 
7 long ftell(FILE *stream);
8 
9 void rewind(FILE *stream);

 

fseek(3) 函数族的函数用来控制和获取文件位置指针所在的位置,从而可以使咱们灵活的读写文件。

介绍一下 fseek(3) 函数的参数列表:

  stream:这个已经不须要多介绍了吧,就是准备修改文件位置指针的文件流;

  offset:基于 whence 参数的偏移量;

  whence:相对于文件的哪里;有三个宏定义能够做为它的参数:SEEK_SET(文件首), SEEK_CUR(当前位置), or SEEK_END(文件尾);

返回值:

  成功返回 0;失败返回 -1,而且会设置 errno。

单独看参数列表也许你还有所疑惑,那么我写点简单的伪代码做为例子:

1 fseek(fp, -10, SEEK_CUR); // 从当前位置向前偏移10个字节。
2 fseek(fp, 2GB, SEEK_SET); // 能够制造一个空洞文件,如迅雷刚开始下载时产生的文件。

ftell(3) 函数以字节为单位得到文件指针的位置。

fseek(fp, 0, SEEK_END) + ftell(3) 能够计算出文件总字节大小。

还有一个值得你们注意的问题:

fseek(3) 和 ftell(3) 的参数和返回值使用了 long,因此取值范围为 -2GB ~ (2GB-1),而 ftell(3) 只能表示 2G-1 以内的文件大小,因此可使用 fseeko(3) 和 ftello(3) 函数替代它们,但它们只是方言(SUSv2, POSIX.1-2001.)。

因为这两个函数比较古老,因此设计的时候认为 +-2GB 的取值范围已经足够用了,而没有意识到科技发展如此迅速的今天,2GB 大小的文件已经彻底不能知足实际的需求了。

rewind(3) 函数将文件位置指针移动到文件起始位置,至关于:

1 (void) fseek(stream, 0L, SEEK_SET)

 

 

9.getline(3)

 1 getline - delimited string input
 2 
 3 #include <stdio.h>
 4 
 5 ssize_t getline(char **lineptr, size_t *n, FILE *stream);
 6 
 7 Feature Test Macro Requirements for glibc (see feature_test_macros(7)):
 8 
 9 getline():
10     Since glibc 2.10:
11         _POSIX_C_SOURCE >= 200809L || _XOPEN_SOURCE >= 700
12     Before glibc 2.10:
13         _GNU_SOURCE

 

这个函数是一个很是好用的函数,它能帮助咱们一次获取一行数据,而不管这个数据有多长。

参数列表:
  lineptr:一个一级指针的地址,它会将读取到的数据填写到一级指针指向的位置,并将>该位置回填到该参数中。指针初始必须置为NULL,该函数会根据指针是否为 NULL 来决定是否须要分配新的内存。
  n:是由该函数回填的申请的内存缓冲区的总大小,长度初始必须置为0。
虽然很好用,可是各位童鞋别高兴得太早了,该函数仅支持 GNU 标准,因此是方言,你们仍是本身封装一个备用吧。

另外,想要使用这个函数必须在编译的时候指定 -D_GNU_SOURCE 参数:

1 $> gcc -D_GNU_SOURCE

 

固然若是不想在编译的时候添加参数,也能够在引用头文件以前 #define _GNU_SOURCE,只是比较丑陋而已。

还有一个办法,是在 makefile 中配置 CFLAGS += -D_GNU_SOURCE,这样即省去了编译时手动写参数的麻烦,也避免了代码中的丑陋。

10.fflush(3)

1 fflush - flush a stream
2 
3 #include <stdio.h>
4 
5 int fflush(FILE *stream);

fflush(3) 函数的做用是刷新缓冲区,提到这个函数就要讲讲缓冲区了。

 缓冲区的做用是为了合并系统调用,在上面讲 STDIO 与 SYSIO 的区别时你们已经看到什么是合并系统调用了。

Linux 系统中有三种缓冲形式:无缓冲、行缓冲和全缓冲。

无缓冲:须要马上输出时使用,例如 stderr;

行缓冲:遇到换行符时进行刷新、缓冲区满了的时候刷新、强制刷新(fflush(3));而标准输出(stdout)是行缓冲,由于涉及到终端设备;

全缓冲:只有缓冲区满了的时候和强制刷新(fflush(3))时才会刷新,这是 Linux 默认的缓冲模式,但终端设备除外,终端设备使用行缓冲模式;

当数据被放入缓冲区的时候是不会经过系统调用(read(3)、write(3))送到内核中的,只有缓冲区被刷新的时候数据才会经过系统调用进入内核。而刷新缓冲区就是 fflush(3) 函数的做用。

fflush(3) 的参数是具体要刷新的流,当参数为 NULL 时会刷新全部的输出流。

 

好了,时间不早了,今天先写到这里。

 

== 补充 == 2016.02.20 ==============================

有园友提问:函数括号里的数字表示的是什么意思?

其实这个数字表示的是该函数在 man 手册的第几章里能够查询到。

man 手册一共有 8 章,每一章保存不同的内容,这八章的内容分别是:

第一章:shell 命令。如:ls、vim,查询方法:>$ man ls

第二章:系统调用。如:open、close,查询方法:>$ man 2 open 或 >$ man close。由于第一章也有 open,因此 man 的参数中要加章节号;由于第一章中没有 close,因此查询 close 不须要加章节号。

第三章:库函数。如:printf、fopen,查询方法:>$ man 3 printf 或 >$ man fopen

第四章:/dev 下的文件。如:zero。

第五章:一些配置文件的格式。如:/etc/shadow,查询方法:>$ man shadow

第六章:预留给游戏的,由游戏本身定义。如:sol。

第七章:附件和变量。如 iso-8859-1。

第八章:只能由 root 执行的系统管理命令。如 mkfs。

由于有些函数的名字会在不一样的章节出现,这种函数在查询 man 手册的时候若是不加上章节号默认会查询低编号的章节。

好比 printf,若是你输入的查询命令是 >$ man printf,那么打开的并非 printf 标准库函数的手册,而是 printf shell 命令的手册,想要查询 printf 函数的手册就须要在 man 命令后面加上该函数所在的章节数,正确的命令是 >$ man 3 printf,这样就会打开 man 手册第三章的 printf 函数。

另外,若是想查看每一个章节中都有哪些内容,可使用 >$ man -aw 命令获得 man 手册的安装路径,而后找到对应的 man 章节的目录,再用 zcat 命令查看那些 gz 文件。

以上补充内容参考《linux man 手册各个章节的意义和用法》。

相关文章
相关标签/搜索