一、IO函数
1.使用recv()函数接收数据
此函数用于接收数据,函数原型:
#include<sys/types.h>
#include<sys/socket.h>
//此函数从套接字s中接收数据放到缓冲区buf,buf长度为len,操作方式由flags指定。
//参数s:套接口文件描述符,由系统调用socket()返回的。
//参数buf:指针,指向接收网络数据的缓冲区。
//参数s:接收缓冲区的大小,以字节为单位。
sszie_t recv(int s,void *buf,size_t len,int flags);
参数flags用于设置接收数据的方式,可选择的值如下所示:值可以是表中按位或生成的复合值。
-
MSG_ DONTWAIT
: 这个标志将单个IO操作设为非阻塞方式,而不需要在套接字上打开非阻塞标志,执行IO操作,然后关闭非阻塞标志。 -
MSG_ERRQUEUE
: 该错误的传输依赖于所使用的协议。 -
MSG_OOB
: 这个标志可以接收带外数据,而不是接收一般数据。 -
MSG_PEEK
: 这个标志用于查看可读的数据,在recv()函数执行后,内核不会将这些数据丢弃。 -
MSG_TRUNC
: 在接收数据后,如果用户的缓冲区大小不足以完全复制缓冲区中的数据,则将数据截断,仅靠复制用户缓冲区大小的数据。其他的数据会被丢弃。 -
MSG_WAITALL
: 这个标志告诉内核在没有读到请求的字节数之前不使读操作返回。如果系统支持这个标志,可以去掉readn()函数而用下面的代替:
#define readn(fd ,ptr,n) recv(fd ptr,n,MSG_WAITALL)。
设置了MSG_WAITALL,发生下面情况:
- 捕获一个信号;
- 连接终止;
- 在套接字上发生错误。
这时函数返回的字节数仍然会比请求的少。当指定WAITALL标志时,函数会复制与用户指定的长度相等的数据,如果内核中的当前数据不能满足要求,会一直等待直到数据足够的时候才返回。
此函数返回值是成功接收的字节数,当返回值为-1时错误发生
,可通过errno获取错误码,另一方使用正常方式关闭连接的时候返回值为0,如:调用close()函数关闭连接,例:
-
recv()函数通常用于TCP类型的套接字,UDP使用recvfrom()函数接收数据,当然在数据报套接字绑定地址和端口后,也可以使用 recv()函数接收数据。
-
recv()函数从内核的接收缓冲区中复制数据到用户指定的缓冲区,当内核中的数据比指定的缓冲区小时, 一 般情况下(没有采用
MSG_WAITALL
标志)会复制缓冲区中的所有数据到用户缓冲区,并返回数据的长度。- 当内核接收缓冲区中的数据比用户指定的多时,会将用户指定长度 len 的接收缓冲区中的数据复制到用户指定地址,其余的数据需要下次调用接收函数的时候再复制,内核在复制用户指定的数据之后,会销毁已经复制完毕的数据,并进行调整。
-
2.使用send()和sendto函数发送数据
send()函数用于发送数据,函数原型:
#include<sys/types.h>
#include<sys/socket.h>
//将缓冲区buf中大小为len的数据,通过套接字文件描述符按照flags指定的方式发送出去。
//参数含义与recv中的含义一致,它的返回值是成功发送的字节数。
ssize_t send(int s,const void *buf,size_t len,int flags);
//sendto可以在无连接的套接字上指定一个目标地址
ssize_t sendto(int s,const void *buf,size_t len len,int flags,const struct sockaddr *destaddr,socklen_t destlen);
//对面向连接的套接字,目标地址是被忽略的,因为连接中隐含目标地址。
//对于无连接的套接字,除非先调用connect设置目标地址,复制不能使用send。
-
用户缓冲区buf中数据通过send()发送并不一定全部发送出去,所以要检查send()函数的返回值,按值与计划发送的字节长度len是否相等来判断如何进行下一步操作。
- sen()函数的返回值小于len的时候,表明缓冲区仍然有部分数据没有发生成功,这时需要重新发送剩余部分的数据,通常的剩余数据发送方法是对原来buf中的数据位置进行偏移,偏移的大小为发送成功的字节数。
send()函数发生错误的时候返回值为-1
,这时可以查看errno获得错误码,用正常方式关闭连接的时候返回值为0,如:调用close()函数关闭连接,例:
- 函数send()
只能
用于套接字处于连接状态的描述符,之前必须用
connect()函数或者其他函数进行连接。 - 对于send()函数和write()函数之间的差别是表示发送方式的flag, 当flag为0时,send()函数和write()函数完全一 致。而且
send(s,buf,len,flags)
与sendto(s,buf,len,flags, NULL,0)
是等价的。
3.使用readv()函数接收数据
readv()函数用于接收多个缓冲区数据,函数原型如下:
#include<sys/uio.h>
//此函数从套接字描述符s中读取count块数据放到缓冲区向量vector中。
ssize_t readv(int s,const struct iovec *vector,int count);
此函数返回值为成功接收到的字节数,当为-1时错误发生,可以查看errno获得错误码:
参数vector为一个指向向量的指针,结构strcut iovec
在文件<sys/uio.h>
中定义:
struct iovec{
void *iov_base;//向量的缓冲区地址
size_t iov_len;//向量缓冲区的大小,以字节为单位
};
- 在调用readv()函数的时候必须指定
iovec的iov_base的长度
,将值放到成员iov_len
中。 - 参数vector指向一 块结构vector的内存,大小由count指定,如下图所示(阴影部分表示需要设置的vector成员变量的值。)。
- 结构vector的成员变量
iov_base
指向内存空间,iov_len
表示内存的长度。
4、使用writev()函数发送数据
writev()函数可向多个缓冲区同时写入数据,函数原型如下:
#include<sys/uio.h>
//此函数向套接字描述符s中写入在向量vector中保存的count块数据。
ssize_t writev(int fd,const struct iovec *vector,int count);
writev()函数返回值为成功发送的字节数,当为-1时错误发生,可通过errno错误码查看:
参数vector为一个指向向量的指针,结构strcut iovec
在文件<sys/uio.h>
中定义:
struct iovec{
void *iov_base;//向量的缓冲区地址
size_t iov_len;//向量缓冲区的大小,以字节为单位
};
-
在调用 writev()函数的时候必须指定iovec 的iov_base 的长度,将值放到成员 iov_len中。
-
参数 vector指向一 块结构vector的内存,大小由count指定,如下图 所示,阴影部分表示需要设置的vector成员变量的值。
-
结构vector的成员变量iov_base指向内存空间,iov_len表示内存的长度。
-
与readv()函数相区别的是,writev()函数的vector内存空间的值都已经设定好了。
5.使用recvmsg()函数接收数据
recvmsg()函数用于接收数据,与recv()函数、readv()函数比较,这个函数的使用复杂点。
①.函数recvmsg()原型含义
函数原型如下:
#include<sys/types.h>
#include<sys/socket.h>
//此函数从套接字s中接收数据放到缓冲区msg中,操作的方式由flags指定。
ssize_t recvmsg(int s,struct msghdr *msg,int flags);
函数的返回值为成功接收到的字节数,当为-1时错误发生,通过errno获取错误码,另一方使用正常方式关闭连接的时候返回值为0,如调用close()函数关闭连接:
recvmsg()函数的flags参数表示数据接收的方式,(值可以采用按位或的复合值):
②.地址结构msghdr
函数recvmsg()中用到结构msghdr的原型如下:
struct msghdr{
void *msg_name; //可选地址
socklen_t msg_namelen;//地址长度
struct iovec *msg_iov;//接收数据的数组
size_t msg_iovlen;//msg_iov中的元素数量
void *msg_control;//ancillary data,see below
socklen_t msg_controllen;//ancillary data buffer lem
int msg_flags;//接收消息的标志
};
- 成员msg_name表示源地址,即为一 个指向struct sockaddr 的指针,当套接字还没有连接的时候有效。
- 成员msg_namelen表示msg_name指向结构的长度。
- 成员msg_iov 与函数readv()中的含义一 致。
- 成员msg_iovlen 表示msg_iov 缓冲区的字节数。
- 成员msg_control指向缓冲区,根据msg_flags的值,会放入不同的值。
- 成员msg_controllen 为msg_control指向缓冲区的大小。
- 成员msg_flags为操作的方式。
recv() 函数通常用于TCP类型的套接字,UDP使用 recvfr om() 函数接收数据,当然在数据报套接字绑定地址和端口后,也可以使用 recv() 函数接收数据。
③.函数recvmsg()用户空间与内核空间的交互
-
函数
recvmsg()
从内核的接收缓冲区
中复制数据到用户指定的缓冲区
,当内核中的数据比指定的缓冲区小
时,一般情况下(没有采用MSG_ WAITALL
标志)会复制缓冲区中的所有数据到用户缓冲区,并返回数据的长度。- 当内核接收缓冲区中的数据比用户指定的
多
时,会将用户指定长度 len 的接收缓冲区中的数据复制到用户指定地址,其余的数据需要下次调用接收函数的时候再复制,内核在复制用户指定的数据之后,会销毁已经复制完毕的数据,并进行调整。
- 当内核接收缓冲区中的数据比用户指定的
使用 一 个 msghdr
结构的头部数据如下图所示,msg_name
为指向一 个 20 个字节缓冲区的指针, msg_iov
为指向4 个向量的指针,每个向量的缓冲区大小为60 个字节。本机的IP地址为 192. 168. 1. 15 1
。
使用recvmsg()函数接收来自192.168.1.150的发送到192.168.1.151的200个UDP数据,则接收数据后msghdr结构的情况如图所示:
6.使用sendmsg()函数发送数据
函数sendmsg()可用于多个缓冲区发送数据,函数原型如下:
#include<sys/uio.h>
//此函数向套接字描述符s中按照结构msg的设定写入数据,操作方式由flags指定。
ssize_t sendmsg(int s,const struct msghdr *msg,int flags);
函数 sendmsg( )
与recvmsg()
相区别的地方在于sendmsg
的操作方式由flags
参数设定,而recvmsg
的操作方式由参数msg
结构里的成员变量msg_ flags
指定。
例如,向IP 地址为192.168.1.200 主机的9999 端口发送300个数据,协议为UDP 协议,将msg 参数中的向量缓冲区大小设为100 个,使用3个向量,msg的状态如图9 .5 所示。
7.IO函数的比较
-
函数
read()/write()和readv()/writev()
可以对所有的文件描述符使用;recv()/send()、recvfrom()/write()和recvmsg()/sendmsg()
只能操作套接字描述符。 -
函数
readv()/writev()和recvmsg()/sendmsg()
可以操作多个缓冲区,read()/write()、recv()/send()
和recvfrom()/sendto()
只能操作单个缓冲区。 -
函数
recv()/ send()、recvfrom()/sendto()和recvmsg()/sendmsg()
具有可选标志。 -
函数
recvfrom()/sendto()和recvmsg()/sendmsg()
可以选择对方的IP地址。 -
函数
recvmsg()/sendmsg()
有可选择的控制信息,能进行高级操作。
8.函数readn、readline和writen
ssize_t readn(int fd,void *buf,size_t count);//读指定N字节数据
ssize_t write(int fd,void *buf,size_t count);//写指定N字节数据
ssize_t readline(int fd,void *buf, size_t maxlen);//读一行文本,长度不超过maxlen,一次读1个字节
二、使用IO函数的例子
1.客户端处理框架的例子
①.客户端程序框架
步骤如下:
- 程序的输入参数进行判断,查看是否输入了要连接的服务器IP地址。
- 挂接信号
SIGINT
的处理函数和sig_proccess()
和SIGPIPE
的处理函数sig_pipe()
,用于处理子进程退出信号和套接字连接断开的情况。 - 建立 一 个流式套接字,结果放置在
s
中。 - 对要绑定的地址结构进行赋值,IP 地址为用户输入的值,端口为 8888。
- 连接服务器。
- 调用函数
process_conn_client()
进行客户端数据的处理,这个函数在不同的模式下,收发函数的实现方式不同。 - 处理完毕后关闭套接字。
②.客户端程序框架代码
在程序的开始调用函数signal()注册SIGINT和SIGPIPE信号的处理函数,然后连接服务器并进行数据处理。
#include<stdio.h>
#include<stdlib.h>
#include<strings.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<netinet/in.h>
#include<signal.h>
#include<arpa/inet.h>
#include"fun.h"
extern void sig_proccess(int signo);
extern void sig_pipe(int signo);
extern void process_conn_client(int s);
static int s;
void sig_proccess_client(int signo)//客户端信号处理回调函数
{
printf("Catch a exit signal\n");
close(s);
exit(0);
}
#define PORT 8888
int main(int argc,char *argv[])
{
struct sockaddr_in server_addr;//服务器地址结构
int err;//返回值
if(argc == 1){
printf("PLS input server addr\n");
return 0;
}
signal(SIGINT,sig_proccess);//挂接SIGINT信号,处理函数sig_proccess
signal(SIGPIPE,sig_pipe);//挂接SIGPIPE信号,处理函数sig_pipe
s=socket(AF_INET,SOCK_STREAM,0);//建立一个流式套接字
if(s<0){//建立套接字出错
printf("socket error\n");
return -1;
}
//设置服务器地址
bzero(&server_addr,sizeof(server_addr));//将导致结构清零
server_addr.sin_family = AF_INET;//将协议族设置为AF_INET
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
//IP地址为本地任意IP地址
server_addr.sin_port = htons(PORT);//设置服务器端口为8888
inet_pton(AF_INET,argv[1],&server_addr.sin_addr);//将用户输入的字符串类型的IP地址转为整型
connect(s,(struct sockaddr*)&server_addr,sizeof(struct sockaddr));//连接服务器
process_conn_client(s);//客户端处理过程
close(s);//关闭
}
2.服务器端程序框架
服务器端处理程序是一个程序框架,为后面使用3种类型的收发函数建立基本的架构,函数process_conn_server()是进行服务器端处理的函数,不同收发函数的实现方式:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<unistd.h>
#include<netinet/in.h>
#include<signal.h>
#include"fun.h"
extern void sig_proccess(int signo);
#define PORT 8888//监听端口地址
#define BACKLOG 2//侦听队列长度
int main(int argc,char *argv[])
{
int ss,sc;//ss为服务器的socket描述符,sc为客户端的socket描述符
struct sockaddr_in server_addr;//服务器地址结构
struct sockaddr_in client_addr;//客户端地址结构
int err;//错误值
pid_t pid;//分叉的进行id
signal(SIGINT,sig_proccess);//挂接SIGINT信号,处理函数sig_proccess
signal(SIGPIPE,sig_proccess);//挂接SIGPIPE信号,处理函数sig_pipe
ss = socket(AF_INET,SOCK_STREAM,0);//建立一个流式套接字
if(ss<0){//出错
printf("socket error\n");
return -1;
}
//设置服务器地址
bzero(&server_addr,sizeof(server_addr));//清零
server_addr.sin_family = AF_INET;//协议族
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);//本地地址
server_addr.sin_port = htons(PORT);//服务器端口
//绑定地址结构到套接字描述符
err = bind(ss,(struct sockaddr*)&server_addr,sizeof(server_addr));
if(err<0){//绑定出错
printf("listen error\n");
return -1;}
err = listen(ss,BACKLOG);//设置侦听队列长度
if(err<0){//出错
printf("listen error\n");
return -1;}
for(;;){
int addrlen = sizeof(struct sockaddr);
//接收客户端连接
sc = accept(ss,(struct sockaddr*)&client_addr,&addrlen);
if(sc<0){//客户端连接出错
continue;//结束循环
}
//建立新的进程处理到来的连接
pid = fork();//分叉进程
if(pid==0){//子进程中
close(ss);//在子进程中关闭服务器的侦听
process_conn_server(sc);//处理连接
}else{
close(sc);//在父进程中关闭客户端的连接
}
}
}
3.使用recv()和send()函数
使用recv()和send()函数进行网络数据收发时服务器和客户端的实现:
//服务器实现代码
//使用recv()函数从套接字文件描述符s中读取数据到缓冲区buffer中,
//不能接收到数据,退出;成功,利用接收到的数据构建发送给客户端的响应字符串
//调用send()函数将响应字符串发送给客户端
void process_conn_server(int s)
{
ssize_t size = 0;
char buffer[1024];//数据缓冲区
for(;;){//循环处理过程
size = recv(s,buffer,1024,0);//从套接字中读取数据放到缓冲区buffer中
if(size == 0){//没有数据
return;
}
sprintf(buffer,"%ld bytes altogether\n",size);//构建响应字符,为接收到客户端字节的数量
send(s,buffer,strlen(buffer)+1,0);//发给客户端
}
}
//客户端处理代码
//是个循环过程,客户端调用read()函数从标准输入读取输入信息;
//调用send()函数将信息发给服务器后,调用recv()函数接收服务器端的响应,
//并将服务器端响应结果写到标准输出端。
void process_conn_client(int s)
{
ssize_t size = 0;
char buffer[1024];//数据缓冲区
for(;;){//循环处理
size = read(0,buffer,1024);//从标准输入中读取数据放到缓冲区buffer中
if(size>0){//读到数据
send(s,buffer,size,0);//发给服务器
size = recv(s,buffer,1024,0);//从服务器读取数据
write(1,buffer,size);//写到标准输出
}
}
}
//信号SIGINT处理函数
void sig_proccess(int signo)
{
printf("Catch a exit signal\n");
_exit(0);
}
void sig_piep(int sign)
{
printf("Catch a SIGPIPE signal\n");
}
4.使用readv()和write()函数
下面代码代替上面函数process_conn_client()和process_conn_server(),使用readv()和write()函数进行读写:
//服务器实现代码
//使用readv()和write()进行数据IO的服务器处理的代码,利用向量来接收和发送网络数据。
//3个向量完成数据的接收和响应;申请3个向量,每个大小为10个字符。
//利用公共的30个字节大小的缓冲区buffer来初始化3个向量的地址缓冲区,将每个向量的向量长度设置为10。
//调用readv()读取客户端的数据后,利用3个缓冲区构建响应信息,最后将响应信息发给服务器端。
#include<sys/uio.h>
#include<string.h>
#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>
static struct iovec *vs = NULL,*vc = NULL;
void process_conn_server(int s)//服务器对客户端的处理
{
char buffer[30];//向量缓冲区
sszie_t size = 0;
struct iovec *v = (struct iovec*)malloc(3*sizeof(struct iovec));//申请3个向量
if(!v){
printf("Not enough memory\n");
return;
}
vs = v;//挂接全局变量,便于释放管理
//每个向量10个字节的空间
v[0].iov_base = buffer;//0~9
v[1].iov_base = buffer+10;//10~19
v[2].iov_base = buffer+20;//20~29
v[0].iov_len = v[1].iov_len = v[2].iov_len = 10;//初始化长度为10
for(;;){//循环处理
size = readv(s,v,3);//从套接字中读取数据放到向量缓冲区中
if(size == 0){//没有数据
return;
}
//构建响应字符,为接收到客户端字节的数量,分别放到3个缓冲区中
sprintf(v[0].iov_base,"%d",size);//长度
sprintf(v[1].iov_base,"bytes alt");//"bytes alt"字符串
sprintf(v[2].iov_base,"ogether\n");//"ogether\n"字符串
//写入字符串长度
v[0].iov_len = strlen(v[0].iov_base);
v[1].iov_len = strlen(v[1].iov_base);
v[2].iov_len = strlen(v[2].iov_base);
write(s,v,3);//发给客户端
}
}
//客户端处理代码
//使用3个10字节大小的向量来完成数据的发送和接收操作
void process_conn_client(int s)
{
char buffer[30];//向量缓冲区
ssize_t size = 0;
struct iovec *v = (struct iovec*)malloc(3*sizeof(struct iovec));//申请3个向量
if(!v){
printf("Not enough memory\n");
return;
}
//挂接全局变量,便于释放管理
vc = v;
//每个向量10个字节的空间
v[0].iov_base = buffer;//0~9
v[1].iov_base = buffer+10;//10~19
v[2].iov_base = buffer+20;//20~29
//初始化长度为10
v[0].iov_len = v[1].iov_len = v[2].iov_len = 10;
int i =0;
for(;;){//循环处理
//从标准输入中读取数据放到缓冲区buffer中
size = read(0,v[0].iov_base,10);
if(size>0){//读取数据
v[0].iov_len = size;
write(s,v,1);//发送给服务器
v[0].iov_len = v[1].iov_len = v[2].iov_len = 10;
size = readv(s,v,3);//从服务器读取数据
for(i=0;i<3;i++){
if(v[i].iov_len>0){
write(1,v[i].iov_base,v[i].iov_len);//写到标准输出
}
}
}
}
}
//信号处理代码
//向量的内存空间是动态申请的,程序退出的时候不能自动释放,所以在信号SIGIN到来时,
//先释放申请的内存空间,再退出应用程序
void sig_proccess(int signo)
{
printf("Catch a exit signal\n");
free(vc);
free(vs);
_exit(0);
}
void sig_pipe(int sign)
{
printf("Catch a SIGPIPE signal\n");
free(vc);
free(vs);
_exit(0);
}
5.使用recvmsg()和sendmsg()函数
下面代码代替上面函数process_conn_client()和process_conn_server(),使用recvmsg()和sendmsg()函数进行读写:
//服务器端实现代码
//使用消息函数进行IO的服务器处理过程同样适合3个10字节大小的向量缓冲区来保存数据
//但是并不是直接对这些向量进行操作,而是将向量挂载消息结构msghdr的msg_iov成员变量上
//进行操作,并将向量的存储空间长度设置为30。在服务器端调用函数recvmsg()从套接字s中接收数据到消息msg中,将接收到的信息进行处理后,调用sendmsg()函数将响应数据通过套接字s发出。
#include<sys/uio.h>
#include<string.h>
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
static struct iovec *vs = NULL,*vc = NULL;
//服务器对客户端的处理
void process_conn_server(int s)
{
char buffer[30];//向量的缓冲区
ssize_t size = 0;
struct msghdr msg;//消息结构
struct iovec *v = (struct iovec*)malloc(3*sizeof(struct iovec));//申请3个向量
if(!v){
printf("Not enough memory\n");
return;
}
//挂接全局变量,便于释放管理
vs = v;
msg.msg_name = NULL;//无名字域
msg.msg_namelen = 0;//名字域长度为0
msg.msg_control = NULL;//无控制域
msg.msg_controllen = 0;//控制域长度为0
msg.msg_iov = v;//挂接向量指针
msg.msg_iovlen = 30;//接收缓冲区长度为30
msg.msg_flags = 0;//无特殊操作
//每个向量10个字节的空间
v[0].iov_base = buffer;//0~9
v[1].iov_base = buffer+10;//10~19
v[2].iov_base = buffer+20;//20~29
//初始化长度为10
v[0].iov_len = v[1].iov_len = v[2].iov_len = 10;
for(;;){//循环处理过程
//从套接字中读取数据放到向量缓冲区中
size = recvmsg(s,&msg,0);
if(size == 0){//没有数据
return;
}
//构建响应字符,为接收到客户端字节的数量,分别放到3个缓冲区中
sprintf(v[0].iov_base,"%d",size);//长度
sprintf(v[1].iov_base,"bytes alt");//"bytes alt"字符串
sprintf(v[2].iov_base,"ogether\n");//"ogether\n"字符串
//写入字符串长度
v[0].iov_len = strlen(v[0].iov_base);
v[1].iov_len = strlen(v[1].iov_base);
v[2].iov_len = strlen(v[2].iov_base);
sendmsg(s,&msg,0);
}
}
//客户端处理代码
//与服务器对应,客户端的实现也将3个向量挂接在一个消息上进行数据的收发操作
void process_conn_client(int s)
{
char buffer[30];//向量缓冲区
ssize_t size = 0;
struct msghdr msg;//消息结构
struct iovec *v = (struct iovec*)malloc(3*sizeof(struct iovec));//申请3个向量
if(!v){
printf("Not enough memory\n");
return;
}
//挂接全局变量,便于释放管理
vs = v;
//初始化消息
msg.msg_name = NULL;//无名字域
msg.msg_namelen = 0;//名字域长度为0
msg.msg_control = NULL;//无控制域
msg.msg_controllen = 0;//控制域长度为0
msg.msg_iov = v;//挂接向量指针
msg.msg_iovlen = 30;//接收缓冲区长度为30
msg.msg_flags = 0;//无特殊操作
//每个向量10个字节的空间
v[0].iov_base = buffer;//0~9
v[1].iov_base = buffer+10;//10~19
v[2].iov_base = buffer+20;//20~29
//初始化长度为10
v[0].iov_len = v[1].iov_len = v[2].iov_len = 10;
int i = 0;
for(;;){
//从标准输入中读取数据放到缓冲区buffer中
size = read(0,v[0].iov_base,10);
if(size >0 ){//读到数据
v[0].iov_len = size;
sendmsg(s,&msg,0);//发送给服务器
v[0].iov_len = v[1].iov_len = v[2].iov_len = 10;
size = recvmsg(s,&msg,0);//从服务器读取数据
for(i=0;i<3;i++){
if(v[i].iov_len>0){
write(1,v[i].iov_base,v[i].iov_len);//写到标准输出
}
}
}
}
}
//信号处理代码
//向量的内存空间是动态申请的,程序退出的时候不能自动释放,所以在信号SIGIN到来时,
//先释放申请的内存空间,再退出应用程序
void sig_proccess(int signo)
{
printf("Catch a exit signal\n");
free(vc);
free(vs);
_exit(0);
}
void sig_pipe(int sign)
{
printf("Catch a SIGPIPE signal\n");
free(vc);
free(vs);
_exit(0);
}
三、IO模型
IO的方式有阻塞IO、非阻塞IO模型、IO复用、信号驱动、异步IO等。
1.阻塞IO模型
阻塞IO是最通用的IO类型,使用这种模型进行数据接收的时候,在数据没有到之前程序会一直等待。例如,对于函数recvfrom()
,内核会一直阻塞该请求直到有数据到来才返回,如下图所示。
2.非阻塞IO模型
当把套接字设置成非阻塞的IO,则对每次请求,内核都不会阻塞,会立即返回;当没
有数据的时候,会返回一个错误。例如,对recvfrom()
函数,前几次都没有数据返回,直到最后内核才向用户层的空间复制数据,如下图所示:
3.IO复用
使用IO复用模型可以在等待的时候加入超时的时间,当超时时间没有到达的时候与阻塞的情况一致,而当超时时间到达仍然没有数据接收到,系统会返回,不再等待。select()函数按照一定的超时时间轮询,直到需要等待的套接字有数据到来,利用recvfrom()函数,将数据复制到应用层,如下图所示。
4.信号驱动IO模型
信号驱动的IO在进程开始的时候注册一个信号处理的回调函数,进程继续执行,当信号发生时,即有了IO的时间,这里就有数据到来,利用注册的回调函数将到来的数据用recvfrom()接收到,如下图所示。
5.异步IO模型
异步IO与前面的信号驱动IO相似,其区别在于信号驱动IO
当数据到来的时候,使用信号通知注册的信号处理函数,而异步IO
则在数据复制完成的时候才发送信号通知注册的信号处理函数,如下图所示。
①.System V异步I/O
-
System V中,异步 I/O是
STREAMS
系统的一部分,它只对STREAMS设 备和STREAMS
管道起作用。System V的异步I/O信号是SIGPOLL
。 -
除了调用ioctl指定产生
SIGPOLL
信号的条件以外,还应为该信号建立信号 处理程序。对于SIGPOLL
的默认动作是终止该进程,所以应当在 调用ioctl之前建立信号处理程序。 -
为了对一个
STREAMS
设备启动异步I/O,需要调用ioctl
,将它的第二
个参 数设置成I_SETSIG
,第3个参数如下图所示。
②.BSD 异步I/O
在BSD中,异步I/O是信号SIGIO和SIGURG
的组合。SIGIO是通用异步I/O信号,SIGURG则只用来通知进程网络连接上的带外数据已经到达。
接收SIGIO信号,需执行以下3步:
-
调用signal或sigaction为SIGIO信号建立信号处理程序。
-
以命令
F_SETOWN
调用fcntl
来设置进程ID或进程组ID, 用于接收对于该描述符的信号。 -
以命令F_SETFL调用fcntl设置O_ASYNC文件状态标志,使在该描述符上可以进行异步I/O。(仅能对指向终端或网络的描述符执行)。
对于SIGURG
信号,只需执行第1步和第2步。该信号仅对引用支持带外数 据的网络连接描述符而产生,如TCP连接
③. POSIX异步I/O
POSIX异步I/O接口为对不同类型的文件进行异步I/O提供了相同的方 法。异步I/O接口使用AIO
控制块来描述I/O操作。aiocb
结构定义了AIO控制 块。该结构至少包括下面这些字段(具体的实现可能还包含有额外的字段):
struct aiocb {
int aio_fildes; //被打开用来读或写的文件描述符
off_t aio_offset; //偏移量
volatile void *aio_buf;//地址
size_t aio_nbytes; //读或写的字节数
int aio_reqprio; //为异步 I/O请求提示顺序
struct sigevent aio_sigevent; //如何通知应用程序
int aio_lio_opcode; //只能用于基于列表的异步I/O
};
`aio_lio_opcode`字段指定了该操作是一个读操作(LIO_READ)、写操作(LIO_WRITE),还是将被忽略的空操作(LIO_NOP)。
struct sigevent {
int sigev_notify;//控制通知如下的3类型:
//a.SIGEV_NONE 异步I/O请求完成后,不通知进程。
//b.SIGEV_SIGNAL:异步I/O请求完成后,产生由sigev_signo字段指定的信号。捕捉且指定SA_SIGINFO标志,信号将入队。signifo结构中si_value设置为sigev_value。
//c.SIGEV_THREAD :异步I/O请求完成,由sigev_notify_function字段指定 的函数被调用,sigev_value字段被传入作为它的唯一参数。除非 sigev_notify_attributes 字段被设定为pthread 属性结构的地址,且该结构指定了一个另外的线程属性,否则该函数将在分离状态下的一个单独的线程中执行。否则该函数将在分离状态下的一个单独的线程中执行。
int sigev_signo;
union sigval sigev_value;
void (*sigev_notify_function)(union sigval);
pthread_attr_t *sigev_notify_attributes;};
- 异步I/O操作必须显式地指定偏移量,异步I/O接口并不影响由操作系 统维护的文件偏移量。
- 不应不在同一个进程里把异步I/O函数和传统I/O函数混在 一起用在同一个文件上。
- 使用 异步I/O接口向一个以追加模式(使用O_APPEND)打开的文件中写入数据, AIO控制块中的
aio_offset
字段会被系统忽略。
#include <aio.h>
int aio_read(struct aiocb *aiocb);//读操作
int aio_write(struct aiocb *aiocb); //写操作
//两个函数的返回值:若成功,返回0;若出错,返回−1
#include <aio.h>
//强制所有等待中的异步操作不等待而写入持久化的存储中
int aio_fsync(int op, struct aiocb *aiocb);
//op 参数设定为O_DSYNC执行起会像调用了fdatasync。
//果op参数设定为O_SYNC执行起会像调用了了fsync
//aio_fildes字段指定了其异步写操作被同步的文件。
//返回值:若成功,返回0;若出错,返回−1
//获知一个异步读、写或者同步操作的完成状态
#include <aio.h>
int aio_error(const struct aiocb *aiocb);
//返回值:0:异步操作成功完成;-1:对aio_error的调用失败;EINPROGRESS 异步读、写或同步操作仍在等待;其他情况 其他任何返回值是相关的异步操作失败返回的错误码。
//获取异步操作的返回值。
#include <aio.h>
ssize_t aio_return(const struct aiocb *aiocb);
//对每个异步操作只调用一次aio_return,操作系统释放掉包含了I/O操作返回值的记录。
//aio_return函数本身失败,会返回−1,并设置errno。
//来阻塞进程
#include <aio.h>
int aio_suspend(const struct aiocb *const list[], int nent, const struct timespec *timeout);
//list:个指向AIO控制块数组的指针
//nent:了数组中的条目数,数组中的空指针会被跳过,其他条目都必须指向已用于初始化异步I/O操作 的AIO控制块。
//信号中断返回-1,并将errno设置为EINTR。
//时间超过timeout参数所指定的时间限制将返回-1,errno设置为EAGAIN。
//何I/O操作完成返回0。
//所有的异步I/O操作都已完成,将在不阻塞的情况下直接返回。
// 返回值:若成功,返回0;若出错,返回−1
//不想再完成的等待中的异步I/O操作取消
#include <aio.h>
int aio_cancel(int fd, struct aiocb *aiocb);
//fd:指定了那个未完成的异步I/O操作的文件描述符
//aiocb参数为 NULL,系统将会尝试取消所有该文件上未完成的异步I/O操作。
//aio_cancel函数可能会返回以下4个值中的一个:
//a.AIO_ALLDONE 所有操作在尝试取消它们之前已经完成。
//b.AIO_CANCELED 所有要求的操作已被取消。
//c.AIO_NOTCANCELED 至少有一个要求的操作没有被取消。
//-1 对aio_cancel的调用失败,错误码将被存储在errno中。
//提交一系列由一个AIO控制块列表描述的I/O请求。
#include <aio.h>
int lio_listio(int mode, struct aiocb *restrict const list[restrict], int nent, struct sigevent *restrict sigev);
//mode决定了I/O是否真的是异步的,为LIO_WAIT将在所有由列表指定的I/O 操作完成后返回,sigev被忽略。
//mode:设定为LIO_NOWAIT,将在I/O请求入队后立即返回。
//进程将在所有I/O操作完成后,按照sigev参数指定的,被异步地通知。
//sigev:NULL不被通知。
//list:指向AIO控制块列表,该列表指定了要运行的I/O操作的。
//nent:指定了数组中的元素个数。
//AIO控制块列表可以包含NULL指针,这些条目将被 忽略。
//返回值:若成功,返回0;若出错,返回−1
实现会限制我们不想完成的异步 I/O 操作的数量,这些限制都是运行时不 变量,如下图所示:
调用 sysconf
函数并把 name
参数设置为_SC_IO_LISTIO_MAX
来设定 AIO_LISTIO_MAX
的值,设置为_SC_AIO_MAX
来设定 AIO_MAX
的值,设置为_SC_AIO_PRIO_DELTA_MAX
来设定AIO_PRIO_DELTA_MAX
的值
例:用ROT-13和异步I/O翻译一个文件
#include "apue.h"
#include <ctype.h>
#include <fcntl.h>
#include <aio.h>
#include <errno.h>
#define BSZ 4096
#define NBUF 8
enum rwop {
UNUSED = 0,
READ_PENDING = 1,
WRITE_PENDING = 2
};
struct buf {
enum rwop op;
int last;
struct aiocb aiocb;
unsigned char data[BSZ];
};
struct buf bufs[NBUF];
unsigned char translate(unsigned char c)
{
/* same as before */
}
int main(int argc, char* argv[])
{
int ifd, ofd, i, j, n, err, numop;
struct stat sbuf;
const struct aiocb *aiolist[NBUF];
off_t off = 0;
if (argc != 3)
err_quit("usage: rot13 infile outfile");
if ((ifd = open(argv[1], O_RDONLY)) < 0)
err_sys("can't open %s", argv[1]);
if ((ofd = open(argv[2], O_RDWR|O_CREAT|O_TRUNC, FILE_MODE)) < 0)
err_sys("can't create %s", argv[2]);
if (fstat(ifd, &sbuf) < 0)
err_sys("fstat failed");
/* initialize the buffers */
for (i = 0; i < NBUF; i++) {
bufs[i].op = UNUSED;
bufs[i].aiocb.aio_buf = bufs[i].data;
bufs[i].aiocb.aio_sigevent.sigev_notify = SIGEV_NONE;
aiolist[i] = NULL;
}
numop = 0;
for (;;) {
for (i = 0; i < NBUF; i++) {
switch (bufs[i].op) {
case UNUSED:
/*
* Read from the input file if more data
* remains unread.
*/
if (off < sbuf.st_size) {
bufs[i].op = READ_PENDING;
bufs[i].aiocb.aio_fildes = ifd;
bufs[i].aiocb.aio_offset = off;
off += BSZ;
if (off >= sbuf.st_size)
bufs[i].last = 1;
bufs[i].aiocb.aio_nbytes = BSZ;
if (aio_read(&bufs[i].aiocb) < 0)
err_sys("aio_read failed");
aiolist[i] = &bufs[i].aiocb;
numop++;
}
break;
case READ_PENDING:
if ((err = aio_error(&bufs[i].aiocb)) == EINPROGRESS)
continue;
if (err != 0) {
if (err == -1)
err_sys("aio_error failed");
else
err_exit(err, "read failed");
}
/*
* A read is complete; translate the buffer
* and write it.
*/
if ((n = aio_return(&bufs[i].aiocb)) < 0)
err_sys("aio_return failed");
if (n != BSZ && !bufs[i].last)
err_quit("short read (%d/%d)", n, BSZ);
for (j = 0; j < n; j++)
bufs[i].data[j] = translate(bufs[i].data[j]);
bufs[i].op = WRITE_PENDING;
bufs[i].aiocb.aio_fildes = ofd;
bufs[i].aiocb.aio_nbytes = n;
if (aio_write(&bufs[i].aiocb) < 0)
err_sys("aio_write failed");
/* retain our spot in aiolist */
break;
case WRITE_PENDING:
if ((err = aio_error(&bufs[i].aiocb)) == EINPROGRESS)
continue;
if (err != 0) {
if (err == -1)
err_sys("aio_error failed");
else
err_exit(err, "write failed");
}
/*
* A write is complete; mark the buffer as unused.
*/
if ((n = aio_return(&bufs[i].aiocb)) < 0)
err_sys("aio_return failed");
if (n != bufs[i].aiocb.aio_nbytes)
err_quit("short write (%d/%d)", n, BSZ);
aiolist[i] = NULL;
bufs[i].op = UNUSED;
numop--;
break;
}
}
if (numop == 0) {
if (off >= sbuf.st_size)
break;
} else {
if (aio_suspend(aiolist, NBUF, NULL) < 0)
err_sys("aio_suspend failed");
}
}
bufs[0].aiocb.aio_fildes = ofd;
if (aio_fsync(O_SYNC, &bufs[0].aiocb) < 0)
err_sys("aio_fsync failed");
exit(0);
}
6.存储映射I/O
存储映射I/O能将一个磁盘文件映射到存储空间中的缓冲区上,于是,当从缓冲区中取数据时,就相当于读文件中的相应字节。
将数据存入缓冲区时,相应字节就自动写入文件,就可以在不使用read和write的情况下执行I/O。
//告诉内核将一个给定的文件映射到一个存储区 域中。
#include <sys/mman.h>
void *mmap(void *addr, size_t len, int prot, int flag, int fd, off_t off);
//addr:指定映射存储区的起始地址。通常设置为0,表示由系统选择该映射区的起始地址。
//fd:指定要被映射文件的描述符。
//len:映射的字节数.
//off:要映射字节在文件中的起始偏移量
//prot:参数如图所示。不能超过open模式访问权限,例:文件只读打开,映射存储区不能指定为PROT_WRITE
//返回值:若成功,返回映射区的起始地址;若出错,返回MAP_FAILED
下图为存储映射文件示意图:起始位置表示mmap返回值,映射存储区位于堆栈之间。
flag参数属性:
-
MAP_FIXED
返回值必须等于addr。未指定此标志,而且addr非0,则内核只把addr视为在何处设置映射区的一种建议,不保证会使用所要求的地址。将addr指定为0可获得最大可移植性。 -
MAP_SHARED
:描述了本进程对映射区所进行的存储操作的配 置。此标志指定存储操作修改映射文件,也就是,存储操作相当于对该文件的write
。必须指定本标志或下一个标志(MAP_PRIVATE
),但不能同时指定两者。 -
MAP_PRIVATE
:对映射区的存储操作导致创建该映射文件的 一个私有副本。所有后来对该映射区的引用都是引用该副本。
off、addr
的值(指定了MAP_FIXED)通常被要求是系统虚拟存储 页长度的倍数。- 虚拟存储页长可用带参数
_SC_PAGESIZE或_SC_PAGE_SIZE
的sysconf
函数得到。因为off和addr常常指定为0,所以这种要求一般并不重要。- 映射文件的起始偏移量受系统虚拟存储长度的限制,所以不能用mmap将数据添加到文件中,必须先加长该文件。
- 虚拟存储页长可用带参数
与映射区相关的信号:
-
SIGSEGV
用于指示进程试图访问对它不可用的存储区,映射存储区被mmap指定为只读,进程试图将数据存入这个映射存储区也会产生此信号。 -
映射区的某个部分在访问时已经不存在,则产生
SIGBUS
信号。
子进程能通过fork继承存储映射区(子进程复制父进程地址空间,而存 储映射区是该地址空间中的一部分),新程序则不能通 过exec继承存储映射区。
//更改一个现有映射的权限。
#include <sys/mman.h>
int mprotect(void *addr, size_t len, int prot);
//prot的合法值与mmap中prot参数的一样
//addr的值必须是系统页长的整数倍。
// 返回值:若成功,返回0;若出错,返回-1
-
修改的页是通过
MAP_SHARED
标志映射到地址空间的,那么修改并不 会立即写回到文件中。 -
何时写回脏页由内核的守护进程决定,决定的依 据是系统负载和用来限制在系统失败事件中的数据损失的配置参数。
-
只修改了一页中的一个字节,当修改被写回到文件中时,整个页都会被写 回。
#include <sys/mman.h>
//共享映射中的页已修改,调用 msync将该页冲洗到被映射的 文件中。
//映射私有,则不修改被映射的文件。地址与页边界对齐。
int msync(void *addr, size_t len, int flags);
//
//flags:MS_ASYNC:调试要写的页;MS_SUNC:返回之前等待写操作完成;MS_INCALIDATE:可选,通知丢弃没有同步的页。
//返回值:若成功,返回0;若出错,返回-1
//解除映射区
#include <sys/mman.h>
int munmap(void *addr, size_t len);
//返回值:若成功,返回0;若出错,返回−1
//修改映射位置和大小
#include<sys/mman.h>
void *mremap(void *oldaddr,size_t oldszie,size_t newsize,int flags,...);
//oldaddr和oldsize指定需扩展或收缩的既有映射的位置和大小。
//oldaddr指定的地址必须是分页对齐的,通常mmap的返回值。
//newsize:映射新大小
//flag:MREMAP_MAYMOVE重新指定位置;MPREMAP_FLXED和MREMAP_MAYMOVE一起使用。
//创建非线性映射步骤:
//a.使用mmap()创建一个映射。
//b.使用一个或多个remp_flie_pages调用来调整内存分页和文件分页之间对应关系。
#include<sys/mman.h>
int remp_flie_pages(void *addr,size_t size,int port,size_t pgoff,int flags);
//pgoff:指定了文件域的起始位置
//size:指定文件区域的长度,单位字节。
//port:会被忽略为0;
//flags:未被使用
//addr:标识分页需调整的既有映射;指定了通过pgoff和size标识出的文件分页所处的内存地址。
//addr和size:系统分页大小整数倍
//使用mmap调用来映射通过文件描述符fd引用的打开着的文件的3个分页。
ps = sysconf(_SC_PAGESIZE);
addr = mmap(0,3*ps,PROT_READ|PORT_WRITE,MAP_SHARD,fd,0);
//创建非线性映射
remp_file_pages(addr,ps,0,2,0);
remp_flie_pages(addr+2*ps,ps,0,0,0);
例:在父进程和子进程之间共享一个匿名映射。
#ifdef USE_MAP_ANON
#define _BSD_SOURCE /* Get MAP_ANONYMOUS definition */
#endif
#include <sys/wait.h>
#include <sys/mman.h>
#include <fcntl.h>
#include "tlpi_hdr.h"
int main(int argc, char *argv[])
{
int *addr; /* Pointer to shared memory region */
#ifdef USE_MAP_ANON /* Use MAP_ANONYMOUS */
addr = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (addr == MAP_FAILED)
errExit("mmap");
#else /* Map /dev/zero */
int fd;
fd = open("/dev/zero", O_RDWR);
if (fd == -1)
errExit("open");
addr = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (addr == MAP_FAILED)
errExit("mmap");
if (close(fd) == -1) /* No longer needed */
errExit("close");
#endif
*addr = 1; /* Initialize integer in mapped region */
switch (fork()) { /* Parent and child share mapping */
case -1:
errExit("fork");
case 0: /* Child: increment shared integer and exit */
printf("Child started, value = %d\n", *addr);
(*addr)++;
if (munmap(addr, sizeof(int)) == -1)
errExit("munmap");
exit(EXIT_SUCCESS);
default: /* Parent: wait for child to terminate */
if (wait(NULL) == -1)
errExit("wait");
printf("In parent, value = %d\n", *addr);
if (munmap(addr, sizeof(int)) == -1)
errExit("munmap");
exit(EXIT_SUCCESS);
}
}
四、select()函数和pselect()函数
函数select()和pselect()用于IO的复用,它们监视多个文件描述符的集合,判断是否有符合条件的时间发生。
1.select()函数
-
函数select()与之前的函数recv()和send()
直接操作文件描述符不同
。- 使用select()函数可以先对需要操作的文件描述符
进行查询
,查看目标文件描述符是否可以进行读、写或者错误操作,然后当文件描述符满足操作的条件的时候才进行真正的IO操作。
- 使用select()函数可以先对需要操作的文件描述符
①.简介
#include<sys/select.h>
#include<sys/time.h>
#include<sys/types.h>
#include<unistd.h>
int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout);
-
nfds: 一个整型的变量,它比
所有文件描述符集合中的文件描述符的最大值大1
。使用select的时候必须计算最大值的文件描述的值,将值通过nfds传入。 -
readfds: 这个文件描述符集合监视文件集中的
任何文件是否有数据可读
,当select()函数返回的时候,readfds将清除其中不可读的文件描述符,只留下可读的文件描述符,即可以被函数recv()、read()等进行读数据的操作。 -
writefds: 这个文件描述符集合监视文件集中的
任何文件是否有数据可写
,当select()函数返回的时候,readfds将清除其中的不可写的文件描述符,只留下可写的文件描述符,即可以被send()、write()函数等进行写数据的操作。 -
exceptfds: 这个文件集将监视文件集中的
任何文件是否发生错误
,其实,它可用于其他的用途。例如,监视带外数据00B
,带外数据使用MSG_OOB
标志发送到套接字上。当select()函数返回的时候,readfds将清除其中的其他文件描述符,只留下可读00B数据。 -
timeout: 设置在select()所监视的文件集合中的事件没有发生时,
最长的等待时间
,当超过此时间时,函数会返回。当超时时间为NULL时,表示阻塞操作,会一直等待,直到某个监视的文件集中的某个文件描述符符合返回条件。当timeout的值为0时,select会立即返回。 -
sigmask: 信号掩码。
函数select()返回值为0、-1或者一个大于1的整数值
:
当监视的文件集中有文件描述符 符合要求,即读文件描述符集中的文件可读、写文件描述符中的文件可写或者错误文件描述符中的文件发生错误时,返回值为大于0的正值;
当超时的时候返回0;
当返回值为-1发生错误,由errno指定:
-
函数select()和函数pselect()允许程序监视多个文件描述符,当一个或者多个监视的文件描述符准备就绪,可以进行
IO操作
的时候返回。函数监视一个文件描述符的对应操作是否可以进行,例如对监视读文件集的对文件描述符可操作。 -
函数可以同时监视
3类
文件描述符。将监视在readfds文件描述符集合中的文件是否可读
,即判断对此文件描述符进行读操作是否被阻塞; -
函数监视writefds文件描述符集合中的文件是否
可写
,即判断是否对此文件描述符进行写操作是否阻塞; -
函数还监视文件描述符集合exceptfds中的文件描述符是否
发生意外
。当函数退出的时候,上述的集合发生了改变。 -
不需要监视某种文件集时,可以将对应的文件集设置为
NULL
。如果所有的文件集和均为NULL, 则表示等待 一 段时间。
timeout的类型结构如下:
struct timeval{
time_t tv_sec;//秒
long tv_usec;//微秒
};
- 成员tv_sec表示超时的秒数
- 成员tv_usec表示超时的微秒数,即1/1000000s。
4个宏可以操作文件描述符的集合:
- FD_ZERO():清理文件描述符的集合;
- FD_SET():向某个文件描述符集合加入文件描述符;
- FD_CLR():从某个文件描述符的集合中取出某个文件描述符。
- FD_ISSET():测试某个文件描述符是否是某个集合中的一员。
文件描述符的交换存在最大的限制,其最大值为FD_SETSIZE,当超出最大值时,将发生不能确定的事情。
②.select函数的例子
使用select()函数监视标准输入是否有数据处理,设置超时时间为5s,如果出错,则打印出错信息;如果标准输入有数据输入,则打印输入信息;如果等待超时,则打印超时信息。
#include<stdio.h>
#include<sys/time.h>
#include<sys/types.h>
#include<unistd.h>
int main(void){
fd_set rd;//读文件集合
struct timeval tv;//时间间隔
int err;//错误值
//监视标准输入是否可以读数据
FD_ZERO(&rd);
FD_SET(0,&rd);
//设置5s的等待超时
tv.tv_sec = 5;
tv.tv_usec = 0;
err = select(1,&rd,NULL,NULL,&tv);//函数返回,查看返回条件
if(err = -1)//出错
perror("select()");
else if(err)//标准输入有数据输入,可读
printf("Dara is available now.\n");//FD_ISSET(0,&rd)的值为真
else
printf("No data within five seconds.\n");//超时,没有数据到达
return 0;
}
2.pselect()函数
函数select()是用一种超时轮循的方式查看文件的读写错误的可操作性,在Linux下,还有一种相似的函数pselect()。
①.简介
函数原型:
#include<sys/select.h>
#include<sys/time.h>
#include<sys/types.h>
#include<unistd.h>
int pselect(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,const struct timespec *timeout,const sigset_t *sigmask);
pselect()函数的含义与select()函数一致,除了如下几点:
- 超时的时间结构是一个纳秒级的结构,原型如下所示。不过在 Linux 平台下内核调度的精度为
10毫秒级
,所以即使设置了纳秒级的分辨率,也达不到设置的精度。
struct timespec{
long tv_sec;//超时的秒数
long tv_nsec;//超时得到纳秒数
};
-
增加了进入 pselect()函数时替换掉的信号处理方式,当
sigmask
为NULL 的时候,与select的方式一致。 -
select()函数在执行之后可能会改变 timeout 的值,修改为还有多少时间剩余,而pselect()函数不会修改该值。
与select()函数相比,pselect()函数的代码如下:
ready = pselect(nfds,&readfds,&writefds,&exceptfds,timeout,&sigmask);
对于下面的select()函数,在进入select()函数之前先手动将信号的掩码改变,并保存之前掩码值;select()函数执行后,再恢复为之前的信号掩码值。
sigset_t origmask;
sigprocmask(SIG_SETMASK,&sigmask,&origmask);
read = select(nfds,&readfds,&writefds,&exceptfds,timeout);
sigprocmask(SIG_SETMASK,&origmask,NULL);
②.pselcet()函数的例子
下面是 一 个使用 pselect()的简单例子。在例子中先清空信号,然后将 SIG C H LD 信号加入到要处理的信号集合中。设置pselect()监视的信号时,在挂载用户信号的同时将系统原来的信号保存下来,方便程序退出的时候恢复原来的设置。
#include<sys/select.h>
#include<sys/time.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdio.h>
#include<signal.h>
int child_events = 0;
void child_sig_handler(int x){//信号处理函数
child_events++;//调用次数+1
signal(SIGCHLD,child_sig_handler);//重新设定信号回调函数
}
int main(int argc,char *argv){
//设定信号掩码sigmask和原始的信号掩码orig_sigmask
sigset_t sigmask,orig_sigmask;
sigemptyset(&sigmask);//清空信号
sigaddset(&sigmask,SIGCHLD);//将SIGCHLD信号加入sigmask
//设定信号SIG_BLOCK的掩码sigmask, 并将原始的掩码保存到orig_sigmask中
sigprocmask(SIG_BLOCK,&sigmask,&orig_sigmask);
//挂接对信号SIGCHLD的处理函数child_sig_handler()
signal(SIGCHLD,child_sig_handler());
for(;;){//主循环
for(;child_events > 0;child_events--){//判断是否退出
//处理动作
}
//pselect IO复用
r=pselect(1,&rd,&wr,&er,0,&orig_sigmask);
//主程序
}
}
五、poll()函数和ppoll()函数
除了使用select()函数进行文件描述符监视,还有一组函数也可以完成相似功能,即函数poll()和函数ppoll()。
1.poll()函数
poll()函数等待某个文件描述符上的某个事情的发生,函数原型:
#include<poll.h>
int poll(struct pollfd *fds,nfds_t nfds,int timeout);
poll()函数监视在fds数组指明的一 组文件描述符上发生的动作,当满足条件或者超时的时候会退出。
-
参数fds是一个指向结构pollfd 数组的指针,监视的文件描述符和条件放在里面。
-
参数nfds是比监视的最大描述符的值大1的值。
-
参数 timeout是超时时间,单位为毫秒,当为负值时,表示永远等待。
poll()函数返回值的:
- 大于0: 表示成功,等待的某个条件满足,返回值为满足条件的监视文件描述符的数量。
- 0:表示超时。
- -1表示发生错误, errno 的错误代码如下图所示。
结构struct pollfd的原型如下:
struct pollfd{
int fd;//监视文件描述符。
short events;//请求的事件;表示输入的监视事件。
short revents;//返回的监视事件,即返回时发生的事件。
};
2.ppoll()函数
函数原型:
#include<poll.h>
int ppoll(struct pollfd *fds,nfds_t nfds,const struct timespec *timeout,const sigset_t *sigmask);
其区别同函数select()和pselect()的区别相同,主要有两点:
- 超时时间
timeout
, 采用了纳秒级的变量。 - 可以在ppoll()函数的处理过程中挂接临时的信号掩码。
ppoll()函数的代码如下:
ready = ppoll(&fds,nfds,timeout,&sigmask);
与poll()函数的如下代码一致:
sigset_t origmask;
sigprocmask(SIG_SETMASK,&sigmask,&origmask);
read = ppoll(&fds,nfds,timeout,&sigmask);
sigprocmask(SIG_SETMASK,&origmask,NULL);
六、非阻塞编程
1.非阻塞方式程序设计介绍
非阻塞方式的操作与阻塞方式的操作最大的不同点是函数的调用立刻返回
,不管数据是否成功读取或者成功写入。使用fcntl()将套接字文件描述符按照如下的代码进行设置后,可以进行非阻塞的编程:
fcntl(s,F_SETFL,O_NONBLOCK);//s:套接字文件描述符;
//F_SETFL命令将套接字s设置为非阻塞方式后,在进行读写操作就可以正常返回。
非阻塞程序设计的例子
函数accept()可以使用非阻塞的方式轮询等待客户端的到来,在之前要设置O_NONBLOCK
方式。下面使用了轮询的方式使用accept()和recv()函数:
-
当客户端发送HELLO字符串时,发送OK响应给客户端并
关闭客户端
; -
当客户端发送SHUTDOWN字符串给服务器时,服务器发送BYE的客户端并
关闭客户端,然后退出程序
。
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<fcntl.h>
#include<unistd.h>
#include<netinet/in.h>
#include<string.h>
#define PORT 9999
#define BACKLOG 4
#define BUFFER_SIZE 1024
int main(int argc,char *argv[])
{
struct sockaddr_in local;
struct sockaddr_in client;
int len;
int s_s = -1,s_c = -1;
//初始化结构
local.sin_family = AF_INET;
local.sin_port = htons(PORT);
local.sin_addr.s_addr = htonl(-1);
//建立套接字描述符
s_s = socket(AF_INET,SOCK_STREAM,0);
//设置非阻塞方式
fcntl(s_s,F_SETFL,O_NONBLOCK);
listen(s_s,BACKLOG);
char buffer[BUFFER_SIZE];
for(;;)
{
//轮询接收客户端
while(s_c<0){//等待客户端到来
s_c = accept(s_s,(struct sockaddr*)&client,&len);
}
//轮询接收,当接收到数据的时候退出while循环
while(recv(s_c,buffer,1024,0)<=0)
;
//接收到客户端的数据
if(strcmp(buffer,"HELLO")==0){//判断是否为HELLO字符串
send(s_s,"OK",3,0);//发送响应
close(s_c);//关闭连接
continue;//继续等待客户端连接
}
if(strcmp(buffer,"SHUTDOWN")==0){//判断是否为SHUTDIWN字符串
send(s_s,"BYE",3,0);//发送BYE字符串
close(s_c);//关闭客户端连接
break;//退出注循环
}
}
close(s_s);
return 0;
}
注:使用轮询的方式进行查询十分浪费CPU等资源。