海北公司网站建设多少钱,网站业务建设是什么,商城app制作教程,昌平区网站建设文章目录 一.IO的基本概念二.钓鱼五人组三.五种IO模型四.高级IO重要概念1.同步通信 VS 异步通信2.阻塞 VS 非阻塞 五.其他高级IO六.阻塞IO七.非阻塞IO 一.IO的基本概念 什么是IO#xff1f; I/O#xff08;input/output#xff09;也就是输入和输出#xff0c;在著名的冯诺… 文章目录 一.IO的基本概念二.钓鱼五人组三.五种IO模型四.高级IO重要概念1.同步通信 VS 异步通信2.阻塞 VS 非阻塞 五.其他高级IO六.阻塞IO七.非阻塞IO 一.IO的基本概念 什么是IO I/Oinput/output也就是输入和输出在著名的冯诺依曼体系结构当中将数据从输入设备拷贝到内存就叫做输入将数据从内存拷贝到输出设备就叫做输出。 对文件进行的读写操作本质就是一种IO文件IO对应的外设就是磁盘。对网络进行的读写操作本质也是一种IO网络IO对应的外设就是网卡。 OS如何得知外设当中有数据可读取 输入就是操作系统将数据从外设拷贝到内存的过程操作系统一定要通过某种方法得知特定外设上是否有数据就绪。
并不是操作系统想要从外设读取数据时外设上就一定有数据。比如用户正在访问某台服务器当用户的请求报文发出后就需要等待从网卡当中读取服务器发来的响应数据但此时对方服务器可能还没有收到我们发出的请求报文或是正在对我们的请求报文进行数据分析也有可能服务器发来的响应数据还在网络中路由。但操作系统不会主动去检测外设上是否有数据就绪这种做法一定会降低操作系统的工作效率因为大部分情况下外设当中都是没有数据的因此操作系统所做的大部分检测工作其实都是徒劳的。操作系统实际采用的是中断的方式来得知外设上是否有数据就绪的当某个外设上面有数据就绪时该外设就会向CPU当中的中断控制器发送中断信号中断控制器再根据产生的中断信号的优先级按顺序发送给CPU。每一个中断信号都有一个对应的中断处理程序存储中断信号和中断处理程序映射关系的表叫做中断向量表当CPU收到某个中断信号时就会自动停止正在运行的程序然后根据该中断向量表执行该中断信号对应的中断处理程序处理完毕后再返回原被暂停的程序继续运行。
需要注意的是CPU不直接和外设打交道指的是在数据层面上而外设其实是可以直接将某些控制信号发送给CPU当中的某些控制器的。 OS如何处理从网卡中读取到的数据包 操作系统任何时刻都可能会收到大量的数据包因此操作系统必须将这些数据包管理起来。所谓的管理就是“先描述再组织”在内核当中有一个结构叫做sk_buff该结构就是用来管理和控制接收或发送数据包的信息的。
为了说明sk_buff的作用下面给出一个简化版的sk_buff结构 当操作系统从网卡当中读取到一个数据包后会将该数据依次交给链路层、网络层、传输层、应用层进行解包和分用最终将数据包中的数据交给了上层用户那对应到这个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的接收缓冲区供用户读取即可 发送数据时对数据进行封装也是同样的道理就是依次在数据前面拷贝上对应的报头最后再将数据发送出去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;
#elseunion {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 */};
#endifstruct 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_XFRMstruct sec_path *sp;
#endifunsigned 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_XPSunion {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;
#endifunion {__u32 mark;__u32 dropcount;__u32 reserved_tailroom;};#ifdef __GENKSYMS____be16 inner_protocol;
#elseunion {__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;
#endifunion {__u32 mark;__u32 dropcount;__u32 reserved_tailroom;};#ifdef __GENKSYMS____be16 inner_protocol;
#elsekmemcheck_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_XPSunion {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;
#endifunion {__u32 mark;__u32 dropcount;__u32 reserved_tailroom;};#ifdef __GENKSYMS____be16 inner_protocol;
#elseunion {__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主要分为两步
第一步是等即等待IO条件就绪。第二步是拷贝也就是当IO条件就绪后将数据拷贝到内存或外设。
任何IO的过程都包含“等”和“拷贝”这两个步骤但在实际的应用场景中“等”消耗的时间往往比“拷贝”消耗的时间多因此要让IO变得高效最核心的办法就是尽量减少“等”的时间。
二.钓鱼五人组
IO的过程其实和钓鱼是非常类似的。
钓鱼的过程同样分为“等”和“拷贝”两个步骤只不过这里的“等”指的是等鱼上钩“拷贝”指的是当鱼上钩后将鱼从河里“拷贝”到我们的鱼桶当中。IO时“等”消耗的时间往往比“拷贝”消耗的时间多钓鱼也恰好符合这个特点钓鱼时我们大部分时间都在等鱼上钩而当鱼上钩后只需要一瞬间就能将鱼“拷贝”上来。 在谈论高效的IO之前我们先来看看什么样的钓鱼方式才是高效的。
下面给出五个人的钓鱼方式
张三拿了1个鱼竿将鱼钩抛入水中后就死死的盯着浮漂什么也不做当有鱼上钩后就挥动鱼竿将鱼钓上来。李四拿了1个鱼竿将鱼钩抛入水中后就去做其他事情然后定期观察浮漂如果有鱼上钩则挥动鱼竿将鱼钓上来否则继续去做其他事情。王五拿了1个鱼竿将鱼钩抛入水中后在鱼竿顶部绑一个铃铛然后就去做其他事情如果铃铛响了就挥动鱼竿将鱼钓上来否则就根本不管鱼竿。赵六拿了100个鱼竿将100个鱼竿抛入水中后就定期观察这100个鱼竿的浮漂如果某个鱼竿有鱼上钩则挥动对应的鱼竿将鱼钓上来。田七田七是一个有钱的老板他给了自己的司机一个桶、一个电话、一个鱼竿让司机去钓鱼当鱼桶装满的时候再打电话告诉田七来拿鱼而田七自己则开车去做其他事情去了。 张三、李四、王五的钓鱼效率是否一样为什么 张三、李四、王五的钓鱼效率本质上是一样的。
首先他们的钓鱼方式都是一样的都是先等鱼上钩然后再将鱼钓上来。其次因为他们每个人都是拿的一根鱼竿当河里有鱼来咬鱼钩时这条鱼咬哪一个鱼钩的概率都是相等的。
因此张三、李四、王五他们三个人的钓鱼的效率是一样的他们只是等鱼上钩的方式不同而已张三是死等李四是定期检测浮漂而王五是通过铃铛来判断是否有鱼上钩。
需要注意的是这里问的是他们的钓鱼效率是否是一样的而不是问他们整体谁做的事最多如果说整体做事情的量的话那一定是王五做得最多李四次之张三最少。 张三、李四、王五它们三个人分别和赵六比较谁的钓鱼效率更高 赵六毫无疑问是这四个人当中钓鱼效率最高的因为赵六同时在等多个鱼竿上有鱼上钩因此在单位时间内赵六的鱼竿有鱼上钩的概率是最大的。
为了方便计算我们假设赵六拿了97个鱼竿加上张三、李四、王五的鱼竿一共就有100个鱼竿。当河里有鱼来咬鱼钩时这条鱼咬张三、李四、王五的鱼钩的概率都是百分之一而咬赵六的鱼钩的概率就是百分之九十七。因此在单位时间内赵六的鱼竿上有鱼的概率是张三、李四、王五的97倍。
而高效的钓鱼就是要减少单位时间内“等”的时间增加“拷贝”的时间所以说赵六的钓鱼效率是这四个人当中最高的。
赵六的钓鱼效率之所以高是因为赵六一次等待多个鱼竿上的鱼上钩此时就可以将“等”的时间进行重叠。 如何看待田七的这种钓鱼方式 田七让自己的司机帮自己钓鱼自己开车去做其他事情去了此时这个司机具体怎么钓鱼已经不重要了他可以模仿张三、李四、王五、赵六任何一个人的钓鱼方式进行钓鱼。
最重要的是田七本人并没有参与整个钓鱼的过程他只是发起了钓鱼的任务而真正钓鱼的是司机田七在司机钓鱼期间可能在做任何其他事情如果将钓鱼看作是一种IO的话那田七的这种钓鱼方式就叫做异步IO。
而对于张三、李四、王五、赵六来说他们都需要自己等鱼上钩当鱼上钩后又需要自己把鱼从河里钓上来对应到IO当中就是需要自己进行数据的拷贝因此他们四个人的钓鱼方式都叫做同步IO。 五种IO模型 实际这五个人的钓鱼方式分别对应的就是五种IO模型。
张三这种死等的钓鱼方式对应就是阻塞IO。李四这种定时检测是否有鱼上钩的方式就是非阻塞IO。王五这种通过设置铃铛得知事件是否就绪的方式就是信号驱动IO。赵六这种一次等待多个鱼竿上有鱼的钓鱼方式就是IO多路转接。田七这种让别人帮自己钓鱼的钓鱼方式就是异步IO。
通过这里的钓鱼例子我们可以看到阻塞IO、非阻塞IO和信号驱动IO本质上是不能提高IO的效率的但非阻塞IO和信号驱动IO能提高整体做事的效率。
其中这个钓鱼场景中的各个事物都能与IO当中的相关概念对应起来比如这里钓鱼的河对应就是内核这里的每一个人都是进程或线程鱼竿对应的就是文件描述符或套接字装鱼的桶对应的就是用户缓冲区。
三.五种IO模型 阻塞IO 阻塞IO就是在内核将数据准备好之前系统调用会一直等待。
图示如下 阻塞IO是最常见的IO模型所有的套接字默认都是阻塞方式。
比如当调用recvfrom函数从某个套接字上读取数据时可能底层数据还没有准备好此时就需要等待数据就绪当数据就绪后再将数据从内核拷贝到用户空间最后recvfrom函数才会返回。在recvfrom函数等待数据就绪期间在用户看来该进程或线程就阻塞住了本质就是操作系统将该进程或线程的状态设置为了某种非R状态然后将其放入等待队列当中当数据就绪后操作系统再将其从等待队列当中唤醒然后该进程或线程再将数据从内核拷贝到用户空间。
以阻塞方式进行IO操作的进程或线程在“等”和“拷贝”期间都不会返回在用户看来就像是阻塞住了因此我们称之为阻塞IO。 非阻塞IO 非阻塞IO就是如果内核还未将数据准备好系统调用仍然会直接返回并且返回EWOULDBLOCK错误码。
图示如下 非阻塞IO往往需要程序员以循环的方式反复尝试读写文件描述符这个过程称为轮询这对CPU来说是较大的浪费一般只有特定场景下才使用。
比如当调用recvfrom函数以非阻塞方式从某个套接字上读取数据时如果底层数据还没有准备好那么recvfrom函数会立马错误返回而不会让该进程或线程进行阻塞等待。因为没有读取的数据因此该进程或线程后续还需要继续调用recvfrom函数检测底层数据是否就绪如果没有就绪则继续错误返回直到某次检测到底层数据就绪后再将数据从内核拷贝到用户空间然后进行成功返回。每次调用recvfrom函数读取数据时就算底层数据没有就绪recvfrom函数也会立马返回在用户看来该进程或线程就没有被阻塞住因此我们称之为非阻塞IO。
阻塞IO和非阻塞IO的区别在于阻塞IO当数据没有就绪时后续检测数据是否就绪的工作是由操作系统发起的而非阻塞IO当数据没有就绪时后续检测数据是否就绪的工作是由用户发起的。 信号驱动IO 信号驱动IO就是当内核将数据准备好的时候使用SIGIO信号通知应用程序进行IO操作。
图示如下 当底层数据就绪的时候会向当前进程或线程递交SIGIO信号因此可以通过signal或sigaction函数将SIGIO的信号处理程序自定义为需要进行的IO操作当底层数据就绪时就会自动执行对应的IO操作。
比如我们需要调用recvfrom函数从某个套接字上读取数据那么就可以将该操作定义为SIGIO的信号处理程序。当底层数据就绪时操作系统就会递交SIGIO信号此时就会自动执行我们定义的信号处理程序进程将数据从内核拷贝到用户空间。
信号的产生是异步的但信号驱动IO是同步IO的一种。
我们说信号的产生异步的因为信号在任何时刻都可能产生。但信号驱动IO是同步IO的一种因为当底层数据就绪时当前进程或线程需要停下正在做的事情转而进行数据的拷贝操作因此当前进程或线程仍然需要参与IO过程。
判断一个IO过程是同步的还是异步的本质就是看当前进程或线程是否需要参与IO过程如果要参与那就是同步IO否则就是异步IO。 IO多路转接 IO多路转接也叫做IO多路复用能够同时等待多个文件描述符的就绪状态。
图示如下 IO多路转接的思想
因为IO过程分为“等”和“拷贝”两个步骤因此我们使用的recvfrom等接口的底层实际上都做了两件事第一件事就是当数据不就绪时需要等第二件事就是当数据就绪后需要进行拷贝。虽然recvfrom等接口也有“等”的能力但这些接口一次只能“等”一个文件描述符上的数据或空间就绪这样IO效率太低了。因此系统为我们提供了三组接口分别叫做select、poll和epoll这些接口的核心工作就是“等”我们可以将所有“等”的工作都交给这些多路转接接口。因为这些多路转接接口是一次“等”多个文件描述符的因此能够将“等”的时间进行重叠当数据就绪后再调用对应的recvfrom等函数进行数据的拷贝此时这些函数就能够直接进行拷贝而不需要进行“等”操作了。
IO多路转接就像现实生活中的黄牛一样只不过IO多路转接更像是帮人排队的黄牛因为多路转接接口实际并没有帮我们进行数据拷贝的操作。这些排队黄牛可以一次帮多个人排队此时就将多个人排队的时间进行了重叠。 异步IO 异步IO就是由内核在数据拷贝完成时通知应用程序。
图示如下 进行异步IO需要调用一些异步IO的接口异步IO接口调用后会立马返回因为异步IO不需要你进行“等”和“拷贝”的操作这两个动作都由操作系统来完成你要做的只是发起IO。当IO完成后操作系统会通知应用程序因此进行异步IO的进程或线程并不参与IO的所有细节。
四.高级IO重要概念
1.同步通信 VS 异步通信
同步和异步关注的是消息通信机制。
所谓同步就是在发出一个调用时在没有得到结果之前该调用就不返回但是一旦调用返回就得到返回值了换句话说就是由调用者主动等待这个调用的结果。异步则是相反调用在发出之后这个调用就直接返回了所有没有返回结果换句话说当一个异步过程调用发出后调用者不会立刻得到结果而是在调用发出后被调用者通过状态、通知来通知调用者或通过回调函数处理这个调用。 为什么非阻塞IO在没有得到结果之前就返回了 IO是分为“等”和“拷贝”两步的当调用recvfrom进行非阻塞IO时如果数据没有就绪那么调用会直接返回此时这个调用返回时并没有完成一个完整的IO过程即便调用返回了那也是属于错误的返回。因此该进程或线程后续还需要继续调用recvfrom轮询检测数据是否就绪当数据就绪后最后再把数据从内核拷贝到用户空间这才是一次完整的IO过程。
因此在进行非阻塞IO时在没有得到结果之前虽然这个调用会返回但后续还需要继续进行轮询检测因此可以理解成调用还没有返回而只有当某次轮询检测到数据就绪并且完成数据拷贝后才认为该调用返回了。 同步通信 VS 同步与互斥 在多进程和多线程当中有同步与互斥的概念但是这里的同步通信和进程或线程之间的同步是完全不相干的概念。
进程/线程同步指的是在保证数据安全的前提下让进程/线程能够按照某种特定的顺序访问临界资源从而有效避免饥饿问题谈论的是进程/线程间的一种工作关系。而同步IO指的是进程/线程与操作系统之间的关系谈论的是进程/线程是否需要主动参与IO过程。
因此当看到“同步”这个词的时候一定要先明确这个同步是同步通信的同步还是同步与互斥的同步。
2.阻塞 VS 非阻塞
阻塞和非阻塞关注的是程序在等待调用结果消息、返回值时的状态。
阻塞调用是指调用结果返回之前当前线程会被挂起调用线程只有在得到结果之后才会返回。非阻塞调用指在不能立刻得到结果之前该调用不会阻塞当前线程。
五.其他高级IO
非阻塞IO记录锁系统V流机制I/O多路转接也叫I/O多路复用readv和writev函数以及存储映射IOmmap这些统称为高级IO。
六.阻塞IO
系统中大部分的接口都是阻塞式接口比如我们可以用read函数从标准输入当中读取数据。
#include iostream
#include unistd.h
#include fcntl.hint 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数组当中并且将读取到的数据输出到显示器上面最后我们就看到了我们输入的字符串。 说明一下
C当中的cin和C语言当中的scanf也可以读取从键盘输入的字符但是cin和scanf会提供用户缓冲区为了避免这些因素的干扰因此这里选择使用read函数进行读取。
七.非阻塞IO
打开文件时默认都是以阻塞的方式打开的如果要以非阻塞的方式打开某个文件需要在使用open函数打开文件时携带O_NONBLOCK或O_NDELAY选项此时就能够以非阻塞的方式打开文件。 这是在打开文件时设置非阻塞的方式如果要将已经打开的某个文件或套接字设置为非阻塞此时就需要用到fcntl函数。 fcntl函数 fcntl函数的函数原型如下
int fcntl(int fd, int cmd, ... /* arg */);参数说明
fd已经打开的文件描述符。cmd需要进行的操作。…可变参数传入的cmd值不同后面追加的参数也不同。
fcntl函数常用的5种功能与其对应的cmd取值如下
复制一个现有的描述符cmdF_DUPFD。获得/设置文件描述符标记cmdF_GETFD或F_SETFD。获得/设置文件状态标记cmdF_GETFL或F_SETFL。获得/设置异步I/O所有权cmdF_GETOWN或F_SETOWN。获得/设置记录锁cmdF_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号文件描述符设置为非阻塞就行了。
代码如下
#include iostream
#include unistd.h
#include fcntl.h
#include signal.h
#include cstring
#include cerrno
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函数以非阻塞方式读取标准输入时如果底层数据不就绪那么read函数就会立即返回但当底层数据不就绪时read函数是以出错的形式返回的此时的错误码会被设置为EAGAIN或EWOULDBLOCK。 因此在以非阻塞方式读取数据时如果调用read函数时得到的返回值是-1此时还需要通过错误码进一步进行判断如果错误码的值是EAGAIN或EWOULDBLOCK说明本次调用read函数出错是因为底层数据还没有就绪因此后续还应该继续调用read函数进行轮询检测数据是否就绪当数据继续时再进行数据的读取。
此外调用read函数在读取到数据之前可能会被其他信号中断此时read函数也会以出错的形式返回此时的错误码会被设置为EINTR此时应该重新执行read函数进行数据的读取。 因此在以非阻塞的方式读取数据时如果调用read函数读取到的返回值为-1此时并不应该直接认为read函数在底层读取数据时出错了而应该继续判断错误码如果错误码的值为EAGAIN、EWOULDBLOCK或EINTR则应该继续调用read函数再次进行读取。
运行代码后当我们没有输入数据时程序就会不断调用read函数检测底层数据是否就绪。 一旦我们进行了输入操作此时read函数就会在轮询检测时检测到紧接着立马将数据读取到从内核拷贝到我们传入的buffer数组当中并且将读取到的数据输出到显示器上面。 本文到此结束码文不易话请多多支持哦