1.基本介绍:Linux中一切都是“文件”。文件只能必须先打开才能访问,文件打开方式只有三种,只读,只写,读写模式。文件打开后是通过唯一表示符来引用的,其是打开文件关联的元数据到文件本身的映射,在Linux内核中,文件被一个整数描述,成为文件描述符,大多数对文件操作都是涉及文件描述符来说明。
2.访问文件之前,必须要打开文件,通过系统调用,使用open()函数
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char* name,int flags);
int open(const char* name,int flags,mode_t mode);
如果open调用成功,返回文件描述符,name:文件的起始位置(可以是路径),打开方式有参数flages确定。
flages:其可以是一位或者是多个标志位的或运算组合,下面是常见的几种标志位
O_RDONLY:只读, O_WRONLY:只写, O_RDWR:读或者写
int fd;
fd=open("/home/user/test",O_RDONLY);
if(fd==-1){
//error
}
如上是只以读的模式打开文件进行执行。注意,创建文件时,用户对每一个文件都有属性,假如用户对该文件只有读权限,就不能以写的方式打开它,反之依然,可以通过命令修改文件用户权限.其他更多flags不多用,自行查找。
补充,还可以用open()函数来创造新的文件,再创建新文件时,要附加mode参数,一般如下
//前提 用户要有写权限
int fd=open("name",O_WRONLY|O_CREAT|O_TRUNC,mode_t mode)
mode一般带S_IRWXU,即文件有读,写,读或写的权限。(本质是写入磁盘的权限由mode参数和用户文件创建的掩码进行运算获得。)上述操作等价于(mode取0666或0644),mode即指明创造文件时的所有者,组用户,所有用户三个方面的权限是怎样的。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int creat(const char* name,mode_t,mode);
3.write(),read()函数
对于网络编程来说,此两个函数不能用socket专属文件描述符去配合设置并使用,始终成为非阻塞I/O,但是可在系统编写文件的文件描述符下(open()调用),使之成为阻塞,非阻塞I/O。
先介绍read()函数:
#include <unistd.h>
ssize_t read(int fd,void* buf,size_t len);
调用open()创建的套接字时,会从fd指向文件当前的偏移量开始读len个字节,存入到buf的内存中,多次调用,fd指向文件的文件指针会自己移动,返回实际读取到的字节数,不一定真的会读len个字节。当文件读到末尾或者网络编程中对面连接的文件描述符已经关闭时,返回为0.不是阻塞。
对于返回-1时,可以查看errno(配合<errno.h>,<string.h>)查看情况,或errno=EAGAIN,表示当前没有数据可读,表示阻塞(只会在非阻塞设置下)
或errno!=EINTR或者!=EAGAIN,表明发生更严重的错误,可以关闭套接字了.(也适用于SOCKET类传递接受客户端或者服务端的消息)
errno!=EINTR 表示在读取任何字节之前接收到信号。调用可以重新执行
一般read可以循环读,比如:
ssize_t ret;
while((len!=0)&&(read=read(fd,buf,len))!=0){
if(ret!=-1){
if(errno==EINTR)
continue;
perror("read");
break;
}
len-=ret;//总长度减少
buf+=ret;//存入数组的指针进行移动
}
非阻塞(O_NONBLOCK)下需要判断errno==EINTR或 errno==EAGAIN.
SOCKET专属入下:
现在介绍write()函数:
#include <unistd.h>
ssize_t write(int fd,const void* buf,size_t count);
对于文件来说是从fd指向的文件中,将buf指向的指针开始写入count字节当中去,返回写入的实际字节数。只需要注意两点。1:以O_APPEND方式去写入文件,可以确保文件指针总是在文件末尾追加写,可以防止多线程的竞争问题。2.write()可以不用考虑非阻塞,不常用。
补充WRITE()的行为:当发起write()调用时,首先会将数据拷贝到缓冲区,内核收集数据后会进行数据排序,然后再写入磁盘(页回写机制)。所以可能会发生,数据还未存入磁盘,系统崩溃。所以就有了同步I/O,以确保数据能在何时写入磁盘。
同步I/O:
#include <unistd.h>
int fsync(int fd);
上述函数可以确保和文件描述符所指向的文件相关的·数据·都写到磁盘上,文件描述符必须以写方式打开,并且该函数会回调创建的时间戳或者其他文件属性。但函数fdatasync()却不会进行回调,执行比fsync()快。但上述两个函数不能同步更新文件目录项,所以需要对文件目录进行同步调用上述两个函数。
open()函数的标志位:O_SYNC。使得文件操作符对应的WRITE(),READ()。都是确保同步性,同时也是开销最大的,这个要注意。
int fd;
fd=open(file,O_WRONLY|O_SYNC);
所以WRITE(),READ()都是直接从内核与用户态相交互。
关闭文件:
#include <unistd.h>
int close(int fd);
close函数可以关闭当前进程的文件描述符与FD之间的映射,但注意关闭文件操作并非意味着该文件的数据已经被写入磁盘了,需要运用到同步操作确定。
下面是一般的文件打开调用过程:(操作系统内有详细解释)
4.lseek()函数:
一般情况下,I/O是线性的,但有时候我们需要跳跃式读取文件,所以lseek()系统调用能将文件描述符的位置指针设置为定制,lseek()只更新文件位置。
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd,off_t pos,int t);
lseek()函数的行为依靠参数t:
SEEK_CUR: 将文件位置设置为当前值再加上偏移量pos(可为负数),pos=0则返回当前位置的值。
SEEK_END:将文件位置设置为文件长度加上pos偏移量,pos=0说明设置为文件指针在末尾。
SEEK_SET:将文件位置设置为POS值,如果POS=0,则设置成文件开始。
文件读和写操作公用一个文件指针.例:
#include <iostream>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
int main(){
int fd,n;
char mag[]="hello world nide";
char ch;
fd=open("lseek.txt",O_RDWR|O_CREAT,0644);//这时 文件指针指向开始位置 0
if(fd<0){
perror("open error");
exit(1);
}
write(fd,mag,strlen(mag));//文件指针移动strlen(mag)个单位,但后面没东西了
lseek(fd,0,SEEK_SET);//回到文本最开始
while((n=read(fd,&ch,1))){
if(n<0){
perror("read errror");
exit(1);
}else{
write(STDOUT_FILENO,&ch,n);
}
}
close(fd);
return 0;
}
如果注释掉lseek函数 那么执行while循环读是无法读到任何东西的,因为现在文件指针指向nide后面,没有数据,只有通过lseek移动文件指针到最开始才行。
用SEEK-END可以扩充文件大小,但对处于文件末尾的文件指针使用写入数据的操作是没有意义的。
5.I/O多路复用。
1).select()函数(如何运用见网络编程笔记)
#include <sys/select.h>
int select (int n,fd_set* readfd,fd_set* writefd,fd_set* ex,struct timeval* timeout);
FD_CLR(int fd,fd_set* set);
FD_ISSET(int fd,fd_set* set);
FD_SET(int fd,fd_set* set);
FD_ZERO(fd_set* set);
select函数的2,3,4参数是等待不同事件的。2号参数是监听是否有数据可读,3号参数是监听是否有某个写操作是无阻塞完成的(即在监听事件内是否有通过文件描述符发生写事件),4号参数是监听监听的文件描述符是否有异常或者是出现外带数据等。成功返回时,每个集合都修改成只包含相应事件类型的I/O就绪的文件描述符。第一个参数是监听集合中,最大的文件描述符数+1,第五个参数是设置的超时事件。这里要注意,每一次SELECT()调用后,都要重置超时事件。
//FD_ZERO() 对fd_set变量进行全部归零(删除全部文件描述符)
fd_set writefd;
FD_ZERO(&writefd);
//FD_SET()往指定集中加入描述符以供监听 FD_CLR() 从指定集中删除指定文件描述符
FD_SET(fd,&writefd);
FD_CLR(fd,&writefd);
//FD_ISSET() 用于检查监听的文件描述符是否准备就绪。准备就绪返回非零值
if(FD_ISSET(fd,&readfds))
//fd is no blocking to read
select()实现可移植sleep()函数功能。通常如下操作可代替sleep()函数。
struct timeval tv;
tv.tv_sec=0;
tv.tv_usec=500;
select(0,NULL,NULL,NULL,&tv);//代替sleep函数
简单实例:
#include <iostream>
#include <sys/time.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/select.h>
const int timeout=5;
const int BUF_SIZE=1024;
int main(){
struct timeval tv;
fd_set readfd;
FD_ZERO(&readfd);
tv.tv_sec=timeout;
tv.tv_usec=0;
FD_SET(STDIN_FILENO,&readfd);
int ret=select(STDIN_FILENO+1,&readfd,NULL,NULL,&tv);
if(ret==-1){
perror("select");
exit(1);
}else if(ret==0){
std::cout<<"error timeout"<<std::endl;
return 0;
}
if(FD_ISSET(STDIN_FILENO,&readfd)){
char buf[BUF_SIZE];
int len=read(STDIN_FILENO,buf,sizeof(buf));
if(len==-1){
perror("read");
exit(1);
}
if(len){
buf[len]='\0';
std::cout<<buf<<std::endl;
}
}
return 0;
}
/*通俗易懂理解,&readfd中成员就是有数据传递过来了,我可以发生读事件 writefd中成员就是有数据可以写了,我可以给对面写数据,这里对应标准数据就是从标准输入中读取事件*/
2.)poll()函数
#include <poll.h>
int poll(struct pollfd* fds,nfds_t nfds,int timeout);
第一个参数是pollfd类的数组,里面存入的是需要监听的SOCKET,第二个参数是pollfd结构体的数量,第三个是超时时间(毫秒)。
pollfd结构体如下
struct pollfd {
int fd ;//要监视的文件描述符
short events;//监视文件描述符事件的位掩码
short revent;//结果事件的位掩码
}
内核在返回时会设置revents变量,event变量中的请求的所有的事件都可能在revent变量中返回。
对应事件情况如下:
若给上述编号,从1开始,1等价于2和3的组合,1和4的组合等价于select的读事件,POLLIN,POLLOUT本身针对I/O事件就是非阻塞可读/写事件。标识位可多种组合,不会发生冲突,就是一个文件描述符可能发生多种情况的事件(对准备结果分类讨论),比如监听某个文件描述符是否可读写:POLLIN|POLLOUT。
#include <iostream>
#include <sys/time.h>
#include <unistd.h>
#include <sys/types.h>
#include <poll.h>
const int timeout=5;
int main(){
struct pollfd fds[2];
int res;
fds[0].fd=STDIN_FILENO;
fds[0].events=POLLIN;
fds[1].fd=STDOUT_FILENO;
fds[1].events=POLLOUT;
res=poll(fds,2,timeout*1000);
if(res==-1){
perror("poll");
exit(1);
}
if(!res){
std::cout<<"time out"<<std::endl;
}
if(fds[0].revents&POLLIN){
std::cout<<"stdin is readable"<<std::endl;
}
if(fds[1].revents&POLLOUT){
std::cout<<"stdout is writeable"<<std::endl;
}
return 0;
}
在真正的网络编程中,pollfd结构体会重复被调用,但是每一次事情的结果之后,内核都会将revents清空。
标准输入对应的是读事件(对内读),标准输出对应的是写事件(对外写)。