UNIX环境高级编程之一:IO

一、UNIX操作系统概述

在这里插入图片描述
上图是UNIX操作系统的基本架构。操作系统最核心的部分就是内核(kernel),内核暴露出了一系列接口——即系统调用(system call)——供外部调用,不同的操作系统内核提供了不同的系统调用接口。

我们的应用程序可以直接使用系统调用;也可以不直接使用系统调用,而直接使用在系统调用之上做了一层封装的库或软件,例如上图中的shell和库。

举个例子,在Linux系统中,要实现往标准输出上打印这个功能,我们可以直接使用write系统调用来完成,也可以使用printfC标准库函数来完成。其实printf在系统调用上做了一层封装,其底层还是通过系统调用实现往标准输出打印的功能。

一般来说,我们应该尽可能的选择使用库,而非直接使用系统调用。因为前面提到了,不同系统内核提供的系统调用接口不一样,直接使用系统调用会影响程序的可移植性

下面两个网址给出了x86_64架构的Linux和Windows系统中分别支持的系统调用:

可以看到,对于打开文件这个操作,Linux上对应的系统调用为open,而Windows上为NtOpenFile,显然直接使用系统调用会影响程序的移植性。而使用C库函数fopen则没有该问题(虽然Linux和Windows平台上对fopen的实现是不同的,但是C标准库向我们屏蔽了底层的细节,使得我们在Linux和Windows上都可以使用fopen来实现文件的打开)。

我们的程序中,可以使用系统调用来实现IO,也就是系统IO;也可以调用printf等标准库函数来实现IO,也就是标准IO。下面将介绍这两种IO。

二、标准IO

fopen/fclose

#include <stdio.h>
FILE *fopen(const char *pathname, const char *mode);
int fclose(FILE *stream); 

第二个参数mode

  • 在遵守POSIX规范的系统中,b模式修饰符会被忽略。如果要考虑兼容C89或者程序移植性,可以加上b
  • 只看mode开头的一个或两个字符:传入rw相当于r,传入r+w相当于r+
  • rr+模式要求文件必须已经存在,其他模式文件不存在会创建文件

void *指针可以赋值给任意类型的指针类型,无须显示进行类型转换。

Q:fopen返回的文件指针指向的内容位于内存中的哪个区域?

fopen返回一个FILE指针,FILE是一个结构体。所以fopen里面的逻辑应该是创建一个结构体,然后返回这个结构体的地址。

首先这个结构体不能是局部变量,也就是不能位于栈中,因为函数不能返回非静态局部变量的地址。如果位于栈中,函数返回后结构体对应的内存就不存在了。

其次这个结构体也不能声明为static变量,如果声明为static变量,那么该变量在整个程序运行期间都有效,只会被初始化一次,后续所有调用fopen的函数都共享同一个FILE结构体。

其实,真正的答案是FILE指针指向的结构体位于堆上,使用malloc进行分配,必须调用fclose进行释放。

使用gdb跟踪到glibc源码可以看到fopen实际上调用的是iofpen.c文件里的_IO_new_fopen

FILE *
_IO_new_fopen (const char *filename, const char *mode)
{
  return __fopen_internal (filename, mode, 1);
}

进一步调用__fopen_internal

FILE *
__fopen_internal (const char *filename, const char *mode, int is32)
{
  struct locked_FILE
  {
    struct _IO_FILE_plus fp;
#ifdef _IO_MTSAFE_IO
    _IO_lock_t lock;
#endif
    struct _IO_wide_data wd;
  } *new_f = (struct locked_FILE *) malloc (sizeof (struct locked_FILE));

  if (new_f == NULL)
    return NULL;
#ifdef _IO_MTSAFE_IO
  new_f->fp.file._lock = &new_f->lock;
#endif
  _IO_no_init (&new_f->fp.file, 0, 0, &new_f->wd, &_IO_wfile_jumps);
  _IO_JUMPS (&new_f->fp) = &_IO_file_jumps;
  _IO_new_file_init_internal (&new_f->fp);
  if (_IO_file_fopen ((FILE *) new_f, filename, mode, is32) != NULL)
    return __fopen_maybe_mmap (&new_f->fp.file);

  _IO_un_link (&new_f->fp);
  free (new_f);
  return NULL;
}

上述代码中首先使用malloc分配了一个locked_FILE类型的结构体,并将其指针存在了new_f。结构体变量中的_IO_FILE_plus定义如下(在libioP.c文件中):

struct _IO_FILE_plus
{
	FILE file;
 	const struct _IO_jump_t *vtable;
};

该结构体变量包含FILE类型的成员。所以之前使用malloc分配locked_FILE类型的结构体内存的同时,实际上也分配了一个FILE变量,该结构体变量的指针由__fopen_maybe_mmap()生成并返回。所以说,FILE结构体变量是通过malloc分配的。

Q: fopen创建出的文件的权限?

0666 & ~umask0666umask的反码 按位与

0666的含义参考fopen的man page:

0666 = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH,也就是rw-rw-rw-的意思。

获取/打印错误信息

C程序中最近一次的错误信息对应的错误编号会存储在全局变量errno中。

#include <stdio.h>
void perror(const char *s);

在字符串s后面加上 以及 当前errno对应的错误信息,并打印到stderr。

#include <string.h>
char *strerror(int errnum);

返回errnum对应的错误信息。

fgetc/getc/getchar

#include <stdio.h>

int fgetc(FILE *stream);
int getc(FILE *stream);
int getchar(void);
int ungetc(int c, FILE *stream);

getchar() 等价于 getc(stdin)

fgetc()getc()等价,唯一的区别在于getc可能用宏实现,这可能会带来一些副作用,参考https://stackoverflow.com/questions/14008907/fputc-vs-putc-in-c

fputc/putc/putchar

#include <stdio.h>

int fputc(int c, FILE *stream);
int fputs(const char *s, FILE *stream);
int putc(int c, FILE *stream);
int putchar(int c);
int puts(const char *s);

gets/puts/fgets/fputs

char *gets(char *s); // 从stdin读取
int puts(const char *s); // 往stdout中输出,附带一个换行符
char *fgets(char *s, int size, FILE *stream);
int fputs(const char *s, FILE *stream);
  • fgets遇到EOF换行符就会停止读取
  • 对于fgets,会默认给读到的字符串末尾加一个\0,因此fgets实际上最多读取stream中的size - 1个字符。
  • 不推荐使用getsgets对于从stdin输入的字符串长度没有限制,可能会导致缓冲区溢出。
#include <stdio.h>

#define SIZE 5

/*
 * 输入"AAA换行", buf中存放"AAA\n\0"
 * 输入"AAAA换行",buf中存放"AAAA\0", stdin中还有一个'\n'没被读取
 * 输入"AAAAA换行",buf中存放"AAAA\0", stdin中还有"A\n"没被读取
 */
int main(int argc, char *argv[]) {
    char buf[SIZE];
    fgets(buf, SIZE, stdin);
    fputs(buf, stdout);
    return 0;
}

fread/fwrite

#include <stdio.h>

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

size_t fwrite(const void *ptr, size_t size, size_t nmemb,
              FILE *stream);
  • fread:从stream中读取nmemb个成员,每个成员大小为size个字节,读取结果存放到ptr指向的内存地址。返回成功读取的成员个数,失败或没读取到(即到达文件末尾)返回0。
  • fwrite:将ptr指针所指向位置开始的的nmemb个大小为size的成员写入到stream,返回成功写入的成员个数。如果发生失败或者写入了0个成员,则返回0。

之前介绍的fgetc/fputc/fgets/fputs等等都是对字符进行操作,而fread/fwrite是以字节流的视角进行操作的,更适合于操作二进制文件。

printf/scanf

#include <stdio.h>

int printf(const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);
int dprintf(int fd, const char *format, ...);
int sprintf(char *str, const char *format, ...);
int snprintf(char *str, size_t size, const char *format, ...);

  • sprintf的特殊用法:可以完成atoi的相反功能,即把一个数字转换为字符串,例如sprintf(s, "%d", num)
  • sprintf由于没对参数中的字符串长度进行限制,因此读取的字符串长度会超过给str分配的大小,造成缓冲区溢出问题,这一点和gets函数类似。为了解决这个问题,可以使用snprintf作为替代,它最多往str对应的内存区域中写入size个字节(包括末尾的\0)。
#include <stdio.h>

int scanf(const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);
int sscanf(const char *str, const char *format, ...);
  • 类似的,假如我们想通过scanf获取一个字符串(%s),由于没有对字符串长度进行限制,因此也可能导致缓冲区溢出问题。

fseek/ftell/fseeko/ftello

#include <stdio.h>

int fseek(FILE *stream, long offset, int whence);
long ftell(FILE *stream);

fseek用于设置文件位置指针 (file position indicator) 的位置。其中offset的单位是字节,whence表示从哪个字节开始偏移,通常取SEEK_SET, SEEK_CUR, SEEK_END,分别代表文件开头、当前指针位置、文件末尾。

注意:

  • 文件指针的位置是从0开始编号的
  • 假设文件有n字节,则SEEK_END对应的位置编号为n(即第n+1个字节)

ftell返回当前文件位置指针的位置。

C语言规范并没有对long类型的长度作出明确规定,只是要求long类型的长度大于等于int类型的长度,因此即使在64位机器上,long类型的长度也可能是32位。如果long类型的长度为32位的话,可以表示的数的范围是: − 2 31 -2^{31} 231 2 31 − 1 2^{31} - 1 2311。由于ftell不可能返回负数,所以其返回值的范围是 0 0 0 2 31 − 1 2^{31} - 1 2311,也就是最多支持 2 G B 2GB 2GB 的文件,而现在很多文件都不止 2 G B 2GB 2GB 了。

在遵循POSIX规范或SUSv2规范的系统中,我们可以使用fseeko/ftello

#include <stdio.h>

int fseeko(FILE *stream, off_t offset, int whence);
off_t ftello(FILE *stream);

这两个函数与fseek/ftell唯一的不同点就是把long类型换成了off_t类型,off_t类型的长度可能依具体的实现而不同,不过我们可以显式的定义宏_FILE_OFFSET_BITS=64来指定off_t的长度为64比特。这样就确保了在任何遵循POSIX规范或SUSv2规范的系统中,我们都通过fseeko/ftello可以操作最大 2 63 − 1 2^{63} - 1 2631字节的文件。

额外思考下面的问题

从man page可以看出,fseek/ftell遵循POSIX.1-2001, POSIX.1-2008, C89, C99规范,而fseeko/ftello遵循POSIX.1-2001, POSIX.1-2008, SUSv2规范。

这就意味着,fseeko/ftello只能在遵循POSIX.1-2001, POSIX.1-2008, SUSv2规范的系统上使用,这对程序的可移植性有一定的影响。举个例子,在Windows平台上是不能使用fseeko/ftello的,因为Windows平台不遵循POSIX规范。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
目 录 译者序 译者简介 前言 第1章 UNIX基础知识 1 1.1 引言 1 1.2 登录 1 1.2.1 登录名 1 1.2.2 shell 1 1.3 文件和目录 2 1.3.1 文件系统 2 1.3.2 文件名 2 1.3.3 路径名 2 1.3.4 工作目录 4 1.3.5 起始目录 4 1.4 输入和输出 5 1.4.1 文件描述符 5 1.4.2 标准输入、标准输出和标准 出错 5 1.4.3 不用缓存的I/O 5 1.4.4 标准I/O 6 1.5 程序和进程 7 1.5.1 程序 7 1.5.2 进程和进程ID 7 1.5.3 进程控制 7 1.6 ANSI C 9 1.6.1 函数原型 9 1.6.2 类属指针 9 1.6.3 原始系统数据类型 10 1.7 出错处理 10 1.8 用户标识 11 1.8.1 用户ID 11 1.8.2 组ID 12 1.8.3 添加组ID 12 1.9 信号 12 1.10 UNIX时间值 14 1.11 系统调用和库函数 14 1.12 小结 16 习题 16 第2章 UNIX标准化及实现 17 2.1 引言 17 2.2 UNIX标准化 17 2.2.1 ANSI C 17 2.2.2 IEEE POSIX 18 2.2.3 X/Open XPG3 19 2.2.4 FIPS 19 2.3 UNIX实现 19 2.3.1 SVR4 20 2.3.2 4.3+BSD 20 2.4 标准和实现的关系 21 2.5 限制 21 2.5.1 ANSI C限制 22 2.5.2 POSIX限制 22 2.5.3 XPG3限制 24 2.5.4 sysconf、pathconf 和fpathconf 函数 24 2.5.5 FIPS 151-1要求 28 2.5.6 限制总结 28 2.5.7 未确定的运行时间限制 29 2.6 功能测试宏 32 2.7 基本系统数据类型 32 2.8 标准之间的冲突 33 2.9 小结 34 习题 34 第3章 文件I/O 35 3.1 引言 35 3.2 文件描述符 35 3.3 open函数 35 3.4 creat函数 37 3.5 close函数 37 3.6 lseek函数 38 3.7 read函数 40 3.8 write函数 41 3.9 I/O的效率 41 3.10 文件共享 42 3.11 原子操作 45 3.11.1 添加至一个文件 45 3.11.2 创建一个文件 45 3.12 dup和dup2函数 46 3.13 fcntl函数 47 3.14 ioctl函数 50 3.15 /dev/fd 51 3.16 小结 52 习题 52 第4章 文件和目录 54 4.1 引言 54 4.2 stat, fstat和lstat函数 54 4.3 文件类型 55 4.4 设置-用户-ID和设置-组-ID 57 4.5 文件存取许可权 58 4.6 新文件和目录的所有权 60 4.7 access函数 60 4.8 umask函数 62 4.9 chmod和fchmod函数 63 4.10 粘住位 65 4.11 chown, fchown和 lchown函数 66 4.12 文件长度 67 4.13 文件截短 68 4.14 文件系统 69 4.15 link, unlink, remove和rename 函数 71 4.16 符号连接 73 4.17 symlink 和readlink函数 76 4.18 文件的时间 76 4.19 utime函数 78 4.20 mkdir和rmdir函数 79 4.21 读目录 80 4.22 chdir, fchdir和getcwd函数 84 4.23 特殊设备文件 86 4.24 sync和fsync函数 87 4.25 文件存取许可权位小结 88 4.26 小结 89 习题 89 第5章 标准I/O库 91 5.1 引言 91 5.2 流和FILE对象 91 5.3 标准输入、标准输出和标准出错 91 5.4 缓存 91 5.5 打开流 94 5.6 读和写流 96 5.6.1 输入函数 96 5.6.2 输出函数 97 5.7 每次一行I/O 98 5.8 标准I/O的效率 99 5.9 二进制I/O 100 5.10 定位流 102 5.11 格式化I/O 103 5.11.1 格式化输出 103 5.11.2 格式化输入 103 5.12 实现细节 104 5.13 临时文件 105 5.14 标准I/O的替代软件 108 5.15 小结 108 习题 108 第6章 系统数据文件和信息 110 6.1 引言 110 6.2 口令文件 110 6.3 阴影口令 112 6.4 组文件 113 6.5 添加组ID 114 6.6 其他数据文件 115 6.7 登录会计 116 6.8 系统标识 116 6.9 时间和日期例程 117 6.10 小结 121 习题 121 第7章 UNIX进程的环境 122 7.1 引言 122 7.2 main 函数 122 7.3 进程终止 122 7.3.1 exit和_exit函数 122 7.3.2 atexit函数 124 7.4 命令行参数 125 7.5 环境表 126 7.6 C程序的存储空间布局 126 7.7 共享库 127 7.8 存储器分配 128 7.9 环境变量 130 7.10 setjmp 和longjmp函数 132 7.10.1 自动、寄存器和易失变量 134 7.10.2 自动变量的潜在问题 136 7.11 getrlimit 和setrlimit函数 136 7.12 小结 139 习题 140 第8章 进程控制 141 8.1 引言 141 8.2 进程标识 141 8.3 fork函数 142 8.4 vfork 函数 145 8.5 exit函数 147 8.6 wait和waitpid函数 148 8.7 wait3和wait4函数 152 8.8 竞态条件 153 8.9 exec函数 156 8.10 更改用户ID和组ID 160 8.10.1 setreuid 和setregid函数 162 8.10.2 seteuid和 setegid函数 163 8.10.3 组ID 163 8.11 解释器文件 164 8.12 system函数 167 8.13 进程会计 171 8.14 用户标识 175 8.15 进程时间 176 8.16 小结 178 习题 178 第9章 进程关系 180 9.1 引言 180 9.2 终端登录 180 9.2.1 4.3+BSD终端登录 180 9.2.2 SVR4终端登录 182 9.3 网络登录 182 9.3.1 4.3+BSD网络登录 182 9.3.2 SVR4网络登录 183 9.4 进程组 183 9.5 对话期 184 9.6 控制终端 185 9.7 tcgetpgrp 和tcsetpgrp函数 187 9.8 作业控制 187 9.9 shell执行程序 189 9.10 孤儿进程组 193 9.11 4.3+BSD实现 195 9.12 小结 197 习题 197 第10章 信号 198 10.1 引言 198 10.2 信号的概念 198 10.3 signal函数 203 10.3.1 程序起动 205 10.3.2 进程创建 206 10.4 不可靠的信号 206 10.5 中断的系统调用 207 10.6 可再入函数 209 10.7 SIGCLD语义 211 10.8 可靠信号术语和语义 213 10.9 kill和raise函数 213 10.10 alarm和pause函数 214 10.11 信号集 219 10.12 sigprocmask 函数 220 10.13 sigpending函数 222 10.14 sigaction函数 223 10.15 sigsetjmp 和siglongjmp函数 226 10.16 sigsuspend函数 229 10.17 abort函数 234 10.18 system函数 235 10.19 sleep函数 240 10.20 作业控制信号 241 10.21 其他特征 243 10.21.1 信号名字 243 10.21.2 SVR4信号处理程序的附 加参数 244 10.21.3 4.3+BSD信号处理程序的附 加参数 244 10.22 小结 244 习题 244 第11章 终端I/O 246 11.1 引言 246 11.2 综述 246 11.3 特殊输入字符 250 11.4 获得和设置终端属性 254 11.5 终端选择标志 254 11.6 stty命令 258 11.7 波特率函数 259 11.8 行控制函数 260 11.9 终端标识 260 11.10 规范方式 263 11.11 非规范方式 266 11.12 终端的窗口大小 270 11.13 termcap, terminfo和 curses 271 11.14 小结 272 习题 272 第12章 高级I/O 273 12.1 引言 273 12.2 非阻塞I/O 273 12.3 记录锁 275 12.3.1 历史 276 12.3.2 fcntl记录锁 276 12.3.3 锁的隐含继承和释放 280 12.3.4 4.3+BSD的实现 281 12.3.5 建议性锁和强制性锁 284 12.4 流 288 12.4.1 流消息 289 12.4.2 putmsg和putpmsg函数 290 12.4.3 流ioctl操作 291 12.4.4 write至流设备 294 12.4.5 写方式 294 12.4.6 getmsg和getpmsg函数 294 12.4.7 读方式 295 12.5 I/O多路转接 296 12.5.1 select函数 298 12.5.2 poll函数 301 12.6 异步I/O 303 12.6.1 SVR4 303 12.6.2 4.3+BSD 303 12.7 readv和writev函数 304 12.8 readn和writen函数 306 12.9 存储映射I/O 307 12.10 小结 311 习题 311 第13章 精灵进程 312 13.1 引言 312 13.2 精灵进程的特征 312 13.3 编程规则 313 13.4 出错记录 314 13.4.1 SVR4流log驱动程序 315 13.4.2 4.3+BSD syslog设施 316 13.5 客户机-服务器模型 319 13.6 小结 319 习题 319 第14章 进程间通信 320 14.1 引言 320 14.2 管道 320 14.3 popen和pclose函数 325 14.4 协同进程 330 14.5 FIFO 333 14.6 系统V IPC 335 14.6.1 标识符和关键字 336 14.6.2 许可权结构 337 14.6.3 结构限制 337 14.6.4 优点和缺点 337 14.7 消息队列 338 14.8 信号量 342 14.9 共享存储 346 14.10 客户机-服务器属性 351 14.11 小结 353 习题 353 第15章 高级进程间通信 355 15.1 引言 355 15.2 流管道 355 15.3 传送文件描述符 358 15.3.1 SVR4 360 15.3.2 4.3BSD 361 15.3.3 4.3+BSD 364 15.4 open服务器第1版 366 15.5 客户机-服务器连接函数 371 15.5.1 SVR4 372 15.5.2 4.3+BSD 375 15.6 open服务器第2版 378 15.7 小结 385 习题 385 第16章 数据库函数库 386 16.1 引言 386 16.2 历史 386 16.3 函数库 386 16.4 实现概述 388 16.5 集中式或非集中式 390 16.6 并发 391 16.6.1 粗锁 391 16.6.2 细锁 391 16.7 源码 392 16.8 性能 409 16.8.1 单进程的结果 410 16.8.2 多进程的结果 410 16.9 小结 412 习题 412 第17章 与PostScript打印机通信 413 17.1 引言 413 17.2 PostScript通信机制 413 17.3 假脱机打印 415 17.4 源码 417 17.5 小结 434 习题 434 第18章 调制解调器拨号器 435 18.1 引言 435 18.2 历史 435 18.3 程序设计 436 18.4 数据文件 437 18.5 服务器设计 439 18.6 服务器源码 439 18.7 客户机设计 463 18.7.1 终端行规程 463 18.7.2 一个进程还是两个进程 464 18.8 客户机源码 465 18.9 小结 474 习题 474 第19章 伪终端 476 19.1 引言 476 19.2 概述 476 19.2.1 网络登录服务器 477 19.2.2 script程序 478 19.2.3 expect程序 479 19.2.4 运行协同进程 479 19.2.5 观看长时间运行程序的输出 479 19.3 打开伪终端设备 480 19.3.1 SVR4 481 19.3.2 4.3+BSD 482 19.4 pty_fork函数 484 19.5 pty程序 486 19.6 使用pty程序 489 19.6.1 utmp文件 489 19.6.2 作业控制交互 489 19.6.3 检查长时间运行程序的输出 491 19.6.4 script程序 491 19.6.5 运行协同进程 492 19.6.6 用非交互模式驱动交互式 程序 492 19.7 其他特性 494 19.7.1 打包模式 494 19.7.2 远程模式 494 19.7.3 窗口大小变化 495 19.7.4 信号发生 495 19.8 小结 495 习题 495 附录A 函数原型 497 附录B 其他源代码 512 附录C 习题答案 518 参考书目 536

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值