请选择 进入手机版 | 继续访问电脑版
设为首页收藏本站

嵌入式天空

 找回密码
 我要注册

扫一扫,访问微社区

Unix高级编程之十四 高级IO

2015-2-7 20:39| 发布者: lizhiyong| 查看: 1386| 评论: 0|原作者: 李志勇

摘要: 高级IO 非阻塞IO 默认情况下read、write等函数有可能会阻塞(比如管道中没数据read就会阻塞,管道满了write就会阻塞),当然也可以把IO设置为非阻塞的,即不能读或者写的时候出错返回而不是阻塞等待 1.在open的时候 ...

高级IO

非阻塞IO

默认情况下readwrite等函数有可能会阻塞(比如管道中没数据read就会阻塞,管道满了write就会阻塞),当然也可以把IO设置为非阻塞的,即不能读或者写的时候出错返回而不是阻塞等待

 

1.open的时候加O_NONBLOCK标志

2.fcntl对已经打开的文件进行设置

 

记录锁

记录锁用来保护文件不被并发写

 

Fcntl记录锁

int fcntl(int fd, int cmd, ... /* arg */ );

fd指向被锁保护的文件

cmd的取值为:

F_GETLK             测试锁的状态

F_SETLKW          如果加锁不成功则阻塞等待

F_SETLK              如果加锁不成功则出错返回

arg的结构为:

struct flock {

short l_type;              /* F_RDLCK, F_WRLCK, F_UNLCK */

short l_whence;           /*SEEK_SET, SEEK_CUR, SEEK_END */

off_t l_start;               /* Starting offset for lock */

off_t l_len;               /* Number of bytes to lock */

pid_t l_pid;               /* PID of process blocking our lock (F_GETLK only) */

};

注意:如果len0的话,加锁的范围是从指定的start开始一直到文件能够达到的最偏移处(如果文件变长了,则变长的部分依然是被加锁的),如果whenceSEEK_SETstart0len0的话是对整个文件进行加锁

 

使用记录锁注意一下几点

1.在没有写锁的情况下读锁可以共存

2.如果进程A对文件的[10-100]字节加锁,然后对[20-50]解锁,那么内核会维护两把锁,分别是[10-20][50-100],如果A进程又对[20-50]加了相同类型的锁,则内核又会变成维护一把锁,即[10-100]

3.记录锁是进程之间使用的,在进程内部并没有排他性,比如进程B[10-100]加了读锁,然后进程B又要在[10-100]加写锁,则读锁被替换为写锁

4.如果进程A[10-100]加读锁,进程A又要对[30-50]加写锁,结果[30-50]变成了写锁,其他区域不变

 

死锁

按下列加锁顺序变会发生ABBA死锁:

进程A[10-100]加锁

进程B[300-400]加锁

然后进程A又对[300-400]加锁

进程B又对[10-100]加锁

 

注意:发生死锁后,内核会选择一个进程加锁失败(并不会死等)

 

注意:

1.关闭一个文件时,调用进程在这个文件上的所有锁都将被解锁(即使多个fd指向同一个文件,关闭其中一个文件描述符后,在其他描述符上加的锁也被解锁)

2.进程终止后该进程所加的所有锁都被解锁,

3.fork产生的子进程不继承父进程的锁状态,子进程需要调用fcntl来获得锁

4.exec执行新程序后,新程序会继承exec之前所获得的锁(除非在exec之前关闭文件或者给文件描述符加O_CLOEXEC

 

记录锁的实现

fd1 = open(pathname, ...);
write_lock(fd1, 0, SEEK_SET, 1);

if ((pid = fork()) > 0) {

fd2 = dup(fd1);
fd3 = open(pathname, ...);

} else if (pid == 0) {

read_lock(fd1, 1, SEEK_SET, 1);     

}

 

建议性锁和强制性锁

Linux下可以使用强制性锁,但是被锁的文件要打开设置组ID位,并关闭组执行位

对于强制性锁的话,没有获得锁时是不能访问文件的(要不阻塞等待(文件描述符阻塞),要不立即出错返回(非阻塞模式))

 

注意:linux需要mount –o mand /dev/sda1 /mnt

 

IO多路转接

当一个进程需要同时监视多个文件描述符时,可以用多线程(需要线程间同步)或者多进程(进程间同步),当然更为简洁有效的办法是IO多路转接

 

Select函数

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

nfds为需要监视的文件描述符中最大的那个加1

readfdswritefdsexceptfds是三个文件描述符集体(用来指定需要监视的文件描述符),该函数返回后这个三个集会发生变化(存放符合条件的文件描述符)

timeout是超时时间,当select睡眠超过timeout(时间长度而不是时间点)指定的时间时后会返回0,如果timeout=0select函数只是按要求检测一下即返回,timeout=NULL阻塞直到收到信号或者文件描述符满足了条件

 

void FD_CLR(int fd, fd_set *set);

int  FD_ISSET(int fd, fd_set *set);

void FD_SET(int fd, fd_set *set);

void FD_ZERO(fd_set *set);

 

Pselect函数

int pselect(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timespec *timeout, const sigset_t *sigmask);

该函数和select函数的区别是,执行这个函数时候会阻塞sigmask中的信号(加到信号屏蔽字),该函数返回后会自动回复信号屏蔽字,还有就是这个函数的timeout不会被修改,但在select返回后会把timeout修改为剩余的睡眠时间

 

返回值:如果返回值大于0说明返回值是满足条件的文件描述符的个数,如果返回值是0说明没有满足条件的文件描述符(由于超时而返回),出错返回-1

 

可以用select监视输入设备,比如鼠标键盘,fifo等,如果所监视fifo的写端关闭的,那么fifo的读端将一直是可读状态(即select不会阻塞)

 

poll函数

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

fds是要监视的文件描述符及对应事件的数组

nfds是数组的元素个数

timeout是超时时间(如果为-1则永不超时,为0的话不阻塞)

 

fds类型

struct pollfd {

int   fd;        /* 要监视的文件描述符*/

short events;     /* 要监视的事件(读、写等)*/

short revents;    /* 已经发生的事件 */

};

 

Epoll函数族

创建epoll对象

int epoll_create(int size);

sizeepoll对象能够监视的文件描述符个数(在2.6.8内核之后该参数无效,但必须大于0

 

添加、删除、修改要监视的文件描述符及其事件类型

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

epfdepoll_create的返回值

op可以是:

EPOLL_CTL_ADD

EPOLL_CTL_MOD

EPOLL_CTL_DEL

fd是要监视的文件描述符

event的类型:

typedef union epoll_data {

void        *ptr;

int          fd;

uint32_t     u32;

uint64_t     u64;

} epoll_data_t;

 

struct epoll_event {

uint32_t     events;      /* Epoll events */

epoll_data_t  data;         /* User data variable */

};

 

等待事件发生

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

epfdepoll_create的返回值

events是一个数组首地址

maxevents是一次能返回的最大事件个数(一般传递events数组的大小)

timeout是超时时间(timeout=-1则一直阻塞,timeout=0则不阻塞)

 

注意:对于fifo来说,如果读端以非阻塞方式打开,在写端没有打开的情况,读端是不会阻塞的,每次读都返回0,但是epoll/poll/select可以阻塞(监视只有读端非阻塞打开的fifo),但是当写端打开又关闭的时候epoll就不阻塞了(监视读端非阻塞或者阻塞打开的fifo),fifo关闭后读端是一直能读的,每次读都返回0

 

selectpollepoll性能比较

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

调用select函数时候,需要把三个表示位图的量readfdswritefdsexceptfds从用户空间拷贝到内核空间,并且内核需要依次判断这三个量的每一位(最大是nfds)来确定是否需要监视该文件描述符,如果需要则调用内核的poll函数,当有事件发生的时候内核在把发生事件的文件描述符重新填写到这三个量中,并且再次把这三个量拷贝到用户空间,也就是说内核会修改这个三个量,这个函数返回后用户需要查看需要监视的描述符是否在这三个量中,每次调用这个函数之前都得对这个三个量重新初始化

 

缺点:

1.每次调用都需要重新初始化readfdswritefdsexceptfds

2.每次调用都会出现一次从用户空间到内核空间的拷贝

3.每次返回都会出现一次从内核空间到用户空间的拷贝

4.内核需要依次查看readfdswritefdsexceptfds的每一位

5.返回后用户需要依次查看自己的文件描述符是否有事件发生

6.能够监视的文件描述符个数有限(默认2048

 

因此,select在监视大量的文件描述符,但是发生事件零散的时候效率非常低

 

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

调用poll函数时,需要把fds指向的含有nfds个元素的数组拷贝到内核空间,然后内核把该数组中的每一个文件描述符都调用内核的poll,当有事件发生的时候,再次把数组从内核拷贝到用户态,fds只需要初始化一次,但每次poll返回后用户需要依次查看fds数组中的文件描述符是否有事件发生

 

缺点:

1.每次调用都会出现一次从用户空间到内核空间的拷贝

2.每次返回都会出现一次从内核空间到用户空间的拷贝

3.返回后需要用户依次扫描fds数组,因此会做很多没必要的检查

 

相比select函数poll的优点

1.理论上支持无数个文件描述符

2.省去了内核检查位图的繁琐操作

 

因此,poll函数会比select的效率稍微高一点

 

int epoll_create(int size);

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

先调用epoll_create在内核中创建epoll对象,然后调用epoll_ctl把文件描述符和关心的事件类型添加到内核(用红黑树维护,方便增删改查),但是要添加多个事件的话需要多次调用epoll_ctl,每调用一次epoll_ctl都会有一次从用户空间到内核空间的拷贝(event,调用epoll_wait等待事件发生,events是用户分配好的数组,数组大小是maxevents,当有事件发生时候,内核会把发生事件的文件描述符对应的event直接写到events指向的空间(在这里内核使用了共享内存,因此省去了由内核空间向用户空间拷贝这一步),该函数返回后用户也不需要遍历整个数组(如果返回值是num,则只需要查看前num个元素即可)

 

缺点:

1.只能在linux平台使用

 

相比selectpoll的优势:

1.函数返回后不需要内核空间到用户空间的拷贝

2.函数返回后用户不需要遍历整个数组

3.可以随时调用epoll_ctl

1

鲜花

握手

雷人

路过

鸡蛋

刚表态过的朋友 (1 人)

最新评论

QQ|Archiver|手机版|小黑屋|EBMSKY Inc. ( 冀ICP备17022971号-1  

GMT+8, 2019-8-22 02:32 , Processed in 0.047659 second(s), 24 queries .

Powered by Discuz! X3.2

© 2014-2018 Comsenz Inc. 【嵌入式天空】设计