文件I/O也称系统调用I/O,是操作系统“用户态”运行的进程和硬件交互提供的一组接口,即操作系统内核留给用户程序的一个接口。
在C语言中常见的输入输出函数(scanf,printf)实际上都是调用了文件I/O函数,因为系统API效率高,一次性读取。
一、缓存与IO
下图以fgets / fputs 示意了I/O缓冲区的作用,使用fgets / fputs 函数时在用户程序中也需要分配缓冲 区(图中的buf1和buf2 ),注意区分用户程序的缓冲区和C标准库的I/O缓冲区。
I/O缓存区:用户进程空间内核缓冲区 :内核层
磁盘缓存 大概的基本流程标准
应用内核缓冲技术导致的结果是:提高了磁盘的I/O效率;一次读入大量的数据放在缓冲区,需要的时候从缓冲区取得数据。减少系统调用次数read和write等
二、函数open(打开/创建 一个文件/设备)
#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);
第一个参数:字符串形式的路径和文件名
第二个参数:操作标志O_RDONLY 只读模式 O_WRONLY 只写模式 O_RDWR 读写模式 O_EXEC 只执行打开 O_SEARCH 只搜索打开(应用于目录)
上述 5 种标志位是互斥的,也就是不可同时使用,但可与下列的标志位利用 OR(|) 运算符组合。
第三个参数:操作模式O_APPEND:每次写时追加到文件的尾端。 O_CLOEXEC:把FD_CLOEXEC常量设置为文件描述符标志。 O_CREAT:若此文件不存在则创建它。使用此选项时,open函数需同时说明第3个参数mode(openat函数需说明第4个参数mode),用mode指定该新文件的访问权限位。 O_DIRECTORY:如果path引用的不是目录,则出错。 O_EXCL:如果同时指定了O_CREAT,而文件已经存在,则出错。用此可以测试一个文件是否存在,如果不存在,则创建此文件,这使测试和创建两者称为一个原子操作。 O_NOCTTY:如果path引用的是终端设备,则不将该设备分配作为此进程的控制终端。 O_NOFOLLOW:如果path引用的是一个符号链接,则出错。 O_NONBLOCK:如果path引用的是一个FIFO、一个特殊文件或一个字符特殊文件,则此选项为文件的本次打开操作和后序的I/O操作设置非阻塞方式。 O_SYNC :使每次write等待物理I/O操作完成,包括由该write操作引起的文件属性更新所需的I/O。 O_TRUNC:如果此文件存在,而且为只读或读-写成功打开,则将其长度截断为0。 O_TTY_INIT:如果打开一个还未打开的终端设备,设置非标准termios参数值,使其符合Single UNIX Specification(以及POSIX.1)中同步输入和输出选项的一部分。 O_DSYNC:使每次write要等待物理I/O操作完成,但是如果该写操作并不影响读取刚写入的数据,则不需要等待文件属性被更新。 O_RSYNC:使每一个以文件描述符作为参数进行的read操作等待,直至所有对文件同一部分挂起的写操作都完成。
代码实现:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>#define buff_size 1024
#define FLAGS1 O_CREAT // O_CREAT表示没有文件则创建,并赋予权限,如果存在则覆盖
#define FLAGS2 O_CREAT | O_RDWR // O_RDWR表示以读写的方式打开文件
#define FLAGS3 O_RDWR | O_APPEND // O_APPEND表示在文件中追加内容
#define FLAGS4 O_CREAT | O_RDWR | O_EXCL // O_EXCL表示文件存在则返回-1int main(int argc, char *argv[])
{int fd;char buf[buff_size] = "World";if (argc < 2) //如果没有传递文件名,则退出{printf("./app filename\n");exit(1); //不管在任何函数中,exit(1)都会退出,和return不一样}//fd = open(argv[1], O_CREAT, 0644);fd = open(argv[1], FLAGS4);// 向打开的文件中写入数据write(fd, buf, strlen(buf));printf("fd = %d\n", fd);// 关闭文件close(fd); return 0;
}
三、函数read(从打开的文件中读取数据)
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
1、参数解析
第一个参数:文件描述符
第二个参数:缓冲区的首地址
第三个参数:读取的数据大小2、返回值
成功返回读取到的数据大小,若已到文件尾,返回 0,失败返回 -1。很多种情况可使实际读到的字节数少于要求读的字节数:
(1)读普通文件时,在读到要求字节数之前已经到达了文件尾端。
(2)当从终端设备读时,通常一次最多读一行。
(3)当从网络读时,网络中的缓冲机制可能造成返回值小于所要求读的字节数。
(4)当从管道或FIFO读时,如若管道包含的字节少于所需的数量,那么read将只返回实际可用的字节数。
(5)当从某些面向记录的设别(如磁带)读时,一次最多返回一个记录。
(6)当一信号造成中断,而已经读了部分数据量时。
代码:
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>#define buff_size 1024
#define FLAG O_CREAT | O_WRONLY | O_TRUNCint main(int argc, char *argv[])
{char buf[buff_size];int fd_src, fd_dest, len;if (argc < 3){printf("./mycpp src dest\n");exit(1);}fd_src = open(argv[1], O_RDONLY);fd_dest = open(argv[2], FLAG, 0644);// 成功则返回读到的字节数;读到文件末尾返回0;读失败则返回-1while ((len = read(fd_src, buf, buff_size)) > 0)if (write(fd_dest, buf, len) != len) // write返回读到的字节数printf("write error\n");if (len < 0)printf("read error\n");close(fd_src);close(fd_dest);exit(0);
}
四、函数write(从打开的文件中写入数据)
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
1、参数解析第一个参数:文件描述符
第二个参数:缓冲区首地址
第三个参数:数据的长度2、返回值
成功返回写入的数据长度,失败返回 -1。其返回值通常与 count 的值相同,否则表示出错。write 出错的一个常见原因是磁盘已写满,或者超过了一个给定进程的文件长度限制。对于普通文件,写操作从文件的当前偏移量处开始。如果在打开文件时,指定了O_APPEND选项,则在每次写操作之前,将文件偏移量设置在文件的当前结尾处。在一次成功写之后,该文件偏移量增加实际写的字节数。
五、函数lseek(文件偏移量)
#include <sys/types.h>
#include <unistd.h>
off_t lseek (int fd, off_t offset, int whence);
1、参数解析
第一个参数:文件描述符
第二个参数:偏移量
第三个参数:从什么地方开始偏移若whence是SEEK_SET,则将该文件的偏移量设置为距文件开始处offset个字节;若whence是SEEK_CUR,则将该文件的偏移量设置为其当前值加offset,offset可为正或负;若whence是SEEK_END,则将该文件的偏移量设置为文件长度加offset,offset可正可负。
偏移起始位置:**文件头0(SEEK_SET),当前位置1(SEEK_CUR),文件尾2(SEEK_END))**为基准,偏移offset(指针偏移量)个字节的位置。2、返回值
若成功,返回新的文件偏移量;若出错,返回为 -1。
代码:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>int main(int argc, char *argv[])
{int fd = open("abc.txt", O_RDWR); // 打开一个文件if (fd < 0){perror("open abc"); // 如果打开不成功则报错,并打印报错信息exit(1);}lseek(fd, 0x1000, SEEK_SET); // 拓展文件,一定要有一次写操作;write(fd, "a", 1); // 写入一个字节close(fd);fd = open("edf.txt", O_RDWR);if (fd < 0){perror("open edf"); // 如果打开不成功则报错exit(1);}printf("file size = %d\n", lseek(fd, 0, SEEK_END)); // 获取文件大小close(fd);return 0;
}
六、函数fcntl(改变已打开文件的属性)
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
1、参数解析
第一个参数:文件描述符
第二个参数:操作的命令F_DUPFD/F_DUPFD_CLOEXEC:复制文件描述符的功能,寻找最小的有效的大于等于第三个参数 arg 的描述符作为新的描述符。与 dup2 函数有所不同的是,不会强制关闭已经被占用的描述符。
F_GETFD/F_SETFD:获取/设置文件描述符的标志。
F_GETFL/F_SETFL:获取/设置文件状态的标志。
F_GETOWN/F_SETOWN:获取/设置异步 I/O 所有权。
F_SETLK/F_GETLK:加锁/解锁/测试锁是否存在。
第三个参数:可变长参数2、返回值
F_DUPFD:成功返回新的文件描述符
F_GETFD:成功返回文件描述符的标志值
F_GETFL:成功返回文件状态的标志值
F_GETOWN:成功返回异步 I/O 所有权
其他操作成功返回 0, 所有的操作失败返回 -1
3、函数功能(1)复制文件描述符。
(2)获取/设置文件描述符的标志。
(3)获取/设置文件状态的标志。
(4)获取/设置异步 I/O 所有权。
(5)实现文件锁的功能。
代码:
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>#define MSG_TRY "try again\n"int main(int argc, char *argv[])
{char buf[1024];int flags;flags = fcntl(STDIN_FILENO, F_GETFL); // 获取标准输入的属性flags |= O_NONBLOCK; // 改变属性if (fcntl(STDIN_FILENO, F_SETFL, flags) == -1) // 重新设置新的属性{perror("fcntl");exit(1);}int i;for (i = 0; i < 5; ++i) // 设置阻塞条件,只轮询五次{n = read(fd, buf, buf_size); // 如果不阻塞则读文件if (n > 0) // 读文件成功则退出break;if (errno != EAGAIN) // EAGAIN表示再尝试一次,可能是没有数据{perror("read /dev/tty");exit(1);}sleep(1); // 睡眠一秒write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY)); // 打印一句话到标准输出}if (i == 5 ) // 轮询之后还是没有读取到数据,则打印超时write(STDOUT_FILENO, MSG_TIMEOUT, strlen(MSG_TIMEOUT)); // 打印一句话到标准输出elsewrite(STDOUT_FILENO, buf, n); // 读取到了数据,写到标准输出close(fd);exit(0);
}
七、函数ioctl(获取设备的属性)
int ioctl(int fd, ind cmd, …);
其中fd是用户程序打开设备时使用open函数返回的文件标示符,cmd是用户程序对设备的控制命令,至于后面的省略号,那是一些补充参数,一般最多一个,这个参数的有无和cmd的意义相关。
代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>int main(int argc, char *argv[])
{int fd;struct winsize size;if (isatty(STDOUT_FILENO) == 0)exit(1);if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &size) < 0){perror("ioctl error");exit(1);}printf("R = %d\nC = %d", size.ws_row, size.ws_col); // 获取终端屏幕的大小return 0;
}
八、函数dup和dup2(复制现有的文件描述符)
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
1、返回值
成功返回新的文件描述符,失败返回 -1。
2、Note
文件描述符的复制本质上就是让多个文件描述符对应同一个文件表,也就是对应同一个文件。由 dup 返回的心的文件描述符一定是当前可用文件描述符中的最小数值。
newfd 作为参数 oldfd 的拷贝,如果 newfd 已经打开,则先将其关闭;如若 oldfd 等于 newfd,则 dup2 返回 newfd,而不关闭它。否则,newfd 的 FD_CLOEXEC 文件描述符标志被清除,这样 newfd 在进程调用 exec 时是打开状态。
代码:
//dup/dup2函数的使用
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main(void)
{//1.打开/创建一个文件int fd = open("d.txt",O_RDWR|O_CREAT/*|O_EXCL*/,0644);if(-1 == fd){perror("open"),exit(-1);}printf("fd = %d\n",fd);//3//2.使用dup函数复制文件描述符// int fd2 = fd;int fd2 = dup(fd);if(-1 == fd2){perror("dup"),exit(-1);}printf("fd2 = %d\n",fd2);//4//3.针对不同的描述符进行处理write(fd,"A",1);write(fd2,"a",1);//打开/创建一个新文件int fd3 = open("e.txt",O_RDWR|O_CREAT,0644);if(-1 == fd3){perror("open"),exit(-1);}printf("fd3 = %d\n",fd3);//5//使用dup2函数进行描述符的拷贝// fd 到 fd3的拷贝 fd3 和 fd4相等int fd4 = dup2(fd,fd3);printf("fd3 = %d,fd4 = %d\n",fd3,fd4);// 5 5 write(fd3,"1",1); //d.txtwrite(fd4,"2",1); //d.txtwrite(fd,"3",1); //d.txt//4.关闭所有描述符close(fd);close(fd2);close(fd3);return 0;
}
九、阻塞I/O与非阻塞I/O
1、简介
(1)IO请求的两个阶段:
1. 等待资源阶段:IO请求一般需要请求特殊的资源(如磁盘、RAM、文件),当资源被上一个使用者使用没有被释放时,IO请求就会被阻塞,直到能够使用这个资源;
2. 使用资源阶段:真正进行数据接收和发生。
(2)在等待数据阶段,IO分为阻塞IO和非阻塞IO:
1. 阻塞IO: 资源不可用时,IO请求一直阻塞,直到反馈结果(有数据或超时);
2. 非阻塞IO:资源不可用时,IO请求离开返回,返回数据标识资源不可用。
(3)在使用资源阶段,IO分为同步IO和异步IO:
1. 同步IO:应用阻塞在发送或接收数据的状态,直到数据成功传输或返回失败;
2. 异步IO:应用发送或接收数据后立刻返回,数据写入OS缓存,由OS完成数据发送或接收,并返回成功或失败的信息给应用。
2、阻塞I/O
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>int main(int argc, char *argv[])
{char buf[1024];// STDIN_FILENO表示从标准输入读取数据。int len = read(STDIN_FILENO, buf, sizeof(buf)); // STDOUT_FILENO表示写到标准输出,如果没有从标准输入接收到数据,// 则标准输出会一直等待。write(STDOUT_FILENO, buf, len); exit(0);
}
3、非阻塞I/O
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>#define FLAG O_RDONLY | O_NONBLOCK
#define MSG_TRY "try again\n"
#define MSG_TIMEOUT "timeout\n"
#define buf_size 1024int main(int argc, char *argv[])
{char buf[buf_size];int fd, n;fd = open("/dev/tty", FLAG); // 以非阻塞方式打开一个文件if (fd < 0){perror("open /dev/tty"); // 如果打开失败,则打印失败原因,退出程序exit(1);}int i;for (i = 0; i < 5; ++i) // 设置阻塞条件,只轮询五次{n = read(fd, buf, buf_size); // 如果不阻塞则读文件if (n > 0) // 读文件成功则退出break;if (errno != EAGAIN) // EAGAIN表示再尝试一次,可能是没有数据{perror("read /dev/tty");exit(1);}sleep(1); // 睡眠一秒write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY)); // 打印一句话到标准输出}if (i == 5 ) // 轮询之后还是没有读取到数据,则打印超时write(STDOUT_FILENO, MSG_TIMEOUT, strlen(MSG_TIMEOUT)); // 打印一句话到标准输出elsewrite(STDOUT_FILENO, buf, n); // 读取到了数据,写到标准输出close(fd);exit(0);
}