一.记录锁
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是一个可选的标志,使用它会通知操作系统丢弃与底层存储器没有同步的任何页。