周梦康 发表于 2015-11-07 4266 次浏览 标签 : Linux

阻塞I/O实例

从标准输入读取,并向标准输入写入

#include <unistd.h>
#include <errno.h>

#define BUFFSIZE	4096

int main(void)
{
	int		n;
	char	buf[BUFFSIZE];

	while ((n = read(STDIN_FILENO, buf, BUFFSIZE)) > 0)
	{
		if (write(STDOUT_FILENO, buf, n) != n)
		{
			perror("write error :");
		}
	}

	if (n < 0)
	{
		perror("read error :");
	}

	return 0;
}

read函数返回读取的字节数,此值用作要写的字节数。当达到输入文件的尾端时,read返回0,程序停止执行。

[zhoumengkang@localhost unix]$ gcc cp_test.c
[zhoumengkang@localhost unix]$ ./a.out > test
zmkzmk
test
hahaha
^C
[zhoumengkang@localhost unix]$ cat test
zmkzmk
test
hahaha
[zhoumengkang@localhost unix]$ ./a.out < test
zmkzmk
test
hahaha
[zhoumengkang@localhost unix]$ ./a.out < test > test2
[zhoumengkang@localhost unix]$ ll
总用量 0
-rwxr-xr-x 1 zhoumengkang zhoumengkang 6842 11月  7 19:31 a.out
-rw-r--r-- 1 zhoumengkang zhoumengkang  306 11月  7 19:31 cp_test.c
-rw-r--r-- 1 zhoumengkang zhoumengkang   19 11月  7 19:43 test
-rw-r--r-- 1 zhoumengkang zhoumengkang   19 11月  7 2015 test2
[zhoumengkang@localhost unix]$ cat test2
zmkzmk
test
hahaha

非阻塞I/O实例

从标准输入读取500000字节,并试图将它们写到标准输出上。

对于一个给定的描述符,有两种方式将其指定为非阻塞I/O:

  1. 如果调用open获得描述符,则可以指定O_NONBLOCK标志。(具体参考man 2 open或者书的第50页)

  2. 对于已经打开的描述符,则可以调用fcntl,由该函数打开O_NONBLOCK文件状态标志。(具体参考man 2 fcntl或者书的第65~68页)

int fcntl(int fd, int cmd, .../* int arg */) 
//cmd = F_GETFL|F_SETFL 获取/设置文件状态标志

在修改文件描述符标志或文件状态标志时,必需谨慎,先要获得现在的标志值,然后修改为新的标志值,最后将其设置为文件描述符的标志值。 

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <errno.h>

char 	buf[500000];
void 	set_fl(int fd, int flags);
void 	clr_fl(int fd, int flags);

int main(void)
{
	int		ntowrite, nwrite;
	char	*ptr;

	ntowrite = read(STDIN_FILENO, buf, sizeof(buf));
	fprintf(stderr, "read %d bytes\n", ntowrite);

	set_fl(STDOUT_FILENO, O_NONBLOCK); /* set nonblocking */

	ptr = buf;

	while (ntowrite > 0)
	{
		errno 	= 0;
		nwrite 	= write(STDOUT_FILENO, ptr, ntowrite);
		fprintf(stderr, "nwrite = %d, errno = %d\n", nwrite, errno);

		if (nwrite > 0)
		{
			ptr += nwrite;
			ntowrite -= nwrite;
		}
	}

	clr_fl(STDOUT_FILENO, O_NONBLOCK); /* clear nonblocking */

	exit(0);
}


/**
 * fd		需要修改的文件描述符
 * flags	需要清除的文件状态标志
 */

void set_fl(int fd, int flags)
{
	int		val;
	if ((val = fcntl(fd, F_GETFL, 0)) < 0)
	{
		perror("fcntl F_GETFL error: ");
	}

	val |= flags; /* turn no flags */

	if (fcntl(fd, F_SETFL, val) < 0)
	{
		perror("fcntl F_SETFL error: ");
	}
}

/**
 * fd		需要修改的文件描述符
 * flags	需要开启的文件状态标志
 */

void clr_fl(int fd, int flags)
{
	int		val;
	if ((val = fcntl(fd, F_GETFL, 0)) < 0)
	{
		perror("fcntl F_GETFL error: ");
	}

	val &= ~flags; /* turn flags off */

	if (fcntl(fd, F_SETFL, val) < 0)
	{
		perror("fcntl F_SETFL error: ");
	}
}

首先输入到普通文件,发现只write了一次。

[zhoumengkang@localhost unix]$ ls -l /etc/services 
-rw-r--r--. 1 root root 641020 10月  2 2013 /etc/services
[zhoumengkang@localhost unix]$ ./a.out < /etc/services > temp.file
read 500000 bytes
nwrite = 500000, errno = 0
[zhoumengkang@localhost unix]$ ls -l temp.file 
-rw-r--r-- 1 zhoumengkang zhoumengkang 500000 11月  8 20:20 temp.file

输出到终端时

[zhoumengkang@localhost unix]$ ./a.out < /etc/services 2>stderr.out

发现调用了很多次write很多时候都是失败的

[zhoumengkang@localhost unix]$ cat stderr.out 
read 500000 bytes
nwrite = 7015, errno = 0
nwrite = -1, errno = 11
nwrite = -1, errno = 11
nwrite = -1, errno = 11
nwrite = -1, errno = 11
nwrite = -1, errno = 11
nwrite = -1, errno = 11
nwrite = 6882, errno = 0
nwrite = -1, errno = 11
nwrite = -1, errno = 11
nwrite = -1, errno = 11
nwrite = -1, errno = 11
nwrite = -1, errno = 11
nwrite = -1, errno = 11
nwrite = -1, errno = 11
nwrite = -1, errno = 11
nwrite = -1, errno = 11
...后面不列举了

然后我把第一行去掉,最了一次统计

[zhoumengkang@localhost unix]$ wc -l stderr.out 
1534 stderr.out
[zhoumengkang@localhost unix]$ awk '{a[$6]++}END{for(i in a){print i,a[i]}}' stderr.out 
11 1461
0 73
[zhoumengkang@localhost unix]$ awk '{a[$3]++}END{for(i in a){print i,a[i]}}' stderr.out |sort -k1n
-1, 1461
438, 1
6679, 1
6794, 1
6799, 1
6815, 2
6835, 2
6836, 2
6842, 1
6849, 1
6852, 1
6853, 1
6856, 1
6858, 1
6862, 1
...

在此实例中,程序发出了1534write调用,但是只有73次是真正输出了数据,其余1461次都只返回了错误。

这种形式的循环称为轮询,在多用户系统上用它会浪费CPU时间。

同样是非阻塞I/O为什么输出到指定文件的时候是1次 write 调用就能完成,而输出到终端时,则分了1000多次次呢?

是因为终端的缓冲区不能一次性容纳500000 byte么?我通过查看错误输出,发现最多的时候也才只能一次写入7154 byte,所以可以粗略估计在我测试的当前环境下终端驱动程序一次能接受的数据量就是7154 byte,不知道这样推测是否正确(可能1000次I/O的统计量还不够)。

[zhoumengkang@localhost unix]$ awk '{a[$3]++}END{for(i in a){print i,a[i]}}' stderr.out |sort -k1nr|head -5
7154, 1
7121, 1
7094, 1
7082, 2
7075, 1

I/O 多路转接的引出:两个描述符的阻塞读

为了加深记忆写了两个简单的例子,第一个是先打开一个文件读取,输出到标准输出,然后从标准输入读取输出到标准输出。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

int main(void)
{
	int		fd;
	ssize_t read_content;
	char 	buf[100];

	fd = open("./test.txt",O_RDONLY);
	read_content = read(fd,buf,sizeof(buf));
	write(STDOUT_FILENO,buf,read_content);
	close(fd);

	read_content = read(STDIN_FILENO,buf,sizeof(buf));
	write(STDOUT_FILENO,buf,read_content);

	exit(0);
}

编译之后运行,就会先输出./test.txt的内容,然后等待我在终端输入,输入完毕之后回车,终端输出,程序退出。

第二个是先从标准输入读取,输出到标准输出,然后从一个打开的文件读取,输出到标准输出。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

int main(void)
{
	int		fd;
	ssize_t read_content;
	char 	buf[100];

	read_content = read(STDIN_FILENO,buf,sizeof(buf));
	write(STDOUT_FILENO,buf,read_content);
	close(fd);

	fd = open("./test.txt",O_RDONLY);
	read_content = read(fd,buf,sizeof(buf));
	write(STDOUT_FILENO,buf,read_content);

	exit(0);
}

这个时候,如果我一直不在终端输入并回车,那么就一直阻塞,无法读取./test.txt里面的内容。

书中指出,可以使用多进程,父进程和子进程分别处理两个,子进程结束的时候会给父进程发送SIGCHLD信号,但是父进程结束的时候需要给子进程发送SIGUSR1信号,但是这会使得程序变得更加复杂。Pass!

使用多线程,虽然避免了终止的复杂性,但却要求处理两个线程间的同步。Pass!

I/O 多路转接的引出:非阻塞 I/O 轮询的方式

对两个描述符都设置为非阻塞读,对第一个描述符发一个 read,如果输入上有数据,则读取数据并处理。如果无数据刻度,则调用立即返回。等待一秒,然后对第二个描述符作同样的处理。循环操作。

这种方法的不足之处就是浪费 CPU 时间,因为大多数时间实际上是无数据可读的,因此在执行 read 系统调用浪费了大量的时间。

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <errno.h>
 
char   buf[100];
void   set_fl(int fd, int flags);
void   clr_fl(int fd, int flags);
void   print_stdin();
void   print_txt_content();
 
int main(void)
{
	while(1)
	{
		print_txt_content();
		sleep(1);
    	print_stdin();
	}
	
    exit(0);
}

void print_txt_content()
{
	int     fd;
    ssize_t read_content;

	fd = open("./test.txt",O_RDONLY);
	set_fl(fd, O_NONBLOCK); /* set nonblocking */
    read_content = read(fd,buf,sizeof(buf));
    close(fd);

    if (read_content <= 0)
    {
    	return ;
    }
    
    write(STDOUT_FILENO,buf,read_content);
}

void print_stdin()
{
	ssize_t     read_content;
 
 	set_fl(STDIN_FILENO, O_NONBLOCK); /* set nonblocking */
    read_content = read(STDIN_FILENO, buf, sizeof(buf));
	clr_fl(STDIN_FILENO, O_NONBLOCK); /* clear nonblocking */
    
    if (read_content <= 0)
    {
    	return ;
    }
    
    write(STDOUT_FILENO, buf, read_content);
}
 
 
/**
 * fd     需要修改的文件描述符
 * flags  需要清除的文件状态标志
 */
 
void set_fl(int fd, int flags)
{
    int     val;
    if ((val = fcntl(fd, F_GETFL, 0)) < 0)
    {
        perror("fcntl F_GETFL error: ");
    }
 
    val |= flags; /* turn no flags */
 
    if (fcntl(fd, F_SETFL, val) < 0)
    {
        perror("fcntl F_SETFL error: ");
    }
}
 
/**
 * fd     需要修改的文件描述符
 * flags  需要开启的文件状态标志
 */
 
void clr_fl(int fd, int flags)
{
    int     val;
    if ((val = fcntl(fd, F_GETFL, 0)) < 0)
    {
        perror("fcntl F_GETFL error: ");
    }
 
    val &= ~flags; /* turn flags off */
 
    if (fcntl(fd, F_SETFL, val) < 0)
    {
        perror("fcntl F_SETFL error: ");
    }
}

引出一种新的技术,成为异步 I/O。利用这种技术,进程告诉内核:当描述符准备好可以进行 I/O 时,用一个信号通知它。可移植性的问题我暂不考虑,还有一个问题,这种信号对每个进程而言只有一个(SIGPOLL)。也 Pass!

I/O多路转接 select 方法的使用

书上没有给出完整的例子自己先写一个,相比后面写服务端肯定也要再用到。

select的返回值是准备好的描述符,书中特别强调这里的准备好,在下面的例子中是描述符读集,若对读集中的一个描述符进行read操作不会阻塞,则认为此描述符准备好了。觉得说的比较含糊,实际发现是当该描述符有内容可读,即为准备好了。

书中也特别强调一个描述符的阻塞并不影响select是否阻塞。这在下面的例子中也有体现,标准输入是阻塞的,但是fd1,fd2都是准备好的,所以程序并没有阻塞。

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/select.h>
#include <sys/time.h>

int main(void)
{
	fd_set     read_set;
    int        fd1, fd2, maxfdp;
    struct     timeval time_val;
 
    fd1 = open("./test.txt",O_RDONLY);
    fd2 = open("./temp.file",O_RDONLY);
    maxfdp = fd2 + 1;
 
    FD_ZERO(&read_set);
    FD_SET(0, &read_set);
    // FD_SET(fd1, &read_set);
    // FD_SET(fd2, &read_set);

    time_val.tv_sec = 10;
    time_val.tv_usec = 0;

	while(1)
	{

		switch(select(maxfdp, &read_set, NULL, NULL, &time_val))
		{
			case -1:
				perror("select error:");
				break;
			case 0:
				printf("no fd prepared\n");
				break;
			default:
				for (int i = 0; i < maxfdp; ++i)
	            {
	                if (FD_ISSET(i, &read_set))
	                {
	                    printf("fd %d in the read_set\n", i);
	                    if (i == STDIN_FILENO)
	                    {
	                    	exit(0);
	                    }
	                }
	            }
				break;
		}
		sleep(1);
	}

	return 0;
}

测试结果:当编译完之后一直阻塞,当我在终端输入内容并回车,则程序退出。和我预期的一样。

[zhoumengkang@localhost unix]$ gcc select_demo.c -std=c99
[zhoumengkang@localhost unix]$ ./a.out 
zmk
fd 0 in the read_set
[zhoumengkang@localhost unix]$ zmk

但是,如果打开注释的两行,则当我在终端输入回车,也一直没退出。为什么会这样呢?

[zhoumengkang@localhost unix]$ gcc select_demo.c -std=c99
[zhoumengkang@localhost unix]$ ./a.out 
fd 3 in the read_set
fd 4 in the read_set
zmk
fd 3 in the read_set
fd 4 in the read_set
zmk
fd 3 in the read_set
fd 4 in the read_set
^C
[zhoumengkang@localhost unix]$

select会重写read_set中的内容,while中第一次select返回之后,read_set中描述符集被设置为第一次可读的描述符,所以不包括0了。需要注意的是,select会重写read_set中的内容,如果需要循环使用select,下次select之前需要重写给read_set添加值的(一般是清空之后重新添加,也就是说应该把19~24行的代码放到while循环中)。 

原文:http://segmentfault.com/q/1010000003979356/a-1020000003981086 很感谢这位网友给我解答问题。

改为下面这样就等同于每次都重新初始化了,当我输入内容就会退出了。同时测试发现在select函数中,如果timevalNULL时,如果没有可用的 fd,那么会一直阻塞,也就不会循环了。

int main(void)
{
	fd_set     read_set, read_set_cp;
    int        fd1, fd2, maxfdp;
    struct     timeval time_val;
 
    fd1 = open("./test.txt",O_RDONLY);
    fd2 = open("./temp.file",O_RDONLY);
    maxfdp = fd2 + 1;
 
	FD_ZERO(&read_set);
    FD_SET(0, &read_set);
    FD_SET(fd1, &read_set);
    FD_SET(fd2, &read_set);

    time_val.tv_sec = 10;
    time_val.tv_usec = 0;

	while(1)
	{
		read_set_cp = read_set;
		switch(select(maxfdp, &read_set_cp, NULL, NULL, &time_val))
		{
			case -1:
				perror("select error:");
				break;
			case 0:
				printf("no fd prepared\n");
				break;
			default:
				for (int i = 0; i < maxfdp; ++i)
	            {
	                if (FD_ISSET(i, &read_set_cp))
	                {
	                    printf("fd %d in the read_set\n", i);
	                    if (i == STDIN_FILENO)
	                    {
	                    	exit(0);
	                    }
	                }
	            }
				break;
		}
		sleep(1);
	}

	return 0;
}

I/O多路转接 poll 方法的使用

#include <poll.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/time.h>
#include <memory.h>

int main(void)
{
	int		pollfd_size = 2;
	int 	fd1,fd2;
	char 	buf[128];
	int		read_content;

	struct pollfd pollfd_array[pollfd_size];

	fd1 = open("./test.txt", O_RDONLY);
    fd2 = open("./temp.file", O_RDONLY);

    memset (pollfd_array, 0, sizeof(pollfd_array));

	pollfd_array[0].fd = fd1;
	pollfd_array[0].events = POLLIN | POLLERR;

	pollfd_array[1].fd = fd2;
	pollfd_array[1].events = POLLIN | POLLERR;
    

	switch(poll(pollfd_array, pollfd_size, 5000))
	{
		case -1:
			perror("poll error:");
			break;
		case 0:
			printf("no data prepared\n");
			break;
		default:
			for (int i = 0; i < pollfd_size; ++i)
            {
                if (pollfd_array[i].events & POLLIN)
                {
                    read_content = read(pollfd_array[i].fd, buf, sizeof(buf));
                    write(STDOUT_FILENO,buf,read_content);
                }
            }
			break;
	}

	return 0;
}


评论列表