Linux 高级IO

目录

    • 传统艺能
    • 概念
    • 高级IO
    • 五种 IO 模型
      • 阻塞 IO
      • 非阻塞 IO
      • 信号驱动IO
      • 多路转接IO
    • 异步IO
    • 同步通信 VS 异步通信
    • 同步通信 VS 同步与互斥
    • 阻塞 VS 非阻塞
    • 阻塞IO
    • 非阻塞IO
      • fcntl函数
      • 实现SetNonBlock函数
      • 以非阻塞轮询方式读取标准输入

传统艺能

小编是双非本科大二菜鸟不赘述,欢迎米娜桑来指点江山哦
在这里插入图片描述
1319365055

非科班转码社区诚邀您入驻
小伙伴们,满怀希望,所向披靡,打码一路向北
一个人的单打独斗不如一群人的砥砺前行
这是和梦想合伙人组建的社区,诚邀各位有志之士的加入!!
社区用户好文均加精(“标兵”文章字数2000+加精,“达人”文章字数1500+加精)
直达: 社区链接点我


Linux 高级IO_第1张图片

概念

其实在 c++ 的笔记里面我已经详细提过了,这时还是做一下书面总结:IO 即 input/output ,在冯诺依曼体系中将数据从输入设备拷贝到内存就叫做输入,将数据从内存拷贝到输出设备就叫做输出

对文件进行读写操作本质是一种IO,文件IO对应的外设是磁盘;对网络进行读写操作本质也是一种IO,网络IO对应的外设是网卡

Linux 高级IO_第2张图片
O S 如何得知外设当中有数据可读? \color{red} {OS 如何得知外设当中有数据可读?} OS如何得知外设当中有数据可读?

操作系统不会主动检测外设上是否有数据就绪,这种做法一定会降低工作效率,因为大部分情况下外设都是没数据的,因此操作系统所做的大部分检测工作其实是徒劳。

系统实际采用的是中断的方式来得知外设上是否有数据就绪的,当某个外设上面有数据就绪时,该外设就会向 CPU 当中的中断控制器发送中断信号,中断控制器再根据产生的中断信号的优先级按顺序发送给 CPU。

每一个中断信号都有一个对应的中断处理程序,存储中断信号和中断处理程序映射关系的表叫做中断向量表,当CPU收到某个中断信号时就会自动停止正在运行的程序,然后根据该中断向量表执行该中断信号对应的中断处理程序,处理完毕后再返回原被暂停的程序继续运行(需要注意的是,CPU不直接和外设打交道指的是在数据层面上,而外设其实是可以直接将某些控制信号发送给 CPU 的某些控制器的)

O S 如何处理网卡读到的数据包? \color{red} {OS 如何处理网卡读到的数据包?} OS如何处理网卡读到的数据包?

操作系统任何时刻都可能收到大量数据包,因此操作系统必须将数据包管理起来。所谓的管理就是 “先描述,再组织” 原则,此时内核当中会维护一个结构叫 sk_buff,该结构就是用来管理和控制接收或发送数据包的信息的,下面给出一个简化版的 sk_buff 结构:

Linux 高级IO_第3张图片

当操作系统从网卡当中读取到一个数据包后,会将该数据依次交给链路层、网络层、传输层、应用层进行解包和分用,最终将数据包中的数据交给了上层用户,那对应 sk_buff 结构具体是如何进行数据包解包和分用的呢?

当从网卡中读取到一个数据包后,就会定义出 sk_buff 结构,然后用 sk_buff 结构当中的 data 指针指向读到的数据包,并将定义出来的这个 sk_buff 与其他 sk_buff 用双链表的形式组织起来,此时操作系统对各个数据包的管理就变成了对双链表的增删查改等操作。

接下来需要将读取的数据包交给最底层的链路层处理,进行解包和分用,此时就是让 sk_buff 结构中的 mac_header 指针指向最初的数据包,然后向后读取链路层的报头,剩下的就是需要交给网络层处理的有效载荷了解包。

这时链路层就需要将有效载荷向上交付给网络层进行解包和分用了,这里所说的向上交付只是形象的说法,实际向上交付并不是直接交付数据,我们只需要让 sk_buff 结构当中的 network_header 指针,指向数据包中链路层报头之后的数据即可,然后继续向后读取网络层的报头,便完成了网络层的解包。

紧接着就是传输层对数据进行处理了,同样的道理,让 sk_buff 结构当中的 transport_header 指针,指向数据包中网络层报头之后的数据,然后继续向后读取传输层的报头,便完成了传输层的解包。传输层解包后就可以根据具体使用的传输层协议,对应将剩下的数据拷贝到 TCP 或 UDP 的接收缓冲区供用户读取即可

Linux 高级IO_第4张图片

发送数据时对数据进行封装也是同样的道理,就是依次在数据前面拷贝上对应的报头,最后再将数据发送出去(UDP)或拷贝到发送缓冲区(TCP)即可。也就是说,在封装和解包的过程中,本质存储位置是没有发生变化的,实际只是在用不同指针对数据进行操作而已。

但内核中的sk_buff并不像上面那样简单:

一方面,为了保证高效的网络报文处理效率,这就要求sk_buff的结构也必须是高效的
另一方面,sk_buff 结构需要被内核协议中的各个协议共同使用,因此 sk_buff 必须能够兼容所有网络协议

因此 sk_buff 实际是非常复杂的,在云服务器中 sk_buff 结构定义如下(带伙看个大概乐子就行了,实在太多了):

struct sk_buff {
#ifdef __GENKSYMS__
	/* These two members must be first. */
	struct sk_buff          *next;
	struct sk_buff          *prev;
	ktime_t         tstamp;
#else
	union {
		struct {
			/* These two members must be first. */
			struct sk_buff          *next;
			struct sk_buff          *prev;

			union {
				ktime_t         tstamp;
				struct skb_mstamp skb_mstamp;
				__RH_KABI_CHECK_SIZE_ALIGN(ktime_t a,
				struct skb_mstamp b);
			};
		};
		struct rb_node  rbnode; /* used in netem, ip4 defrag, and tcp stack */
	};
#endif
	struct sock             *sk;
	struct net_device       *dev;

	/*
	* This is the control buffer. It is free to use for every
	* layer. Please put your private variables there. If you
	* want to keep them across layers you have to do a skb_clone()
	* first. This is owned by whoever has the skb queued ATM.
	*/
	char                    cb[48] __aligned(8);

	unsigned long           _skb_refdst;
#ifdef CONFIG_XFRM
	struct  sec_path        *sp;
#endif
	unsigned int            len,
		data_len;
	__u16                   mac_len,
		hdr_len;
	union {
		__wsum          csum;
		struct {
			__u16   csum_start;
			__u16   csum_offset;
		};
	};
	__u32                   priority;
	kmemcheck_bitfield_begin(flags1);
	__u8                    RH_KABI_RENAME(local_df, ignore_df) :1,
	cloned : 1,
		 ip_summed : 2,
				 nohdr : 1,
					 nfctinfo : 3;
	__u8                    pkt_type : 3,
	fclone : 2,
		 ipvs_property : 1,
					 peeked : 1,
						  nf_trace : 1;
	kmemcheck_bitfield_end(flags1);
	__be16                  protocol;

	void(*destructor)(struct sk_buff *skb);
#if defined(CONFIG_NF_CONNTRACK) || defined(CONFIG_NF_CONNTRACK_MODULE)
	struct nf_conntrack     *nfct;
#endif
#if IS_ENABLED(CONFIG_BRIDGE_NETFILTER)
	struct nf_bridge_info   *nf_bridge;
#endif

	/* fields enclosed in headers_start/headers_end are copied
	* using a single memcpy() in __copy_skb_header()
	*/
	/* private: */
	RH_KABI_EXTEND(__u32    headers_start[0])
		/* public: */

		int                     skb_iif;

	RH_KABI_REPLACE(__u32   rxhash,
		__u32   hash)

		__be16                  vlan_proto;
	__u16                   vlan_tci;

#ifdef CONFIG_NET_SCHED
	__u16                   tc_index;       /* traffic control index */
#ifdef CONFIG_NET_CLS_ACT
	__u16                   tc_verd;        /* traffic control verdict */
#endif
#endif

	__u16                   queue_mapping;
	kmemcheck_bitfield_begin(flags2);
#ifdef CONFIG_IPV6_NDISC_NODETYPE
	__u8                    ndisc_nodetype : 2;
#endif
	__u8                    pfmemalloc : 1;
	__u8                    ooo_okay : 1;
	__u8                    RH_KABI_RENAME(l4_rxhash, l4_hash) :1;
	__u8                    wifi_acked_valid : 1;
	__u8                    wifi_acked : 1;
	__u8                    no_fcs : 1;
	__u8                    head_frag : 1;
	/* Indicates the inner headers are valid in the skbuff. */
	__u8                    encapsulation : 1;
	RH_KABI_EXTEND(__u8                     encap_hdr_csum : 1)
		RH_KABI_EXTEND(__u8                     csum_valid : 1)
		RH_KABI_EXTEND(__u8                     csum_complete_sw : 1)
		RH_KABI_EXTEND(__u8                     xmit_more : 1)
		RH_KABI_EXTEND(__u8                     inner_protocol_type : 1)
		RH_KABI_EXTEND(__u8                     remcsum_offload : 1)
		/* 0/2 bit hole (depending on ndisc_nodetype presence) */
		kmemcheck_bitfield_end(flags2);

#if defined CONFIG_NET_DMA_RH_KABI || defined CONFIG_NET_RX_BUSY_POLL || defined CONFIG_XPS
	union {
		unsigned int    napi_id;
		RH_KABI_EXTEND(unsigned int     sender_cpu)
			RH_KABI_DEPRECATE(dma_cookie_t, dma_cookie)
	};
#endif
#ifdef CONFIG_NETWORK_SECMARK
	__u32                   secmark;
#endif
	union {
		__u32           mark;
		__u32           dropcount;
		__u32           reserved_tailroom;
	};

#ifdef __GENKSYMS__
	__be16                  inner_protocol;
#else
	union {
		__be16          inner_protocol;
		__u8            inner_ipproto;
	};
#endif

	__u16                   inner_transport_header;
	__u16                   inner_network_header;
	__u16                   inner_mac_header;
	__u16                   transport_header;
	__u16                   network_header;
	__u16                   mac_header;

	RH_KABI_EXTEND(kmemcheck_bitfield_begin(flags3))
		RH_KABI_EXTEND(__u8     csum_level : 2)
		RH_KABI_EXTEND(__u8     rh_csum_pad : 1)
		RH_KABI_EXTEND(__u8     rh_csum_bad_unused : 1) /* one bit hole */
		RH_KABI_EXTEND(__u8     offload_fwd_mark : 1)
		RH_KABI_EXTEND(__u8     sw_hash : 1)
		RH_KABI_EXTEND(__u8     csum_not_inet : 1)
		RH_KABI_EXTEND(__u8     dst_pending_confirm : 1)
		RH_KABI_EXTEND(__u8     offload_mr_fwd_mark : 1)
		/* 7 bit hole */
		RH_KABI_EXTEND(kmemcheck_bitfield_end(flags3))

		/* private: */
		RH_KABI_EXTEND(__u32    headers_end[0])
		/* public: */

		/* RHEL SPECIFIC
		*
		* The following padding has been inserted before ABI freeze to
		* allow extending the structure while preserve ABI. Feel free
		* to replace reserved slots with required structure field
		* additions of your backport, eventually moving the replaced slot
		* before headers_end, if it need to be copied by __copy_skb_header()
		*/
		u32                     rh_reserved1;
	u32                     rh_reserved2;
	u32                     rh_reserved3;
	u32                     rh_reserved4;
	union {
		unsigned int    napi_id;
		RH_KABI_EXTEND(unsigned int     sender_cpu)
			RH_KABI_DEPRECATE(dma_cookie_t, dma_cookie)
	};
#endif
#ifdef CONFIG_NETWORK_SECMARK
	__u32                   secmark;
#endif
	union {
		__u32           mark;
		__u32           dropcount;
		__u32           reserved_tailroom;
	};

#ifdef __GENKSYMS__
	__be16                  inner_protocol;
#else
	kmemcheck_bitfield_begin(flags1);
	__u8                    RH_KABI_RENAME(local_df, ignore_df) :1,
	cloned : 1,
		 ip_summed : 2,
				 nohdr : 1,
					 nfctinfo : 3;
	__u8                    pkt_type : 3,
	fclone : 2,
		 ipvs_property : 1,
					 peeked : 1,
						  nf_trace : 1;
	kmemcheck_bitfield_end(flags1);
	__be16                  protocol;

	void(*destructor)(struct sk_buff *skb);
#if defined(CONFIG_NF_CONNTRACK) || defined(CONFIG_NF_CONNTRACK_MODULE)
	struct nf_conntrack     *nfct;
#endif
#if IS_ENABLED(CONFIG_BRIDGE_NETFILTER)
	struct nf_bridge_info   *nf_bridge;
#endif

	/* fields enclosed in headers_start/headers_end are copied
	* using a single memcpy() in __copy_skb_header()
	*/
	/* private: */
	/* private: */
	RH_KABI_EXTEND(__u32    headers_start[0])
		/* public: */

		int                     skb_iif;

	RH_KABI_REPLACE(__u32   rxhash,
		__u32   hash)


		__be16                  vlan_proto;
	__u16                   vlan_tci;

#ifdef CONFIG_NET_SCHED
	__u16                   tc_index;       /* traffic control index */
#ifdef CONFIG_NET_CLS_ACT
	__u16                   tc_verd;        /* traffic control verdict */
#endif
#endif

	__u16                   queue_mapping;
	kmemcheck_bitfield_begin(flags2);
#ifdef CONFIG_IPV6_NDISC_NODETYPE
	__u8                    ndisc_nodetype : 2;
#endif
	__u8                    pfmemalloc : 1;
	__u8                    ooo_okay : 1;
	__u8                    RH_KABI_RENAME(l4_rxhash, l4_hash) :1;
	__u8                    wifi_acked_valid : 1;
	__u8                    wifi_acked : 1;
	__u8                    no_fcs : 1;
	__u8                    head_frag : 1;
	/* Indicates the inner headers are valid in the skbuff. */
	__u8                    encapsulation : 1;
	RH_KABI_EXTEND(__u8                     encap_hdr_csum : 1)
		RH_KABI_EXTEND(__u8                     csum_valid : 1)
		RH_KABI_EXTEND(__u8                     csum_valid : 1)
		RH_KABI_EXTEND(__u8                     csum_complete_sw : 1)
		RH_KABI_EXTEND(__u8                     xmit_more : 1)
		RH_KABI_EXTEND(__u8                     inner_protocol_type : 1)
		RH_KABI_EXTEND(__u8                     remcsum_offload : 1)
		/* 0/2 bit hole (depending on ndisc_nodetype presence) */
		kmemcheck_bitfield_end(flags2);

#if defined CONFIG_NET_DMA_RH_KABI || defined CONFIG_NET_RX_BUSY_POLL || defined CONFIG_XPS
	union {
		unsigned int    napi_id;
		RH_KABI_EXTEND(unsigned int     sender_cpu)
			RH_KABI_DEPRECATE(dma_cookie_t, dma_cookie)
	};
#endif
#ifdef CONFIG_NETWORK_SECMARK
	__u32                   secmark;
#endif
	union {
		__u32           mark;
		__u32           dropcount;
		__u32           reserved_tailroom;
	};

#ifdef __GENKSYMS__
	__be16                  inner_protocol;
#else
	union {
		__be16          inner_protocol;
		__u8            inner_ipproto;
	};
#endif

	__u16                   inner_transport_header;
	__u16                   inner_network_header;
	__u16                   inner_mac_header;
	__u16                   transport_header;
	__u16                   network_header;
	__u16                   mac_header;

	RH_KABI_EXTEND(kmemcheck_bitfield_begin(flags3))
		RH_KABI_EXTEND(__u8     csum_level : 2)
		RH_KABI_EXTEND(__u8     rh_csum_pad : 1)
		RH_KABI_EXTEND(__u8     rh_csum_bad_unused : 1) /* one bit hole */
		RH_KABI_EXTEND(__u8     offload_fwd_mark : 1)
		RH_KABI_EXTEND(__u8     sw_hash : 1)
		RH_KABI_EXTEND(__u8     csum_not_inet : 1)
		RH_KABI_EXTEND(__u8     dst_pending_confirm : 1)
		RH_KABI_EXTEND(__u8     offload_mr_fwd_mark : 1)
		/* 7 bit hole */
		RH_KABI_EXTEND(kmemcheck_bitfield_end(flags3))

		/* private: */
		RH_KABI_EXTEND(__u32    headers_end[0])
		/* public: */

		/* RHEL SPECIFIC
		*
		* The following padding has been inserted before ABI freeze to
		* allow extending the structure while preserve ABI. Feel free
		* to replace reserved slots with required structure field
		* additions of your backport, eventually moving the replaced slot
		* before headers_end, if it need to be copied by __copy_skb_header()
		*/
		u32                     rh_reserved1;
	u32                     rh_reserved2;
	u32                     rh_reserved3;
	u32                     rh_reserved4;

	/* These elements must be at the end, see alloc_skb() for details.  */
	sk_buff_data_t          tail;
	sk_buff_data_t          end;
	unsigned char           *head,
		*data;
	unsigned int            truesize;
	atomic_t                users;
};

高级IO

首先 IO 主要分为两步:

  1. 等,即等待IO条件就绪
  2. 拷贝,也就是当IO条件就绪后将数据拷贝到内存或外设

任何 IO 过程都包含 “等” 和 “拷贝” 这两个步骤,但在实际的应用场景中 “等” 消耗的时间往往比 “拷贝” 消耗的时间多得多,因此要让 IO 变得高级,最核心的办法就是尽量减少 “等” 的时间!

五种 IO 模型

IO 中存在 5 中基本模型:

  1. 阻塞IO(死等)
  2. 非阻塞IO(周期性检测)
  3. 信号驱动IO(信号探测)
  4. IO多路转接(设置多个鱼竿钓鱼)
  5. 异步IO (找人代等,自己跑路)

阻塞IO、非阻塞IO和信号驱动IO本质上是不能提高IO的效率的,但非阻塞IO和信号驱动IO能提高整体做事的效率。

阻塞 IO

阻塞IO就是在内核将数据准备好之前,系统调用会一直等待:
Linux 高级IO_第5张图片
阻塞IO是最常见的IO模型,所有的套接字,默认都是阻塞方式

比如当调用 recvfrom 函数从某个套接字上读取数据时,可能底层数据还没有准备好,此时就需要等待数据就绪,当数据就绪后再将数据从内核拷贝到用户空间,最后 recvfrom 函数才会返回。

在 recvfrom 函数等待数据就绪期间,在用户看来该进程或线程就阻塞住了,本质就是操作系统将该进程或线程的状态设置为了某种非 R 状态,然后将其放入等待队列当中,当数据就绪后操作系统再将其从等待队列当中唤醒,然后将数据从内核拷贝到用户空间。以阻塞方式进行 IO 操作的进程或线程,在 “等” 和 “拷贝” 期间都不会返回,在用户看来就像是阻塞住了,因此我们称之为阻塞IO

非阻塞 IO

非阻塞IO是内核还没有将数据准备好时,内核调用会直接返回,并返回 EWOULDBLOCK 错误码:
Linux 高级IO_第6张图片

非阻塞IO往往需要程序员以循环的方式反复尝试读写文件描述符,这个过程称为轮询,这对CPU来说是较大的浪费,一般只有特定场景下才使用。

比如当调用 recvfrom 函数以非阻塞方式从某个套接字上读取数据时,如果底层数据还没有准备好,那么 recvfrom
函数会立马错误返回,而不会让该进程或线程进行阻塞等待。
因为没有读取的数据,因此该进程或线程后续还需要继续调用 recvfrom 函数,检测底层数据是否就绪,没有就绪则继续错误返回,直到某次检测到底层数据就绪后,再将数据从内核拷贝到用户空间然后进行成功返回。

每次调用 recvfrom 函数读取数据时,就算底层数据没有就绪,recvfrom 函数也会立马返回,在用户看来该进程或线程就没有被阻塞住,因此我们称之为非阻塞IO。

阻塞IO和非阻塞IO的区别在于,阻塞IO当数据没有就绪时,后续检测数据是否就绪的工作是由操作系统发起的,而非阻塞IO当数据没有就绪时,后续检测数据是否就绪的工作是由用户发起的。

信号驱动IO

信号驱动IO就是当内核将数据准备好的时候,使用 SIGIO 信号通知应用程序进行IO操作:

Linux 高级IO_第7张图片
当底层数据就绪的时候会向当前进程或线程递交 SIGIO 信号,因此可以通过 signalsigaction 函数将 SIGIO 信号处理程序自定义为需要进行的 IO 操作,当底层数据就绪时就会自动执行对应的IO操作。

比如我们需要调用 recvfrom 函数从某个套接字上读取数据,那么就可以将该操作定义为 SIGIO 的信号处理程序。当底层数据就绪时,操作系统就会递交 SIGIO 信号,此时就会自动执行我们定义的信号处理程序,进程将数据从内核拷贝到用户空间。

信号的产生是异步的,但信号驱动IO是同步IO的一种。

信号的产生异步的,因为信号在任何时刻都可能产生,但因为当底层数据就绪时,当前进程或线程需要停下正在做的事情,转而进行数据的拷贝操作,因此当前进程或线程仍然需要参与IO过程。判断一个IO过程是同步的还是异步的,本质就是看当前进程或线程是否需要参与IO过程,如果要参与那就是同步IO,否则就是异步IO。

多路转接IO

IO 多路转接也叫做IO多路复用,能够同时等待多个文件描述符的就绪状态:

Linux 高级IO_第8张图片
因为 IO 过程分为 “等” 和 “拷贝” ,因此我们使用 recvfrom 等接口的底层实际上都做了两件事

  1. 当数据不就绪时需要等
  2. 第二件事就是当数据就绪后需要进行拷贝

虽然recvfrom等接口也有 “等” 的能力,但这些接口一次只能“等”一个文件描述符上的数据或空间就绪,这样 IO 效率太低了。因此系统为我们提供了三组接口,分别叫做 select、poll 和 epoll这些接口的核心工作就是“等”,我们可以将所有“等”的工作都交给这些多路转接接口。

因为多路转接接口是一次“等”多个文件描述符的,因此能够将“等”的时间重叠,当数据就绪后再调用对应的 recvfrom 等函数进行数据的拷贝,此时这些函数就能够直接拷贝,而不需要进行 “等” 了。

IO多路转接就像现实生活中演唱会帮人排队的黄牛,因为多路转接接口实际并没有帮我们进行数据拷贝的操作。这些排队黄牛可以一次帮多个人排队,此时就将多个人排队的时间进行了重叠!

异步IO

异步IO就是由内核在数据拷贝完成时,通知应用程序:
Linux 高级IO_第9张图片

进行异步IO需要调用一些特定接口,调用后会立马返回,因为异步 IO 不需要你进行 “等” 和 “拷贝” ,这两个动作都由操作系统来完成,你要做的只是发起 IO,当 IO 完成后操作系统会通知应用程序,因此进行异步 IO 的进程或线程并不参与 IO 的所有细节!

同步通信 VS 异步通信

同步和异步关注的是消息通信机制

所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回,但是一旦返回调用,就能得到返回值;换句话说,就是由调用者主动等待这个调用的结果

异步则是相反,调用在发出之后,这个调用就直接返回了,所有没有返回结果;换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果;而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用

为什么非阻塞IO在没有得到结果之前就返回了?

IO 分为“等”和“拷贝”,当调用 recvfrom 进行非阻塞IO时,如果数据没有就绪,那么调用会直接返回,此时这个调用返回时并没有完成一个完整的IO过程,即便调用返回了那也是属于错误的返回。因此后续还需要继续调用 recvfrom,轮询检测数据是否就绪,这才是完整的IO过程。

因此,在进行非阻塞IO时,在没有得到结果之前,虽然这个调用会返回,但后续还需要继续进行轮询检测,因此可以理解成调用还没有返回,而只有当某次轮询检测到数据就绪并且完成数据拷贝后才认为调用返回了。

同步通信 VS 同步与互斥

在多进程和多线程当中有同步与互斥的概念,当看到“同步”这个词的时候,一定要先明确这个同步是同步通信的同步,还是同步与互斥的同步, 因为两者是完全不相干的概念! \color{red} {因为两者是完全不相干的概念!} 因为两者是完全不相干的概念!

进程/线程同步指在保证数据安全的前提下,让进程/线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,谈论的是进程/线程间的一种工作关系。

而同步IO指的是进程/线程与操作系统之间的关系,谈论的是进程/线程是否需要主动参与IO过程

阻塞 VS 非阻塞

阻塞和非阻塞关注的是程序在等待调用结果(消息、返回值)时的状态。

阻塞调用是指调用结果返回之前,当前线程会被挂起,调用线程只有在得到结果之后才会返回;非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

阻塞IO

系统中大部分的接口都是阻塞式接口,可以用 read 函数从标准输入当中读取数据:

#include 
#include 
#include 

int main()
{
	char buffer[1024];
	while (true){
		ssize_t size = read(0, buffer, sizeof(buffer)-1);
		if (size < 0){
			std::cerr << "read error" << std::endl;
			break;
		}
		buffer[size] = '\0';
		std::cout << "echo# " << buffer << std::endl;
	}
	return 0;
}

程序运行后,如果不进行输入操作,此时进程就会阻塞,根本原因就是因为此时底层数据不就绪,read 函数需要进行阻塞等待,一旦我们进行了输入,此时 read 就会检测到数据就绪,然后从内核读取数据再拷贝到我们传入的 buffer 数组中,并且将读取到的数据输出到显示器上面:

Linux 高级IO_第10张图片

非阻塞IO

打开文件时默认都是以阻塞的方式打开的,如果要以非阻塞的方式打开某个文件,需要使用 open 函数打开文件时携带O_NONBLOCKO_NDELAY选项,此时就能够以非阻塞的方式打开文件,我们在打开文件时设置非阻塞方式:
在这里插入图片描述
如果想将已经打开的某个文件或套接字设置为非阻塞,此时就需要用到 fcntl 函数

fcntl函数

函数原型如下:

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

fd 为已经打开的文件描述符,cmd 为需要进行的操作,… 是可变参数,传入的 cmd 值不同,后面追加的参数也不同。

fcntl函数常用的5种功能与其对应的 cmd 取值如下:

复制一个现有的描述符(cmd=F_DUPFD)
获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD)
获得/设置文件状态标记(cmd=F_GETFL或F_SETFL)
获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN)
获得/设置记录锁(cmd=F_GETLK, F_SETLK或F_SETLKW)

函数调用成功则返回值取决于具体进行的操作,调用失败则返回 -1,同时错误码会被设置。

实现SetNonBlock函数

我们可以定义一个函数,该函数用于将指定文件描述符设置为非阻塞状态。

先调用 fcntl 函数获取该文件描述符对应的文件状态标记(这是一个位图),此时 fcntl 的 cmd 值为 F_GETFL。在获取到的文件状态标记上添加非阻塞标记 O_NONBLOCK,再次调用 fcntl 对文件状态标记进行设置,此时 fcntl 传入的 cmd 值变为F_SETFL

bool SetNonBlock(int fd)
{
	int fl = fcntl(fd, F_GETFL);
	if (fl < 0){
		std::cerr << "fcntl error" << std::endl;
		return false;
	}
	fcntl(fd, F_SETFL, fl | O_NONBLOCK);
	return true;
}

以非阻塞轮询方式读取标准输入

此时在调用 read 读取标准输入之前,调用 SetNonBlock 将 0 号文件描述符设置为非阻塞就行了

需要注意的是, read 函数以非阻塞方式读取标准输入时,如果底层数据不就绪,那么 read 就会立即返回,但会以出错的形式返回,此时错误码会被设置为EAGAINEWOULDBLOCK

因此在以非阻塞方式读取数据时,如果调用 read 时得到的返回值 -1,此时还需要通过错误码进一步进行判断;如果错误码的值是EAGAINEWOULDBLOCK,说明出错是因为底层数据还没有就绪,因此后续还应继续调用 read 进行轮询检测数据是否就绪,当数据继续时再进行数据的读取。

此外,调用 read 在读取到数据之前可能会被其他信号中断,此时 read 也会以出错的形式返回,此时的错误码会被设置为EINTR,此时应该重新执行 read 进行数据的读取:

#include 
#include 
#include 
#include 
#include 
#include 
bool SetNonBlock(int fd)
{
	int fl = fcntl(fd, F_GETFL);
	if (fl < 0){
		std::cerr << "fcntl error" << std::endl;
		return false;
	}
	fcntl(fd, F_SETFL, fl | O_NONBLOCK);
	return true;
}
int main()
{
	SetNonBlock(0);
	char buffer[1024];
	while (true){
		ssize_t size = read(0, buffer, sizeof(buffer)-1);
		if (size < 0){
			if (errno == EAGAIN || errno == EWOULDBLOCK){ //底层数据没有就绪
				std::cout << strerror(errno) << std::endl;
				sleep(1);
				continue;
			}
			else if (errno == EINTR){ //在读取数据之前被信号中断
				std::cout << strerror(errno) << std::endl;
				sleep(1);
				continue;
			}
			else{
				std::cerr << "read error" << std::endl;
				break;
			}
		}
		buffer[size] = '\0';
		std::cout << "echo# " << buffer << std::endl;
	}
	return 0;
}

因此在以非阻塞的方式读取数据时,如果调用 read 读取到的返回值为 -1,此时并不应该直接认为是在底层读取时出错了,而应该继续判断错误码,如果错误码的值为EAGAINEWOULDBLOCKEINTR则应该继续调用 read 再次进行读取

运行代码后,当我们没有输入数据时,程序就会不断调用 read 检测底层数据是否就绪,一旦进行了输入,此时 read 就会在轮询检测时检测到,紧接着立马将数据读取到从内核拷贝到我们传入的buffer数组当中,并且将数据输出到显示器。

你可能感兴趣的:(Linux,C++,linux,网络,高级IO,c++)