UNIX环境高级编程之一:IO

一、UNIX操作系统概述

UNIX环境高级编程之一:IO_第1张图片
上图是UNIX操作系统的基本架构。操作系统最核心的部分就是内核(kernel),内核暴露出了一系列接口——即系统调用(system call)——供外部调用,不同的操作系统内核提供了不同的系统调用接口。

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

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

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

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

  • https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md#x86_64-64_bit
  • https://j00ru.vexillium.org/syscalls/nt/64/

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

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

二、标准IO

fopen/fclose

#include 
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 
void perror(const char *s);

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

#include 
char *strerror(int errnum);

返回errnum对应的错误信息。

fgetc/getc/getchar

#include 

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 

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 

#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 

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 

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 

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 

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 

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规范。

你可能感兴趣的:(Linux系统编程,linux)