從套接字上得到擴充套件的更為可靠的出錯資訊
阿新 • • 發佈:2018-12-23
在前一篇中,我們提到在對端主機上沒有建立指定的UDP套接字時,我們向其傳送一個UDP包,會得到一個目的埠不可達的ICMP出錯報文。但核心在處理完該報文後,給應用程式僅僅返回一個ECONNREFUSED錯誤號,所以應用程式能知道的全部資訊就是連線被拒絕,至於為什麼被拒絕,沒有辦法知道。我們可以通過套接字選項的設定,讓核心返回更為詳細的出錯資訊,以利於除錯程式,發現問題。下面是通過套接字選項傳遞擴展出錯資訊的一個示例程式。關於核心原理的分析,在下一篇給出。
#include <sys/socket.h>
#include <linux/types.h>
#include <linux/errqueue.h>
#include <sys/ioctl.h>
#include "my_inet.h"
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
int ip_control_msg( struct cmsghdr *msg )
{
int ret = 0;
switch( msg->cmsg_type ){
case IP_RECVERR:
{
struct sock_extended_err *exterr;
exterr = (struct sock_extended_err *)(CMSG_DATA(msg));
printf("ee_errno: %u\n", exterr->ee_errno );
printf("ee_origin: %u\n", exterr->ee_origin );
printf("ee_type: %u\n", exterr->ee_type );
printf("ee_code: %u\n", exterr->ee_code );
printf("ee_pad: %u\n", exterr->ee_pad );
printf("ee_info: %u\n", exterr->ee_info );
printf("ee_data: %u\n", exterr->ee_data );
}
ret = -1;
break;
default:
break;
}
return ret;
}
int control_msg( struct msghdr *msg )
{
int ret = 0;
struct cmsghdr *control_msg = CMSG_FIRSTHDR( msg );
while( control_msg != NULL ){
switch( control_msg->cmsg_level ){
case SOL_IP:
ret = ip_control_msg( control_msg );
break;
default:
break;
}
control_msg = CMSG_NXTHDR( msg, control_msg );
}
return ret;
}
int main()
{
int i;
struct sockaddr_in dest;
dest.sin_family = MY_PF_INET;
dest.sin_port = htons(16000);
dest.sin_addr.s_addr = 0x013010AC;
int fd = socket( MY_PF_INET, SOCK_DGRAM, MY_IPPROTO_UDP );
if( fd < 0 ){
perror("socket: ");
return -1;
}
if( connect( fd, (struct sockaddr*)&dest, sizeof(dest) ) < 0 ){
perror("connect: ");
return -1;
}
int val = 1;
if( setsockopt( fd, SOL_IP, IP_RECVERR, &val, sizeof(val) ) == -1 ){
perror("setsockopt: ");
return -1;
}
int bwrite = send( fd, "abcdefg", 7, 0 );
if( bwrite == -1 ){
perror("send: ");
return -1;
}
char buf[1024];
char control_buf[1024];
struct msghdr msg;
struct iovec iov = { buf, 1024 };
memset( &msg, 0, sizeof(msg) );
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = &control_buf;
msg.msg_controllen = 1024;
int bread = recvmsg( fd, &msg, MSG_ERRQUEUE );
if( bread == -1 ){
perror("recv: ");
return -1;
}
if( control_msg( &msg ) >= 0 )
printf("successed!\n");
else
printf("failed!\n");
close( fd );
return 0;
}
執行結果:
ee_errno: 111 //ECONNREFUSED
ee_origin: 2 //SO_EE_ORIGIN_ICMP
ee_type: 3 //目的不可達
ee_code: 3 //埠不可達
ee_pad: 0
ee_info: 0
ee_data: 0
failed!
我們來看這個應用程式背後,核心真正做了一些什麼事情。
代表MY_INET域套接字的結構體struct inet_sock有一個成員recverr,它佔1bit長度,可能的取值是1或0,當為0時表示socket上出錯時,只通過系統呼叫嚮應用程式返回錯誤號,不提供進一步的詳細資訊。當取值為1時,則表示socket上出錯時,則向struct inet_sock的成員sk_error_queue(一個sk_buff的佇列)存入一個特殊的struct sk_buff,在sk_buff的成員cb中放入詳細的錯誤資訊,應用程式通過特定的系統呼叫可以取得詳細的出錯資訊。
recverr的值可以通過套接字選項操作進行設定,它是一個IP層的選項,對應的選項名是IP_RECVERR。下面的程式碼就是將它的值設為1(開啟選項):
int val = 1;
if( setsockopt( fd, SOL_IP, IP_RECVERR, &val, sizeof(val) ) == -1 )
;//deal with error
當打開了這個選項後,我們在該socket上傳送UDP資料報,按照前面文章提及的測試環境執行,172.16.48.2繼續會收到ICMP目的不可達報文,在差錯資料報處理時,會達到函式myudp_err,該函式會設定socket的成員sk_err,同時,它也會檢查recverr成員,如果為1,則要在sk_error_queue佇列中放入一個特殊的出錯資訊sk_buff。該sk_buff保留了出錯的那個源UDP資料報,同時在它的cb成員中儲存了一個結構體struct sock_exterr_skb,該結構體記錄了詳細的出錯資訊,下面是其定義:
struct sock_exterr_skb
{
union {
struct inet_skb_parm h4;
#if defined(CONFIG_IPV6) || defined (CONFIG_IPV6_MODULE)
struct inet6_skb_parm h6;
#endif
} header;
struct sock_extended_err ee;
u16 addr_offset;
u16 port;
};
addr_offset和port是出錯UDP資料報的地址和埠號,ee的定義如下:
struct sock_extended_err
{
__u32 ee_errno; //錯誤號。
__u8 ee_origin; //產生錯誤的源,我們的環境下,產生錯誤的源為一個ICMP包。
__u8 ee_type; //ICMP型別。
__u8 ee_code; //ICMP程式碼。
__u8 ee_pad;
__u32 ee_info; //用於EMSGSIZE時找到的MTU。
__u32 ee_data;
};
我們儲存了出錯資訊,應用程式要取得這個出錯資訊,必須使用特定的系統呼叫,recvmsg可以獲得詳細的出錯資訊,同時,呼叫介面上必須使用標誌MSG_ERRQUEUE表示取錯誤佇列,下面是recvmsg的定義:
ssize_t recvmsg(int s, struct msghdr *msg, int flags);
flags置MSG_ERRQUEUE,msg結構控制資訊成員msg_control和msg_controllen需要分配一個快取,用於輔助資訊的傳遞。關於接收,可以檢視前面一篇的原始碼和man recvmsg,這裡不再重複。
#include <sys/socket.h>
#include <linux/types.h>
#include <linux/errqueue.h>
#include <sys/ioctl.h>
#include "my_inet.h"
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
int ip_control_msg( struct cmsghdr *msg )
{
int ret = 0;
switch( msg->cmsg_type ){
case IP_RECVERR:
{
struct sock_extended_err *exterr;
exterr = (struct sock_extended_err *)(CMSG_DATA(msg));
printf("ee_errno: %u\n", exterr->ee_errno );
printf("ee_origin: %u\n", exterr->ee_origin );
printf("ee_type: %u\n", exterr->ee_type );
printf("ee_code: %u\n", exterr->ee_code );
printf("ee_pad: %u\n", exterr->ee_pad );
printf("ee_info: %u\n", exterr->ee_info );
printf("ee_data: %u\n", exterr->ee_data );
}
ret = -1;
break;
default:
break;
}
return ret;
}
int control_msg( struct msghdr *msg )
{
int ret = 0;
struct cmsghdr *control_msg = CMSG_FIRSTHDR( msg );
while( control_msg != NULL ){
switch( control_msg->cmsg_level ){
case SOL_IP:
ret = ip_control_msg( control_msg );
break;
default:
break;
}
control_msg = CMSG_NXTHDR( msg, control_msg );
}
return ret;
}
int main()
{
int i;
struct sockaddr_in dest;
dest.sin_family = MY_PF_INET;
dest.sin_port = htons(16000);
dest.sin_addr.s_addr = 0x013010AC;
int fd = socket( MY_PF_INET, SOCK_DGRAM, MY_IPPROTO_UDP );
if( fd < 0 ){
perror("socket: ");
return -1;
}
if( connect( fd, (struct sockaddr*)&dest, sizeof(dest) ) < 0 ){
perror("connect: ");
return -1;
}
int val = 1;
if( setsockopt( fd, SOL_IP, IP_RECVERR, &val, sizeof(val) ) == -1 ){
perror("setsockopt: ");
return -1;
}
int bwrite = send( fd, "abcdefg", 7, 0 );
if( bwrite == -1 ){
perror("send: ");
return -1;
}
char buf[1024];
char control_buf[1024];
struct msghdr msg;
struct iovec iov = { buf, 1024 };
memset( &msg, 0, sizeof(msg) );
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = &control_buf;
msg.msg_controllen = 1024;
int bread = recvmsg( fd, &msg, MSG_ERRQUEUE );
if( bread == -1 ){
perror("recv: ");
return -1;
}
if( control_msg( &msg ) >= 0 )
printf("successed!\n");
else
printf("failed!\n");
close( fd );
return 0;
}
執行結果:
ee_errno: 111 //ECONNREFUSED
ee_origin: 2 //SO_EE_ORIGIN_ICMP
ee_type: 3 //目的不可達
ee_code: 3 //埠不可達
ee_pad: 0
ee_info: 0
ee_data: 0
failed!
我們來看這個應用程式背後,核心真正做了一些什麼事情。
代表MY_INET域套接字的結構體struct inet_sock有一個成員recverr,它佔1bit長度,可能的取值是1或0,當為0時表示socket上出錯時,只通過系統呼叫嚮應用程式返回錯誤號,不提供進一步的詳細資訊。當取值為1時,則表示socket上出錯時,則向struct inet_sock的成員sk_error_queue(一個sk_buff的佇列)存入一個特殊的struct sk_buff,在sk_buff的成員cb中放入詳細的錯誤資訊,應用程式通過特定的系統呼叫可以取得詳細的出錯資訊。
recverr的值可以通過套接字選項操作進行設定,它是一個IP層的選項,對應的選項名是IP_RECVERR。下面的程式碼就是將它的值設為1(開啟選項):
int val = 1;
if( setsockopt( fd, SOL_IP, IP_RECVERR, &val, sizeof(val) ) == -1 )
;//deal with error
當打開了這個選項後,我們在該socket上傳送UDP資料報,按照前面文章提及的測試環境執行,172.16.48.2繼續會收到ICMP目的不可達報文,在差錯資料報處理時,會達到函式myudp_err,該函式會設定socket的成員sk_err,同時,它也會檢查recverr成員,如果為1,則要在sk_error_queue佇列中放入一個特殊的出錯資訊sk_buff。該sk_buff保留了出錯的那個源UDP資料報,同時在它的cb成員中儲存了一個結構體struct sock_exterr_skb,該結構體記錄了詳細的出錯資訊,下面是其定義:
struct sock_exterr_skb
{
union {
struct inet_skb_parm h4;
#if defined(CONFIG_IPV6) || defined (CONFIG_IPV6_MODULE)
struct inet6_skb_parm h6;
#endif
} header;
struct sock_extended_err ee;
u16 addr_offset;
u16 port;
};
addr_offset和port是出錯UDP資料報的地址和埠號,ee的定義如下:
struct sock_extended_err
{
__u32 ee_errno; //錯誤號。
__u8 ee_origin; //產生錯誤的源,我們的環境下,產生錯誤的源為一個ICMP包。
__u8 ee_type; //ICMP型別。
__u8 ee_code; //ICMP程式碼。
__u8 ee_pad;
__u32 ee_info; //用於EMSGSIZE時找到的MTU。
__u32 ee_data;
};
我們儲存了出錯資訊,應用程式要取得這個出錯資訊,必須使用特定的系統呼叫,recvmsg可以獲得詳細的出錯資訊,同時,呼叫介面上必須使用標誌MSG_ERRQUEUE表示取錯誤佇列,下面是recvmsg的定義:
ssize_t recvmsg(int s, struct msghdr *msg, int flags);
flags置MSG_ERRQUEUE,msg結構控制資訊成員msg_control和msg_controllen需要分配一個快取,用於輔助資訊的傳遞。關於接收,可以檢視前面一篇的原始碼和man recvmsg,這裡不再重複。