文章概括描述文段
上篇文章中介绍了linux中基本的io处理操作,对于一般的文件读取是没有问题的,但是考虑到终端以及网络等可能存在阻塞情况下,这时候就需要我们使用标志位 O_NONBLOCK 。其次,对于文件的阻塞和非阻塞操作,如果是一个已经打开的文件呢又该如何操作,fcntl 便应运而生了。
阻塞与非阻塞
读常规文件是不会阻塞的,不管读多少字节,read一定会在有限的时间内返回。从终端设备或网络读则不一定,如果从终端输入的数据没有换行符,调用read读终端设备就会阻塞,如果网络上没有接收到数据包,调用read从网络读就会阻塞,至于会阻塞多长时间也是不确定的,如果一直没有数据到达就一直阻塞在那里。同样,写常规文件是不会阻塞的,而向终端设备或网络写则不一定。
现在明确一下阻塞(Block)这个概念。当进程调用一个阻塞的系统函数时,该进程被置于睡眠(Sleep)状态,这时内核调度其它进程运行,直到该进程等待的事件发生了(比如网络上接收到数据包,或者调用sleep指定的睡眠时间到了)它才有可能继续运行。与睡眠状态相对的是运行(Running)状态,在Linux内核中,处于运行状态的进程分为两种情况:
- 正在被调度执行。CPU处于该进程的上下文环境中,程序计数器(eip)里保存着该进程的指令地址,通用寄存器里保存着该进程运算过程的中间结果,正在执行该进程的指令,正在读写该进程的地址空间。
- 就绪状态。该进程不需要等待什么事件发生,随时都可以执行,但CPU暂时还在执行另一个进程,所以该进程在一个就绪队列中等待被内核调度。系统中可能同时有多个就绪的进程,那么该调度谁执行呢?内核的调度算法是基于优先级和时间片的,而且会根据每个进程的运行情况动态调整它的优先级和时间片,让每个进程都能比较公平地得到机会执行,同时要兼顾用户体验,不能让和用户交互的进程响应太慢。
阻塞读终端
对于终端设备来说,如果终端没有输入,那么read将一直阻塞,知道用户输入enter后才会返回结果。
#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;
}
非阻塞读终端
如果在open一个设备时指定了O_NONBLOCK标志,read/write就不会阻塞。以read为例,如果设备暂时没有数据可读就返回-1,同时置errno为EWOULDBLOCK(或者EAGAIN,这两个 宏定义的值相同),表示本来应该阻塞在这里(would block,虚拟语气),事实上并有阻塞而是直接返回错误,调用者应该试着再读一次(again)。这种行为方式称为轮询 (Poll),调用者只是查询一下,而不是阻塞在这里死等,这样可以同时监视多个设备。
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#define DEV_TTY "/dev/tty"
#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 device error!");
exit(1);
}
// 轮训标志
try_again:
// 非阻塞读数据,立即返回
n = read(fd,buf,sizeof(buf));
if(n < 0){
// 未读取到任何数据,重新读取
if(errno == EAGAIN)
{
// 输出重读标志
write(STDOUT_FILENO,MSG_TRY,strlen(MSG_TRY));
// 睡眠一会
sleep(3);
goto try_again;
}
else
{
perror("read device error!");
exit(1);
}
}
// 写入输出终端
write(STDOUT_FILENO,buf,n);
close(fd);
return 0;
}
用非阻塞I/O实现等待超时的例子。既保证了超时退出的逻辑又保证了有数据到
达时处理延迟较小。
非阻塞升级,等待超时
对于非阻塞读取终端数据,逻辑上是采用轮训的方式,这样处理起来有一点不好就是要一直的sleep睡眠等待。在一定程度上造成性能的浪费,。当然排除select,poll以及epoll等高效的异步IO转接方式,另一种可以替代的方式就是规定在一定的时间范围呢进行轮训,如果超时了就直接推出,给出有效提示。
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <stdio.h>
#define DEV_TTY "/dev/tty"
#define MSG_TRY "try again \n"
#define MSG_TIMEOUT "read device time out!\n"
int main(void)
{
char buf[10];
int fd,n;
fd = open(DEV_TTY,O_RDONLY | O_NONBLOCK);
if(fd<0)
{
perror("open device error!");
exit(1);
}
// ls * 10 的超时等待
for (int i = 0; i < 10; ++i)
{
/* code */
n = read(fd,buf,sizeof(buf));
if(n < 0){
if(errno == EAGAIN)
{
// 未读取有效数据,睡眠等待轮训
write(STDOUT_FILENO,MSG_TRY,strlen(MSG_TRY));
sleep(1);
}
else
{
/* code */
perror("read device error!");
exit(1);
}
}
else
{
// 读取到有效数据,直接打印输出
write(STDOUT_FILENO,buf,n);
close(fd);
return 0;
}
}
// 读取超时,打印输出超时信息
write(STDOUT_FILENO,MSG_TIMEOUT,strlen(MSG_TIMEOUT));
close(fd);
return 0;
}
使用 fcntl 修改文件状态位
先前我们以read终端设备为例介绍了非阻塞I/O,为什么我们不直接对STDIN_FILENO做非阻塞read,而要重新open一遍/dev/tty呢?因为STDIN_FILENO在程序启动时已经被自动打开了,而我们需要在调用open时指定O_NONBLOCK标志。这里介绍另外一种办法,可以用 fcntl 函数改变一个已打开的文件的属性,可以重新设置读、写、追加、非阻塞等标志(这些标志称为File Status Flag),而不必重新open文件。
概述
#include <fcntl.h>
int fcntl(int fildes, int cmd, ...);
参数描述
filsed: 唯一文件描述符
cmd: 操作命令
根据cmd命令,我们可以选择对一打开文件描述符进行属性操作,例如通过命令 F_GETFD 来获取当前文件描述符的属性,通过 F_SETFD 来设置已经打开的文件描述符的属性,从而避免从新打开时添加对应的属性。
| cmd | 描述 |
|---|---|
| F_DUPFD | 复制文件描述词 |
| F_GETFD | 读取文件描述词标志 |
| F_SETFD | 设置文件描述词标志 |
| F_GETFL | 读取文件状态标志 |
| F_SETFL | 设置文件状态标志 |
| … | … |
更多设置参考 Man Page
在 F_SETFL 命令中,其中O_RDONLY, O_WRONLY, O_RDWR, O_CREAT, O_EXCL, O_NOCTTY 和 O_TRUNC不受影响,可以更改的标志有 O_APPEND,O_ASYNC, O_DIRECT, O_NOATIME 和 O_NONBLOCK。
返回值
返回值根据命令的不同有不同的返回值,例如 F_DUPFD 会返回一个新的文件描述符,F_GETFL 会返回文件的属性位等,如果出错会反悔 -1, 并且置位 errno,具体参考 Man Page。
实力代码
本片主要讲述文件属性值的设置,下面代码展示了如何通过 fctnl 函数对已经打开的文件进行非阻塞读取操作。
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <stdio.h>
#define DEV_TTY "/dev/tty"
#define MSG_TRY "try again \n"
#define MSG_TIMEOUT "read device time out!\n"
int main(void)
{
char buf[10];
int fd,n;
fd = open(DEV_TTY,O_RDONLY);
// 通过fcntl的命令 F_GETFL 获取打开文件的访问属性
int flags = fcntl(fd,F_GETFL);
printf("flags0 is %d\n",flags);
// 属性添加 O_NONBLOCK
flags |= O_NONBLOCK;
printf("flags1 is %d\n",flags);
// 通过命令 F_SETFL 将新的文件属性设置到文件中去
fcntl(fd,F_SETFL, flags);
if(fd<0)
{
perror("open device error!");
exit(1);
}
for (int i = 0; i < 5; ++i){
/* code */
n = read(fd,buf,sizeof(buf));
if(n < 0){
if(errno == EAGAIN){
write(STDOUT_FILENO,MSG_TRY,strlen(MSG_TRY));
sleep(1);
} else {
/* code */
perror("read device error!");
exit(1);
}
} else {
write(STDOUT_FILENO,buf,n);
close(fd);
return 0;
}
}
write(STDOUT_FILENO,MSG_TIMEOUT,strlen(MSG_TIMEOUT));
close(fd);
return 0;
}
当然 fctnl 的功能还有很多,比如文件锁(读共享,写私有)等,后续篇幅会在锁机制中统一介绍,这里单纯的介绍一下设置文件非阻塞访问属性。
lseek修改文件访问指针
lseek函数的作用是用来重新定位文件读写的位移。
概述
#include <unistd.h>
off_t lseek(int fildes, off_t offset, int whence);
参数解释
fildes: 文件描述符
offset: 为正则向文件末尾移动(向前移),为负数则向文件头部(向后移)
whence: 文件便偏移参考值
SEEK_SET:
从文件头部开始偏移offset个字节
SEEK_CUR:
从文件当前读写的指针位置开始,增加offset个字节的偏移量
SEEK_END:
文件偏移量设置为文件的大小加上偏移量字节
返回值
如果设备不支持lseek,则lseek返回-1,并将errno设置为ESPIPE。
注意 fseek 和 lseek 在返回值上有细微的差别, fseek 成功时返回 0 ,失败时返回 -1 ,要返回当前偏移量需调用 ftell ,而 lseek 成功时返回当前偏移量失败时返回 -1 。
代码示例
// 获取文件大小
int lseek(fd,0,SEEK_END)
// 扩充文件大小
int add_len = 1024*8;
int fd=open("test.txt",O_RDWR);
if(fd == -1)
{
perror("open test.txt");
exit(-1);
}
lseek(fd,add_len-1,SEEK_END);
总结
经过前面博文中对 open,read,write,close 等函数的介绍,基本能够完成一般文件的 IO 操作,此篇幅有详细介绍了阻塞与非阻塞读取数据,文件属性修改函数 fcntl 的功能,其能够实现对打开文件的属性操作,函数功能非常强大。同时,lseek函数能够获取一打开文件的大小,还可以扩充文件的大小(lseek后必须进行一个write操作)。博文参考刑文鹏老师的Linux系统编程资料,特此记录以备不时之需。
参考学习资料邢文鹏老师