Unix系统编程文件IO系列博客,源于《Linux C一站式编程》的阅读笔记和《APUE2》的阅读笔记。
Unix系统可用的文件IO函数包括打开文件、读文件、写文件等等。
1、文件描述符
对于内核来说,所有打开文件都通过文件描述符引用。文件描述符是一个非负的整数,当打开一个现有的文件或者创建一个文件的时候,内核向进程返回一个文件描述符。当读写一个文件时,使用open或create返回文件描述符标识该文件,将其作为参数传给read或write。
POSIX标准程序中0,1,2分别表示标准输入,标准输出,标准错误相关联:
- STDIN_FILENO 0
- STDOUT_FILENO 1
- STDERR_FILENO 2
新打开文件返回文件描述符表中未使用的最小文件描述符。文件描述符的变化范围为:0 ~ OPEN_MAX - 1。该值存在于/proc/sys/fs/file_max文件下,使用cat命令查看该值可以得到当前系统允许打开的最大文件个数。
使用ulimit命令查看当前默认设置的最大打开文件个数:
可以看到当前默认设置的最大打开文件个数是1024个,这个数值可以通过ulimit命令进行修改。下图中将该值修改成4096。
2、比较系统函数和C标准函数
我们知道,C 标准提供的函数我们可以再不用的平台(不管是windows, Linux, Unix, Mac, 甚至嵌入式芯片等)使用。那么也就是说C 标准函数并不依赖于硬件。不管是在Windows下还是Linux下,系统都会给我们提供一套所谓的API函数,这些函数也就是直接和操作系统交互的函数。当然,这些函数是依赖于硬件平台的。
那么,上述的这两者之间是什么关系? 其实,C 标准函数是建立在系统函数之上的一套函数。在Unix系统中,系统函数负责和系统内核进行交互,C标准函数则是通过调用系统函数实现对内核的交互的。他们之间的关系可以通过下图看到:
C 标准库函数带有一个buffer, 在写文件过程中,会先把内容写到buffer中,当buffer中的内容写满了,或者主动要求刷新的时候,才会将内容写到磁盘中。每一个FILE文件流都有一个缓冲区buffer,默认大小8192Byte。
系统函数则会直接向磁盘文件中写内容。
1- 用Unbuffered I/O函数每次读写都要进内核 调一个系统调用比调一个用户空间的函数要慢很多, 所以在用户空间开辟I/O缓冲区还是必要的,用C标准I/O库函数就比较方便,省去了自己管理I/O缓冲区的麻烦。
2- 用C标准I/O库函数要时刻注意I/O缓冲区和实际文件有可能不一致,在必要时需调用 fflush(3)
3- 当网络设备接收到数据时应用程序也希望第一时间被通知到,所以网络编程通常直接调用Unbuffered I/O函数。
3、进程控制块(PCB, Processing Control Block)
进程控制块(PCB)是系统为了管理进程设置的一个专门的数据结构。系统用它来记录进程的外部特征,描述进程的运动变化过程。同时,系统可以利用PCB来控制和管理进程,所以说,PCB(进程控制块)是系统感知进程存在的唯一标志。
每个进程都会有两个和进程相关的结构体:
- task_struct 位于/usr/src/linux-headers/include/linux/sched.h下
- files_struct
在linux 中每一个进程都由task_struct 数据结构来定义. task_struct就是我们通常所说的PCB。它是对进程控制的唯一手段也是最有效的手段.
4、文件打开open
使用man 2 open 命令查看open函数的定义及相关的描述:
NAME
open, creat - open and possibly create a file or device
SYNOPSIS
#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);
int creat(const char *pathname, mode_t mode);
从man的结果上看,系统提供给我们了两个open函数,两者的不同在于参数的个数。
- const char *pathname
该参数表示的是要要打开文件的文件路径。和fopen一样,pathname既可以是相对路径也可以是绝对路径。 - int flags
。flags参数有一系列常数值可供选择,可以同时选择多个常数用按位或运算符连接起来,所以这些常数的宏定义都以O_开头,表示or。
必选项:以下三个常数中必须指定一个,且仅允许指定一个。 - O_RDONLY 只读打开
- O_WRONLY 只写打开
- O_RDWR 可读可写打开
可选项 有很多,需要使用的时候可以使用man去查看,下面列举部分可选项:
- O_APPEND 表示追加。如果文件已有内容,这次打开文件所写的数据附加到文件的末尾
而不覆盖原来的内容。 - O_CREAT 若此文件不存在则创建它。使用此选项时需要提供第三个参数mode,表示该
文件的访问权限。 - O_EXCL 如果同时指定了O_CREAT,并且文件已存在,则出错返回。
- O_TRUNC 如果文件已存在,并且以只写或可读可写方式打开,则将其长度截断(Truncate)
为0字节。 O_NONBLOCK 对于设备文件,以O_NONBLOCK方式打开可以做非阻塞I/O(Nonblock I/
O),非阻塞I/O在下一节详细讲解。mode_t mode
指定文件权限,可以用八进制数表示,比如0644表示-rw-r-r–,也可以用S_IRUSR、S_IWUSR等宏定义按位或起来表示,详见open(2)的Man Page。要注意的是,文件权限由open的mode参数和当前进程的umask掩码共同决定。补充说明一下Shell的umask命令。Shell进程的umask掩码可以用umask命令查看:
open() 和 C标准函数fopen()的区别
1、以可写的方式fopen一个文件时,如果文件不存在会自动创建,而open一个文件时必须明确指定O_CREAT才会创建文件,否则文件不存在就出错返回。
2、以w或w+方式fopen一个文件时,如果文件已存在就截断为0字节,而open一个文件时必须明确指定O_TRUNC才会截断文件,否则直接在原来的数据上改写。
5、文件关闭close
NAME
close - close a file descriptor
SYNOPSIS
#include <unistd.h>
int close(int fd);
关闭函数比较简单,直接传入一个需要关闭的文件的文件描述符就可以了。
- 注意
一个进程终止时,内核对该进程所有尚未关闭的文件描述符调用close关闭,所以即使用户程序不调用close,在终止时内核也会自动关闭它打开的所有文件。但是对于一个长年累月运行的程序(比如网络服务器),打开的文件描述符一定要记得关闭,否则随着打开的文件越来越多,会占用大量文件描述符和系统资源。
6、文件读写read/write
系统给出的两个函数说明:
NAME
read - read from a file descriptor
SYNOPSIS
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
NAME
write - write to a file descriptor
SYNOPSIS
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
- size_t类型和ssize_t类型:
- ssize_t:表示有符号数
- size_t:表示无符号数
7、编程试验
7.1 open函数使用
测试使用open函数,打开一个名为open.txt的文件,文件不存在则创建。
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
int main(int args, char* argv[])
{
printf("Test Unix API open function\n");
int rt = open("open.txt", O_CREAT, 0777);
if(rt == -1)
{
printf("open file error!\n");
exit(1);
}
return 0;
}
7.2 测试最大文件打开个数
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
int main(int args, char* argv[])
{
//printf("Test Unix API open function\n");
int index = 0;
char name[1024] = {0};
int rt = 0;
while(1)
{
sprintf(name, "file%04d", ++index);
if(-1 == open(name, O_CREAT, 0777))
{
printf("error!\n");
exit(1);
}
}
return 0;
}
7.3 终端读写测试
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
int main(int args,char* argv[])
{
char buf[127] = {0};
int cnt = 0;
cnt = read(STDIN_FILENO, buf, 127);
if(cnt < 0)
{
perror("Read STDIN_FILENO error!");
exit(1);
}
write(STDOUT_FILENO, buf, cnt);
return 0;
}
7.4 读入文件,拷贝写入另一文件
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#define SIZE 8192
int main(int argc, char *argv[])
{
char buf[SIZE];
int fd_src, fd_dest, len;
if (argc < 3) {
printf("agrc error\n");
exit(1);
}
fd_src = open(argv[1], O_RDONLY);
fd_dest = open(argv[2], O_CREAT | O_WRONLY | O_TRUNC, 0644);
while ((len = read(fd_src, buf, sizeof(buf))) > 0)
write(fd_dest, buf, len);
close(fd_src);
close(fd_dest);
return 0;
}