Linux文件阻塞的五个解决方法

版权声明:转载请声明 https://blog.csdn.net/qq_40732350/article/details/82823603

前面的 fcntl()函数解决了文件的共享问题, 接下来该处理 I/O 复用的情况了。

总的来说, I/O 处理的模型有 5 种。 

1.阻塞 I/O 模型:

在这种模型下, 若所调用的 I/O 函数没有完成相关的功能, 则会使进程挂起, 直到相关数据到达才会返回。 如常见对管道设备、 终端设备和网络设备进行读写时经常会出现这种情况。

2.非阻塞 I/O 模型:

在这种模型下, 当请求的 I/O 操作不能完成时, 则不让进程睡眠,而且立即返回。 非阻塞 I/O 使用户可以调用不会阻塞的 I/O 操作, 如 open()、 write()和 read()。 如果该操作不能完成, 则会立即返回出错(如打不开文件) 或者返回 0(如 在缓冲区中没有数据可以读取或者没空间可以写入数据)。 如何实现非阻塞IO访问: O_NONBLOCK: 只能在打开的时候设置 fcntl: 可以在文件打开后设置 

3.I/O 多路复用模型:

在这种模型下, 如果请求的 I/O 操作阻塞, 且它不是真正阻塞I/O, 而是让其中的一个函数等待, 在此期间, I/O 还能进行其他操作。 外部表现为阻塞式,内部非阻塞式自动轮询多路阻塞式IO。其实内部就是while加非阻塞(类似),但是它内部不会占用CPU 太多时间。

优点:

不会浪费太多CPU时间,且达到while加非阻塞的效果

缺点:

1.select和poll本身是阻塞式的,当里面的条件没满足或者超时,就会一直阻塞,

2.这两个函数可以同时注册多个阻塞,但是只要有一个阻塞发生就会马上退出函数,而不会等其它阻塞发生,更不会等全部阻塞发生后才退出 

4.信号驱动 I/O 模型:

在这种模型下, 进程要定义一个信号处理程序, 系统可以自动捕获特定信号的到来, 从而启动 I/O。 这是由内核通知用户何时可以启动一个 I/O操作决定的。它是非阻塞的。 当有就绪的数据时, 内核就向该进程发送 SIGIO 信号。 无论我们如何处理 SIGIO 信号, 这种模型的好处是当等待数据到达时, 可以不阻塞。 主程序继续执行,只有收到 SIGIO 信号时才去处理数据即可。 

优点:快速,因为是内核进行处理

缺点:只能处理一种情况,因为只有一个信号

5.异步 I/O 模型:

在这种模型下, 进程先让内核启动 I/O 操作, 并在整个操作完成后通知该进程。 这种模型与信号驱动模型的主要区别在于: 信号驱动 I/O 是由内核通知我们何时可以启动一个 I/O 操作, 而异步 I/O 模型是由内核通知进程 I/O 操作何时完成的。 现在, 并不是所有的系统都支持这种模型。

6.特殊方式——轮询

不断对文件进行判断是否能使用,

优点:简单

缺点:浪费CPU时间

1.非阻塞IO

对于open,write和read有两种方法使其非阻塞

(1)如果调用open获得描述符,则可指定O_NONBLOCK标志。

fd = open("test", O_REWR | O_NONBLOCK);

(2)对于调已经打开的一个描述符,则可用fcntl,由该函数打开O_NONBLOCK文件状态标志(见3.14节),图3-12中的函数可用来为一个描述符打开任一文件状态标志。

fctnl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK);

 

2.I/O多路复用

1.select()函数的语法要点:

文件描述符集处理函数:

使用 select()函数之前,

使用 FD_ZERO()初始化文件描述符集

FD_SET()来设置文件描述符集

(在需要重复调用 select()函数时, 先把一次初始化好的文件描述符集备份下来,每次读取它即可)。

在 select()函数返回后,

可循环使用 FD_ISSET()来检查描述符集, 在执行完对相关文件描述符的操作后,

使用 FD_CLR()来清除描述符集。

FD_ISSET(int fd, fd_set *set) 这个函数很有意思,当注册完一个文件后,再调用这个函数检查这个文件描述符,得到的值式非零, 当调用了select()后,再退出后,如果这个文件描述符的阻塞没有发生,则再调用这个函数就会返回0,如果发生了就会返回非0,

可以用于检测阻塞有没有发生

另外, select()函数中的 timeout 是一个 struct timeval 类型的指针,该结构体如下所示:

struct timeval
{
    long tv_sec; /* 秒 */
    long tv_unsec; /* 微秒 */
}

2.poll()函数的语法要点:

timeout ==-1 永远等待

timeout ==0 不等待

timeout> 0 等待tiweout毫秒

3.select的变体pselect函数:

改进:

更精准的超时时间:

select的超时值用timeval结构指定, timespec结构以秒和纳秒表示超时值,而非秒和微秒。

pselect的超时值被声明为const,这保证了调用pselect不会改变此值。

pselect可使用可选信号屏蔽字。

siamask=NULL,那么在与信号有关的方面, pselect的运行状况和select相同。

否则, sigmask指向一信号屏蔽字,在调用, pselect时,以原子操作的方式安装该信号屏蔽字。在返回时,恢复以前的信号屏蔽字。

select()函数的实例。

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/select.h>
#include <sys/time.h>
int main(void)
{
	// 读取鼠标
	int fd = -1, ret = -1;
	char buf[200];
	fd_set myset;   //创建文件描述符集
	struct timeval tm;
	fd = open("/dev/input/mouse0", O_RDONLY);
	if (fd < 0)
	{
		perror("open:");
		return -1;
	}
	// 当前有2个fd,一共是fd一个是0
	// 处理myset
	FD_ZERO(&myset);     //初始化文件描述符集
	FD_SET(fd, &myset);  //添加文件描述
	FD_SET(0, &myset);  //添加文件描述
	if (FD_ISSET(0, &myset))
	{
		printf("键盘读出的内容是:.\n");
	}
	if (FD_ISSET(fd, &myset))
	{
		printf("鼠标读出的内容是:.\n");
	}
	tm.tv_sec = 5;
	tm.tv_usec = 0;
	ret = select(fd+1, &myset, NULL, NULL, &tm);
	if (ret < 0)
	{
		perror("select: ");
		return -1;
	}
	else if (ret == 0)
	{
		printf("超时了\n");
	}
	else
	{
		// 等到了一路IO,然后去监测到底是哪个IO到了,处理之
		if (FD_ISSET(0, &myset))
		{
			// 这里处理键盘
			memset(buf, 0, sizeof(buf));
			read(0, buf, 5);
			printf("键盘读出的内容是:[%s].\n", buf);
		}
		if (FD_ISSET(fd, &myset))
		{
			// 这里处理鼠标
			memset(buf, 0, sizeof(buf));
			read(fd, buf, 50);
			printf("鼠标读出的内容是:[%s].\n", buf);
		}
	}
	return 0;
}

poll()函数的实例。

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <poll.h>
int main(void)
{
	// 读取鼠标
	int fd = -1, ret = -1;
	char buf[200];
	struct pollfd myfds[2] = {0};
	fd = open("/dev/input/mouse0", O_RDONLY);
	if (fd < 0)
	{
		perror("open:");
		return -1;
	}
	// 初始化我们的pollfd
	myfds[0].fd = 0;			// 键盘
	myfds[0].events = POLLIN;	// 等待读操作
	myfds[1].fd = fd;			// 鼠标
	myfds[1].events = POLLIN;	// 等待读操作
	ret = poll(myfds, 2, 10000);
	if (ret < 0)
	{
		perror("poll: ");
		return -1;
	}
	else if (ret == 0)
	{
		printf("超时了\n");
	}
	else
	{
		// 等到了一路IO,然后去监测到底是哪个IO到了,处理之
		if (myfds[0].events == myfds[0].revents)
		{
			// 这里处理键盘
			memset(buf, 0, sizeof(buf));
			read(0, buf, 5);
			printf("键盘读出的内容是:[%s].\n", buf);
		}
		if (myfds[1].events == myfds[1].revents)
		{
			// 这里处理鼠标
			memset(buf, 0, sizeof(buf));
			read(fd, buf, 50);
			printf("鼠标读出的内容是:[%s].\n", buf);
		}
	}
	return 0;
}

3.异步IO

int aio_read(struct aiocb *aiocbp);  /* 提交一个异步读 */
int aio_write(struct aiocb *aiocbp); /* 提交一个异步写 */
int aio_cancel(int fildes, struct aiocb *aiocbp); /* 取消一个异步请求(或基于一个fd的所有异步请求,aiocbp==NULL) */
int aio_error(const struct aiocb *aiocbp);        /* 获取一个异步请求的状态(进行中EINPROGRESS?还是已经结束或出错?) */
ssize_t aio_return(struct aiocb *aiocbp);         /* 获取一个异步请求的返回值(跟同步读写定义的一样) */
int aio_suspend(const struct aiocb * const list[], int nent, const struct timespec *timeout); /* 阻塞等待,让所有IO请求完成 */
int aio_fsync(int op, struct aiocb *aiocbp);  //不等待,把数据写入磁盘中
int lio_listio(int mode, struct aiocb *const aiocb_list[],//发起一系列异步I/O请求
                      int nitems, struct sigevent *sevp);

重要结构体:

strcut aiocb{
int               aio_fildes;        /* 要被读写的fd */
volatile void    *aio_buf;           /* 读写操作对应的内存buffer */
off_t             aio_offset;        /* 读写操作对应的文件偏移 */
size_t            aio_nbytes;        /* 需要读写的字节长度 */
int               aio_reqprio;       /* 请求的优先级 */
struct sigevent   aio_sigevent;      /* 异步事件,定义异步操作完成时的通知信号或回调函数 */
int               aio_lio_opconde  //不知道
}

I/O事件完成后,如何通知应用程序,有下面的结构体表示:

struct sigevent {
int sigev_notify; //通知类型
int sigev_signo; //信号的编号
union sigval sigev_value; //sigev_notify_function传递的参数
void (*sigev_notify_function)(union sigval); /* 异步IO请求完成后,执行的函数 */
pthread_attr_t *sigev_notify_attributes; /* notify attrs */
};

sigev_notify字段控制通知的类型。取值可能是以下3个中的一个。

SIGEV_NONE

异步IO请求完成后,不通知进程。

SIGEV_SIGNAL

异步IO请求完成后,产生由sigev_signo字段指定的信号。如果应用程序已选择捕捉信号,且在建立信号处理程序的时候指定了SA SIGINFO标志,那么该信号将被入队(如果实现支持排队信号),信号处理程序会传送给一个siqinfo结构,该结构的si_value字段被设置为sigev_value (如果使用了SA_SIGINFO标志),

SIGEV_THREAD

当异步IO请求完成时,由sigev_notify_function字段指定的函数被调用.siqev_value字段被传入作为它的唯一参数。除非sigev-notify attributes字段被设定为pthread属性结构的地址,且该结构指定了一个另外的线程属性,否则该函数将在分离状态下的一个单独的线程中执行。

(1)异步读aio_read

(2)异步写aio_write

(3)强制写磁盘aio_fsync

要想强制所有等待中的异步操作不等待而写入持久化的存储中,可以设立一个AIO控制块并,调用aio_fsync函数。

AIO控制块中的aio_fildes字段指定了其异步写操作被同步的文件。

如果op参数设定为O_DSYNC,那么操作执行起来就会像调用了fdatasync一样。否则,

如果op参数设定为O_SYNC,那么操作执行起来就会像调用了fsync一样。

像aio_read和aio_write函数一样,在安排了同步时,aio_fsync操作返回。在异步同步操作完成之前,数据不会被持久化。AIO控制块控制我们如何被通知,就像aio read和 aio_write函数一样。

(4)获取异步请求的状态aio_error

返回值为下面4种情况中的一种。

0:异步操作成功完成。需要调用aio return函数获取操作返回值。

-1:对aio_error的调用失败。这种情况下, errno会告诉我们为什么。

EINPROGRESS :异步读、写或同步操作仍在等待。

其他情况: 其他任何返回值是相关的异步操作失败返回的错误码。

(5)获取一个异步请求的返回值aio_return

如果异步操作完成,可以用这个或返回值

不应该在异步操作完成之前,调用这个函数

(6)阻塞等待,让所有IO请求完成aio_suspend

如果一个事件(或一个进程)完成了,然而阻塞的异步IO还没完成,就可以调用这个函数,等待所有异步IO完成

(7) 取消一个异步请求aio_cancel

(8)发起一系列异步I/O请求lio_listio

实例代码:

注意:由于是动态链接,因此编译时要加  -ltr

1.aio_read

#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <fcntl.h>
#include <aio.h>

#define BUFFER_SIZE 1024
int MAX_LIST = 2;
int main(int argc,char **argv)
{
    //aio操作所需结构体
    struct aiocb rd;

    int fd,ret,couter;

    //将rd结构体清空
    bzero(&rd,sizeof(rd));


    //为rd.aio_buf分配空间
    rd.aio_buf = malloc(BUFFER_SIZE + 1);

    //填充rd结构体
    rd.aio_fildes = 0;
    rd.aio_nbytes =  BUFFER_SIZE;
    rd.aio_offset = 0;

    //进行异步读操作
    ret = aio_read(&rd);
    if(ret < 0)
    {
        perror("aio_read");
        exit(1);
    }

    couter = 0;
//  循环等待异步读操作结束
    while(aio_error(&rd) == EINPROGRESS)
    {
        printf("第%d次\n",++couter);
		sleep(1);
    }
    //获取异步读返回值
    ret = aio_return(&rd);
    printf("内容为 [%s]\n", (char *)rd.aio_buf);
    printf("返回值为:%d\n",ret);

    return 0;
}

# gcc 1.c -lrt
# ./a.out 
第1次
第2次
第3次
dfasdfasd
内容为 [dfasdfasd
]返回值为:10

2.aio_suspend

#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <fcntl.h>
#include <aio.h>

#define BUFFER_SIZE 1024
int MAX_LIST = 2;
int main(int argc,char **argv)
{
    //aio操作所需结构体
    struct aiocb rd;

    int fd,ret,couter;
	
    struct aiocb *aiocb_list[2] = {0};

    //将rd结构体清空
    bzero(&rd,sizeof(rd));

    //为rd.aio_buf分配空间
    rd.aio_buf = malloc(BUFFER_SIZE + 1);

    //填充rd结构体
    rd.aio_fildes = 0;
    rd.aio_nbytes =  BUFFER_SIZE;
    rd.aio_offset = 0;

    //将读fd的事件注册
    aiocb_list[0] = &rd;

    //进行异步读操作
    ret = aio_read(&rd);
    if(ret < 0)
    {
        perror("aio_read");
        exit(1);
    }

    if ( (ret = aio_return(&rd)) > 0)
	{
		printf("内容为 [%s]\n", (char *)rd.aio_buf);
		printf("\n\n返回值为:%d\n",ret);
		exit(0);
	}
    printf("我等等你 \n");
    aio_suspend(aiocb_list,MAX_LIST,NULL);
    if ( (ret = aio_return(&rd)) > 0)
	{
		printf("内容为 [%s]\n", (char *)rd.aio_buf);
		printf("返回值为:%d\n",ret);
		exit(0);
	}
    exit(0);
}

# ./a.out 
我等等你 
dfasfd
内容为 [dfasfd
]
返回值为:7

3.lio_listio函数

aio同时还为我们提供了一个可以发起多个或多种I/O请求的接口lio_listio

这个函数效率很高,因为我们只需一次系统调用(一次内核上下位切换)就可以完成大量的I/O操作

其函数原型如下 int lio_listio(int mode,struct aiocb *list[],int nent,struct sigevent *sig);

第一个参数mode可以有俩个实参,LIO_WAIT和LIO_NOWAIT,前一个会阻塞该调用直到所有I/O都完成为止,后一个则会挂入队列就返回

#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <fcntl.h>
#include <aio.h>

int MAX_LIST = 2;
int main(int argc,char **argv)
{
    struct aiocb *listio[2];
    struct aiocb rd,wr;
    int fd,ret;
	char buf[20] = {"Hello world"};

    //异步读事件
    bzero(&rd,sizeof(rd));

    rd.aio_buf = (char *)malloc(BUFFER_SIZE);
    if(rd.aio_buf == NULL)
    {
        perror("aio_buf");
    }

    rd.aio_fildes = 0;
    rd.aio_nbytes = 1024;
    rd.aio_offset = 0;
    rd.aio_lio_opcode = LIO_READ;   ///lio操作类型为异步读

    //将异步读事件添加到list中
    listio[0] = &rd;


    //异步写事件
    fd = open("test.txt",O_WRONLY | O_APPEND);
    if(fd < 0)
    {
        perror("test.txt");
		exit(-1);
    }

    bzero(&wr,sizeof(wr));

    wr.aio_buf = (char *)malloc(BUFFER_SIZE);
    if(wr.aio_buf == NULL)
    {
        perror("aio_buf");
    }

    wr.aio_fildes = 1;
    wr.aio_nbytes = strlen(buf);
	wr.aio_buf = buf;
    wr.aio_lio_opcode = LIO_WRITE;   ///lio操作类型为异步写

    //将异步写事件添加到list中
    listio[1] = &wr;

    //使用lio_listio发起一系列请求
    ret = lio_listio(LIO_WAIT, listio, MAX_LIST, NULL);

    //当异步读写都完成时获取他们的返回值
    ret = aio_return(&rd);
    printf("\n读返回值:%d\n",ret);

    ret = aio_return(&wr);
    printf("\n写返回值:%d\n",ret);

    return 0;
}

# ./a.out 
Hello world123456

读返回值:7

写返回值:11

4.I/O完成时进行异步通知

当我们的异步I/O操作完成之时,我们可以通过信号通知我们的进程也可用回调函数来进行异步通知,接下来我会为大家主要介绍以下回调函数来进行异步通知,关于信号通知有兴趣的同学自己去学习吧

使用回调进行异步通知

该种通知方式使用一个系统回调函数来通知应用程序,要想完成此功能,我们必须在aiocb中设置我们想要进行异步回调的aiocb指针,以用来回调之后表示其自身 实例如下 

#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <fcntl.h>
#include <aio.h>


#define BUFFER_SIZE 1024
struct aiocb rd;
	
int MAX_LIST = 2;
void aio_completion_handler(sigval_t sigval)
{
    //用来获取读aiocb结构的指针
    struct aiocb *prd;
    int ret;

    prd = (struct aiocb *)sigval.sival_ptr;

    printf("hello ,my is handle fuction\n");

    //判断请求是否成功
    if(aio_error(prd) == 0)
    {
        //获取返回值
        ret = aio_return(prd);
        printf("读返回值为:%d\n",ret);
		printf("内容为 [%s]\n", (char *)rd.aio_buf);
    }
}

int main(int argc,char **argv)
{
    int fd,ret;

    //填充aiocb的基本内容
    bzero(&rd,sizeof(rd));

    rd.aio_fildes = 0;
    rd.aio_buf = (char *)malloc(sizeof(BUFFER_SIZE + 1));
    rd.aio_nbytes = BUFFER_SIZE;
    rd.aio_offset = 0;

    //填充aiocb中有关回调通知的结构体sigevent
    rd.aio_sigevent.sigev_notify = SIGEV_THREAD;//使用线程回调通知
    rd.aio_sigevent.sigev_notify_function = aio_completion_handler;//设置回调函数
    rd.aio_sigevent.sigev_notify_attributes = NULL;//使用默认属性
    rd.aio_sigevent.sigev_value.sival_ptr = &rd;//在aiocb控制块中加入自己的引用

    //异步读取文件
    ret = aio_read(&rd);
    if(ret < 0)
    {
        perror("aio_read");
    }

    printf("异步读以开始\n");
    sleep(5);

    printf("异步读结束\n");

    return 0;
}

# ./a.out 
异步读以开始
12345
hello ,my is handle fuction
读返回值为:6
内容为 [12345
]
异步读结束

线程会掉是通过使用aiocb结构体中的aio_sigevent结构体来控制的, 其定义如下

struct sigevent
{
    sigval_t sigev_value;
    int sigev_signo;
    int sigev_notify;
    union {
        int _pad[SIGEV_PAD_SIZE];
         int _tid;

        struct {
            void (*_function)(sigval_t);
            void *_attribute;   /* really pthread_attr_t */
        } _sigev_thread;
    } _sigev_un;
}

#define sigev_notify_function   _sigev_un._sigev_thread._function
#define sigev_notify_attributes _sigev_un._sigev_thread._attribute
#define sigev_notify_thread_id   _sigev_un._tid1

4.信号驱动 I/O 模型

实现步骤:

1.利用signal或signaction函数定义信号SIGIO的处理函数

2.利用fcntl函数对文件描述符在状态发生变化产生SIGIO信号时,设置信号发送的对象

3.在运用open函数打开文件时或运用fcntl函数将已打开的文件设置为O_ASTNC方式

实例代码:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>

int mousefd = -1;

// 绑定到SIGIO信号,在函数内处理异步通知事件
void func(int sig)
{
	char buf[200] = {0};
	
	if (sig != SIGIO)
		return;

	read(mousefd, buf, 50);
	printf("鼠标读出的内容是:[%s].\n", buf);
}
int main(void)
{
	char buf[200];
	int flag = -1;
	
	mousefd = open("/dev/input/mouse0", O_RDONLY);  //打开鼠标文件
	if (mousefd < 0)
	{
		perror("open:");
		return -1;
	}	
	// 把鼠标的文件描述符设置为可以接受异步IO
	flag = fcntl(mousefd, F_GETFL);
	flag |= O_ASYNC;
	fcntl(mousefd, F_SETFL, flag);
	// 把异步IO事件的接收进程设置为当前进程
	fcntl(mousefd, F_SETOWN, getpid());
	
	// 注册当前进程的SIGIO信号捕获函数	
	signal(SIGIO, func);	
	for(;;)
	{		
		printf("buf \n");		
		pause();  //等待一个信号
	}
	
	return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_40732350/article/details/82823603
今日推荐