高级I/O:
非阻塞I/O:
系统调用分成两类:“低速”系统调用和其他。低速系统调用是可能会使进程永远阻塞的一类系统调用:
1、如果某些文件类型(如读管道、终端设备和网络设备)的数据并不存在,读操作可能会使调用者永远阻塞
2、如果数据不能被相同的文件类型立即接受(如管道中无空间、网络流控制),写操作可能会使调用者永远阻塞
3、在某种条件发生之前打开某些文件类型可能会发生阻塞(如要打开一个终端设备,需要先等待与之连接的调制器应答,又如
若只写模式打开FIFO,那么在没有其他进程已读模式打开该FIFO时也要等待)
4、对已经加上强制性记录锁的文件进行读写
5、某些ioctl操作
6、某些进程间通信函数
程序是一个非阻塞I/O的实例,它从标准输入读500 000字节,并试图将它们写到标准输出上。该程序先将标准输出设置为非阻塞的,然后
用for循环进行输出,每次write调用的结果都在标准错误上打印出来。函数clr_fl类似于set_fl。这个新函数清除1个或者多个标值位。
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
char buf[500000];
void set_fl(int fd, int flags)
{
int val;
if ((val = fcntl(fd, F_GETFL, 0)) < 0) {
printf("fcntl F_GETFL error
");
exit(1);
}
val |= flags;
if (fcntl(fd, F_SETFL, val) < 0) {
printf("fcntl F_SETFL error
");
exit(1);
}
}
void clr_fl(int fd, int flags)
{
int val;
if ((val = fcntl(fd, F_GETFL, 0)) == -1) {
printf("fcntl F_GETFL error
");
exit(1);
}
val &= ~flags;
if (fcntl(fd, F_SETFL, val) == -1) {
printf("fcntl F_SETFL error
");
exit(1);
}
}
int main(void)
{
int ntowrite,nwrite;
char *ptr;
ntowrite=read(STDIN_FILENO,buf,sizeof(buf));
fprintf(stderr,"read %d bytes
",ntowrite);
set_fl(STDOUT_FILENO,O_NONBLOCK);
ptr=buf;
while(ntowrite>0)
{
errno=0;
nwrite=write(STDOUT_FILENO,ptr,ntowrite);
fprintf(stderr,"nwrite=%d,errno=%d
",nwrite,errno);
if(nwrite>0){
ptr+=nwrite;
ntowrite-=nwrite;
}
}
clr_fl(STDOUT_FILENO,O_NONBLOCK);
exit(0);
}
记录锁:
fcntl记录锁:
#include <fcntl.h>
int fcntl(int fd,int cmd,.../*struct flock *flockptr*/);
返回值:成功,依赖于cmd,否则,返回-1
cmd是F_GETLK、F_SETLK、F_SETLKW
第三个参数(调用flockptr)是一个指向flock结构的指针:
struct flock{
short l_type;//F_RDLCK,F_WRLCK,F_UNLCK
short l_whence;//SEEK_SET,SEEK_CUR,SEEK_END
off_t l_start;//偏移字节,相对于l_whence
off_t l_len;//长度,以字节表示;0表示锁定到EOF
pid_t l_pid;//回到F_GETLK(标记:进程的ID持有的锁能阻塞当前进程(仅由F_GETLK返回))
};
上面的只使用于不同进程提出的锁请求,并不适用于单个进程提出的多个锁请求。
如果一个进程对一个文件区间已经有一把锁,后来该进程又企图在同一文件区间再加一把锁,那么新锁将替换已有的锁。
fcntl函数的3种命令:
F_GETLK 判断flockptr所描述的锁是否会被另外一把锁排斥(阻塞)。如果存在一把锁,它阻止创建由flockptr
所描述的锁,则该现有锁的信息将重写flockptr指向的信息。如果不存在这种情况,则除了将l_type设置为
F_UNLCK之外,flockptr所指向结构种的其他信息保持不变
F_SETLK 设置由flockptr所描述的锁。如果我们试图获得一把读锁(l_type为F_RDLCK)或写锁(l_type为F_WRLCK),
而兼容性规则阻止系统给我们这把锁,那么fcntl会立即出错返回,此是errno设置为EACCES或者EAGAIN。
清除由flockptr指定的锁(l_type为F_UNLCK).
F_SETLKW 是F_SETLK的阻塞版本(w表示的是等待wait)。如果所请求的读锁或者写锁因另外一个进程当前已经对所请求区域的
某部分进行了加锁而不能被授予,那么调用进程会被置为休眠。如果请求创建的锁已经可用,或者休眠由信号中断,则
该进程被唤醒
#include<unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
int lock_reg(int fd,int cmd,int type,off_t offset,int whence,off_t len)
{
struct flock lock;
lock.l_type=type;//F_RDLCK,F_WRLCK,F_UNLCK
lock.l_start=offset;//字节偏移量,相对于l_whence
lock.l_whence=whence;//SEEK_SET,SEEK_CUR,SEEK_END
lock.l_len=len;//子节(0 代表的是EOF)
return(fcntl(fd,cmd,&lock));
}
加锁或者解锁一个文件区域的函数
定义了一个函数lock_test,测试一把锁:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
pid_t lock_test(int fd,int type,off_t offset,int whence,off_t len)
{
struct flock lock;
lock.l_type=type;//F_RDLCK,F_WRLCK,F_UNLCK
lock.l_start=offset;//字节偏移量,相对于l_whence
lock.l_whence=whence;//SEEK_SET,SEEK_CUR,SEEK_END
lock.l_len=len;//子节(0 代表的是EOF)
if(fcntl(fd,F_GETLK,&lock)<0)
printf("fcntl error
");
if(lock.l_type==F_UNLCK)
return 0;
return lock.l_pid;
}
测试一个锁条件的函数
死锁:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
static void lockabyte(const char *name,int fd,off_t offset)
{
if(writew_lock(fd,offset,SEEK_SET,1)<0)
printf("%s:writew_lock error
",name);
printf("%s:got the lock,byte %lld
",name,(long long)offset);
}
int main(void)
{
int fd,
pid_t pid;
if((fd=creat("templock",FILE_MODE))<0)
printf("creat error
");
if(write(fd,"ab",2)!=2)
printf("write error
");
TELL_WAIT();
if((pid=fork())<0)
printf("fork error
")
else if(pid==0)
{
lockabyte("child",fd,0);
TELL_PARENT(getppid());
WAIT_PARENT();
lockabyte("child",fd,1);
}
else
{
lockabyte("parent",fd,1);
TELL_CHILD(pid);
WAIT_CHILD();
lockabyte("parent",fd,0);
}
exit(0);
}
死锁检测实例。
检测到死锁时,内核必须选择一个进程出错返回。具体是父进程接受到错误还是子进程接受到,取决于系统。
锁的隐含继承和释放:
1、锁与进程和文件两者相关联。这由两重含义:第一重很明显,当一个进程终止时,它所建立的锁全部释放;第二重则不太明显,
无论一个描述符何时关闭,该进程通过这一描述符引用的文件上的任何一把锁都会释放(这些锁都是该进程设置的)。这意味着,如果
执行下列4步:
fd1=open(pathname,...);
read_lock(fd1,...);
fd2=dup(fd1);
close(fd2);
则在close(fd2)后,在fd1上设置的锁会被释放。如果将dup替换为open,其效果也一样的:
fd1=open(pathname,...);
read_lock(fd1,...);
fd2=open(pathname,...);
close(fd2);
2、由fork产生的子进程不继承父进程所设置的锁。这意味着,若一个进程得到一把锁,然后调用fork,那么对于父进程获得的锁而言,子进程
被视为另一个进程。对于通过fork从父进程继承过来的描述符,子进程需要调用fcntl才能获得它自己的锁。这个约束时有道理的,因为锁的作用
是阻止多个进程同时写同一个文件。如果子进程通过fork继承父进程的锁,则父进程和子进程就可以同时写同一个文件。
3、在执行exec后,新程序可以继承原执行程序的锁。但是注意,如果对一个文件描述符设置了执行时关闭标志,那么当作为exec的一部分关闭该
文件描述符时,将释放相应文件的所有锁。
在文件整体上加一把锁:
#include <unistd.h>
#include <fcntl.h>
int lockfile(int fd)
{
struct flock fl;
fl.l_type=F_WRLCK;
fl.l_start=0;
fl.l_whence=SEEK_SET;
fl.l_len=0;
return(fcntl(fd,F_SETLK,&fl));
}
4、在文件尾加锁
在相对于文件尾端的字节范围加锁和解锁时需要特别小心。大多数实现按照l_whence的SEEK_CUR或者
SEEK_END值,用l_start以及文件当前位置或者当前长度得到绝对文件偏移量。但是,常常需要相对于
文件的当前长度指定一把锁,但又不能调用fstat来得到当前文件爱年长度,因为我们在该文件上没有锁。
如果想解除的锁中包括第一次write所写的1个字节,那么应指定长度为-1。负的长度值表示在指定偏移量
之前的字节数。
5、建议性锁和强制性锁
建议性锁:并不能阻止对数据库文件有写权限的任何进程写这个数据库文件
强制性锁:内核检查每一个open、read和write,验证调用进程是否违背了正在访问的文件上的某一把锁。
确定是否支持强制性锁:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/wait.h>
int main(int argc,char *argv[])
{
int fd;
pid_t pid;
char buf[5];
struct stat statbuf;
if(argc!=2)
{
fprintf(stderr,"usage:%s filename
",argv[0]);
exit(1);
}
if((fd=open(argc[1],O_RDWR|O_CREAT|O_TRUNC,FILE_MODE))<0)
printf("open error
");
if(write(fd,"abcdef",6)!=6)
printf("write error
");
if(fstat(fd,&statbuf)<0)
printf("fstat error
");
if(fchmod(fd,(statbuf.st_mode & ~S_IXGRP | S_ISGID))<0)
printf("fchmod error");
TELL_WAIT();
if((pid=fork())<0)
{
printf("fork error
");
}
else if(pid>0)
{
if(write_lock(fd,0,SEEK_SET,0)<0)
printf("write_lock error
");
TELL_CHILD(pid);
if(waitpid(pid,NULL,0)<0)
printf("waitpid error
");
}
else{
WAIT_PARENT();
set_fl(fd,O_NONBLOCK);
if(read_lock(fd,0,SEEK_SET,0)!=-1)
printf("child:read_lock succeded
");
printf("read_lock of already-locked region returns %d
",errno);
if(lseek(fd,0,SEEK_SET)==1)
printf("lseek error
");
if(read(fd,buf,2)<0)
printf("read failed (mandatory locking works)
");
else
printf("read ok (no mandatory locking),buf=%2.2s
",buf);
}
exit(0);
}
I/O多路转接:
当从一个描述符读,然后又写到另一个描述符,可以在下列形式的循环中使用阻塞I/O:
while((n=read(STDIN_FILENO,buf,BUFSIZ))>0)
if(write(STDOUT_FILENO,buf,n)!=0)
printf("write error
");
比较号的技术时使用I/O多路转接。为了使用这种技术,先构造一张我们感兴趣的描述符(通常不止一个)的列表,然后调用一个
函数,直到这些描述符的一个已准备好进行I/O时,该函数才返回。poll、pselect、select这3个函数使我们能够执行I/O多
路转接。
使用select时,必须包括<sys/select.h>。但较老的系统还要包括<sys/types.h>、<sys/time.h>、<unistd.h>
函数select和pselect:
在所有POSIX兼容的平台上,select函数使我们可以执行I/O多路转接。传给select的参数告诉内核:
1、我们关心的描述符
2、对于每个描述符我们关心的条件
3、愿意等待多长的时间
从select返回时,内核告诉我们:
1、已准背好的描述符总数量
#include <sys/select.h>
int select(int maxfdp1,fd_set *restrict readfds,fd_set *restrict writefds,
fd_set *restrict exceptfds,struct timeval *restrict tvptr);
返回值:准备就绪的描述符数目;若超时,返回0,若出错,返回-1
tvptr最后一个参数,它指定愿意等待的时间长度,单位为秒和微妙。
3种情况:
tvptr==NULL
永远等待。
tvptr->tv_sec==0 && tvptr->tv_usec==0
根本不等待。测试所有指定的描述符并立即返回。这是轮询系统找到多个描述符而不阻塞select函数的方法
tvptr->tv_sec!=0 || tvptr->tv_usec!=0
等待指定的秒数和微妙数。当指定的描述符之一已准备好,或当指定的时间值已经超过时立即返回。
readfds、writefds、exceptfds是指向描述符集的指针。说明了可读、可写或处于异常条件的描述符集合。
每个描述符集存储在一个fd_set数据类型种。这个数据类型是由实现选择的,它可以为每一个可能的描述符保持一位。
可以认为是一个很大的字符数组。
对于fd_set数据类型,唯一可以进行的处理是:分配一个这种类型的变量,将这种类型的一个变量值赋给同类型的另一个变量,或对这种类型
的变量使用下列4个函数中的一个:
#include <sys/select.h>
int FD_ISSET(int fd,fd_set *fdset);
返回值:若fd在描述符集中,返回非0,否则,返回0
void FD_CLR(int fd,fd_set *fdset);
void FD_SET(int fd,fd_set *fdset);
void FD_ZERO(fd_set *fdset);
可实现为宏或者函数。
调用FD_ZERO将一个fd_set变量的所有位设置为0.
要开启描述符集中的一位,可以调用FD_SET
FD_CLR可以清除一位。
FD_ISSET测试描述符集中的一个指定为是否打开
poll函数:
poll函数可用于任何类型的文件描述符。
#include <poll.h>
int poll(struct pollfd fdarray[],nfds_t nfds,int timeout);
返回值:准备就绪的描述符的数目,若超时,返回0,出错返回-1
struct pollfd{
int fd;//文件描述符来检查,或<0忽略
short events;//感兴趣的fd
short revents;//发生在FD上的事件
};
异步I/O:
AIO接口的API非常简单,它为数据传输提供了必需的功能,并给出了两个不同的通知模型。以下是AIO的接口函数:
1) aio_read,请求异步读操作。
2) aio_error,检查异步请求的状态。
3) aio_return,获得完成的异步请求的返回状态。
4) aio_write,请求异步写操作。
5) aio_suspend,挂起调用进程,直到一个或多个异步请求已经完成(或失败)。
6) aio_cancel,取消异步I/O请求。
7) aio_listio,发起一系列I/O操作。
函数readv和writev:
函数用于在一次函数调用中读、写多个非连续缓冲区。
#include <sys/uio.h>
ssize_t readv(int fd,const struct iovec *iov,int iovcnt);
ssize_t writev(int fd,const struct iovec *iov,int iovcnt);
返回值:已读或已写的字节数;若出错,返回-1
第二个参数是指向iovec结构数组的一个指针:
struct iovec{
void *iov_base;//buffer开始地址
size_t iov_len;//buffer大小
};
函数readn和writen:
函数只是按需多次调用read和weite直到读、写了N字节数据
ssize_t readn(int fd,void *buf,size_t nbytes);
ssize_t writen(int fd,void *buf,size_t nbytes);
返回值:读、写的字节数,出错,返回-1
存储映射I/O:
能将一个磁盘文件映射到存储空间中的一个缓冲区上,于时,当从缓冲区取数据时,就相当于读文件中的相应字节。
为了使用这种功能,应首先告诉内核将一个给定的文件映射到一个存储区域中。这是由mmap函数实现的:
#include <sys/mman.h>
void *mmap(void *addr,size_t len,ing prot,int flag,int fd,off_t off);
返回值:成功,返回映射区的起始地址,出错,返回MAP_FAILED
addr参数用于指定映射存储区的起始地址。
fd参数时指定被映射文件的描述符。
len参数时映射的字节数。
off是映射字节在文件的起始偏移量
prot参数指定了映射存储区的保护要求。
mprotect可以更改一个现有映射的权限:
#include <sys/mman.h>
int mprotect(void *addr,size_t len,int prot);
返回值:成功,0,失败,-1
存储映射区:
int msync(void *addr,size_t len,int flags);
返回值:成功,0,失败,-1
解除映射区:
int munmap(void *addr,size_t len);
返回值:成功,返回0,失败,返回-1
用存储映射I/O复制文件:(头文件不全)
int main(int argc,char *argv[])
{
int fdin,fdout;
void *src,*dst;
size_t copysz;
struct stat sbuf;
off_t fsz=0;
if(argc!=3)
printf("usage:%s < fromfile> <tofile>",argv[0]);
if((fdin=open(argv[1],O_RDONLY))<0)
printf("can't open %s for reading",argv[1]);
if((fdout=open(argv[2],O_RDWR | O_CREAT | O_TRUNC,FILE_MODE))<0)
printf("can't write %s for writing",argv[2]);
if(fstat(fdin,&sbuf)<0)
printf("fstat error
");
if(ftruncate(fdout,sbuf.st_size)<0)//ftruncate会将参数fd指定的文件大小改为参数length指定的大小
printf("ftruncate error
");
while(fsz<sbuf.st_size)
{
if((sbuf.st_size-fsz)>COPYNCR)
copysz=COPYNCR;
else
copysz=sbuf.st_size-fsz;
if((src=mmap(0,copysz,PROT_READ,MAP_SHARED,fdin,fsz))==MAP_FAILED)
printf("mmap error for input
");
if((dst=mmap(0,copysz,PROT_READ | P
ROT_WRITE,MAP_SHARED,fdout,fsz))==MAP_FAILED)
printf("mmap error for output
");
memcpy(dst,src,copysz);
munmap(src,copysz);
munmap(dst,copysz);
fsz+=copysz;
}
exit(0);
}