标准 I/O

简介

  • I/O:input & output
  • stdio:标准 I/O(优先使用,因为可移植性好且封装性好)
  • sysio:系统 I/O(也叫文件 I/O)

常见标准 I/O:

打开/关闭文件输入输出流文件指针操作缓存相关
fopenfgetc,fputcfseekfflush
fclosefgets,fputsftell
fread,fwriterewind
printf 族,scanf 族

FILE 类型始终贯穿标准 I/O

fopen

FILE *fopen(const char *path, const char *mode);
  • path:文件路径
  • mode:访问权限
    • r:只读,文件指针定位到文件开头,要求文件必须存在
    • r+:可读写,文件指针定位到文件开头,要求文件必须存在
    • w:只写,有此文件则清空,无此文件则创建文件,文件指针定位到文件开头
    • w+:可读写,有此文件则清空,无此文件则创建文件,文件指针定位到文件开头
    • a:只写,追加到文件,无此文件则创建文件,文件指针定位到文件末尾(最后一个字节的下一个位置)
    • a+:可读可追加(可写),无此文件则创建文件,读文件加时文件指针定位到文件开头,追加时文件指针定位到文件末尾
    • b:以二进制流打开,可以在以上权限后面加此权限(遵循 POSIX 的系统可以忽略,包括 Linux)
  • 若执行成功函数返回一个 FILE 指针,失败则返回 NULL 并设置全局变量errno
    • errno的定义在 /usr/include/asm-generic的宏中,若想使用需包含errno.h
    • perror()可以输出易读的errno信息,包含在stdio.h
    • strerror()可以返回一个易读的errno信息的字符串,包含在string.h
  • 优秀的编码规范:const 指针表明函数不会改变路径或访问权限

新建出的文件的权限(RWX,通过ls -l查看的那个):

  • 公式:0666 & ~umask(若创建的是目录则为0777 & ~umask
  • 公式中的数字全是八进制数
  • umask(权限掩码)是用户创建的文件的默认权限,可以使用umask指令查看
  • 可见,umask 越大,创建的文件的权限越少
  • umask 在/etc/profile中有定义:UID 大于 199 的用户默认为 002(文件 664、目录 775),其他用户默认为 022(文件 644、目录 755)

fclose

int fclose(FILE *fp);
  • 若执行成功函数返回 0,失败则返回 EOF(一般是 -1,也会有例外)并设置errno
  • 如果文件是输出流,则会表现为先刷新缓冲区再关闭文件

为什么要有fclose()?因为实际上fopen()内隐含了一个malloc(),故打开的文件内存位于堆中;同理,fclose()需隐含一个free()来释放内存

Linux 会对文件描述符的数量进行限制,可以通过ulimit -n命令查看,超过限制数则会弹出too many open files警告,每个用户的文件数目上限可以在/etc/security/limits.conf中修改 在默认情况下至多可以打开 3 + 1021 = 1024 个文件,上限可修改的极限为 1024 * 1024 = 1048576

fgetc、fputc

int fgetc(FILE *stream);
int getc(FILE *stream);
int getchar(void);

这三个函数若执行成功则返回读到的 unsigned char 强转 int,失败或读到文件尾则返回 EOF

getchar()等价于getc(stdin) getc()等价于fgetc(),但getc()是由宏来实现的,而fgetc()是正常的函数实现,相对而言宏实现编译更慢运行更快

int fputc(int c, FILE *stream);
int putc(int c, FILE *stream);
int putchar(int c);

putchar(c)等价于putc(c, stdout) 同上,putc()等价于fputc(),区别是宏实现与函数实现

例程:实现 cp 指令

#include <stdio.h>
#include <stdlib.h>
 
int main(int argc, char **argv){
    FILE *fps, *fpd;
    int ch;

    if(argc < 3){
        fprintf(stderr, "Usage:%s <src_file> <dest_file>\n", argv[0]);
        exit(1);
    }
    
    fps = fopen(argv[1], "r");
    if(fps == NULL){
        perror("fopen()");
        exit(1);
    }
    fpd = fopen(argv[2], "w");
    if(fps == NULL){
        fclose(fps);
        perror("fopen()");
        exit(1);
    }

    while(1){
        ch = fgetc(fps);
        if(ch == EOF){
            break;
        }
        fputc(ch, fpd);
    }
    
    fclose(fpd);
    fclose(fps);
    return 0;
}

fgets、fputs

char *fgets(char *s, int size, FILE *stream);
char *gets(char *s);

gets是危险的,建议使用fgets()

fgets()接收 stdin 时会在读到\n或读到 size - 1 时补上\0并停止 函数成功时返回串的指针,失败或读到文件末尾或未接收到任何字符则返回空指针 易错点:假设不停调用fgets(),输入 size - 1 个字符加换行会先在读到 size - 1 时结束然后在读到\n时再结束一次)

int fputs(const char *s, FILE *stream);
int puts(const char *s);

fread、fwrite

二进制流/文件流的输入输出

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():ptr 从 stream 读 nmemb 个对象,每个对象的大小为 size 字节
  • fwrite():ptr 向 stream 写 nmemb 个对象,每个对象的大小为 size 字节
  • 读写数量以对象为单位,若文件剩余字节数不足以读写一个对象则会直接结束
  • 函数返回已读写的对象数,执行失败或读到文件尾则返回 0

修改例程 mycp

fread()fwrite()代替fgetcfputc

char buf[SIZE];
···
while((n = fread(buf, 1, SIZE, fps)) > 0)
	fwrite(buf, 1, n, fpd);

printf 族、scanf 族

int printf(const char *format, ...);
int fprintf(FILE *stream, const char *fromat, ...);
int sprintf(char *str, const char *format, ...);
int snprintf(char *str, size_t size, const char *format, ...);
  • printf:将格式化串输出到 stdout
  • fprintf:将格式化串输出到 stream 中,可以用于实现重定向
  • sprintf:将格式化串输出到串中
    • 示例:sprintf(str, "%d + %d = %d", 1, 2, 3);->str == "1 + 2 = 3"
    • atoi:字符串转整数,如:"123456" -> 123456、"123a456" -> 123
  • snprintf:带对象数限制的sprintf
int scanf(const char *format, ...);
int fscanf(FILE *stream, const char *fromat, ...);
int sscanf(char *str, const char *format, ...);
  • scanffscanf:同上
  • sscanf:按照格式化串将串内容输入变量中
    • 示例: strcpy(dtm, "Saturday March 25 1989"); sscanf(dtm, "%s %s %d %d", weekday, month, &day, &year);
    • 结果:printf("%s %d, %d = %s\n", month, day, year, weekday);->March 25, 1989 = Saturday

fseek、ftell

int fseek(FILE *stream, long offset, int whence);
long ftell(FILE *stream);
void rewind(FILE *stream);
  • fseek:将文件指针位置设置到相对whence偏移offset处,执行成功返回 0
    • whence:文件开头SEEK_SET、文件指针当前位置SEEK_CUR、文件末尾SEEK_END(正负偏移是允许的)
    • 文件读写会导致文件指针的移动,此时常使用fseek重新定位文件指针
  • ftell:返回文件指针当前位置
  • rewind:等价于(void)fseek(stream, 0L, SEEK_SET),即将文件指针设置到文件开始处
int fseeko(FILE *stream, off_t offset, int whence);
off_t ftell(FILE *stream);
  • 由于历史原因,此前设计选用的 long 类型只能支持 2G 以内的文件,无法满足现代系统的文件大小,故用 off_t 类型支持更大的文件(off_t 大小各系统不同,64 bit Linux 中是 64 位),编译时加上_FILE_OFFSET_BITS宏可以修改 off_t 大小,
  • 仅 POSIX 支持,C89、C99 不支持(即所谓方言)

fflush

int fflush(FILE *stream);
  • 将缓冲区刷新到文件流中,若 stream 为 NULL 则会刷新打开的所有文件

stdout 是行缓冲模式,只在遇到\n\0或行满时才会刷新输出流

示例

printf("before");
while(1);
printf("after");

以上情况无输出,因为 stdout 未被刷新

printf("before\n");
while(1);
printf("after\n");

printf("before");
fflush(stdout);
// or fflush(NULL);
while(1);
printf("after");

则可以正常输出 "before",因为两种方式都刷新了 stdout

关于缓冲区:

  • 缓冲区是在内存中一块指定大小的存储空间,用于临时存储 I/O 数据
  • 作用:合并系统调用、实现数据块复用、缓和 CPU 与 I/O 设备的速度差异
  • 模式:
    • 行缓冲:换行、满时或强制刷新时刷新(如 stdout)
    • 全缓冲:满时刷新或强制刷新(默认,只要不是终端设备)
    • 无缓冲:立即输出(如 stderr)
  • 强制刷新时换行符仅是一个字符,无刷新功能
  • 进程结束时也会强制刷新
  • 更改缓冲区:(了解即可)
    • int setvbuf(FILE *stream, char *buf, int mode, size_t size);
    • buf:分配给用户的缓冲,NULL 时自动分配默认大小
    • mode:无缓冲_IONBF、行缓冲_IOLBF、全缓冲_IOFBF

getline

#define _GNU_SOUECE
#include <stdio.h>

ssize_t getline(char **lineptr, size_t *n, FILE *stream);
  • lineptr:指向存放字符串行的指针,若为 NULL 则由操作系统 malloc(由于是动态分配内存,所以串的指针值很可能会因 realloc 而产生变化,故这里采用二级指针),使用完函数记得 free 掉曾使用的内存
  • n:指定缓冲区的大小,若为系统自行 malloc 则填 0
  • getline会生成从输入流读入的一整行字符串,在文件结束、遇到定界符或达到最大限度时结束生成字符串,成功执行则返回读到的字符数(包括回车符等定界符,但不包括\0),失败则返回 -1
  • getline依旧不被标准 C 支持,但 POSIX 和 C++ 支持
  • getline可以动态地调整缓冲区大小,这是此前函数不支持的
  • _GNU_SOUECE宏建议在 Makefile 中添加,但其实现代编译器已经默认支持该宏

示例

int main(int argc, char **argv) {
    FILE *fp;

    char *linebuf = NULL;
    size_t linesize = 0;
    
    if(argc < 2) {
        fprintf(stderr, "Usage...\n");
    }
    
    fp = fopen(argv[1], "r");
    if(fp == NULL) {
        perror("fopen()");
        exit(1);
    }
    
    while(1) {
    	if(getline(&linebuf, &linesize, fp) < 0)
            break;
       	printf("%d\n", strlen(linebuf));
    }
    
    fclose(fp);
    exit(0);
}

局部变量使用前一定要初始化 进程结束会自动释放内存,故这里没有手动 free 掉

临时文件

char *tmpnam(char *s);
FILE *tmpfile(void);
  • tmpnam会生成并返回一个有效的临时文件名
    • s 为存放文件名的字符串,创建文件名成功会将其返回,若 s 为 NULL 则指向缓冲区,其会在下一次调用函数时被覆盖
    • 函数存在并发问题,多线程下不做处理有可能产生两个甚至更多同名临时文件,造成覆盖
  • tmpfile会产生一个匿名文件
    • 以 w+b 模式创建临时文件,失败将返回 NULL
    • 匿名文件不可见,但在磁盘中,同样有文件描述符和打开计数器等
    • 在被关闭或进程结束时匿名文件会被自动删除

系统调用 I/O

文件描述符

  • 设计open函数打开一个文件的过程中必然会产生一个结构体来描述这个文件,这个结构体记录了包括 inode、引用计数在内的文件操作信息(类似于 FILE 结构体),这些结构体的地址会被存放在进程内部的一个数组中,而文件描述符 fd 实际上就是这个数组的索引
  • 文件描述符是整型的,但ulimit会限制一个用户可打开的文件数量,默认 1024
  • fopen内部实际上也调用了open,区别在于fopen只能打开流式文件,而open能打开设备文件等
  • 每个进程都有自己的描述符表(但会共享一个操作系统维护的文件表),负责记录进程打开的所有文件
  • 文件描述符会优先选择最小的空位,一个文件可以有多个描述符
  • 调用close时会将引用计数减 1,如果此时引用计数为 0 则释放文件

open、close

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
  • flags 和 mode 都是以位图形式表示的,就是 kernel 代码里那种或一大堆宏名然后用掩码提取的形式
  • open是变参函数。变参和 C++ 重载的区别在于变参函数在编译时不会限制传入参数的数量(不报错不代表是正确写法)但重载会严格限制所定义的参数个数;最典型的变参函数就是printf
  • flags:包含必需访问模式和可选模式,用按位或运算串联
    • 必需:只读O_RDONLY、只写WRONLY、读写O_RDWR
    • 可选:可分为创建选项和状态选项两大类,数量庞大故不详细列出,常用如下
      • 创建O_CREAT、确保创建不冲突O_EXCL、追加O_APPEND、文件长度置零O_TRUNC、非阻塞模式打开O_NONBLOCK(默认以阻塞模式打开文件)
    • 对比fopen
      • r == O_RDONLY
      • r+ == O_RDWR
      • w == O_WRONLY | O_CREAT | O_TRUNC
      • w+ == O_RDWR | O_CREAT | O_TRUNC
  • mode:搭配O_CREAT使用,同样是用按位或运算串联,主要规定各种权限
#include <unistd.h>

int close(int fd);
  • 返回 0 表示执行成功,-1 表示失败(一般认为不会执行失败)

文件系统

进程环境

进程控制

信号

线程

高级 I/O

进程间通信

网络套接字