linux 文件I/O 入门

来源:互联网 发布:乔任梁受什么网络暴力 编辑:程序博客网 时间:2024/06/05 22:43
文件I/O
1.1 C标准函数与系统函数的区别

1.1.1 I/O缓冲区
每一个FILE文件流都有一个缓冲区buffer,默认大小8192Byte。
1.1.2 效率
1.1.3 程序的跨平台性
事实上Unbuffered I/O这个名词是有些误导的,虽然write系统调用位于C标准库I/O缓
冲区的底层,但在write的底层也可以分配一个内核I/O缓冲区,所以write也不一定是直接
写到文件的,也可能写到内核I/O缓冲区中,至于究竟写到了文件中还是内核缓冲区中对于
进程来说是没有差别的,如果进程A和进程B打开同一文件,进程A写到内核I/O缓冲区中的数
据从进程B也能读到,而C标准库的I/O缓冲区则不具有这一特性(想一想为什么)。
 第1章文件I/O
1.2 PCB概念
1.2.1 task_struct结构体
/usr/src/linux-headers/include/linux/sched.h
1.2.2 files_struct结构体
1.3 open/close
图 1.2: open
1.3.1 文件描述符
一个进程默认打开3个文件描述符
#define STDIN_FILENO 0
#define STDOUT_FILENO 1
#define STDERR_FILENO 2
新打开文件返回文件描述符表中未使用的最小文件描述符。
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);
返回值:成功返回新分配的文件描述符,出错返回-1并设置errno
在Man Page中open函数有两种形式,一种带两个参数,一种带三个参数,其实在C代码
中open函数是这样声明的:
1.3节open/close 3
int open(const char *pathname, int flags, ...);
最后的可变参数可以是0个或1个,由flags参数中的标志位决定,见下面的详细说明。
pathname参数是要打开或创建的文件名,和fopen一样,pathname既可以是相对路径也
可以是绝对路径。flags参数有一系列常数值可供选择,可以同时选择多个常数用按位或运
算符连接起来,所以这些常数的宏定义都以O_开头,表示or。
必选项:以下三个常数中必须指定一个,且仅允许指定一个。
* O_RDONLY 只读打开
* O_WRONLY 只写打开
* O_RDWR 可读可写打开
以下可选项可以同时指定0个或多个,和必选项按位或起来作为flags参数。可选项有很多,
这里只介绍一部分,其它选项可参考open(2)的Man Page:
* O_APPEND 表示追加。如果文件已有内容,这次打开文件所写的数据附加到文件的末尾
而不覆盖原来的内容。
* O_CREAT 若此文件不存在则创建它。使用此选项时需要提供第三个参数mode,表示该
文件的访问权限。
* O_EXCL 如果同时指定了O_CREAT,并且文件已存在,则出错返回。
* O_TRUNC 如果文件已存在,并且以只写或可读可写方式打开,则将其长度截断(Truncate)为0字节。
* O_NONBLOCK 对于设备文件,以O_NONBLOCK方式打开可以做非阻塞I/O(Nonblock I/
O),非阻塞I/O在下一节详细讲解。
注意open函数与C标准I/O库的fopen函数有些细微的区别:
以可写的方式fopen一个文件时,如果文件不存在会自动创建,而open一个文件时必须
明确指定O_CREAT才会创建文件,否则文件不存在就出错返回。
以w或w+方式fopen一个文件时,如果文件已存在就截断为0字节,而open一个文件时必
须明确指定O_TRUNC才会截断文件,否则直接在原来的数据上改写。
第三个参数mode指定文件权限,可以用八进制数表示,比如0644表示-rw-r-r–,也可
以用S_IRUSR、S_IWUSR等宏定义按位或起来表示,详见open(2)的Man Page。要注意的是,
文件权限由open的mode参数和当前进程的umask掩码共同决定。
补充说明一下Shell的umask命令。Shell进程的umask掩码可以用umask命令查看:
$ umask
0002
用touch命令创建一个文件时,创建权限是0666,而touch进程继承了Shell进程的umask
掩码,所以最终的文件权限是0666&022=0644。
$ touch file123
$ ls -l file123
-rw-rw-r-- 1 xingwenpeng xingwenpeng 0 9月 11 23:48 file123
4 第1章文件I/O
同样道理,用gcc编译生成一个可执行文件时,创建权限是0777,而最终的文件权限是
0777 & 022 = 0755。
xingwenpeng@ubuntu:~$ umask
0002
xingwenpeng@ubuntu:~$ gcc main.c
xingwenpeng@ubuntu:~$ ls -l a.out
-rwxrwxr-x 1 xingwenpeng xingwenpeng 7158 9月 11 23:51 a.out
我们看到的都是被umask掩码修改之后的权限,那么如何证明touch或gcc创建文件的权
限本来应该是0666和0777呢?我们可以把Shell进程的umask改成0,再重复上述实验:
$ rm file123 a.out
$ umask 0
$ touch file123
$ ls -l file123
-rw-rw-rw- 1 xingwenpeng xingwenpeng 0 9月 11 23:52 file123
$ gcc main.c
$ ls -l a.out
-rwxrwxr-x 1 xingwenpeng xingwenpeng 7158 9月 11 23:52 a.out
现在我们自己写一个程序,在其中调用open(“somefile”, O_WRONLY | O_CREAT,
0664);创建文件,然后在Shell中运行并查看结果:
close函数关闭一个已打开的文件:
#include <unistd.h>
int close(int fd);
返回值:成功返回0,出错返回-1并设置errno
参数fd是要关闭的文件描述符。需要说明的是,当一个进程终止时,内核对该进程所有
尚未关闭的文件描述符调用close关闭,所以即使用户程序不调用close,在终止时内核也会
自动关闭它打开的所有文件。但是对于一个长年累月运行的程序(比如网络服务器),打开
的文件描述符一定要记得关闭,否则随着打开的文件越来越多,会占用大量文件描述符和系
统资源。
由open返回的文件描述符一定是该进程尚未使用的最小描述符。由于程序启动时自动打
开文件描述符0、1、2,因此第一次调用open打开文件通常会返回描述符3,再调用open就会
返回4。可以利用这一点在标准输入、标准输出或标准错误输出上打开一个新文件,实现重
定向的功能。例如,首先调用close关闭文件描述符1,然后调用open打开一个常规文件,
则一定会返回文件描述符1,这时候标准输出就不再是终端,而是一个常规文件了,再调用
printf就不会打印到屏幕上,而是写到这个文件中了。后面要讲的dup2函数提供了另外一种
办法在指定的文件描述符上打开文件。
1.3.2 最大打开文件个数
查看当前系统允许打开最大文件个数
1.4节read/write 5
cat /proc/sys/fs/file-max
当前默认设置最大打开文件个数1024
ulimit -a
修改默认设置最大打开文件个数为4096
ulimit -n 4096
1.4 read/write
read函数从打开的设备或文件中读取数据。
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
返回值:成功返回读取的字节数,出错返回-1并设置errno,如果在调read之前已到达文件末尾,则这次read返回0
参数count是请求读取的字节数,读上来的数据保存在缓冲区buf中,同时文件的当前读
写位置向后移。注意这个读写位置和使用C标准I/O库时的读写位置有可能不同,这个读写
位置是记在内核中的,而使用C标准I/O库时的读写位置是用户空间I/O缓冲区中的位置。比
如用fgetc读一个字节,fgetc有可能从内核中预读1024个字节到I/O缓冲区中,再返回第一
个字节,这时该文件在内核中记录的读写位置是1024,而在FILE结构体中记录的读写位置是
1。注意返回值类型是ssize_t,表示有符号的size_t,这样既可以返回正的字节数、0(表
示到达文件末尾)也可以返回负值-1(表示出错)。read函数返回时,返回值说明了buf中
前多少个字节是刚读上来的。有些情况下,实际读到的字节数(返回值)会小于请求读的字
节数count,例如:
读常规文件时,在读到count个字节之前已到达文件末尾。例如,距文件末尾还有30个
字节而请求读100个字节,则read返回30,下次read将返回0。
从终端设备读,通常以行为单位,读到换行符就返回了。
从网络读,根据不同的传输层协议和内核缓存机制,返回值可能小于请求的字节数,后
面socket编程部分会详细讲解。
write函数向打开的设备或文件中写数据。
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
返回值:成功返回写入的字节数,出错返回-1并设置errno
6 第1章文件I/O
写常规文件时,write的返回值通常等于请求写的字节数count,而向终端设备或网络写
则不一定。
1.5 阻塞和非阻塞
读常规文件是不会阻塞的,不管读多少字节,read一定会在有限的时间内返回。从终端
设备或网络读则不一定,如果从终端输入的数据没有换行符,调用read读终端设备就会阻
塞,如果网络上没有接收到数据包,调用read从网络读就会阻塞,至于会阻塞多长时间也是
不确定的,如果一直没有数据到达就一直阻塞在那里。同样,写常规文件是不会阻塞的,而
向终端设备或网络写则不一定。
现在明确一下阻塞(Block)这个概念。当进程调用一个阻塞的系统函数时,该进程被
置于睡眠(Sleep)状态,这时内核调度其它进程运行,直到该进程等待的事件发生了(比
如网络上接收到数据包,或者调用sleep指定的睡眠时间到了)它才有可能继续运行。与睡
眠状态相对的是运行(Running)状态,在Linux内核中,处于运行状态的进程分为两种情
况:
正在被调度执行。CPU处于该进程的上下文环境中,程序计数器(eip)里保存着该进程
的指令地址,通用寄存器里保存着该进程运算过程的中间结果,正在执行该进程的指令,正
在读写该进程的地址空间。
就绪状态。该进程不需要等待什么事件发生,随时都可以执行,但CPU暂时还在执行另
一个进程,所以该进程在一个就绪队列中等待被内核调度。系统中可能同时有多个就绪的进
程,那么该调度谁执行呢?内核的调度算法是基于优先级和时间片的,而且会根据每个进程
的运行情况动态调整它的优先级和时间片,让每个进程都能比较公平地得到机会执行,同时
要兼顾用户体验,不能让和用户交互的进程响应太慢。
下面这个小程序从终端读数据再写回终端。
1.5.1 阻塞读终端
#include <unistd.h>
#include <stdlib.h>
int main(void)
{
char buf[10];
int n;
n = read(STDIN_FILENO, buf, 10);
if (n < 0) {
perror("read STDIN_FILENO");
exit(1);
}
write(STDOUT_FILENO, buf, n);
return 0;
}
执行结果如下:
1.5节阻塞和非阻塞 7
$ ./a.out
hello(回车)
hello
$ ./a.out
hello world(回车)
hello worl$ d
bash: d: command not found
第一次执行a.out的结果很正常,而第二次执行的过程有点特殊,现在分析一下:
Shell进程创建a.out进程,a.out进程开始执行,而Shell进程睡眠等待a.out进程退
出。
a.out调用read时睡眠等待,直到终端设备输入了换行符才从read返回,read只读走10
个字符,剩下的字符仍然保存在内核的终端设备输入缓冲区中。
a.out进程打印并退出,这时Shell进程恢复运行,Shell继续从终端读取用户输入的命
令,于是读走了终端设备输入缓冲区中剩下的字符d和换行符,把它当成一条命令解释执
行,结果发现执行不了,没有d这个命令。
如果在open一个设备时指定了O_NONBLOCK标志,read/write就不会阻塞。以read为例,
如果设备暂时没有数据可读就返回-1,同时置errno为EWOULDBLOCK(或者EAGAIN,这两个
宏定义的值相同),表示本来应该阻塞在这里(would block,虚拟语气),事实上并没
有阻塞而是直接返回错误,调用者应该试着再读一次(again)。这种行为方式称为轮询
(Poll),调用者只是查询一下,而不是阻塞在这里死等,这样可以同时监视多个设备:
while(1) {
非阻塞read(设备1);
if(设备1有数据到达)
处理数据;
非阻塞read(设备2);
if(设备2有数据到达)
处理数据;
...
}
如果read(设备1)是阻塞的,那么只要设备1没有数据到达就会一直阻塞在设备1的read
调用上,即使设备2有数据到达也不能处理,使用非阻塞I/O就可以避免设备2得不到及时处
理。
非阻塞I/O有一个缺点,如果所有设备都一直没有数据到达,调用者需要反复查询做无
用功,如果阻塞在那里,操作系统可以调度别的进程执行,就不会做无用功了。在使用非阻
塞I/O时,通常不会在一个while循环中一直不停地查询(这称为Tight Loop),而是每延迟
等待一会儿来查询一下,以免做太多无用功,在延迟等待的时候可以调度其它进程执行。
while(1) {
非阻塞read(设备1);
if(设备1有数据到达)
处理数据;
非阻塞read(设备2);
if(设备2有数据到达)
8 第1章文件I/O
处理数据;
...
sleep(n);
}
这样做的问题是,设备1有数据到达时可能不能及时处理,最长需延迟n秒才能处理,而
且反复查询还是做了很多无用功。以后要学习的select(2)函数可以阻塞地同时监视多个设
备,还可以设定阻塞等待的超时时间,从而圆满地解决了这个问题。
以下是一个非阻塞I/O的例子。目前我们学过的可能引起阻塞的设备只有终端,所以我
们用终端来做这个实验。程序开始执行时在0、1、2文件描述符上自动打开的文件就是终
端,但是没有O_NONBLOCK标志。所以就像例 28.2 “阻塞读终端”一样,读标准输入是阻塞
的。我们可以重新打开一遍设备文件/dev/tty(表示当前终端),在打开时指定O_NONBLOCK
标志。
1.5.2 非阻塞读终端
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#define MSG_TRY "try again\n"
int main(void)
{
char buf[10];
int fd, n;
fd = open("/dev/tty", O_RDONLY|O_NONBLOCK);
if(fd<0) {
perror("open /dev/tty");
exit(1);
}
tryagain:
n = read(fd, buf, 10);
if (n < 0) {
if (errno == EAGAIN) {
sleep(1);
write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
goto tryagain;
}
perror("read /dev/tty");
exit(1);
}
write(STDOUT_FILENO, buf, n);
close(fd);
return 0;
}
1.6节lseek 9
以下是用非阻塞I/O实现等待超时的例子。既保证了超时退出的逻辑又保证了有数据到
达时处理延迟较小。
1.5.3 非阻塞读终端和等待超时
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#define MSG_TRY "try again\n"
#define MSG_TIMEOUT "timeout\n"
int main(void)
{
char buf[10];
int fd, n, i;
fd = open("/dev/tty", O_RDONLY|O_NONBLOCK);
if(fd<0) {
perror("open /dev/tty");
exit(1);
}
for(i=0; i<5; i++) {
n = read(fd, buf, 10);
if(n>=0)
break;
if(errno!=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));
else
write(STDOUT_FILENO, buf, n);
close(fd);
return 0;
}
1.6 lseek
每个打开的文件都记录着当前读写位置,打开文件时读写位置是0,表示文件开头,通
常读写多少个字节就会将读写位置往后移多少个字节。但是有一个例外,如果以O_APPEND方
式打开,每次写操作都会在文件末尾追加数据,然后将读写位置移到新的文件末尾。lseek
和标准I/O库的fseek函数类似,可以移动当前读写位置(或者叫偏移量)。
#include <sys/types.h>
10 第1章文件I/O
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
参数offset和whence的含义和fseek函数完全相同。只不过第一个参数换成了文件描述
符。和fseek一样,偏移量允许超过文件末尾,这种情况下对该文件的下一次写操作将延长
文件,中间空洞的部分读出来都是0。
若lseek成功执行,则返回新的偏移量,因此可用以下方法确定一个打开文件的当前偏
移量:
off_t currpos;
currpos = lseek(fd, 0, SEEK_CUR);
这种方法也可用来确定文件或设备是否可以设置偏移量,常规文件都可以设置偏移量,
而设备一般是不可以设置偏移量的。如果设备不支持lseek,则lseek返回-1,并将errno
设置为ESPIPE。注意fseek和lseek在返回值上有细微的差别,fseek成功时返回0失败时返
回-1,要返回当前偏移量需调用ftell,而lseek成功时返回当前偏移量失败时返回-1。
1.7 fcntl
先前我们以read终端设备为例介绍了非阻塞I/O,为什么我们不直接对STDIN_FILENO做
非阻塞read,而要重新open一遍/dev/tty呢?因为STDIN_FILENO在程序启动时已经被自动
打开了,而我们需要在调用open时指定O_NONBLOCK标志。这里介绍另外一种办法,可以用
fcntl函数改变一个已打开的文件的属性,可以重新设置读、写、追加、非阻塞等标志(这
些标志称为File Status Flag),而不必重新open文件。
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd, struct flock *lock);
获取和设置文件的访问控制属性
这个函数和open一样,也是用可变参数实现的,可变参数的类型和个数取决于前面的
cmd参数。下面的例子使用F_GETFL和F_SETFL这两种fcntl命令改变STDIN_FILENO的属性,加
上O_NONBLOCK选项,实现和例 28.3 “非阻塞读终端”同样的功能。
1.7.1 用fcntl改变File Status Flag
#include <unistd.h>
#include <fcntl.h>
1.8节ioctl 11
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#define MSG_TRY "try again\n"
int main(void)
{
char buf[10];
int n;
int flags;
flags = fcntl(STDIN_FILENO, F_GETFL);
flags |= O_NONBLOCK;
if (fcntl(STDIN_FILENO, F_SETFL, flags) == -1) {
perror("fcntl");
exit(1);
}
tryagain:
n = read(STDIN_FILENO, buf, 10);
if (n < 0) {
if (errno == EAGAIN) {
sleep(1);
write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
goto tryagain;
}
perror("read stdin");
exit(1);
}
write(STDOUT_FILENO, buf, n);
return 0;
}
1.8 ioctl
ioctl用于向设备发控制和配置命令,有些命令也需要读写一些数据,但这些数据是
不能用read/write读写的,称为Out-of-band数据。也就是说,read/write读写的数据是
in-band数据,是I/O操作的主体,而ioctl命令传送的是控制信息,其中的数据是辅助的数
据。例如,在串口线上收发数据通过read/write操作,而串口的波特率、校验位、停止位通
过ioctl设置,A/D转换的结果通过read读取,而A/D转换的精度和工作频率通过ioctl设置。
#include <sys/ioctl.h>
int ioctl(int d, int request, ...);
获取和设置文件特有的物理属性
d是某个设备的文件描述符。request是ioctl的命令,可变参数取决于request,通常是
一个指向变量或结构体的指针。若出错则返回-1,若成功则返回其他值,返回值也是取决于
request。
以下程序使用TIOCGWINSZ命令获得终端设备的窗口大小。
12 第1章文件I/O
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
int main(void)
{
struct winsize size;
if (isatty(STDOUT_FILENO) == 0)
exit(1);
if(ioctl(STDOUT_FILENO, TIOCGWINSZ, &size)<0) {
perror("ioctl TIOCGWINSZ error");
exit(1);
}
printf("%d rows, %d columns\n", size.ws_row, size.ws_col);
return 0;
}
在图形界面的终端里多次改变终端窗口的大小并运行该程序,观察结果


0 0
原创粉丝点击