IO模型:多路复用及信号驱动(fasync)原理代码详解

多路复用支持

select、poll、epoll都是I/O多路复用的机制,它们允许单个进程或线程同时监视多个文件描述符(如socket、管道、文件等),以等待I/O操作(如读、写、异常)的完成。下面分别解释这三种机制的原理:

1. select

原理概述

  • select机制通过监视一组文件描述符,当其中一个或多个文件描述符就绪时(即可以进行非阻塞的I/O操作),select会返回并告知程序哪些文件描述符已经就绪。

工作流程

  1. 用户态到内核态的拷贝:select函数将用户态的文件描述符集合(fd_set)拷贝到内核态。
  2. 内核遍历:内核遍历所有文件描述符,检查它们的状态(如读缓冲区是否有数据、写缓冲区是否有空间等)。
  3. 就绪状态返回:内核将所有就绪的文件描述符从内核态拷贝回用户态,并返回一个包含就绪文件描述符数量的值。
  4. 用户态处理:用户态程序遍历返回的文件描述符集合,对就绪的文件描述符进行相应的I/O操作。

缺点

  • 每次调用select都需要进行用户态到内核态的拷贝,开销较大。
  • 内核需要遍历所有文件描述符,效率较低。
  • 文件描述符数量有限制,通常是1024个。

2. poll

原理概述

  • poll机制与select类似,也是通过监视一组文件描述符来等待I/O操作的完成。

工作流程

  1. 使用pollfd结构体:poll使用pollfd结构体数组来保存需要监视的文件描述符信息,每个pollfd结构体包含文件描述符、需要监视的事件和实际发生的事件。
  2. 内核遍历:与select类似,内核遍历所有pollfd结构体,检查它们对应的文件描述符的状态。
  3. 就绪状态返回:内核将发生事件的文件描述符信息通过pollfd结构体数组返回给用户态。
  4. 用户态处理:用户态程序遍历pollfd结构体数组,对发生事件的文件描述符进行相应的I/O操作。

优点

  • 无最大文件描述符数量限制,理论上可以监视任意数量的文件描述符。

缺点

  • 与select类似,每次调用poll也需要遍历所有文件描述符,效率较低。
  • 在文件描述符数量较多的情况下,性能问题较为突出。

3. epoll

原理概述

  • epoll是Linux特有的I/O事件通知机制,用于处理大量并发连接中的I/O事件。它采用了事件驱动的方式,只在有事件发生时才触发通知,避免了轮询的开销。

工作流程

  1. 创建epoll实例:通过epoll_create函数创建一个epoll实例,并返回一个文件描述符(epfd)。
  2. 注册事件:通过epoll_ctl函数将需要监视的文件描述符和事件类型注册到epoll实例中。
  3. 等待事件:通过epoll_wait函数阻塞等待注册的事件发生。当事件发生时,epoll_wait会返回触发事件的文件描述符和事件类型。
  4. 处理事件:用户态程序根据epoll_wait返回的文件描述符和事件类型进行相应的I/O操作。

优点

  • 只在有事件发生时才触发通知,避免了轮询的开销。
  • 支持大规模并发,能够同时监视大量的文件描述符。
  • 采用了红黑树和就绪链表等高效的数据结构,提高了性能。
  • 支持水平触发(LT)和边缘触发(ET)两种模式,可以根据需求选择适合的模式。

缺点

  • 仅在Linux系统中可用,不是跨平台的解决方案。

本文程序将使用select:

int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
/*    功能:监听多个描述符,阻塞等待有一个或者多个文件描述符,准备就绪。
        内核将没有准备就绪的文件描述符,从集合中清掉了。

参数:

  • nfds            最大文件描述符数 ,加1
  • readfds        读文件描述符集合
  • writefds        写文件描述符集合
  • exceptfds        其他异常的文件描述符集合
  • timeout        超时时间(NULL)

    返回值:当timeout为NULL时返回0,成功:准备好的文件描述的个数  出错:-1 
           当timeout不为NULL时,如超时设置为0,则select为非阻塞,超时设置 > 0,则无描述符可被操作的情况下阻塞指定长度的时间 
*/

使用函数:

  • void FD_CLR(int fd, fd_set *set);          //功能:将fd 从集合中清除掉
  • int  FD_ISSET(int fd, fd_set *set);        //功能:判断fd 是否存在于集合中
  • void FD_SET(int fd, fd_set *set);         //功能:将fd 添加到集合中
  • void FD_ZERO(fd_set *set);                //功能:将集合清零

接上文代码加入多路复用功能: 

mychar.c:(部分新加入)

unsigned int mychar_poll(struct file *pfile, poll_table *ptb) 
{
	struct mychar_dev *pmydev = (struct mychar_dev *)pfile->private_data;
	//从pfile中得到设备的地址
	unsigned int mask = 0;

	poll_wait(pfile, &pmydev->rq, ptb);
	poll_wait(pfile, &pmydev->wq, ptb);

	if(pmydev->curlen > 0) {
		mask |= POLLIN | POLLRDNORM;
	}
	if(pmydev->curlen < BUF_LEN) {
		mask |= POLLOUT | POLLWRNORM;
	}

	return mask;
}


struct file_operations myops = {
	.owner = THIS_MODULE,
	.open = mychar_open,
	.release = mychar_release,
	.read = mychar_read,
	.write = mychar_write,
	.unlocked_ioctl = mychar_ioctl,
	.poll = mychar_poll,
};

 代码注释:

  1. select调用select(maxfd+1, &rfds, &wfds, NULL, NULL);这一行调用了select函数。maxfd+1select监视的文件描述符中的最大值加1(因为文件描述符是从0开始计数的),这是为了确保所有在rfdswfds集合中的文件描述符都被检查。&rfds&wfds是指向文件描述符集合的指针,分别表示关注可读和可写的文件描述符集合。最后的两个NULL参数表示不关心异常条件和没有超时时间(即无限期等待)。

  2. 错误处理select调用返回后,首先检查返回值ret。如果ret小于0,说明select调用遇到了错误。此时,通过检查errno的值来确定错误的具体原因。

    • errno == EINTR:如果errno的值是EINTR,这表示select调用被信号中断。在这种情况下,通常不需要采取特殊的错误处理措施,因为这不是一个真正的错误,只是调用被中断了。因此,代码通过continue;语句继续循环,以便再次尝试select调用。

    • 其他错误:如果errno的值不是EINTR,则表示遇到了其他类型的错误。此时,通过break;语句退出循环,可能需要进一步处理这个错误,比如记录错误日志、关闭文件描述符等。

实现代码:testmychar_select.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/select.h>
#include <sys/time.h>
#include <unistd.h>
#include <errno.h>

#include "mychar.h"

int main(int argc,char *argv[])
{
	int fd = -1;
	char buf[32] = "";
	int ret = 0;
	fd_set rfds;
	
	if(argc < 2)
	{
		printf("The argument is too few\n");
		return -1;
	}

	fd = open(argv[1],O_RDWR);
	if(fd < 0)
	{
		printf("open %s failed\n",argv[1]);
		return 2;
	}

	while(1)
	{
		FD_ZERO(&rfds);
		FD_SET(fd,&rfds);//功能:将fd 添加到集合中
		
		ret = select(fd+1,&rfds,NULL,NULL,NULL);
		if(ret < 0)
		{
			if(errno == EINTR)
			{
				continue;
			}
			else
			{
				printf("select failed\n");
				break;
			}
		}
		if(FD_ISSET(fd, &rfds))//功能:判断fd 是否存在于集合中
		{
			read(fd, buf, 32);
			printf("buf = %s\n", buf);
		}
	}

	
	close(fd);
	fd = -1;
	return 0;
}

实现效果:

信号驱动支持 

信号注册+fcntl

signal(SIGIO, input_handler); //注册信号处理函数

fcntl(fd, F_SETOWN, getpid());//将描述符设置给对应进程,好由描述符获知PID

oflags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, oflags | FASYNC);//将该设备的IO模式设置成信号驱动模式

 应用模板示例:

//应用模板
int main()
{
	int fd = open("/dev/xxxx",O_RDONLY);

	fcntl(fd, F_SETOWN, getpid());

	oflags = fcntl(fd, F_GETFL);
	fcntl(fd, F_SETFL, oflags | FASYNC);

	signal(SIGIO,xxxx_handler);

	//......
}
    
void xxxx_handle(int signo)
{//读写数据
    
}

fasync:

        在Linux内核中,fasync 机制是一种用于异步通知的机制,它允许内核在文件描述符(如字符设备或套接字)上的特定事件发生时通知用户空间的应用程序。这些事件可能包括数据的可用性、错误发生、设备状态的改变等。

        在字符设备驱动等上下文中,fasync 通常指的是与文件描述符相关联的异步通知功能的启用和禁用。在 file_operations 结构中,fasync 是一个指向函数的指针,该函数负责处理文件描述符的异步通知注册和注销。

mychar_dev结构体成员

struct mychar_dev{  
    // 其他成员...  
    struct fasync_struct *pasync_obj; // 用于存储异步通知队列的指针  
};

pasync_obj

   pasync_obj 是你在设备驱动结构体中定义的一个成员变量,它通常是一个指向 fasync_struct 链表头部的指针。在你的例子中,pasync_obj 被用来存储内核为跟踪你的设备上的异步通知而维护的 fasync_struct 链表的地址。

        当你调用 fasync_helper 来注册或注销文件描述符的异步通知时,你通过传递 &pasync_obj(即指向 pasync_obj 的指针的地址)来告诉 fasync_helper 你的 fasync_struct 链表存储在哪里。这样,fasync_helper 就可以根据需要更新这个链表了。

mychar_fasync 函数

int mychar_fasync(int fd, struct file *pfile, int mode) 
{
	struct mychar_dev *pmydev = (struct mychar_dev *)pfile->private_data;
	return fasync_helper(fd, pfile, mode, &pmydev->pasync_obj);
}

         这个函数是 file_operations 结构中 fasync 成员的回调函数.fasync = mychar_fasync,。它使用 fasync_helper 来处理文件描述符的异步通知注册或注销。注意,fasync_helper 的第四个参数是一个指向指针的指针(即&pmydev->pasync_obj),这是因为 fasync_helper 需要能够修改这个指针,以便将新的 fasync_struct 链表头地址存储在那里。

fasync_struct 链表

   fasync_struct 链表是内核内部用于跟踪所有对异步通知感兴趣的文件描述符的结构(不理解可参考另外一篇文章http://t.csdnimg.cn/RfRxm。当你通过 fasync 机制注册一个文件描述符以接收异步通知时,内核会创建一个 fasync_struct 结构体实例(如果尚不存在的话),并将其添加到该链表中。每个 fasync_struct 结构体都包含有关文件描述符、信号处理函数和可能的其他状态信息的信息。

        当内核需要发送异步通知时(例如,当设备有数据可读时),它会遍历这个链表,并对每个条目调用相应的信号处理函数(这通常涉及到发送一个信号,如 SIGIO,到用户空间)。

/*设备结构中添加如下成员*/
struct fasync_struct *pasync_obj;

/*应用调用fcntl设置FASYNC时调用该函数产生异步通知结构对象,并将其地址设置到设备结构成员中*/
static int hello_fasync(int fd, struct file *filp, int mode) //函数名初始化给struct file_operations的成员.fasync
{
	struct hello_device *dev = filp->private_data; 
	return fasync_helper(fd, filp, mode, &dev->pasync_obj);
}

/*写函数中有数据可读时向应用层发信号*/
if (dev->pasync_obj)
       kill_fasync(&dev->pasync_obj, SIGIO, POLL_IN);
       
/*release函数中释放异步通知结构对象*/
if (dev->pasync_obj) 
	fasync_helper(-1, filp, 0, &dev->pasync_obj);

int fasync_helper(int fd, struct file *filp, int mode, struct fasync_struct **pp);
/*
	功能:在设备文件被关闭时(通常在 release 函数中),需要注销所有异步通知。再次使用 fasync_helper,但传递 -1 作为文件描述符(表示“无效”或“不特定”的文件描述符),并将 mode 设置为 0 来指示注销操作。
	参数:
	返回值:成功为>=0,失败负数
*/

void kill_fasync(struct fasync_struct **, int, int);
/*	
	功能:当设备有数据可读时,可以使用 kill_fasync 函数来向所有已注册的文件描述符发送 SIGIO 信号。但请注意,kill_fasync 的第一个参数应该是一个指向 fasync_struct 指针的指针(即 struct fasync_struct **)。
	参数:
		struct fasync_struct ** 指向保存异步通知结构地址的指针
		int 	信号 SIGIO/SIGKILL/SIGCHLD/SIGCONT/SIGSTOP
		int 	读写信息POLLIN、POLLOUT
*/

 mychar.c:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/wait.h>
#include <linux/sched.h>
#include <linux/poll.h>
#include <asm/uaccess.h>
#include "mychar.h"

//#define MYCHAR_DEV_CNT 3

//保存主设备号
int major = 11;
int minor = 0;
int mychar_num = 1;//MYCHAR_DEV_CNT;//次设备号数量

#define BUF_LEN 100
/*
struct cdev mydev;//创建的字符设备的描述信息
char mydev_buf[BUF_LEN];
int curlen = 0;//定义一个数据总是从下标0开始的长度(上面的mydev_buf)
*/

struct mychar_dev
{
	struct cdev mydev;
	char mydev_buf[BUF_LEN];
	int curlen;//curlen表示设备当前可读取的数据长度
	
	wait_queue_head_t rq;
	wait_queue_head_t wq;
	
	struct fasync_struct *pasync_obj;//异步
};

struct mychar_dev gmydev;

int mychar_open(struct inode *pnode, struct file *pfile)
{
	pfile->private_data =(void *) (container_of(pnode->i_cdev,struct mychar_dev,mydev));
	//已知成员的地址获得所在结构体变量的地址:container_of(成员地址,结构体类型名,成员在结构体中的名称)
	//当驱动程序接收到打开文件的请求(如通过其open方法)时,它有机会为新的file结构体分配一个private_data。这通常涉及到动态分配一个驱动程序特定的数据结构(如struct mychar_dev),并将其地址存储在file->private_data中。之后,每当对该文件执行其他操作(如读、写、关闭等)时,驱动程序都可以通过file->private_data访问到这个数据结构。
	
	return 0;
}

int mychar_release(struct inode *pnode, struct file *pfile)
{
	struct mychar_dev *pmydev = (struct mychar_dev *)pfile->private_data;

	if(pmydev->pasync_obj != NULL) {
		fasync_helper(-1, pfile, 0, &pmydev->pasync_obj);
	}

	return 0;
}

ssize_t mychar_read(struct file *pfile, char __user *puser, size_t count, loff_t *p_pos)
{                   /*结构体表示当前打开的文件 
	 				指向用户空间缓冲区的指针,用于存储从设备读取的数据 
					希望从设备读取的字节数
					指向长偏移量的指针,用于指示在文件中的当前读写位置*/
	struct mychar_dev *pmydev = (struct mychar_dev *)pfile->private_data;
	int size = 0;//表示实际能读到的字节数
	int ret = 0;
	
	/* 判断是否有数据可读 */
	if(pmydev->curlen <= 0)//判断缓冲区当前长度(curlen)是否小于或等于0来检查缓冲区是否为空
	{
		if(pfile->f_flags & O_NONBLOCK)
		{
			//非阻塞
			printk("O_NONBLOCK Not Data Read\n");
			return -1;
		}else { 
			//阻塞
			/* 睡眠 当curlen>0 时返回 否则阻塞*/
			ret = wait_event_interruptible(pmydev->rq, pmydev->curlen > 0);
			/* 信号唤醒 */
			if(ret) {
				return -ERESTARTSYS;
			}
		}
	}

	//如果期望读的字节数大于当前可以读到的字节数
	if(count > pmydev->curlen)
	{
		size = pmydev->curlen;//就把能读到的全都给实际能读到的size
	}
	else
	{
		size = count;//将buf里面未读完的数据往0位置放
	}
	/*用户空间中目标缓冲区的指针 ---内核空间中源数据的指针 ---要复制的字节数
	 * 用于从内核空间复制数据到用户空间*/
	ret = copy_to_user(puser,pmydev->mydev_buf,size);
	if(ret)
	{
		printk("copy_to_read failed\n");
		return -1;
	}
	memcpy(pmydev->mydev_buf,pmydev->mydev_buf + size,pmydev->curlen - size);
	//mydev_buf为内核储存地址,加上size后移到之前读完size大小数据的后面,将size大小后面没读完的数据重新放回0初始位置。拷贝的大小为(之前可读取的总长度curlen-已经读取的size大小)。
	//curlen = curlen - size;
	pmydev->curlen -= size;//然后将curlen往前移动size大小的字节即可回到0位置
	
	wake_up_interruptible(&pmydev->wq);
	return size;//返回实际读的字节
}

//将用户空间的数据拷贝到内核空间,数据本身不变,+const
ssize_t mychar_write(struct file *pfile, const char __user *puser, size_t count, loff_t *p_pos)
{
	//从puser将数据拷贝p_pos起始位置的期望写入字节数count大小字节给内核空间
	int size = 0;//表示实际能写入的字节数
	int ret = 0;
	struct mychar_dev *pmydev = (struct mychar_dev *)pfile->private_data;
	
	if(pmydev->curlen >= BUF_LEN) 
	{
		if(pfile->f_flags & O_NONBLOCK) 
		{	//非阻塞
			printk("O_NONBLOCK Can Not Write Data\n");
			return -1;

		} 
		else 
		{ 	//阻塞
			ret = wait_event_interruptible(pmydev->wq, pmydev->curlen < BUF_LEN);
			/* 信号唤醒 */
			if(ret) {
				return -ERESTARTSYS;
			}
		}
		
	}


	if(count > BUF_LEN-pmydev->curlen)
	{
		size = BUF_LEN - pmydev->curlen;// 剩余空间不足以容纳全部请求的数据
	}
	else
	{
		size = count;// 剩余空间足够大,可以容纳全部请求的数据
	}

	ret = copy_from_user(pmydev->mydev_buf + pmydev->curlen,puser,size);
	if(ret)
	{
		printk("copy_from_read failed\n");
		return -1;
	}
	pmydev->curlen  +=  size;// 更新文件位置
	
	/* 唤醒读阻塞 */
	wake_up_interruptible(&pmydev->rq);
	
	if(pmydev->pasync_obj != NULL)//当设备有数据可读时
	{
		kill_fasync(&pmydev->pasync_obj, SIGIO,POLL_IN);
	}
	
	return size;
}

long mychar_ioctl(struct file *pfile, unsigned int cmd,unsigned long arg)
{
	struct mychar_dev *pmydev = (struct mychar_dev *)pfile->private_data;
	//文件指针 pfile、一个命令码 cmd 和一个参数 arg(用户空间地址)
	int __user *pret = (int *)arg;
	//arg 是一个指向用户空间 int 的指针
	int maxlen = BUF_LEN;
	int ret = 0;
	

	switch(cmd)
	{
	case MYCHAR_IOCTL_GET_MAXLEN:
			ret = copy_to_user(pret,&maxlen,sizeof(int));
			if(ret)
			{
				printk(KERN_ERR"copy to user MAXLEN failed\n");
				return -EFAULT;
			}
			break;
	case MYCHAR_IOCTL_GET_CURLEN:
			ret = copy_to_user(pret,&pmydev->curlen,sizeof(int));
			if(ret)
			{
				printk(KERN_ERR"copy to user CURLEN failed\n");
				return -EFAULT;
			}
			break;
	default:
			printk(KERN_ERR"cmd unknow\n");
			return -EINVAL;//表示无效参数
	}
	return 0;
}

unsigned int mychar_poll(struct file *pfile, poll_table *ptb) 
{
	struct mychar_dev *pmydev = (struct mychar_dev *)pfile->private_data;
	//从pfile中得到设备的地址
	unsigned int mask = 0;

	poll_wait(pfile, &pmydev->rq, ptb);
	poll_wait(pfile, &pmydev->wq, ptb);

	if(pmydev->curlen > 0) {
		mask |= POLLIN | POLLRDNORM;
	}
	if(pmydev->curlen < BUF_LEN) {
		mask |= POLLOUT | POLLWRNORM;
	}

	return mask;
}

int mychar_fasync(int fd, struct file *pfile, int mode) 
{
	struct mychar_dev *pmydev = (struct mychar_dev *)pfile->private_data;
	return fasync_helper(fd, pfile, mode, &pmydev->pasync_obj);
}



struct file_operations myops = {
	.owner = THIS_MODULE,
	.open = mychar_open,
	.release = mychar_release,
	.read = mychar_read,
	.write = mychar_write,
	.unlocked_ioctl = mychar_ioctl,
	.poll = mychar_poll,
	.fasync = mychar_fasync,
};

int __init mychar_init(void)
{
	int ret = 0;
	
	//将主次设备号合成一个完整设备号
	dev_t devno = MKDEV(major,minor);
	//手动申请设备号,若返回值不为0,则自动设置
	ret = register_chrdev_region(devno,mychar_num,"qmycharq");
	if(ret)
	{
		ret = alloc_chrdev_region(&devno,minor,mychar_num,"qmycharq");
		if(ret)
		{
			printk("get devno failed\n");
			return -1;
		}
		major = MAJOR(devno);//自动分配的设备号不一定是11,所以需要分离出其主设备号
							 //因为次设备号都是从0开始,所以不用分离
	}

	cdev_init(&gmydev.mydev,&myops);//初始化字符设备(cdev)结构体
	/*在字符设备驱动的开发中,每个字符设备都需要一个与之对应的 cdev 结构体来表示。
	 * cdev_init 函数的作用就是初始化这个结构体,
	 * 并建立起 cdev 和 file_operations 结构体之间的连接
	 * 以便内核能够知道如何操作这个字符设备。*/

	gmydev.mydev.owner = THIS_MODULE;
	/*用于将字符设备(通过 cdev 结构体表示)的所有者设置为当前模块
	 * mydev 是指向 cdev 结构体的指针,而 THIS_MODULE 是一个宏,
	 * 它在编译时会被替换为指向当前模块 module 结构体的指针*/
	 /*如果驱动程序是作为模块加载的,它通常会使用 THIS_MODULE 宏来指代当前模块,并将 THIS_MODULE 赋值给设备信息的 owner 字段。这样,内核就能够知道哪个模块是设备的拥有者,并在需要时调用该模块的函数。*/

	cdev_add(&gmydev.mydev,devno,1);//将指定字符设备加入到管理内核的hash表中
	
	init_waitqueue_head(&gmydev.rq);
	init_waitqueue_head(&gmydev.wq);
	return 0;
}

void __exit mychar_exit(void)
{
	dev_t devno = MKDEV(major,minor);
	cdev_del(&gmydev.mydev);
	//从内核中移除一个字符设备
	unregister_chrdev_region(devno,mychar_num);
	//参数:需要释放的设备号区域起始值,第一个设备号
	//第二个参数:制定释放设备号数量,即需要释放的设备号区域大小
	
}


MODULE_LICENSE("GPL");

module_init(mychar_init);
module_exit(mychar_exit);

testmychar_signal.c:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/select.h>
#include <sys/time.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>

#include "mychar.h"


int fd = -1;// 定义一个全局变量fd,用于存储文件描述符,初始化为-1表示无效

void sigio_handler(int signo);// 声明信号处理函数,用于处理SIGIO信号 

int main(int argc,char *argv[])
{
	int flg = 0;
	
	if(argc < 2)
	{
		printf("The argument is too few\n");
		return -1;
	}

	signal(SIGIO, sigio_handler);// 注册SIGIO信号的处理函数为sigio_handler 
	
	fd = open(argv[1],O_RDWR);
	if(fd < 0) {
		perror("open");
		return -1;
	}

	fcntl(fd, F_SETOWN, getpid());// 设置文件描述符fd的所有者进程ID为当前进程ID

	flg = fcntl(fd, F_GETFL);	// 获取文件描述符fd的当前标志
	flg |= FASYNC;			// 设置FASYNC标志,以启用异步I/O通知
	fcntl(fd, F_SETFL, flg);	// 应用修改后的标志
	
	while(1);
	
	close(fd);
	fd = -1;
	return 0;
}

void sigio_handler(int signo)
{
	char buf[32] = "";
	read(fd, buf, sizeof(buf));
	printf("buf = %s\n", buf);
}

运行结果: 

  • 13
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值