- 课程视频:Linux 系统编程 - 李慧琴老师
- 深入浅出,点面结合,恪守标准,爆赞
- 参考书目
- UNIX 环境高级编程(第三版)
- Linux 内核设计与实现(第三版)
- 深入理解 Linux 内核(第三版)
- 参考文章
标准 I/O
简介
- I/O:input & output
- stdio:标准 I/O(优先使用,因为可移植性好且封装性好)
- sysio:系统 I/O(也叫文件 I/O)
常见标准 I/O:
打开/关闭文件 | 输入输出流 | 文件指针操作 | 缓存相关 |
---|---|---|---|
fopen | fgetc,fputc | fseek | fflush |
fclose | fgets,fputs | ftell | |
fread,fwrite | rewind | ||
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()
代替fgetc
和fputc
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
:将格式化串输出到 stdoutfprintf
:将格式化串输出到 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, ...);
scanf
、fscanf
:同上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
处,执行成功返回 0whence
:文件开头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
),失败则返回 -1getline
依旧不被标准 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 表示失败(一般认为不会执行失败)
Comments NOTHING