目录
四、什么是socket的阻塞和非阻塞?(本质是send和recv函数阻塞调用)
1、回顾:socket编程的connet(),accept()的阻塞和非阻塞模式下的表现
2、回顾:socket编程的send()和recv()函数在阻塞和非阻塞模式下的表现
一、什么是进程的阻塞和非阻塞?(清楚精辟)
1、进程阻塞
阻塞,从OS和进程的角度上说,阻塞是进程五个的状态之一。是指暂时操作系统把进程从操作系统管理的工作队列中移到等待队列,此时进程不占用CPU资源,但数据和程序储存在内存,如果要使它不占用内存,就要把程序变为阻塞挂起态。
进程阻塞的含义:
另一个角度,放在程序流中来说,就是:某个函数执行成功的条件当前不满足时,该函数会阻塞当前执行线程,程序不往下执行。程序执行流在超过时间到达或执行成功的条件满足后恢复继续执行。
关键词:等待队列,不占用CPU资源,函数执行成功的条件不满足
例子:socket编程中,服务端调用recv()/select()/epoll_wait()来接收客户端的请求,都是阻塞调用式的函数,也就是说socket的接收缓冲区中没有来自客户端的请求或没有来自客户端的数据时,recv()/select()/epoll_wait()不会返回,函数不会继续往下执行。
2、进程非阻塞
进程非阻塞的含义:非阻塞模式相反,从OS和进程的角度上说,进程一直在OS的工作队列中,也就是说进程一直占用着CPU资源。
放在程序流中来说,即使某个函数执行成功的条件当前不满足,该函数也不会阻塞当前执行线程,而是立即返回,继续执行程序流。
关键词:工作队列,占用CPU资源,立即返回
二、什么是I/O的阻塞和非阻塞?
1.阻塞IO模型
最传统的一种IO模型,即在读写数据过程中会发生阻塞现象。当用户线程发出IO请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除block状态。(阻塞模式下用户进程需要等待两次,一次为等待io中的数据就绪,一次是等待内核把数据拷贝到用户空间)
如果数据没有就绪,就会一直阻塞在read方法。具体流程如下图所示:
2.非阻塞IO模型
阻塞IO模式有一个缺点是每次io事件没有就绪时,用户进程需要一直等待,使得用户进程需要一直等待。因此引入了非阻塞IO,当用户线程发起一个read操作后,并不需要等待,而是马上就得到了一个结果。如果结果是一个error时,它就知道数据还没有准备好,于是就返回到用户进程去执行其他任务,等过一段时间后在去查看数据是否准备好。一旦内核中的数据准备好了,并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回。所以事实上,在非阻塞IO模型中,用户线程需要不断地询问内核数据是否就绪,也就说非阻塞IO不会交出CPU,而会一直占用CPU。
典型的非阻塞IO模型一般如下:
while(true)
{
data = socket.read();
if(data!= error)
{
processData();
break;
}
if(data==error)
{
run_user_thread();
}
}
但是对于非阻塞IO就有一个非常严重的问题,在while循环中需要不断地去询问内核数据是否就绪,这样会导致CPU占用率非常高,因此一般情况下很少使用while循环这种方式来读取数据。
三、什么是管道的阻塞和非阻塞?
1、回顾:管道的基础知识
在Linux系统中,管道是进程间通信的媒介。管道是内核里面的一串缓存,通过管道的文件描述符可以找到它,所以拿到管道的文件描述符的进程之间就可以通信。
管道分为匿名管道和命名管道。
(1)匿名管道
匿名管道没有实体,也就是没有提前建立管道文件,但是它在使用时仍然开辟了一块供进程之间读写的缓冲区。父进程持有fd的文件描述符,而如果想通过匿名管道和父进程通信,只能通过父进程 fork() 创建子进程(子进程是父进程的拷贝,它拷贝了父进程的所有代码和资源,也包括文件描述符,从父进程fork()的下一行开始运行),子进程拷贝了父进程 fd 文件描述符,于是双方可以通信。总结:匿名函数的通信范围是有亲缘关系的进程(如父子进程)。
匿名管道的生命周期随进程,随进程的创建而建立,随进程的结束而销毁。
(2)命名管道(FIFO)
命名管道提前创建了类型为管道的设备文件,因此可以在不相关的进程间也能相互通信。因为命令管道,提前创建了一个类型为管道的设备文件,进程只要拿到了命名管道的文件描述符,使用这个设备文件,就可以相互通信。
命名管道叫FIFO是因为遵循如队列一样“先入先出”的读写规则。
2、回顾:管道读写的行为
以FIFO管道为例(匿名管道的通信代码可参考父子进程的通信的代码),如果要创建一个FIFO管道进行通信,程序员需要编写如下流程的代码:
1、设置缓冲区大小
2、使用系统调用函数mkfifo()创建管道
3、使用系统调用open()打开文件,意思是访问命名管道,命名管道的读写有阻塞和非阻塞两种,可以在open()时指定。
4、用while(1)循环+读写偏移量 控制读/写(重点)
流程可参考:Linux进程间通信(四):命名管道 mkfifo()、open()、read()、close() - 52php - 博客园 (cnblogs.com)
具体代码:
读端
//读端
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#define FIFO "/root/myfifo"
void main(int argc,char** argv)
{
char buf_r[100];
int fd, nread;
int k = mkfifo(FIFO,O_CREAT|O_EXCL); // 创建管道
if((k<0)&&(errno!=EEXIST))
printf("cannot create fifoserver\n");
printf("Preparing for reading bytes...\n");
memset(buf_r,0,sizeof(buf_r)); //将buf_r用0填充
/*
void* memset(void *s, int c,size_t n);
memset()会将参数s所指的内存区域前n个字节以参数c填入,然后返回指向s的指针。
在编写程序时,若需要将某一数组做初始化,memset()会相当方便。
*/
fd=open(FIFO,O_RDONLY|O_NONBLOCK,0); //打开命名管道FIFO
if(fd==-1){
printf("open error!");
}
while(1) {
memset(buf_r,0,sizeof(buf_r));
if((nread=read(fd,buf_r,100))==-1) { //read(fd,buf_r,100) 由已打开的文件fd读100个字节到buf_r中
if(errno==EAGAIN) printf("no data yet\n");
}else
printf("read %s from FIFO\n",buf_r);
sleep(1);
}
pause(); //*暂停,等待信号*/
unlink(FIFO); //删除文件
}
写端
//写端
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#define FIFO "/root/myfifo"
void main(int argc,char** argv)
{
int fd,j;
char w_buf[100]; //用于缓存参数传递的信息
int nwrite;
fd=open(FIFO,O_WRONLY|O_NONBLOCK,0); //打开命名管道FIFO
if(argc==1)
{
printf("Please send something\n");
exit(-1);
}
strcpy(w_buf,argv[1]); //将argv[1]字符串复制到w_buf
/*
char *strcpy(char *dest, const char *src);
将参数src字符串拷贝至参数dest所指的地址
返回参数dest的字符串起始地址
存在缓冲溢出的隐患,建议用strncpy()替换
*/
//* 连续10次向管道写入数据 */
for(j=0;j<10;j++){
/*
size_t strlen(const char *s);
返回字符串的长度,不包括结束字符"\0"
*/
if((nwrite=write(fd,w_buf,strlen(w_buf)))==-1)
//write(fd,w_buf,strlen(w_buf) 将w_buf所指内存写入strlen(w_buf)个字节到参数fd所指的文件
{ if(errno==EAGAIN)
printf("The FIFO has not been read yet.Please try later\n");
}else
printf("write %s to the FIFO\n",w_buf);
}
}
此外,匿名管道的通信代码可参考父子进程的通信的代码。
3、管道阻塞/非阻塞的设置
创建管道时,默认open()为阻塞调用函数,如果是非阻塞需要用按位或的方式(“|”)添加O_NONBLOCK,即
open(const char *path, O_WRONLY | O_NONBLOCK);
读的阻塞设置:
对于以只读方式(O_RDONLY)打开的FIFO文件,如果open调用是阻塞的(即第二个参数为O_RDONLY),除非有一个进程以写方式打开同一个FIFO,否则它不会返回;
如果open调用是非阻塞的的(即第二个参数为O_RDONLY | O_NONBLOCK),则即使没有其他进程以写方式打开同一个FIFO文件,open调用将成功并立即返回。
写的阻塞设置:
对于以只写方式(O_WRONLY)打开的FIFO文件,如果open调用是阻塞的(即第二个参数为O_WRONLY),open调用将被阻塞,直到有一个进程以只读方式打开同一个FIFO文件为止;
如果open调用是非阻塞的(即第二个参数为O_WRONLY | O_NONBLOCK),open总会立即返回,但如果没有其他进程以只读方式打开同一个FIFO文件,open调用将返回-1,并且FIFO也不会被打开
具体代码:
/*
#include <unistd.h>
int pipe(int pipefd[2]);
功能:创建一个匿名管道,用来进程间通信。
参数:int pipefd[2] 这个数组是一个传出参数
pipe[0]对应的是管道的读端
pipe[1]对应的是管道的写端
返回值:
成功返回0,失败返回-1
管道默认是阻塞的:如果管道没有数据,read阻塞,如果管道满了,write阻塞
注意:匿名管道只能用于具有关系之间的进程通信,比如父子进程,兄弟进程
*/
//子进程发送数据给父进程,父进程读取到数据输出
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
/*
设置管道非阻塞
int flags=fcntl(fd[0],F_GETFL);//获取文件描述符状态
flags|=O_NONBLOCK;//修改flag的值追加不阻塞
fcntl(fd[0],F_SETFL,flags);//设置新的flag
*/
int main(){
//在fork之前创建管道
int pipefd[2];
int ret=pipe(pipefd);
if(ret==-1){
perror("pipe");
exit(0);
}
int flags=fcntl(pipefd[0],F_GETFL);//获取文件描述符状态
flags|=O_NONBLOCK;//修改flag的值追加不阻塞
fcntl(pipefd[0],F_SETFL,flags);//设置新的flag
//创建子进程
pid_t pid=fork();
if(pid>0){
//父进程
printf("i am parent process,pid : %d\n",getpid());
//从管道的读取端读取数据
//关闭写端
close(pipefd[1]);
char buf[1024]={0};
while(1){
int len=read(pipefd[0],buf,sizeof (buf));//如果管道里没有数据那么会阻塞
printf("len : %d\n",len);
printf("parent recv : %s, pid : %d\n",buf,getpid());
memset(buf,0,1024);
sleep(1);
}
}else if(pid==0){
//子进程
//关闭读端
close(pipefd[0]);
printf("i am child process,pid : %d\n",getpid());
char buf[1024]={0};
while(1){
char *str="hello,i am child";
write(pipefd[1],str,strlen(str));
sleep(5);
}
}
return 0;
}
4、总结:阻塞/非阻塞 读/写会成功还是失败
这张图是最准确的,按这张图就好:
一句话,判断会成功还是失败,要区分关键状态:
1、管道处于打开/关闭状态(指向管道的读端/写端文件描述符是打开还是关闭,打开了说明读/写还可能进行,关闭了说明读/写不会进行了)。
2、管道内有没有数据。
3、读写是阻塞的/非阻塞的。
4、初始时程序流先进行读还是写。
这四种状态是不同的!共同决定管道读/写是成功还是错误。
补充总结:
读管道:
管道中有数据,read会返回实际读到的字节数
管道中无数据:
写端被全部关闭,read返回0(相当于读到文件的末尾)
如果写端没有完全关闭,read阻塞等待
写管道:
管道读端全部被关闭,进程异常终止(进程收到SIGPIPE信号)
管道读端没有被全部关闭
管道已满,write阻塞
管道未满,write将数据写入并返回实际写入的字节数
(以下可能不太全面)
1.写进程阻塞,读进程阻塞。
先运行写进程(被阻塞),再运行读进程,一切正常。
先运行读进程(被阻塞),再运行写进程,一切正常。
2.写进程阻塞,读进程非阻塞。
就改一句代码 fd=open(FIFO_NAME,O_RDONLY | O_NONBLOCK),下面类似。
先运行写进程(被阻塞),再运行读进程,一切正常。
先运行读进程,写端没打开,程序直接崩掉(Segmentation fault (core dumped)),想想也挺自然的,没东西你还要读,而且不愿等。。。
3.写进程非阻塞,读进程阻塞。
先运行写进程,open调用将返回-1,打开失败。
先运行读进程(被阻塞),再运行写进程,一切正常。
4.写进程非阻塞,读进程非阻塞。
其实就是上面2,3类各取一半不正常的情况。。
总结:在管道文件为阻塞读和阻塞写的时候,无论是先读还是先写都要等到另一个操作才能离开阻塞,也就是:如果先读,陷入阻塞,等待写操作;如果先写,陷入阻塞,等待读操作。而非阻塞读和非阻塞写,是无须等待另一个操作的,直接执行read()或者write()能读就读,能写就写,不能就返回-1,非阻塞读写主要是用于自己循环读取,去判断读写的长度。
非阻塞模式下,以只读模式是可以open成功获得文件描述符,即使没有写端;然而,如果写端没有打开的话,只读模式是无法open成功,在确认文件存在(access(file_name, F_OK)==0)的情况下,却open返回-1往往是这个原因,可以使用循环open,或者确保写端已经打开的方法避免。
四、什么是socket的阻塞和非阻塞?(本质是send和recv函数阻塞调用)
最简单的socket()通信模型:
1、回顾:socket编程的connet(),accept()的阻塞和非阻塞模式下的表现
1) 客户端建立连接 connect
阻塞方式下,connect首先发送SYN请求到服务器,当客户端收到服务器返回的SYN的确认时,则connect返回,否则的话一直阻塞。
非阻塞方式,connect将启用TCP协议的三次握手,但是connect函数并不等待连接建立好才返回,而是立即返回,返回的错误码为EINPROGRESS,表示正在进行某种过程。
2)服务端接收连接 accept
阻塞模式下调用accept()函数,没有新连接时,进程会进入睡眠状态,直到有可用的连接,才返回。
非阻塞模式下调用accept()函数立即返回,有连接返回客户端套接字描述符。没有新连接时,将返回EWOULDBLOCK错误码,表示本来应该阻塞。
2、回顾:socket编程的send()和recv()函数在阻塞和非阻塞模式下的表现
(引用索伦大大的文章:)
send()函数在本质上并不是向网络上发送数据,而是将应用层发送缓冲区的数据拷贝到内核缓冲区中,至于数据什么时候会从网卡缓冲区中真正的发到网络中,要根据TCP/IP协议栈的行为来确定。如果socket设置了TCP_NODELAY选项(禁用nagle算法),存放到内核缓冲区的数据就会被立即发送出去;反之,一次放入内核缓冲区的数据包如果太小,系统会在多个小数据包凑成足够大的数据包后才会发出去。
recv函数本质上并不是从网络上收取数据,而是将内核缓冲区中的数据拷贝到应用程序的缓冲区中。拷贝完成后会将内核缓冲区中的该部分数据移除。
- 当socket是阻塞模式时,调用recv()函数,如果内核缓冲区为空,程序会阻塞在recv()调用处直到有数据可读。调用send()函数,如果内核缓冲区已满,程序会阻塞在send()调用处直到缓冲区有空位。
- 当socket是非阻塞模式时,继续调用send/recv函数,send/recv函数不会阻塞程序执行流,而是会返回一个返回值(可能是>0,=0,-1三种,-1会得到一个相关的错误码),各值的具体含义和程序的应对操作见下。
3、非阻塞模式下send和recv函数的返回值总结
返回值n | 返回值含义 |
---|---|
大于0 | 成功发送(send)或接收(recv)n字节 |
0 | 对端关闭连接 |
小于0(-1) | 出错、信号被中断、对端TCP窗口太小导致数据发送不出去或当前网卡缓冲区已无数据可接收 |
逐一介绍三种情况:
返回值大于0。在这种情形下,一定要判断send函数的返回值是不是我们期望发送的字节数,而不是简单的判断其返回值大于0。因为虽然返回值n大于0,但在实际情况下,由于对端的TCP可能因为缺少一部分字节就满了,所以n的值可能为(0, buf_length]。当 0 < n < buf_length时,虽然send函数调用成功,但在业务上并不算正确,因为有部分数据并没有发送出去。
所以,建议要么在返回值n等于buf_length时才认为正确,要么在一个循环中调用send函数,如果数据一次性发送不完,则记录偏移量,下一次从偏移量处接着发送,直到全部发送完为止:
// 推荐的方式:在一个循环里面根据偏移量发送数据
bool SendData(const char* buf, int buf_length)
{
// 已发送的字节数
int sent_bytes = 0;
int ret = 0;
while (true)
{
ret = send(m_hSocket, buf + sent_bytes, buf_length - sent_bytes, 0);
if (ret == -1)
{
if (errno == EWOULDBLOCK)
{
// 严谨做法是:如果发送不出去,应该缓存尚未发送出去的数据
break;
}
else if (errno == EINTR)
continue;
else
return false;
}
else if (ret == 0)
{
return false;
}
sent_bytes += ret;
if (sent_bytes == buf_length)
break;
}
return true;
}
返回值等于0。如果send或recv函数返回0,我们就认为对端关闭了连接,我们这段也关闭连接即可,这是实际开发时最常见的处理逻辑。
返回值小于0.对于send或recv函数返回值小于0的情况,此时并不代表函数一定调用出错。如下表:
3、阻塞与非阻塞socket的优缺点和各自适用场景
优缺点
阻塞socket容易存在的问题:
connect/accept/write导致线程阻塞。(服务关闭/重启线程无法退出)
recv读数据长度不确定。(不能保证读取完整数据,导致无法解析)
非阻塞的socket
在建立连接时要兼容处理返回EINPROGRESS情况
在接收连接、读操作、写操作时要兼容处理返回EWOULDBLOCK错误码的情况
以上情况并非连接异常,而是网络延时或者套接字缓冲区满造成的,一般不宜做异常处理(如返回异常关闭套接字)
使用建议:
使用阻塞socket,通过select等IO复用模型可实现socket阻塞的非阻塞调用。(解决线程阻塞问题)
读写接口:套用封装好的readn/writen函数。(指定时间读不到数据/读不到指定数据算作异常)
五、总结(一定要看!)
1、进程阻塞/非阻塞和其他任务的阻塞/非阻塞的关系?
进程,是运行中的程序,程序流一直在一行行地执行,占用CPU和内存,会调用系统调用函数和I/O交互,和别的进程通信,和网络的客户端/服务端通信、和操作系统交互。
进程的阻塞就是进程调用系统调用函数,试图和I/O交互、和别的进程通信,和网络的客户端/服务端通信、和操作系统交互时,因为该函数的执行成功要等待一些条件,而这些条件暂时没有满足,比如没收到键盘、鼠标等的输入,没收到别的进程的通信,没收到客户端的链接请求/访问请求/数据,调用的函数没有返回,进程的程序流就一直卡着没法往下执行,我们把它叫做进程的阻塞。
I/O、管道、socket的阻塞,就是指调用相关的系统调用函数时,函数没有返回,我们就根据业务名称的不同,称之为I/O、管道、socket的阻塞,其实就是进程的阻塞,只不过这时候进程在调用I/O/管道/socket相关的函数而已。
2、从文件的高度看阻塞/非阻塞
在Linux中,一切都是文件,也就是说,I/O,pipe,socket尽管功能不同,但总的来说都是负责读写通信,因此在Linux中都是用文件描述符来开辟一个内存缓冲区,来读/写的。所以把它们的阻塞和非阻塞从一个文件的高度一起概括,可以说:
- 谓阻塞方式的意思是指,当试图对该文件描述符进行读写时,如果当时没有东西可读,或者暂时不可写,程序就进入等待状态,直到有东西可读或者可写为止。
- 而对于非阻塞状态,如果没有东西可读,或者不可写,读写函数马上返回,而不会等待。非阻塞,就是进程或线程执行此函数时不必非要等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况,如果事件发生则与阻塞方式相同,若事件没有发生则返回一个代码来告知事件未发生,而进程或线程继续执行,所以效率较高。
3、阻塞和非阻塞的优劣?
(1)性能
非阻塞模式下,函数调用立刻返回,进程或线程继续执行,所以效率较高
(2)是否易于控制
阻塞好控制,不发送完数据程序不会走下去。但是对性能有影响。
非阻塞不太好控制,可能和能力有关,但是性能会得到很大提升。
(3)编程的难易:
阻塞式的编程方便。
非阻塞的编程不方便,需要程序员处理各种返回值,处理难度高。
4、应用场景
(一)阻塞模式
常见的通信模型为多线程模型,服务端accept之后,对每个socket创建一个线程去recv。逻辑上简单,适用于并发量小(客户端数目少),连续传输大数据量的情况下,比如文件服务器。还有就是在客户端recv服务器消息的时候也经常用,因为客户端就一个socket,用阻塞模式不影响效率,而且编程逻辑上要简单得多。
此外,阻塞模式逻辑简单,程序结构简单明了,常用于一些特殊场景中。
举例两个阻塞模式的应用场景:
场景一:某程序需要临时发送一个文件,文件分段发送,每发送一段,对端都会给予一个响应,该程序可以单独开一个任务线程,在这个任务线程函数里面,使用先send后recv再send再recv的模式,每次send和rec都是阻塞模式的。
场景二:A端与B端之间的通信只有问答模式,即A端每发送给B端一个请求,B端必定会给A端一个响应,除此之外,B端不会向A端推送任何数据,此时A端就可以采用阻塞模式,在每次send完请求后,都可以直接使用阻塞式的recv函数接收一定要有的应答包。
(二)非阻塞模式
非阻塞模式,常见的通信模型为select模型和IOCP模型。适用于高并发,数据量小的情况,比如聊天室。非阻塞模式一般用于需要支持高并发多QPS的场景(如服务器程序),但这种模式让程序的执行流和控制逻辑变得复杂。
客户端多的情况下,如果采用阻塞模式,需要开很多线程,影响效率。另外,客户端一般不采用非阻塞模式。
参考:
I/O
五种IO模型详解及优缺点_阻塞io的缺点_haitaobiyao的博客-CSDN博客
socket
socket阻塞和非阻塞有哪些影响_mayue_csdn的博客-CSDN博客
网络编程:socket的阻塞模式和非阻塞模式_socket设置非阻塞__索伦的博客-CSDN博客
【转】阻塞与非阻塞socket的优缺点_pleasecallmeTen的博客-CSDN博客
管道
管道g命名管道非阻塞模式通信_非阻塞命名管道_小新同学summer的博客-CSDN博客
Linux进程间通信(四):命名管道 mkfifo()、open()、read()、close() - 52php - 博客园 (cnblogs.com)