(一)一起学 APUE 之 文件 IO

.

.

.

.

.

最近在学习 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 手册为准。

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

好了,通过简单的介绍相信大家对 APUE 的结构已经有了一个大致的了解了,接下来就开始今天的正题:文件 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 获取到具体失败的原因。

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,这样即省去了编译时手动写参数的麻烦,也避免了代码中的丑陋。

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

时间: 2024-10-20 13:53:34

(一)一起学 APUE 之 文件 IO的相关文章

(二) 一起学 APUE 之 文件 IO

. . . . . 昨天我们讨论了标准 IO,今天主要说说系统 IO. 1.文件描述符 在 SYSIO 中贯穿了一个整形数,它就是文件描述符.对内核而言,所有打开的文件都是通过文件描述符引用的.它的作用与 STDIO 中的 FILE 结构体类似,但是它们的工作原理是完全不同的.它实际上是一个由内核保存的数组下标,所以不会是负数,下面我会用一张图来表示它的作用. 图1 SYSIO 文件描述符 图是在 Ubuntu 下好不容易找到了一个画图软件画的,质量不怎么样,小伙伴们先凑合着看吧. 我解释下图上

(三) 一起学 APUE 之 文件和目录

. . . . . 前面两篇博文讲了文件 IO 的基本操作,但是它们操作的都是文件本身所存储的有效数据.而文件系统保存文件的时候不仅仅要存储文件内的数据,还要存储许多亚数据,即文件属性和其它特征数据.这篇博文 LZ 就带领大家讨论文件系统亚数据的操作. 1.stat(2) 1 stat, fstat, lstat - get file status 2 3 #include <sys/types.h> 4 #include <sys/stat.h> 5 #include <u

(四) 一起学 APUE 之 系统数据文件和信息

. . . . . 目录 (一) 一起学 APUE 之 标准 IO (二) 一起学 APUE 之 文件 IO (三) 一起学 APUE 之 文件和目录 (四) 一起学 APUE 之 系统数据文件和信息 1.getpwnam(3).getpwuid(3) 1 getpwnam, getpwuid - get password file entry 2 3 #include <sys/types.h> 4 #include <pwd.h> 5 6 struct passwd *getp

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

. . . . . 目录 (一) 一起学 Unix 环境高级编程 (APUE) 之 标准IO (二) 一起学 Unix 环境高级编程 (APUE) 之 文件 IO (三) 一起学 Unix 环境高级编程 (APUE) 之 文件和目录 (四) 一起学 Unix 环境高级编程 (APUE) 之 系统数据文件和信息 (五) 一起学 Unix 环境高级编程 (APUE) 之 进程环境 (六) 一起学 Unix 环境高级编程 (APUE) 之 进程控制 (七) 一起学 Unix 环境高级编程 (APUE)

关于unix系统接口 普通文件io的小结

首先是常用的几个函数  open , read,write,lseek, close open函数  函数原型  int open(char *  path,int  oflag,...)           返回值是一个文件描述符   path顾名思义就是文件名  oflage文件是打开方式   第三个形参应用于创建文件时使用  /*创建文件其实还有一个create函数使用  以及openat由于还未使用过这个函数和open的差异 所以不在此处累赘*/          open函数  使用i

1.C与c++文件IO

一. C与c++文件IO 1.1文件类型:ASCII文件和二进制文件 首先我不保证文件类型只有这两种.但理解这两种文件对学习文件IO操作非常重要. 1.1.1ASCII文件 ASCII文件也就是文本文件,每个字节存放一个ASCII代码,代表一个字符.可以使用任何编辑器打开,如记事本或者UE等,打开就是你能看懂的字符.比如姓名"richard"就会存储为7个字节,每个字节分别为对应字母的ASCII码.整数10000就会被存为"10000",每个字节为每个字母的ASCI

第七篇:两个经典的文件IO程序示例

前言 本文分析两个经典的C++文件IO程序,提炼出其中文件IO的基本套路,留待日后查阅. 程序功能 程序一打印用户指定的所有文本文件,程序二向用户指定的所有文本文件中写入数据. 程序一代码及其注释 1 #include <iostream> 2 #include <fstream> // 使用文件处理对象记着要包含这个头文件 3 #include <string> 4 #include <vector> 5 6 using namespace std; 7

MySQL系列:innodb引擎分析之文件IO

innodb作为数据库引擎,自然少不了对文件的操作,在innodb中所有需要持久化的信息都需要文件操作,例如:表文件.重做日志文件.事务日志文件.备份归档文件等.innodb对文件IO操作可以是煞费苦心,其主要包括两方面,一个是对异步io的实现,一个是对文件操作管理和io调度的实现.在MySQL-5.6版本的innodb还加入了DIRECT IO实现.做了这么多无非是优化io操作的性能.在innodb的文件IO部分中,主要实现集中在os_file.*和fil0fil.*两个系列的文件当中,其中o

Linux下用文件IO的方式操作GPIO(/sys/class/gpio)(转)

通过sysfs方式控制GPIO,先访问/sys/class/gpio目录,向export文件写入GPIO编号,使得该GPIO的操作接口从内核空间暴露到用户空间,GPIO的操作接口包括direction和value等,direction控制GPIO方向,而value可控制GPIO输出或获得GPIO输入.文件IO方式操作GPIO,使用到了4个函数open.close.read.write. 首先,看看系统中有没有"/sys/class/gpio"这个文件夹.如果没有请在编译内核的时候加入