Unix环境编程-高级IO
一.记录锁
1.记录锁的功能
当一个进程正在读或修改文件的某个部分时,它可以阻止其他进程修改同一文件区。我们不应该从字面上去理解记录锁,实际上它应该叫“区域锁”,因为它锁定的只是文件的一个(也可能是整个文件)。这个区域用来存放多用户的共享区。
2.记录锁的分类
记录锁分为共享读锁和独占写锁,前者也叫做共享锁后者也叫做排他锁。
3.加锁规则
如果一个进程对共享区加了共享读锁,其他进程只能加共享读锁。如果一个进程加了独占写锁,其他进程就不能加任何锁。
4.死锁
如果两个相互等待对方持有并且不释放(已被锁定)的资源是时,则这两个进程就处于死锁状态。如果一个进程已经控制了文件的一个加锁区域,然后它又试图对另一个进程控制的区域加锁,则它就会睡眠,在这种情况下,有发生死锁的可能性。
5.锁的隐含继承和释放
(1)锁与进程和文件两方面有关系,它和前者关系是:当一个进程结束后,他对文件加的锁也就消失了。它和后者的关系是:当进程close文件描述符,切断文件和进程的联系进程所创建的锁也会消失。
(2)由fork产生的子进程不继承父进程所设置的锁。这意味着,若一个进程得到一把锁,然后调用fork,那么对于父进程创建的锁而言,子进程被视为另一个进程,不会拥有该锁。
(3)在执行exec后,新进程可以继承原执行的锁。因为执行exec前后还是一个进程。我们只是改变进程执行的程序,并没有创建新的进程。
6.要注意的问题
    记录锁只是提供竞争进入某段代码区的功能,不会导致对文件操作失败。也就是说,我们对文件进行加锁后,我们还是可以对文件进行操作。
1.
| 名称:: | fcntl | 
| 功能: | 对文加解锁。 | 
| 头文件: | #include <pthread.h> | 
| 函数原形: | int fcntl(int filedes,int cmd,…/*struct flock *flockptr */); | 
| 参数: | filedes 文件描述符 cmd 测试锁或加锁 flockptr 指向flock结构的指针 | 
| 返回值: | 若成功返回0,若失败返回错误编号。 | 
对于记录锁,cmd是F_GETLK,F_SETLKW或F_SETLKW.。
F_GETLK判断由flockptr所描述的锁是否会被另外一把锁排斥。如果存在一把锁,他阻止创建由flockptr所描述的锁,则把该现存锁的信息写到flockptr指向的结构中。如果不存在这种情况除了将l_type设置为F_UNLCK之外,flockptr所描述的其他信息都不变。
F_SETLK和F_SETLKW企图建立一把锁。F_SETLK和F_SETLKW的区别是F_SETLKW是F_SETLK的阻塞版本。如果存在其他锁,调用的进程就被阻塞直道捕捉到信号。
第三个参数是一个指向flock结构的指针:
struct flock{
short l_type;
off_t l_start;
shout l_whence;
off_t l_len;
pid_t l_pid;
};
flock结构说明:
所希望的锁类型:F_RDLCK(共享读锁)、F_WRLCK(独占性写锁)、F_UNLCK(解锁一个区域),这是由 l_type决定的。
要加锁或解锁区域的起始字节偏移量,这是由l_statt和l_whence两者决定。
区域的字节长度,由l_len表示。
具有能阻塞当前进程的锁,其持有的ID存放在l_pid中。
如若l_len为0,则表示锁的区域从其起点(由l_start和l_whence决定)开始直至最大可能位置为止。也就是不管添写到该文件中多少数据,它都处于的范围。
如果想锁住整个文件,通常的方法是将l_start说明为0,l_whence说明为SEEK_SET,1_len说明为0。
还要注意的是,对文件加共享读锁时文件应以只读的方式打开,对文件加独占写锁时文件应以只读的方式打开。
下面是给一个文件加锁和测试锁的程序。
| /*12_1.c加锁程序*/ #include <stdlib.h> #include <fcntl.h> int main(int argc,char *argv[]) { int fd; struct flock lock; if((fd=open(argv[1],O_WRONLY))<0) perror(argv[1]); lock.l_type=F_WRLCK; /*设置flock结构*/ lock.l_start=0; lock.l_whence=SEEK_SET; lock.l_len=0; if(fcntl(fd,F_SETLK,&lock)<0) /*加锁整个文件*/ { perror(argv[1]); exit(1); } sleep(10); close(fd); exit(0); } | 
| /*12_2.c测试锁程序*/ #include <stdlib.h> #include <fcntl.h> int main(int argc,char *argv[]) { int fd; struct flock lock; char buf[]=” if((fd=open(argv[1],O_WRONLY))<0) perror(argv[1]); lock.l_type=F_WRLCK; /*设置flock结构*/ lock.l_start=0; lock.l_whence=SEEK_SET; lock.l_len=0; if(fcntl(fd,F_SETLK,&lock)<0)/*测试共享资源是否加锁*/ { perror(argv[1]); exit(1); } if(lock.l_type==F_UNLCK) printf(“Is not clocked!\n”); else printf(“Is clocked!\n”); if(write(fd,buf,sizeof(buf))<0) perror(“wrire error”); close(fd); exit(0); } | 
先在后台运行加锁程序
#./12_1 12_1.c&
然后在十秒之内运行测试锁的程序
#./12_2 12_1.c
在屏幕上会打印Is clockd!
如果等待10秒12_1.c运行完
再次运行测试锁的程序则会打印Is not clocked!
4.
| 名称:: | pselect | 
| 功能: | 指行I/O多路转接 | 
| 头文件: | #include <sys/select.h> | 
| 函数原形: | int pselect(int masfdp1,fd_set *restrict readfds,fd_set *restrict writefds,fd_set excepfds,const struct timespec *restrict tsptr,const sigset_t *restrict sigmask); | 
| 参数: | maxfdpl 最大描述符加1 readfds 读描述符集 writefds 写描述符集 excepfds 异常描述符集 tsptr 愿意等待时间 sigmask 信号屏蔽集 | 
| 返回值: | 若fd在描述符集中则返回非0值,否则返回0(FD_ISSET) | 
pselect是select的一个变体,除以下几点外,pselect与select相同:
(1)       select的超时值用timeval结构指定,但pselect使用timespec结构指定。timespec以秒和纳秒表示超时值。
(2)       pselect的超时值被定义为const,这保证了调用pselect不会改变此值,。
(3)       对于paselect可以使用一可选的信号屏蔽字。若sigmask为空,那么在于信号有关的方面,pselect和select相同。否则,sigmask指向一信号屏蔽字,在调用pselect时,以原子操作的方式安装该信号屏蔽字。在返回时恢复以前的信号屏蔽字。
5.
| 名称:: | poll | 
| 功能: | 指行I/O多路转接 | 
| 头文件: | #include <sys/select.h> | 
| 函数原形: | int poll(struct pollfd fdarray[],nfds_t nfds,int timeout); | 
| 参数: | fdarray 存放描述符集的数组 nfds fdarray数组元素个数 timeout 超时等待时间 | 
| 返回值: | 准备就绪的描述符数,若超时则返回0,若出错则返回-1 | 
 poll类似于select,但是其接口则有所不同。poll不时为每个状态(可读性,可写性和异常状态)构造一个描述符集,而是构造一个pollfd结构的数组,每个数组元素指定一个描述符编号以及其所关心的状态。
struct pollfd{
       int fd; 文件描述符
       shout events; 
       shout revents;
};
       fdarray数组中的元素由nfds说明。
应将每个数组元素的events成员设置为下表的值。通过这些告诉内核我们对该描述符关系的时什么。返回时,内核设置revents成员,以说明对于该描述符已经发生了什么事件。
| 标志名 | 说明 | 
| POLLIN POLLRDNORM POLLRDBAND POLLPRI | 不阻塞地可读除高优先级外的数据(等效于POLLRDNORM|POLLRDBAD) 不阻塞地可读普通数据(优先级波段为0) 不阻塞地可读非0优先级波段数据 不阻塞地可读高优先级数据 | 
| POLLOUT POLLWRNORM POLLWRBAND | 不阻塞地可写普通数据 与POLLOUT相同 不阻塞地可写非0优先级波段数据 | 
| POLLERR POLLHUP POLLNVAL | 已出错 已挂断 描述符不引用一打开文件 | 
 表头四行测试可读性,接着三行测试可写性,最后三行则是测试异常状态。最后三行是由内核在返回时设置的。即使在events字段中没有指定这三个值,如果相应条件发生,则在revents中也它们。
       当一个描述符被挂断后,就不能再写向该描述符。但是仍可能从该描述符读取数据。
poll的最后一个参数说明我们愿意等待多少时间。如同sellect一样,有三种不同情形:
      (1) timeout==-1永远等待,当所指定的描述符中的一个已准备好,或捕捉到一个信号时则返回。如果捕捉到一个信号,则poll返回-1,error设置为EINTR.
       (2)timeout==0 不等待。测试所有的描述符并立即返回。这是得到多个描述符的状态而不阻塞select函数的轮询方法。
       (3)timeout>0 等待timeout毫秒。当指定的描述符之一已准备好,或当指定的时间值已超过时立即返回。如果在超时还没有一个描述符准备好,则返回值是0。
三、读写多个缓冲区
6.
| 名称:: | readv/writev | 
| 功能: | 散布读/聚集写 | 
| 头文件: | #include <sys/uio.h> | 
| 函数原形: | ssize_t readv(int filedes,const struct iovec *iov,int iovcnt); ssize_t writev(int filedes,const struct iovec *iov,int iovcnt); | 
| 参数: | filedes 文件描述符 iov 指向iovec结构数组的一个指针。 iovcnt 数组元素的个数 | 
| 返回值: | 若成功则返回已读、写的字节数,若出错则返回-1 | 
readv和writev函数用于在一次函数调用中读、写多个非连续缓冲区。有时也将这两个函数成为散布读和聚集写。
       这两个函数的第二个参数是指向iovec结构数组的一个指针:
       struct iovec{
              void *iov_base;
              size_t iov_len;
       };
       writev以顺序iov[0]至iov[iovcnt-1]从缓冲区中聚集输出数据。writev返回输出的字节总数,通常,它应等于所有缓冲区长度之和。
       readv则将读入的数据按上述同样顺序散布读到缓冲区中。readv总是先填满一个缓冲区,然后再填写下一个。readv返回读到的总字节数。如果遇到文件结尾,已无数据可读,则返回0。
下面就是读多个缓冲区的例子: 
| /*12_4.c*/ #include <sys/uio.h> #include <stdio.h> #include <fcntl.h> int main(int argc,char *argv[]) { ssize_t size; char buf1[9]; char buf2[9]; struct iovec iov[2]; fd1=open(argv[1],O_RDONLY); fd2=open(argv[2],O_RDONLY); fd3=open(argv[3],O_WRONLY); size=read(fd1,buf1,sizeof(buf1)); printf(“%s size is:%d\n”,argv[1],size); size=read(fd2,buf2,sizeof(buf2)); printf(“%s size is:%d\n”,argv[2],size); iov[0].iov_base=buf1; iov[0].iov_len=sizeof(buf1); iov[1].iov_base=buf2; iov[1].iov_len=sizeof(buf2); size=writev(fd3,iov,2)); printf(“%s size is:%d\n”,argv[3],size); close(fd1); close(fd2); close(fd3); } | 
先用vi或cat建立三个文件(test1,test2,test3),test写入123456789,test写入abcdefghi. test3为空。
然后运行命令:
#./12_4 test1 test2 test3
在屏幕上会输出:
test1 size is:9
test2 size is:9
test3 size is:18
打开test3,文件内容为123456789abcdefghi.
程序先把test1和test2的内容分别读到缓冲区buf1和buf2中。然后用write把buf1和buf2的内容写至test3.
二.I/O多路转接
       如果我们想从多个文件描述符读或写数据,如果我们用以前学过的函数(read,write等)去处理可能会阻塞在一个文件描述符上,不能处理其他的文件描述符。那是因为我们以前学的I/O处理函数,都是阻塞的I/O处理函数,它们的特点是,如果缓冲区里有数据它们就会把数据写到文件中,如果缓存区没有数据他们就会等待(阻塞)直到有数据可读。这就造成了他们无法对多个文件描述符进行操作。而对多个文件描述符进行操作在网络通信方面却是执关重要的。
       一种比较好的解决方案就是I/O多路转接技术。它现构造一张有关文件描述符的列表,然后调用一个函数,直到这些描述符中的一个已经准备好进行I/O时,该函数才返回。在返回时,它告诉进程那些描述符已经准备好可以进行I/O。poll,selsct,pselect这三个函数使我们能够执行I/O多路转接,下面就分别介绍它们。
2.
| 名称:: | select | 
| 功能: | 指行I/O多路转接 | 
| 头文件: | #include <sys/select.h> | 
| 函数原形: | int select(int maxfdpl,fd_set *restrict readfds,fd_set *restrict writefds,fd_set *testrict exceptfds,struct timeval *testrict tvptr); | 
| 参数: | maxfdpl 最大描述符加1 readfds 读描述符集 writefds 写描述符集 excepfds 异常描述符集 tvptr 愿意等待的时间 | 
| 返回值: | 准备就绪的文件描述符数,若超时则返回0,若出错则返回-1 | 
 select函数使我们可以执行I/O多路转接。传向select的参数告诉内核:我们所关系的描述符。对于每个描述符我们所关心的状态。以及我们愿意等待的时间。从select返回时,内核告诉我们:以准备好的描述符的数量。对于读、写或异常这三个状态中的每一个,那些描述符已经准备好。
       这个函数比较复杂,我们一个一个参数的看。
       第一个参数maxfdp1的意思是“最大描述符加
       中间的三个参数readfds、writefds和exceptfds是指向描述符集的指针。这三个描述符集说明了我们关心的可读(readfds)、可写(writefd)或处于异常条件(wxcepfds)的各个描述符。每个描述符集存放在一个fd_set数据类型中。这种结构相当于一个描述符的数组,它为每个可能的描述符设置1位。
               fd0     fd1     fd2     fd3       fdn
| 0 | 0 | 0 | 0 | … | 
readfdsà
               fd0     fd1     fd2     fd3       fdn
| 0 | 0 | 0 | 0 | … | 
writefdsà
               fd0     fd1     fd2     fd3       fdn
| 0 | 0 | 0 | 0 | … | 
excepfdsà
       可用下面4个函数对描述符集进行操作。
       select的中间三个参数中的任意一个或全部都可以是空指针,这表示对相应状态不关系。如果所有三个指针都是空指针,则select提供了较sleep更精确的计时器。其等待时间可以小于1秒。
       
       tuptr指定最后等待的时间,它的结构是:
struct timeval{
       long tv_sec; 秒
       long tv_usec; 微秒
};
有三种情况:
(1) tvptr==NULL:永远等待。如果捕捉到一个信号则中断此无限等待。当所指定的描述符中的一个已经准备好或捕捉到一个信号则返回。如果捕捉到一个信号,则select返回-1,errno设置为EINTR.
(2) tvptr->tv_sec==0&&tvptr_usec==0 完全不等待。测试所有的描述符并立即返回。这是得到多个描述符的状态而不阻塞select函数的轮询方法。
(3)tvptr->tv_sec!=0||tvptr_usec!=0 等待指定的秒数或微秒数。当指定的描述符之一已准备好,或当指定的时间值已超过时立即返回。如果在超时还没有一个描述符准备好,则返回值是0。
3.
| 名称:: | FD_ISSET/FD_CLR/FD_SET/FD_ZERO | 
| 功能: | 描述符集处理函数 | 
| 头文件: | #include <sys/select.h> | 
| 函数原形: | int FD_ISSET(int fd,fd_set *fdset); void FD_CLR(int fd,fd_set *fdset); void FD_SET(int fd,fd_set *fdset); void FD_ZERO(fd_set *fdset); | 
| 参数: | fdset 描述符集 fd 描述符 | 
| 返回值: | 若fd在描述符集中则返回非0值,否则返回0(FD_ISSET) | 
 调用FD_ZERO将一个指定的fd_set变量的所有位设置为0。调用FD_SET设置一个fd_set变量的指定位。调用FD_CLR将一指定位清除。最后调用FD_ISSET测试一指定位是否设置。声明了一个描述符集后,必须用FD_ZERO清除其所有位,然后在其中设置我们关心的各个位。
下面是select函数实现I/O多路转接的一个例子
| /*12_3.c*/ #include <sys/time.h> #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int main() { int keyboard; int ret=0; char c; fd_set readfd; struct timeval timeout; if((keyboard=open(“/dev/tty”,O_RDONLY|O_NONBLOCK))<0) /*打开标准输入(键盘)的文件描述符*/     exit(1);/如果失败则退出程序*/ while(1) {     timeout.tv_sec=3;/*设置等待时间为3秒*/     timeout.usec=0;     FD_ZERO(&readfd);/*初始化描述符集*/     FD_SET(keyboard,&readfd);/*把标准输入(键盘)加入到描述符集中*/          ret=select(keyboard+1,&readfd,NULL,NULL,&timeout);     if(ret==0)/*如果超时打印下面的语句*/         printf(“Time out!\n”);     if(FD_ISSET(keyboard,&readfd))/*如果描述符集readfd的keyboard位被设置*/     {         read(keyboard,&c,1);/*从键盘上读如一个字符*/         if(c==’\n’)             continue;         printf(“You input is %c\n”,c);         if(c==’q’)              break;     } } } | 
       程序执行后等待用户输入。如果用户输入程序就会把它打印到屏幕上。如果用户在3秒钟未输入任何字符,程序就打印“Time out!”.
本程序实现了一个文件描述符的非阻塞I/O。
四、存储映射I/O
       存储映射I/O使一个磁盘文件与存储空间中的一个缓冲区相映射。于是当从缓冲区中取数据,就相当于读文件中的相应字节。与此类似,将数据存入缓冲区,则相应字节自动地写入文件。这样就可以在不使用read和write的情况下执行I/O。
6.
| 名称:: | mmap | 
| 功能: | 把I/O文件映射到一个存储区域中 | 
| 头文件: | #include <sys/mman.h> | 
| 函数原形: | void *mmap(void *addr,size_t len,int prot,int flag,int filedes,off_t off); | 
| 参数: | addr 指向映射存储区的起始地址 len 映射的字节 prot 对映射存储区的保护要求 flag flag标志位 filedes 要被映射文件的描述符 off 要映射字节在文件中的起始偏移量 | 
| 返回值: | 若成功则返回映射区的起始地址,若出错则返回MAP_FAILED | 
 addr参数用于指定映射存储区的起始地址。通常将其设置为0,这表示由系统选择该映射区的起始地址。
       filedes指要被映射文件的描述符。在映射该文件到一个地址空间之前,先要打开该文件。len是映射的字节数。
       off是要映射字节在文件中的起始偏移量。通常将其设置为0。
       prot参数说明对映射存储区的保护要求。可将prot参数指定为PROT_NONE,或者是PROT_READ(映射区可读),PROT_WRITE(映射区可写),PROT_EXEC(映射区可执行)任意组合的按位或,也可以是PROT_NONE(映射区不可访问)。对指定映射存储区的保护要求不能超过文件open模式访问权限。 
       flag参数影响映射区的多种属性:     
MAP_FIXED 返回值必须等于addr.因为这不利于可移植性,所以不鼓励使用此标志。
MAP_SHARED 这一标志说明了本进程对映射区所进行的存储操作的配置。此标志指定存储操作修改映射文件。
MAP_PRIVATE 本标志导致对映射区建立一个该映射文件的一个私有副本。所有后来对该映射区的引用都是引用该副本,而不是原始文件。
要注意的是必须指定MAP_FIXED或MAP_PRIVATE标志其中的一个,指定前者是对存储映射文件本身的一个操作,而后者是对其副本进行操作。
7.
| 名称:: | memcpy | 
| 功能: | 复制映射存储区 | 
| 头文件: | #include <string.h> | 
| 函数原形: | void *memcpy(void *dest,const void *src,size_t n); | 
| 参数: | dest 待复制的映射存储区 src 复制后的映射存储区 n 待复制的映射存储区的大小 | 
| 返回值: | 返回dest的首地址 | 
 memcpy拷贝n个字节从dest到src。
8.
| 名称:: | munmap | 
| 功能: | 解除存储映射 | 
| 头文件: | #include <sys/mman.h> | 
| 函数原形: | int munmap(caddr_t addr,size_t len); | 
| 参数: | addr 指向映射存储区的起始地址 len 映射的字节 | 
| 返回值: | 若成功则返回0,若出错则返回-1 | 
  进程终止时,或调用了munmap之后,存储映射区就被自动解除映射。关闭文件描述符filedes并不解除映射区。
       munmap不会影响被映射的对象。
下面就是利用上面的两个函数实现的第二个版本的cp命令。
| /*12_5.c*/ #include <unistd.h> #include <fcntl.h> #include <sys/mman.h> #include <sys/stat.h> #include <sys/types.h> int main(int argc,char *argv[]) { int fdin,fdout; void *arc,dst; struct stat statbuf; if(argc!=3) {     printf(“please input two file!\n”);     exit(1); } if((fdin=open(argv[1],O_RDONLY))<0) /*打开原文件*/     perror(argv[1]); if((fdout=open(argv[2],O_RDWR|O_CREAT|O_TRUNC))<0)/*创建并打开目标文件*/     perror(argv[2]); if(fstat(fdin,&statbuf)<0) /*获得文件大小信息*/     printf(“fstat error”); if(lseek(fdout,statbuf.st_size-1,SEEK_SET)==-1)/*初始化输出映射存储区*/     printf(“lseek error”); if(write(fdout,””1)!=1)     printf(“write error”); if((src=mmap(0,statbuf.st_size,PROT_READ,MAP_SHARED,fdin,0))==MAP_FAILED)     /*映射原文件到输入的映射存储区*/     printf(“mmap error);  if((dst=mmap(0,statbuf.st_size,PROT_READ|PROT_WRITE,MAP_SHARED,fdout,0)) ==MAP_FAILED) /*映射目标文件到输出的映射存储区*/     printf(“mmap error); memcpy(dst,src,statbuf.st_size);/*复制映射存储区*/ munmap(src,statbuf.st_size); /*解除输入映射*/ munmap(dst,statbuf.st_size); /*解除输出映射*/ close(fdin); close(fdout); } | 
9.
| 名称:: | mprotect | 
| 功能: | 改变映射存储区的权限 | 
| 头文件: | #include <sys/mman.h> | 
| 函数原形: | int mprotect(void *addr,size_t len,int prot); | 
| 参数: | addr 指向映射存储区的起始地址 len 映射的字节 prot 对映射存储区的保护要求 | 
| 返回值: | 若成功则返回0,若出错则返回-1 | 
 
mprotect可以更改一个现存映射存储区的权限。
10.
| 名称:: | msync | 
| 功能: | 同步文件到存储器 | 
| 头文件: | #include <sys/mman.h> | 
| 函数原形: | int msync(void *addr,size_t len,int flags); | 
| 参数: | addr 指向映射存储区的起始地址 len 映射的字节 prot flags | 
| 返回值: | 若成功则返回0,若出错则返回-1 | 
如果在共享映射区中的页已被修改,那么我们可以调用msync将该页冲洗到映射的文件中。flags参数使我们对如何冲洗存储区有某种程度的控制。我们可以指定MS_ASYNC标志以简被写页的调度。如果我们希望在返回之前等待写操作的完成,则可指定MS_SYNC标志。一定要制定MSASYNC或MS_SYNC中的一个。MS_INVALIDATE是一个可选的标志,使用它会通知操作系统丢弃与底层存储器没有同步的任何页。
 
                    
                
 
 
                
            
         浙公网安备 33010602011771号
浙公网安备 33010602011771号