SOCKET及其编程重点讲义资料

SOCKET及其编程重点讲义资料

2023年7月15日发(作者:)

SOCKET及其编程

1. 从套接字上得到扩展的更为可靠的出错信息

在前一篇中,我们提到在对端主机上没有创建指定的UDP套接字时,我们向其发送一个UDP包,会得到一个目的端口不可达的ICMP出错报文。但内核在处理完该报文后,给应用程序仅仅返回一个ECONNREFUSED错误号,所以应用程序能知道的全部信息就是连接被拒绝,至于为什么被拒绝,没有办法知道。我们可以通过套接字选项的设置,让内核返回更为详细的出错信息,以利于调试程序,发现问题。下面是通过套接字选项传递扩展出错信息的一个示例程序。关于内核原理的分析,在下一篇给出。

#include

#include

#include

#include

#include "my_inet.h"

#include

#include

#include

#include

#include

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: %un", exterr->ee_errno );

printf("ee_origin: %un", exterr->ee_origin );

printf("ee_type: %un", exterr->ee_type );

printf("ee_code: %un", exterr->ee_code );

printf("ee_pad: %un", exterr->ee_pad );

printf("ee_info: %un", exterr->ee_info );

printf("ee_data: %un", 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;

_family = MY_PF_INET;

_port = htons(16000);

_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) );

_iov = &iov;

_iovlen = 1;

_control = &control_buf;

_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!

2. 从套接字上得到扩展的更为可靠的出错信息(续)

接着前一篇,我们来看这个应用程序背后,内核真正做了一些什么事情。

代表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,这里不再重复。

3. 用于表示socket的结构体(1)

用户使用socket系统调用编写应用程序时,通过一个数字来表示一个socket,所有的操作都在该数字上进行,这个数字称为套接字描述符。在系统调用的实现函数里,这个数字就会被映射成一个表示socket的结构体,该结构体保存了该socket的所有属性和数据。在内核的协议中实现中,关于表示socket的结构体,是一个比较复杂的东西,下面一一介绍。

struct socket。

这是一个基本的BSD socket,我们调用socket系统调用创建的各种不同类型的socket,开始创建的都是它,到后面,各种不同类型的socket在它的基础上进行各种扩展。struct socket是在虚拟文件系统上被创建出来的,可以把它看成一个文件,是可以被安全地扩展的。下面是其完整定义:

struct socket {

socket_state state;

unsigned long flags;

const struct proto_ops *ops;

struct fasync_struct *fasync_list;

struct file *file;

struct sock *sk;

wait_queue_head_t wait;

short type;

};

state用于表示socket所处的状态,是一个枚举变量,其类型定义如下:

typedef enum {

SS_FREE = 0, //该socket还未分配

SS_UNCONNECTED, //未连向任何socket

SS_CONNECTING, //正在连接过程中

SS_CONNECTED, //已连向一个socket

SS_DISCONNECTING //正在断开连接的过程中

}socket_state;

该成员只对TCP socket有用,因为只有tcp是面向连接的协议,udp跟raw不需要维护socket状态。

flags是一组标志位,在内核中并没有发现被使用。

ops是协议相关的一组操作集,结构体struct proto_ops的定义如下:

struct proto_ops {

int family;

struct module *owner;

int (*release)(struct socket *sock);

int (*bind)(struct socket *sock, struct sockaddr *myaddr, int sockaddr_len);

int (*connect)(struct socket *sock, struct sockaddr *vaddr, int sockaddr_len, int flags);

int (*socketpair)(struct socket *sock1, struct socket *sock2);

int (*accept)(struct socket *sock,struct socket *newsock, int flags);

int (*getname)(struct socket *sock, struct sockaddr *addr,int *sockaddr_len, int peer);

unsigned int (*poll)(struct file *file, struct socket *sock,

struct poll_table_struct *wait); int (*ioctl)(struct socket *sock, unsigned int cmd, unsigned long arg);

int (*listen)(struct socket *sock, int len);

int (*shutdown)(struct socket *sock, int flags);

int (*setsockopt)(struct socket *sock, int level,

int optname, char __user *optval, int optlen);

int (*getsockopt)(struct socket *sock, int level,

int optname, char __user *optval, int __user *optlen);

int (*sendmsg)(struct kiocb *iocb, struct socket *sock,

struct msghdr *m, size_t total_len);

int (*recvmsg)(struct kiocb *iocb, struct socket *sock,

struct msghdr *m, size_t total_len, int flags);

int (*mmap)(struct file *file, struct socket *sock,struct vm_area_struct * vma);

ssize_t (*sendpage)(struct socket *sock, struct page *page,

int offset, size_t size, int flags);

};

协议栈中总共定义了三个strcut proto_ops类型的变量,分别是myinet_stream_ops,

myinet_dgram_ops, myinet_sockraw_ops,对应流协议, 数据报和原始套接口协议的操作函数集。

type是socket的类型,对应的取值如下:

enum sock_type {

SOCK_DGRAM = 1,

SOCK_STREAM = 2,

SOCK_RAW = 3,

SOCK_RDM = 4,

SOCK_SEQPACKET = 5,

SOCK_DCCP = 6,

SOCK_PACKET = 10,

};

sk是网络层对于socket的表示,结构体struct sock比较庞大,这里不详细列出,只介绍一些重要的成员,

sk_prot和sk_prot_creator,这两个成员指向特定的协议处理函数集,其类型是结构体struct

proto,该结构体也是跟struct proto_ops相似的一组协议操作函数集。这两者之间的概念似乎有些混淆,可以这么理解,struct proto_ops的成员操作struct socket层次上的数据,处理完了,再由它们调用成员sk->sk_prot的函数,操作struct sock层次上的数据。即它们之间存在着层次上的差异。struct proto类型的变量在协议栈中总共也有三个,分别是mytcp_prot,myudp_prot,myraw_prot,对应TCP, UDP和RAW协议。

sk_state表示socket当前的连接状态,是一个比struct socket的state更为精细的状态,其可能的取值如下: enum {

TCP_ESTABLISHED = 1,

TCP_SYN_SENT,

TCP_SYN_RECV,

TCP_FIN_WAIT1,

TCP_FIN_WAIT2,

TCP_TIME_WAIT, TCP_CLOSE,

TCP_CLOSE_WAIT,

TCP_LAST_ACK,

TCP_LISTEN,

TCP_CLOSING,

TCP_MAX_STATES

};

这些取值从名字上看,似乎只使用于TCP协议,但事实上,UDP和RAW也借用了其中一些值,在一个socket创建之初,其取值都是TCP_CLOSE,一个UDP socket connect完成后,将这个值改为TCP_ESTABLISHED,最后,关闭sockt前置回TCP_CLOSE,RAW也一样。

sk_rcvbuf和sk_sndbuf分别表示接收和发送缓冲区的大小。sk_receive_queue和sk_write_queue分别为接收缓冲队列和发送缓冲队列,队列里排列的是套接字缓冲区struct

sk_buff,队列中的struct sk_buff的字节数总和不能超过缓冲区大小的设定。

4.用于表示socket的结构体(2)

接着上一篇,继续介绍struct sock。

sk_rmem_alloc, sk_wmem_alloc和sk_omem_alloc分别表示接收缓冲队列,发送缓冲队列及其它缓冲队列中已经分配的字节数,用于跟踪缓冲区的使用情况。

struct sock有一个struct sock_common成员,因为struct inet_timewait_sock也要用到它,所以把它单独归到一个结构体中,其定义如下:

struct sock_common {

unsigned short skc_family;

volatile unsigned char skc_state;

unsigned char skc_reuse;

int skc_bound_dev_if;

struct hlist_node skc_node;

struct hlist_node skc_bind_node;

atomic_t skc_refcnt;

unsigned int skc_hash;

struct proto *skc_prot;

};

struct inet_sock。

这是INET域专用的一个socket表示,它是在struct sock的基础上进行的扩展,在基本socket的属性已具备的基础上,struct inet_sock提供了INET域专有的一些属性,比如TTL,组播列表,IP地址,端口等,下面是其完整定义:

struct inet_sock {

struct sock sk;

#if defined(CONFIG_IPV6) || defined(CONFIG_IPV6_MODULE)

struct ipv6_pinfo *pinet6;

#endif

__u32 daddr; //IPv4的目的地址。 __u32 rcv_saddr; //IPv4的本地接收地址。

__u16 dport; //目的端口。

__u16 num; //本地端口(主机字节序)。

__u32 saddr; //发送地址。

__s16 uc_ttl; //单播的ttl。

__u16 cmsg_flags;

struct ip_options *opt;

__u16 sport; //源端口。

__u16 id; //单调递增的一个值,用于赋给iphdr的id域。

__u8 tos; //服务类型。

__u8 mc_ttl; //组播的ttl

__u8 pmtudisc;

__u8 recverr:1,

is_icsk:1,

freebind:1,

hdrincl:1, //是否自己构建ip首部(用于raw协议)

mc_loop:1; //组播是否发向回路。

int mc_index; //组播使用的本地设备接口的索引。

__u32 mc_addr; //组播源地址。

struct ip_mc_socklist *mc_list; //组播组列表。

struct {

unsigned int flags;

unsigned int fragsize;

struct ip_options *opt;

struct rtable *rt;

int length;

u32 addr;

struct flowi fl;

} cork;

};

struct raw_sock

这是RAW协议专用的一个socket的表示,它是在struct inet_sock基础上的扩展,因为RAW协议要处理ICMP协议的过滤设置,其定义如下:

struct raw_sock {

struct inet_sock inet;

struct icmp_filter filter;

};

struct udp_sock

这是UDP协议专用的一个socket表示,它是在struct inet_sock基础上的扩展,其定义如下:

struct udp_sock {

struct inet_sock inet; int pending;

unsigned int corkflag;

__u16 encap_type;

__u16 len;

};

struct inet_connection_sock

看完上面两个,我们觉得第三个应该就是struct tcp_sock了,但事实上,struct tcp_sock并不直接从struct inet_sock上扩展,而是从struct inet_connection_sock基础上进行扩展,struct

inet_connection_sock是所有面向连接的socket的表示,关于该socket,及下面所有tcp相关的socket,我们在分析tcp实现时再详细介绍,这里只列出它们的关系。

strcut tcp_sock

这是TCP协议专用的一个socket表示,它是在struct inet_connection_sock基础进行扩展,主要是增加了滑动窗口协议,避免拥塞算法等一些TCP专有属性。

struct inet_timewait_sock

struct tcp_timewait_sock

在struct inet_timewait_sock的基础上进行扩展。

struct inet_request_sock

struct tcp_request_sock

在struct inet_request_sock的基础上进行扩展。

5.创建一个socket

一个socket代表了通信链路的一端,存储或指向与链路有关的所有信息。Linux提供了创建socket的一个系统调用,通过该系统调用,能够得到一个用来访问套接字的描述符:

#include

#include

int socket( int domain, int type, int protocol );

内核中的系统调用函数原型是在net/socket.c 1180行:

asmlinkage long sys_socket( int family, int type, int

protocol );

该函数主要做了两件事情:创建一个代表通讯端点的结构体struct

socket,将这个结构映射到一个文件描述符上,最后将这个描述符返回,也就是我们调用socket得到的套接字描述符。

下面是Linux内核中对结构socket的定义(不同操作系统间,对该结构的定义会有差异):

struct socket {

socket_state state;

unsigned long flags; struct proto_ops *ops;

struct fasync_struct *fasync_list;

struct file *file;

struct sock *sk;

wait_queue_head_t wait;

short type;

};

state是一个内部状态标志:

typedef enum {

SS_FREE = 0, /* 未分配 */

SS_UNCONNECTED, /* 未连接 */

SS_CONNECTING, /* 正在连接当中 */

SS_CONNECTED, /* 已经连向一个套接字 */

SS_DISCONNECTING /* 正在断开连接 */

} socket_state;

flags也是一个标志,下面是它的取值:

#define SOCK_ASYNC_NOSPACE 0

#define SOCK_ASYNC_WAITDATA 1

#define SOCK_NOSPACE 2

#define SOCK_PASSCRED 3

ops是协议相关的一系例操作的集合,包括listen, bind, connect等常用socket操作,struct proto_ops结构体在include/linux/net.h 123行。

fasync_list是一个异步唤醒的列表,结构体struct fasync_struct在include/linux/fs.h 733行

sk是一个网络层的套接字表示,关于结构体struct sock,下文会有专门介绍。

type是套接字的类型:

enum sock_type {

SOCK_STREAM = 1, /*可靠字节流服务套接字,TCP*/

SOCK_DGRAM = 2, /*传输层数据报服务, UDP*/

SOCK_RAW = 3, /*网络层数据报服务, ICMP,

IGMP, 原始IP*/

SOCK_RDM = 4, /*可靠的数据报服务*/

SOCK_SEQPACKET = 5, /*可靠的双向记录流服务*/

SOCK_PACKET = 10, /*已废弃*/

};

暂时放一下struct sock,先来看看sys_socket的第一步创建struct socket中究竟做了些什么(描述越过了一些不是很重要的步骤):

首先,检查传入的用来标识域的协议族变量family是否在合法范围内,关于family,我们只关心其中的几个值,PF_INET表示因特网协议,PF_UNIX是unix文件系统套接字。

然后,对于(family == PF_INET && type == SOCK_PACKET )的情况,因为是已废弃的,给出警告信息。

net_families是一个数组,所有的协议族都在这个数组中注册,数组的项是一个结构体:

struct net_proto_family {

int family;

int (*create)(struct socket *sock, int

protocol);

short authentication;

short encryption;

short encrypt_net;

struct module *owner;

};

对于我们要创建的family,我们必须确保能在这个数组中找到相应的项(即内核支持该域)。

在内存中创建一个struct socket,并将其type赋值为传入的type值。

调用net_families[family]->create完成最后的创建工作。返回。

至此,一个socket就创建成功了。但还有两个问题没有明确:struct sock结构体的内容,以及net_families[family]->create如何完成对socket的创建。下一篇将结合inet域的实际例子进行分析。

6. struct sock详解

结构体sock是套接口在网络层的表示,在代码include/net/sock.h 174行定义,下面是其内容:

struct sock {

struct sock_common __sk_common;

#define sk_family __sk__family

#define sk_state __sk__state

#define sk_reuse __sk__reuse

#define sk_bound_dev_if __sk__bound_dev_if

#define sk_node __sk__node

#define sk_bind_node __sk__bind_node

#define sk_refcnt __sk__refcnt

unsigned char sk_shutdown : 2,

sk_no_check : 2,

sk_userlocks : 4;

unsigned char sk_protocol; unsigned short sk_type;

int sk_rcvbuf;

socket_lock_t sk_lock;

wait_queue_head_t *sk_sleep;

struct dst_entry *sk_dst_cache;

struct xfrm_policy *sk_policy[2];

rwlock_t sk_dst_lock;

atomic_t sk_rmem_alloc;

atomic_t sk_wmem_alloc;

atomic_t sk_omem_alloc;

struct sk_buff_head sk_receive_queue;

struct sk_buff_head sk_write_queue;

int sk_wmem_queued;

int sk_forward_alloc;

unsigned int sk_allocation;

int sk_sndbuf;

int sk_route_caps;

int sk_hashent;

unsigned long sk_flags;

unsigned long sk_lingertime;

struct {

struct sk_buff *head;

struct sk_buff *tail;

} sk_backlog;

struct sk_buff_head sk_error_queue;

struct proto *sk_prot;

struct proto *sk_prot_creator;

rwlock_t sk_callback_lock;

int sk_err,

sk_err_soft;

unsigned short sk_ack_backlog;

unsigned short sk_max_ack_backlog;

__u32 sk_priority;

struct ucred sk_peercred;

int sk_rcvlowat;

long sk_rcvtimeo;

long sk_sndtimeo;

struct sk_filter *sk_filter;

void *sk_protinfo;

struct timer_list sk_timer;

struct timeval sk_stamp;

struct socket *sk_socket;

void *sk_user_data; struct page *sk_sndmsg_page;

struct sk_buff *sk_send_head;

__u32 sk_sndmsg_off;

int sk_write_pending;

void *sk_security;

void (*sk_state_change)(struct sock *sk);

void (*sk_data_ready)(struct sock *sk, int bytes);

void (*sk_write_space)(struct sock *sk);

void (*sk_error_report)(struct sock *sk);

int (*sk_backlog_rcv)(struct sock *sk,

struct sk_buff *skb);

void (*sk_destruct)(struct sock *sk);

};

__sk_common是套接口在网络层的最小表示。下面是其定义:

struct sock_common {

unsigned short skc_family; /*地址族*/

volatile unsigned char skc_state; /*连接状态*/

unsigned char skc_reuse; /*SO_REUSEADDR设置*/

int skc_bound_dev_if;

struct hlist_node skc_node;

struct hlist_node skc_bind_node; /*哈希表相关*/

atomic_t skc_refcnt; /*引用计数*/

};

sk_shutdown是一组标志位,SEND_SHUTDOWN and/or RCV_SHUTDOWN。

sk_userlocks, SO_SNDBUF and SO_RCVBUF。

sk_rcvbuf表示接收缓冲区的字节长度。

sk_rmem_alloc表示接收队列已提交的字节数。

sk_receive_queue表示接收的数据包的队列。

sk_wmem_alloc表示发送队列已提交的字节数。

sk_write_queue表示发送数据包的队列。

sk_sndbuf表示发送缓冲区的字节长度。

sk_flags,SO_LINGER (l_onoff),SO_BROADCAST,SO_KEEPALIVE,SO_OOBINLINE。

sk_prot是指定的域内部的协议处理函数集,它是套接口层跟传输层之间的一个接口,提供诸如bind, accept, close等操作。

sk_ack_backlog表示当前的侦听队列。

sk_max_ack_backlog表示最大的侦听队列。

sk_type表示套接字的类型,如SOCK_STREAM。

sk_protocol表示在当前域中套接字所属的协议。

几个函数指针均属回调函数,分别在套接口状态变化,有数据到达需要处理,有发送空间可用,有错误等时候被回调。最后一个函数sk_destruct在套接口释放时被回调。

7. inet_create如何完成对socket的创建 前面讲到在sys_socket函数中,有一步是调用net_families[family]->create完成最后的创建工作,下面就以inet域的创建来解释这最后一步的创建工作:

1、设socket->state = SS_UNCONNECTED。

2、从数组inetsw中匹配套接字类型和协议类型。inetsw是一个链表数组,也就是说数组的每一项是一个链表,同套接字类型的在同一个链表中。比如,用户这样创建一个TCP协议的套接字:

socket( AF_INET, SOCK_STREAM, IPPROTO_TCP )

最终,内核在inetsw中匹配到的是这样一个结构体:

static struct inet_protosw inetsw_array[] =

{

{

.type = SOCK_STREAM,

.protocol = IPPROTO_TCP,

.prot = &tcp_prot,

.ops = &inet_stream_ops,

.capability = -1,

.no_check = 0,

.flags = INET_PROTOSW_PERMANENT,

}

}

这里最关键的是prot成员和ops成员,tcp_prot将提供TCP协议相关的全部操作,inet_stream_ops将提供域相关的全部操作,包括listen, connect等。

3、socket->ops = 匹配到的那个ops。

4、分配socket->sock。

5、让struct inet_sock指向socket->sock,struct inet_sock是struct

sock的超集。其头部内容即为struct sock。

6、为inet_sock和socket->sock的成员赋初始值。这里,我们可以看到一些平时我们比较关心的问题,比如:inet->mc_ttl = 1。

7、调用socket->sock->sk_prot->init(...)完成整个创建过程。

最后,调用sock_map_fd找一个空闲的文件描述符,映射到这个创建好的套接字上,将这个文件描述符返回。

描述到这里,一个最为粗糙的socket创建过程算是完成了,但留下的问题却更多了,它涉及到很多宠大的结构体,其内容与socket的正常工作都息息相关,下文会陆续给出分析。

struct sock详解

结构体sock是套接口在网络层的表示,在代码include/net/sock.h 174行定义,下面是其内容:

struct sock {

struct sock_common __sk_common;

#define sk_family __sk__family

#define sk_state __sk__state #define sk_reuse __sk__reuse

#define sk_bound_dev_if __sk__bound_dev_if

#define sk_node __sk__node

#define sk_bind_node __sk__bind_node

#define sk_refcnt __sk__refcnt

unsigned char sk_shutdown : 2,

sk_no_check : 2,

sk_userlocks :

4;

unsigned char sk_protocol;

unsigned short sk_type;

int sk_rcvbuf;

socket_lock_t sk_lock;

wait_queue_head_t *sk_sleep;

struct dst_entry *sk_dst_cache;

struct xfrm_policy *sk_policy[2];

rwlock_t sk_dst_lock;

atomic_t sk_rmem_alloc;

atomic_t sk_wmem_alloc;

atomic_t sk_omem_alloc;

struct sk_buff_head sk_receive_queue;

struct sk_buff_head sk_write_queue;

int sk_wmem_queued;

int sk_forward_alloc;

unsigned int sk_allocation;

int sk_sndbuf;

int sk_route_caps;

int sk_hashent;

unsigned long sk_flags;

unsigned long sk_lingertime;

struct {

struct sk_buff *head;

struct sk_buff *tail;

} sk_backlog;

struct sk_buff_head sk_error_queue;

struct proto *sk_prot;

struct proto *sk_prot_creator;

rwlock_t sk_callback_lock;

int sk_err,

sk_err_soft;

unsigned short sk_ack_backlog;

unsigned short sk_max_ack_backlog;

__u32 sk_priority; struct ucred sk_peercred;

int sk_rcvlowat;

long sk_rcvtimeo;

long sk_sndtimeo;

struct sk_filter *sk_filter;

void *sk_protinfo;

struct timer_list sk_timer;

struct timeval sk_stamp;

struct socket *sk_socket;

void *sk_user_data;

struct page *sk_sndmsg_page;

struct sk_buff *sk_send_head;

__u32 sk_sndmsg_off;

int sk_write_pending;

void *sk_security;

void (*sk_state_change)(struct

sock *sk);

void (*sk_data_ready)(struct

sock *sk, int bytes);

void (*sk_write_space)(struct

sock *sk);

void (*sk_error_report)(struct

sock *sk);

int (*sk_backlog_rcv)(struct

sock *sk,

struct sk_buff *skb);

void (*sk_destruct)(struct sock

*sk);

};

__sk_common是套接口在网络层的最小表示。下面是其定义:

struct sock_common {

unsigned

short skc_family; /*地址族*/

volatile unsigned char skc_state; /*连接状态*/

unsigned

char skc_reuse; /*SO_REUSEADDR设置*/

int skc_bound_dev_if;

struct hlist_node skc_node;

struct hlist_node skc_bind_node; /*哈希表相关*/

atomic_t skc_refcnt;

/*引用计数*/ };

sk_shutdown是一组标志位,SEND_SHUTDOWN and/or RCV_SHUTDOWN。

sk_userlocks, SO_SNDBUF and SO_RCVBUF。

sk_rcvbuf表示接收缓冲区的字节长度。

sk_rmem_alloc表示接收队列已提交的字节数。

sk_receive_queue表示接收的数据包的队列。

sk_wmem_alloc表示发送队列已提交的字节数。

sk_write_queue表示发送数据包的队列。

sk_sndbuf表示发送缓冲区的字节长度。

sk_flags,SO_LINGER (l_onoff),SO_BROADCAST,SO_KEEPALIVE,SO_OOBINLINE。

sk_prot是指定的域内部的协议处理函数集,它是套接口层跟传输层之间的一个接口,提供诸如bind, accept, close等操作。

sk_ack_backlog表示当前的侦听队列。

sk_max_ack_backlog表示最大的侦听队列。

sk_type表示套接字的类型,如SOCK_STREAM。

sk_protocol表示在当前域中套接字所属的协议。

几个函数指针均属回调函数,分别在套接口状态变化,有数据到达需要处理,有发送空间可用,有错误等时候被回调。最后一个函数sk_destruct在套接口释放时被回调。

8. 创建一个套接字

我们已经完成了MY_PF_INET域的初始化,虽然留了很多空,但我们至少已经具备了:TCP,

UDP, RAW三种协议;TCP, UDP, ICMP, IGMP四种基本协议;inetsw数组。有了这些,我们可以尝试着创建一个套接字试试。

关于套接字创建的执行流程,前文已有描述,其最终会进入我们的family中的创建函数:

static int myinet_create(struct socket *sock, int protocol);

套接字类型已经包含在sock结构中。MY_PF_INET域中有效的类型是SOCK_STREAM,

SOCK_DGRAM和SOCK_RAW。据此,我们定位到inetsw数组的某一项(一个链表的链表头),然后在这个链表中匹配protocol。

MY_PF_INET域中的常用的protocol是:IPPROTO_IP, IPPROTO_ICMP, IPPROTO_IGMP,

IPPROTO_TCP, IPPROTO_UDP。其中IPPROTO_IP比较特殊,是一个通配符。链表中的protocol匹配可以是严格匹配,也可以是通配符匹配,但最终 protocol必须有一个确定的值,而不能是IPPROTO_IP。因为MY_PF_INET域中inetsw数组只有三项(SOCK_STREAM,

IPPROTO_TCP), (SOCK_DGRAM, IPPROTO_UDP), (SOCK_RAW, IPPROTO_IP),所有这个匹配比较简单。

完成匹配后,我们要把匹配到的inet_protosw结构中的相关的值赋给sock。同时,创建sock的成员结构struct sock *sk,另外还涉及到一个结构体struct inet_sock的初始化,具体内容不在这里详述了。

最后,调用具体协议(TCP, UDP, RAW)的初始化函数,完成最后的创建工作。

到这里为止,我们的MY_PF_INET模块已经能够执行socket系统调用,并作出相应的动作,返回正确的值了。我们在三种协议的初始化函数中分别加入调试输出语句,并观察其行为(udp没有提供init函数)。 基于前面的积累,我们再来仔细分析socket这个系统调用:

#include

#include

int socket(int domain, int type, int protocol);

三个参数中,domain指定了一个套接字域,它会影响到内核具体选择使用哪一个模块。比如,在我们的测试程序中,可以使用MY_PF_INET(30) 使内核使用我们的my_模块,建立因特网协议域。而type表示域中的套接字类型,对应inetsw数组的一项,在我们的 MY_PF_INET域中,其有效的取值是SOCK_STREAM, SOCK_DGRAM和SOCK_RAW。最后一个参数protocol,指定具体的协议类型,它可以使用通配符IPPROTO_IP匹配对应类型的链表中的第一项。下面的测试代码:

int fd0 = socket( MY_PF_INET, SOCK_RAW, MY_IPPROTO_ICMP );

int fd1 = socket( MY_PF_INET, SOCK_RAW, MY_IPPROTO_IGMP );

int fd2 = socket( MY_PF_INET, SOCK_RAW, IPPROTO_IP );

int fd3 = socket( MY_PF_INET, SOCK_DGRAM, MY_IPPROTO_UDP );

int fd4 = socket( MY_PF_INET, SOCK_DGRAM, IPPROTO_IP );

int fd5 = socket( MY_PF_INET, SOCK_STREAM, MY_IPPROTO_TCP );

int fd6 = socket( MY_PF_INET, SOCK_STREAM, IPPROTO_IP );

printf("fd: %d, %d, %d, %d, %d, %d, %dn", fd0, fd1, fd2, fd3, fd4, fd5, fd6);

显然,SOCK_RAW是没有办法通配的。结果就很明显:

fd: 3, 4, -1, 5, 6, 7, 8

我们再来关注一下socket系统调用的一些主要出错情况。

第一种:所选的域不支持。domain在内核中的最大取值是31。共有32个值可选(包括四个空值,和一个保留值)。这个错误在__sock_create中就会被检测出。

第二种:套接字类型与协议类型不匹配。比如SOCK_DGRAM搭配IPPROTO_TCP,肯定会失败。

其余的出错类型,一般为系统错误,不必关注,实际编程中,只要判断返回的fd是否大于零即可。

9. 销毁一个套接字(一)

要使我们的工作得以顺利进行,我们必须把建立与销毁,注册与注销等配对的工作放在一起完成,才能使模块始终处于一个可使用的状态。所以,完成了套接字的创建,我们下一步紧接着面临的工作就是套接字的销毁。

在系统调用层,套接字也是一个文件描述符来表示,所以,关闭套接字跟关闭打开着的文件并没有区别,都是使用close(fd),但同样一个操作,在内核中却发生着很不一样的操作。

我们还需要从socket系统调用创建套接字讲起,当代表套接字的一个struct socket结构被完整创建出来以后,它被映射到一个文件描述符,并且系统把这个文件描述符返回给用户。现在,我们就需要简单了解这个映射过程是怎么样的。

因为我们的目标是重新建立一个INET域的代码,所以不想在关于文件系统的方面走太远,所以只进行简单介绍。我们首先要从系统获取一个未使用的文件描述符,并创建一个struct file结构。同时,我们初始化这个struct file结构,整个初始化过程我们只需要关注其中两步:

sock->file = file;

file->f_op = SOCK_INODE(sock)->i_fop = &socket_file_ops; 在这个file结构的f_op成员中,我们记录了能对该文件(因为,此时,socket提供一组跟普通文件完全一致的操作 socket_file_ops,所以,能够被看成是一个文件)进行的所有操作。其中,包括一个release操作,这在我们销毁一个套接字时非常有用。

最后,我们把文件描述符fd和结构file安装到进程的描述符中。进程的描述符中有一个file_struct结构成员,它是一个进程所拥有的打开的文件的列表。其中有一个成员struct file

**fd,我们使fd[fd] = file。这样,就完成了文件描述符到socket的映射,即我们通过current->files->fd[fd]能找到file,通过 sock->file也能找到file。其实,它们之间还有其它关系。

有了这层关系,我们在close(fd)时,便能通过fd轻松调用到socket_file_e(...)函数,最终完成销毁套接字的工作,有关具体的销毁流程,下回分解。

10. 销毁一个套接字(二)

我们先来简单看一下系统调用close(int fd)的流程。该系统调用会调用到内核中的函数:

asmlinkage long sys_close(unsigned int fd)

参数fd给我们一个很好的线索,我们可以很方便地找到相应的struct file结构:file =

current->files->fd[fd]。取出了这个至关重要的数据结构后,我们归还fd给系统,同时,设current-> file->fd[fd]=NULL。使我们创建的socket完全跟系统和进程分离。最后再销毁struct

file结构。

销毁struct file的很多细节我们不关注,但在某一步,一个叫__fput(struct file *file)的函数中,有这样一个调用:

file->f_op->release(inode, file);

它实际调用到了sock_close函数,该函数又会调用到sock_release函数。sock_release函数又调用我们my_inet模块提供的myinet_release函数完成实际的socket销毁工作,同时,释放inode。

上面讲述的是一个大致的流程,我们重点关注的还是如何在我们的my_inet模块中实现套接字的销毁。下面先看一下myinet_release函数的实现:

int myinet_release(struct socket *sock)

{

struct sock *sk = sock->sk;

if (sk) {

long timeout = 0;

myip_mc_drop_socket(sk);

if (sock_flag(sk, SOCK_LINGER) &&

!(current->flags & PF_EXITING))

timeout = sk->sk_lingertime;

sock->sk = NULL;

sk->sk_prot->close(sk, timeout);

}

return 0;

}

可见,在我们的模块中,我们关注的其实是sock->sk的销毁,它是网络层一个套接字表示。myip_mc_drop_socket是离开组播组(如果曾加入过组播组的话),我们目前不关注。关于timeout我们也暂时不关心。

这里调用到了我们对应的协议的close函数,暂时,我们不去关注很多细节,只要记得在这个函数中应该调用sk_common_release最后完成对sock->sk的资源释放即可。

11. 套接字选项(一)

套接字选项这个话题在socket编程里,可能已经属于中高级话题了,之所以在一开始就把这个话题提上来讲,是因为我们的一个近阶段目标是能够把 MY_PF_INET域的RAW协议走通,并在上面跑起一个ping程序,所以,按照ping程序的要求,接下来,我们必须实现套接字选项系统调用 setsockopt在MY_PF_INET中RAW协议中的相关实现。

下面是该系统调用函数的原型:

#include

int setsockopt( int socket, int level, int option_name,

const void *option_value, size_t option_len);

第一个参数socket是套接字描述符。第二个参数level是被设置的选项的级别,如果想要在套接字级别上设置选项,就必须把level设置为 SOL_SOCKET。option_name指定准备设置的选项,option_name可以有哪些取值,这取决于level,以linux 2.6内核为例(在不同的平台上,这种关系可能会有不同),在套接字级别上(SOL_SOCKET),option_name可以有以下取值:

SO_DEBUG,打开或关闭调试信息。

当option_value不等于0时,打开调试信息,否则,关闭调试信息。它实际所做的工作是在sock->sk->sk_flag中置SOCK_DBG(第10)位,或清SOCK_DBG位。

SO_REUSEADDR,打开或关闭地址复用功能。

当option_value不等于0时,打开,否则,关闭。它实际所做的工作是置sock->sk->sk_reuse为1或0。

SO_DONTROUTE,打开或关闭路由查找功能。

当option_value不等于0时,打开,否则,关闭。它实际所做的工作是在sock->sk->sk_flag中置或清SOCK_LOCALROUTE位。

SO_BROADCAST,允许或禁止发送广播数据。

当option_value不等于0时,允许,否则,禁止。它实际所做的工作是在sock->sk->sk_flag中置或清SOCK_BROADCAST位。

SO_SNDBUF,设置发送缓冲区的大小。

发送缓冲区的大小是有上下限的,其上限为256 * (sizeof(struct sk_buff) + 256),下限为2048字节。该操作将sock->sk->sk_sndbuf设置为val * 2,之所以要乘以2,是防止大数据量的发送,突然导致缓冲区溢出。最后,该操作完成后,因为对发送缓冲的大小作了改变,要检查sleep队列,如果有进程正在等待写,将它们唤醒。

SO_RCVBUF,设置接收缓冲区的大小。

接收缓冲区大小的上下限分别是:256 * (sizeof(struct sk_buff) + 256)和256字节。该操作将sock->sk->sk_rcvbuf设置为val * 2。

12. 套接字选项(二)

(接上文)

SO_KEEPALIVE,套接字保活。

如果协议是TCP,并且当前的套接字状态不是侦听(listen)或关闭(close),那么,当option_value不是零时,启用TCP保活定时器,否则关闭保活定时器。对于所有协议,该操作都会根据option_value置或清sock->sk->sk_flag中的 SOCK_KEEPOPEN位。

SO_OOBINLINE,紧急数据放入普通数据流。

该操作根据option_value的值置或清sock->sk->sk_flag中的SOCK_URGINLINE位。

SO_NO_CHECK,打开或关闭校验和。

该操作根据option_value的值,设置sock->sk->sk_no_check。

SO_PRIORITY,设置在套接字发送的所有包的协议定义优先权。Linux通过这一值来排列网络队列。

这个值在0到6之间(包括0和6),由option_value指定。赋给sock->sk->sk_priority。

SO_LINGER,如果选择此选项, close或 shutdown将等到所有套接字里排队的消息成功发送或到达延迟时间后>才会返回. 否则, 调用将立即返回。

该选项的参数(option_value)是一个linger结构:

struct linger {

int l_onoff; /* 延时状态(打开/关闭) */

int l_linger; /* 延时多长时间 */

};

如果linger.l_onoff值为0(关闭),则清sock->sk->sk_flag中的SOCK_LINGER位;否则,置该位,并赋sk->sk_lingertime值为linger.l_linger。

SO_PASSCRED,允许或禁止SCM_CREDENTIALS 控制消息的接收。

该选项根据option_value的值,清或置sock->sk->sk_flag中的SOCK_PASSCRED位。

SO_TIMESTAMP,打开或关闭数据报中的时间戳接收。

该选项根据option_value的值,清或置sock->sk->sk_flag中的SOCK_RCVTSTAMP位,如果打开,则还需设sock->sk->sk_flag中的SOCK_TIMESTAMP位,同时,将全局变量netstamp_needed加1。

SO_RCVLOWAT,设置接收数据前的缓冲区内的最小字节数。

在Linux中,缓冲区内的最小字节数是固定的,为1。即将sock->sk->sk_rcvlowat固定赋值为1。

SO_RCVTIMEO,设置接收超时时间。

该选项最终将接收超时时间赋给sock->sk->sk_rcvtimeo。

SO_SNDTIMEO,设置发送超时时间。

该选项最终将发送超时时间赋给sock->sk->sk_sndtimeo。

SO_BINDTODEVICE,将套接字绑定到一个特定的设备上。

该选项最终将设备赋给sock->sk->sk_bound_dev_if。

SO_ATTACH_FILTER和SO_DETACH_FILTER。

关于数据包过滤,它们最终会影响sk->sk_filter。

以上所介绍的都是在SOL_SOCKET层的一些套接字选项,如果超出这个范围,给出一些不在这一level的选项作为参数,最终会得到- ENOPROTOOPT的返回值。但以上的分析仅限于这些选项对sock-sk的值的影响,这些选项真正如何发挥作用,我们的探索道路将漫漫其修远。

13. 套接字选项(三)

如果不在套接字级别上设置选项,即setsockopt系统调用的参数level不设为SOL_SOCKET,那么sys_setsockopt的实现会直接调用sock->ops->setsockopt。对MY_PF_INET域的RAW协议来讲,sock->ops = myinet_sockraw_ops,而myinet_sockraw_kopt = sock_common_setsockopt。 而sock_common_setsockopt直接调用sock->sk->sk_prot->setsockopt。对于RAW协议来讲,即myraw_setsockopt。

下面关注myraw_setsockopt的实现。对于RAW协议来讲,level还可以有两种取值:SOL_IP和SOL_RAW。 myraw_setsockopt首先检查level是否为SOL_IP,如果是,调用myip_setsockopt函数,该函数实现IP级别上的选项,否则,为SOL_RAW级别上的选项,SOL_RAW级别上只有一个选项,即ICMP_FILTER,在MY_IPPROTO_ICMP协议下有效。它激活绑定到MY_IPPROTO_ICMP协议的一个用于myraw socket特殊的过滤器。该值对每种ICMP消息都有一个位(掩码),可以把那种ICMP消息过滤掉,缺省时是不过滤ICMP消息。

对于ICMP_FILTER选项,myraw_setsockopt调用myraw_seticmpfilter函数,它把option_value赋给 sock->sk->filter,option_value是一个结构体:

struct icmp_filter {

__u32 data;

};

它是一个32位的位掩码。

关于该位掩码,我们目前知道的是最低位为回显应答的位掩码,由于目前我们的MY_PF_INET域代码还没完善,我们在PF_INET域上进行测试,把下面的代码添加到一个ping程序中,ping程序就收不到来自服务器的回应包了:

#include

#include

#include

#include

#include

int main()

{

struct icmp_filter filter;

socklen_t size = sizeof( struct icmp_filter );

int fd = socket( PF_INET, SOCK_RAW, IPPROTO_ICMP );

if( fd < 0 )

perror("error: ");

getsockopt( fd, SOL_RAW, ICMP_FILTER, &filter, &size );

printf("the filter: %xn", );

= 1;

int err = setsockopt( fd, SOL_RAW, ICMP_FILTER, &filter, sizeof(struct icmp_filter) );

if( err < 0 )

perror("error: ");

memset( &filter, 0, sizeof( struct icmp_filter ) );

getsockopt( fd, SOL_RAW, ICMP_FILTER, &filter, &size );

printf("new filter: %xn", );

close(fd);

return 0;

}

14. 套接字选项(四)

继续讲关于myraw_setsockopt的实现,如果level是SOL_IP,则调用myip_setsockopt函数。

myip_setsockopt的操作对像是struct socket sock的成员struct sock sk。并把sk强制转化为struct inet_sock: inet = inet_sk(sk)。

如果option_name在MRT_BASE和MRT_BASE+10之间,则调用myip_mroute_setsockopt函数,关于mroute,后面再给出分析。

IP_OPTIONS:设置将由该套接字发送的每个包的IP选项。

其option_value是一个结构体struct ip_options。该选项首先分配一个这样的结构体,然后用这个结构体替代inet->opt指向的结构体。如果协议类型是 SOCK_STREAM的话,从struct

tcp_sock *tp中,tp->ext_header_len减去旧的inet->opt->optlen, 再加上新的opt->optlen。最后调用tcp_sync_mss进行同步,有关TCP的一些细节,我们在实现TCP协议时再分析。

IP_PKTINFO:传递一条包含pktinfo结构(该结构提供一些来访包的相关信息)的IP_PKTINFO辅助信息。

这个选项只对数据报类的套接字有效。

struct in_pktinfo

{

unsigned int ipi_ifindex; /* 接口索引 */

struct in_addr ipi_spec_dst; /* 路由目的地址 */

struct in_addr ipi_addr; /* 头标识目的地址 */

};

ipi_ifindex指的是接收包的接口的唯一索引。ipi_spec_dst指的是路由表记录中的目的地址,而ipi_addr 指的是包头中的目的地址。如果给 sendmsg传递了IP_PKTINFO,那么外发的包会通过在ipi_ifindex中指定的接口发送出去,同时把ipi_spec_dst设置为目的地址。

myip_setsockopt的代码实现中只是根据option_value是否为0,置或清inet->cmsg_flags的IP_CMSG_PKTINFO位。

IP_RECVTTL:

该选项根据option_value的值是否为0,置或清inet->cmsg_flags的IP_CMSG_TTL位,具体用途,留待日后分析。

IP_RECVTOS:

如果打开了这个选项,则IP_TOS辅助信息会与来访包一起传递。它包含一个字节用来指定包头中的服务/优先>级字段的类型。该字节为一个布尔整型标识。该选项根据option_value的值是否为0,置或清inet->cmsg_flags的IP_CMSG_TOS位。

IP_RECVOPTS:

用一条IP_OPTIONS控制信息传递所有来访的IP选项给用户。路由头标识和其它选项已经为本地主机填好.此选项不支持SOCK_STREAM套接字。该选项根据option_value的值是否为0,置或清inet->cmsg_flags的IP_CMSG_RECVOPTS位。

IP_RETOPTS:

等同于IP_RECVOPTS但是返回的是带有时间戳的未处理的原始选项和在这段路由中未填入的路由记录项目。该>选项根据 option_value的值是否为0,置或清inet->cmsg_flags的IP_CMSG_RETOPTS位。 IP_TOS:

设置源于该套接字的每个IP包的Type-Of-Service(TOS 服务类型)字段。它被用来在网络上区分包的优先级>。TOS是单字节的字段。定义了一些的标准TOS标识:IPTOS_LOWDELAY用来为交互式通信最小化延迟时间,IPTOS_THROUGHPUT用来优化吞吐量,IPTOS_RELIABILITY用来作可靠性优化, IPTOS_MINCOST应该被用作“填充数据”,对于这些数据,低速传输是无关紧要的。至多只能声明这些 TOS 值中的一个,其它的都是无效的,应当被清除。缺省时,Linux首先发送IPTOS_LOWDELAY数据报,但是确切的做法要看配置的排队规则而定。一些高优先级的层次可能会要求一个有效的用户标识0或者CAP_NET_ADMIN能力。优先级也可以以于协议无关的方式通过( SOL_SOCKET,

SO_PRIORITY )套接字选项来设置。

该选项的操作置inet->tos = val,sk->sk_priority = rt_tos2priority(val),同时,清sk->sk_dst_cache。

IP_TTL:设置从此套接字发出的包的当前生存时间字段。

该选项置inet->uc_ttl = option_value。

IP_HDRINCL:

该选项只对SOCK_RAW有效,如果提供的话,用户可在用户数据前面提供一个ip头。该选项的操作根据option_value是否为零,置inet->hdrincl为1或0。

IP_MTU_DISCOVER:

为套接字设置Path MTU Discovery setting(路径MTU发现设置)。该选项的操作置inet->pmtudisc = option_value,option_value只允许取值0,1,2。

IP_SOL层上余下的选项还有:

IP_RECVERR,IP_MULTICAST_TTL,IP_MULTICAST_LOOP,IP_MULTICAST_IF,

IP_ADD_MEMBERSHIP,IP_DROP_MEMBERSHIP,IP_MSFILTER,IP_BLOCK_SOURCE,

IP_UNBLOCK_SOURCE,IP_ADD_SOURCE_MEMBERSHIP,IP_DROP_SOURCE_MEMBERSHIP, MCAST_JOIN_GROUP,MCAST_LEAVE_GROUP,MCAST_JOIN_SOURCE_GROUP, MCAST_LEAVE_SOURCE_GROUP,MCAST_BLOCK_SOURCE,MCAST_UNBLOCK_SOURCE, MCAST_MSFILTER,IP_ROUTER_ALERT,IP_FREEBIND,IP_IPSEC_POLICY, IP_XFRM_POLICY。

在涉及到相关内容时,再进行一一分析。

15.向套接字写数据

套接字写有多个实现接口,我们只以其中一个接口write为线索,对套接字写(网络数据发送)的流程进行分析。系统调用write会调用内核函数sys_write,sys_write调用vfs_write完成实际的写操作。

vfs_write会先调用file->f_op->write(file从套接字描述符获得)。如果file->f_op-> write不存在,则调用do_sync_write。该函数会调用sock_aio_write,sock_aio_write又会调用

__sock_sendmsg,然后到myinet_sendmsg,最后才到sk->sk_prot->sendmsg,对于RAW协议来讲,即myraw_sendmsg。

sock_aio_write的函数原型如下:

static ssize_t sock_aio_write(struct kiocb *iocb, const char __user *ubuf,

size_t size, loff_t pos)

ubuf是用户待发送数据,size是数据长度,pos是文件位置(永远为零)。在这个函数里,会把用户待发送数据封装成一个struct msghdr结构:

struct msghdr { void * msg_name; /* Socket name */

int msg_namelen; /* Length of name */

struct iovec * msg_iov; /* Data blocks */

__kernel_size_t msg_iovlen; /* Number of blocks */

void * msg_control; /* Per protocol magic (eg BSD file descriptor passing) */

__kernel_size_t msg_controllen; /* Length of cmsg list */

unsigned msg_flags;

};

如果用户代码为: write(fd, "abcdef", 6 ),则在sock_aio_write中封装成的msghdr结构为:

struct msghdr thehdr{

.msg_name = NULL,

.msg_namelen = 0,

.msg__base = "abcdef",

.msg__len = 6,

.msg_iovlen = 1,

.msg_control = NULL,

.msg_controllen = 0,

.msg_flags = 0

};

myraw_sendmsg的函数原型为:

static int myraw_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,

size_t len)

所以,它拿到的是已经封装好的消息msg。该函数所做的第一件事情是检查len,其最大长度是16位(0xffff),然后,确认msg->msg_flags中没有MSG_OOB(RAW不支持带外数据的发送)。

如果msg->msg_namelen不等于零,则name中存储的是域和目的地址的信息,如果等于零,则当前必须是已经建立了TCP连接的,否则数据不知道发往哪儿。

接下来,查看控制数据缓冲区长度是否为零,如果不是,则有控制信息msg->msg_control,调用ip_cmsg_send发送控制信息(实际上,主要是填充一个结构体struct ipcm_cookie ipc,从代码来看,该结构应该用于构建ip头)。

inet->hdrincl表示需要自己来构建ip头,所以如果inet->hdrincl==1,并且,ipc->opt!=NULL则返回出错信息:无效参数。

接下来判断目的地址是否为组播地址(组播地址的最高四位为1110),是则作相应处理。

接下来,声明并初始化一个struct flowi结构,如果不是自己构建ip头,则调用raw_probe_proto_opt

接下来的内容,暂时未能很好理解,留待下文分析。

16. 操作网络设备的几个命令

一般,用户在shell中使用ifconfig命令对网络接口进行参数配置,及接口的打开,关闭等操作。ifconfig实现网络接口配置的原理在于代表网络接口的结构体struct net_device的成员ip_ptr。前文已经讲过,ip_ptr实际指向的是一个结构体struct in_device,in_device有一个成员struct in_ifaddr *ifa_list,它指向一个链表,链表的每一项代表一个IP地址。对这个链表操作即可实现对网络接口的配置。

网络接口的操作命令按功能可以分为两组,第一组为查询命令:SIOCGIFADDR,SIOCGIFBRDADDR,SIOCGIFDSTADDR, SIOCGIFNETMASK。分别用于查询网络接口的IP地址,广播地址,目的地址,子网掩码。第二组为设置命令:SIOCSIFADDR,

SIOCSIFFLAGS,SIOCSIFBRDADDR,SIOCSIFNETMASK,SIOCSIFDSTADDR。分别用于设置网络接口的IP地址,标志位,广播地址,子网掩码,目的地址。这些命令所要查询和设置的信息全部在结构体struct in_ifaddr中。

用户空间的应用程序通过系统调用ioctl使用这些命令,ioctl的函数原型如下:

#include

int ioctl(int d, int request, ...);

以上九个命令使用的参数为同一类型,即struct ifreq,其定义可在include/linux/if.h中找到。

下面是两组命令在my_inet模块中的使用示例,因为整个my_inet模块代码还很不完善,通过my_inet模块新添加的IP地址并不能正常使用。

#include

#include

#include

#include

#include "my_inet.h"

#include

#include

#include

int main()

{

struct ifreq req;

strncpy( _name, "eth0", IFNAMSIZ );

struct sockaddr_in *sin;

int fd = socket( MY_PF_INET, SOCK_RAW, MY_IPPROTO_ICMP );

if( fd < 0 ){

perror("error: ");

return -1;

}

sin = (struct sockaddr_in *)&_addr;

if( ioctl( fd, SIOCGIFADDR, &req) == 0 )

printf("%sn", inet_ntoa(sin->sin_addr.s_addr) );

else

perror("ioctl error: ");

sin = (struct sockaddr_in *)&_broadaddr;

if( ioctl( fd, SIOCGIFBRDADDR, &req) == 0 )

printf("%sn", inet_ntoa(sin->sin_addr.s_addr) );

else

perror("ioctl error: "); sin = (struct sockaddr_in *)&_dstaddr;

if( ioctl( fd, SIOCGIFDSTADDR, &req) == 0 )

printf("%sn", inet_ntoa(sin->sin_addr.s_addr) );

else

perror("ioctl error: ");

sin = (struct sockaddr_in *)&_netmask;

if( ioctl( fd, SIOCGIFNETMASK, &req) == 0 )

printf("%sn", inet_ntoa(sin->sin_addr.s_addr) );

else

perror("ioctl error: ");

close( fd );

return 0;

}

#include

#include

#include

#include

#include "my_inet.h"

#include

#include

#include

int main()

{

struct ifreq req;

strncpy( _name, "eth0:0", IFNAMSIZ );

struct sockaddr_in *sin;

int fd = socket( MY_PF_INET, SOCK_RAW, MY_IPPROTO_ICMP );

if( fd < 0 ){

perror("error: ");

return -1;

}

sin = (struct sockaddr_in *)&_addr;

sin->sin_family = MY_PF_INET;

if( inet_aton("172.16.48.10", &sin->sin_addr) == 0 ){

perror("inet_aton error: ");

return -1;

} if( ioctl( fd, SIOCSIFADDR, &req ) == 0 ){

printf("success!n");

}else{

perror("ioctl failed: ");

}

_flags = IFF_UP;

if( ioctl( fd, SIOCSIFFLAGS, &req ) == 0 ){

printf("success!n");

}else{

perror("ioctl failed: ");

}

sin->sin_family = MY_PF_INET;

if( inet_aton("172.16.48.255", &sin->sin_addr) == 0 ){

perror("inet_aton error: ");

return -1;

}

if( ioctl( fd, SIOCSIFBRDADDR, &req ) == 0 ){

printf("success!n");

}else{

perror("ioctl failed: ");

}

sin->sin_family = MY_PF_INET;

if( inet_aton("255.255.255.0", &sin->sin_addr) == 0 ){

perror("inet_aton error: ");

return -1;

}

if( ioctl( fd, SIOCSIFNETMASK, &req ) == 0 ){

printf("success!n");

}else{

perror("ioctl failed: ");

}

close( fd );

return 0;

}

要想让上述的代码发挥实际的作用,只要把所有的MY_PF_INET改成PF_INET即可。功能再作增强,就是一个ifconfig程序了。

17. 加入对raw socket(原始套接字)类型的支持

变量inetsw_array是inet域的一个全局数组,其类型是struct inet_protosw,该结构体的定义如下: struct inet_protosw {

struct list_head list;

unsigned short type;

int protocol;

struct proto *prot;

const struct proto_ops *ops;

int capability;

char no_check;

unsigned char flags;

};

type是指套接字的类型,也就是系统调用socket的第二个参数,inet域支持的套接字类型有SOCK_STREAM(流套接字),它是一个有序的,可靠的,基于连接的双向字节流;SOCK_DGRAM(数据报套接字),该类型的套接字是不可靠的,无连接的,有可能乱序的;SOCK_RAW(原始套接字),所谓原始,是因为该类型的套接字不提供传输层的服务,协议栈只为该类型的套接字自动添加网络层首部,常用于网络层的附属协议(icmp, igmp等)。

protocol是一个传输层协议号,传输层的协议包括IPPROTO_TCP,IPPROTO_UDP;或者对于SOCK_RAW来讲,它是一个通配协议号IPPROTO_IP,用于通配网络层的附属协议icmp,igmp等。对于传输层协议来讲,IPPROTO_TCP对应的套接字类型总是SOCK_STREAM,IPPRTO_UDP对应的套接字类型总是STREAM_DGRAM,所以在socket系统调用时,可以不必指定协议号,而直接使用通配符IPPROTO_IP。

prot是一个传输层协议绑定的操作集,比如对于IPPROTO_TCP,它就是tcp_prot,对于IPPROTO_UDP,它就是udp_prot。而对于类型为SOCK_RAW的套接字,它没有相应的传输层协议,而是用于通配所有的网络层附属协议,所以,prot就是所有网络层附属协议共用的一个操作集raw_prot。

ops是套接字类型绑定的操作集,对应于SOCK_STREAM, SOCK_DGRAM, SOCK_RAW,操作集分别为inet_stream_ops,inet_dgram_ops,inet_sockraw_ops。

capability是操作这类别套接字所需要的权限,除了原始套接字需要CAP_NET_RAW权限之外,其它两类套接字不需要特殊权限(-1)。

flags的可能取值如下:

#define INET_PROTOSW_REUSE 0x01 /* Are ports automatically reusable? */

#define INET_PROTOSW_PERMANENT 0x02 /* Permanent protocols are unremovable. */

#define INET_PROTOSW_ICSK 0x04 /* Is this an inet_connection_sock? */

inetsw是一个链表数组,每一项都是一个struct inet_protosw结构体的链表,总共有SOCK_MAX项,在inet_init函数对INET域进行初始化的时候,调用函数inet_register_protosw把数组inetsw_array中定义的套接字类型全部注册到inetsw数组中,相同套接字类型,不同协议类型的在数组的同一项,以套接字类型为索引,在系统实际使用的时候,只使用inetsw,而不使用inetsw_array,目前inet域不存在相同套接字类型的多个协议(原始套接字使用通配符,所以也不存在这个问题)。

使用系统调用socket创建一个RAW类型的套接字,并且网络层附属协议为icmp的时候,首先会在函数__sock_create中创建一个传输层的struct socket,如下:

struct socket {

socket_state state; unsigned long flags;

const struct proto_ops *ops;

struct fasync_struct *fasync_list;

struct file *file;

struct sock *sk;

wait_queue_head_t wait;

short type = SOCK_RAW;

};

这里需要补充一下关于协议域的问题,Linux内核支持多个协议域,除了当前正关心的INET域之外,还有UNIXF域,IPX协议等等,一个域包含一个协议族。完成一个相对独立的完整的网络通讯能力。每个域都有一个域号,比如INET域的域号为AF_INET(2),所有的已注册域号都包含在一个全局数组net_families中,net_families是类型为struct

net_proto_family的数组,该结构体的定义如下:

struct net_proto_family{

int family;

int (*create)(struct socket *sock, int protocol);

short authentication;

short encryption;

short encrypt_net;

struct module *owner;

};

INET域就在net_families数组的第三项(下标为2),其结构体定义如下:

static struct net_proto_family inet_family_ops = {

.family = PF_INET,

.create = inet_create,

.owner = THIS_MODULE,

};

通过函数sock_register注册到数组中,因为socket系统调用的第一个参数指定了协议域号,所以__sock_create可以通过net_families[2]->create调用到INET域的创建函数,完成进一步的socket创建工作。

inet_create函数通过套接字类型socket->type找到inetsw[SOCK_RAW],并从该链表(其实只有一项)的头部开始匹配网络层附属协议号,因为SOCK_RAW类型的套接字采用通配符,所以匹配成功。同时,还需要检查权限,inet_create函数进一步完善了struct socket的内容:

struct socket {

socket_state state = SS_UNCONNECTED;

unsigned long flags = INET_PROTOSW_REUSE;

const struct proto_ops *ops = &inet_sockraw_ops;

struct fasync_struct *fasync_list;

struct file *file;

struct sock *sk;

wait_queue_head_t wait;

short type = SOCK_RAW;

};

struct socket是传输层的套接字,它只看到套接字类型这一层次,所以其操作函数是inet_sockraw_ops。inet_create还为其成员sk创建了一个网络层的套接字,其内容如下(只列出本文内容相关的部分):

struct sock{

.sk_family = AF_INET;

.sk_prot = &raw_prot;

.sk_prot_creator = &raw_prot;

.sk_reuse = 1;

.sk_protocol = IPPROTO_ICMP;

... ...

}

这样,在一个套接字上作一个操作,比如说send,会先调用struct socket->ops->sendmsg(),该函数会调用struct socket->sk->sk_prot->sendmsg完成实际的发送工作。

综上所述,现在要加入对原始套接(SOCK_RAW)的支持,首先要在数组inetsw_array中提供一个原始套接字类型的struct inet_protosw,在INET域初始化的时候会被加入到inetsw数组中,同时提供一个原始套接字上的所有网络层附属协议的通用操作集raw_prot和原始套接字类型的操作集inet_sockraw_ops,并实现它们的全部函数即可。中间涉及到具体的协议,还会有一些协议相关的支持问题要处理。

18. 注册协议操作集

前面讲到,在数组inetsw_array中有INET域支持的全部套接字类型和协议类型,在一个结构体struct inet_protosw中,记录了一个套接字类型,该类型下的一种协议类型,以及该类型套接字的操作集和该类型协议的操作集,对于原始套接字,其协议类型为通配类型,通配所有网络层附属协议,其协议操作集为所有网络层附属协议的通过协议操作集,即struct

proto raw_prot。在INET域进行初始化的时候,其要做的第一件事情就是通过函数proto_register注册协议操作集。

struct proto的成员slab是一个后备高速缓冲区,所有该类型下的网络层套接字struct sock都是从该后备高速缓冲区分配内存,struct proto的成员obj_size记录的是单个网络层套接字的空间大小,供分配slab时使用,其值是sizeof(struct raw_sock),raw_sock是对inet_sock的一个扩充(inet_sock是对sock的扩充)。在inet_create创建套接字的时候,调用sk_alloc创建struct sock,该函数直接从slab中分配内存。proto_register要做的第一件事情就是创建slab,struct proto还有成员rsk_prot和twsk_prot需要分配内存,也要在proto_register函数中完成,它是TCP协议相关的,暂时不关注。

struct proto还有一个成员node,供proto_register把它插入到一个链表proto_list中。所有的协议操作集组织成一个链表放在一起。

对原始套接字来讲,协议操作集中剩下的内容就是所有的操作函数了。

struct inet_sock是INET域对struct sock的扩充。对于原始套接字类型,inet_sock的成员num置为协议类型,而struct raw_sock是对inet_sock的扩充,是完整意义上的原始套接字,定义如下:

struct raw_sock {

struct inet_sock inet;

struct icmp_filter filter;

};

struct proto raw_prot的raw_init函数判断如果inet_sock->num的值是IPPROTO_ICMP,则为filter清零。 所有由应用程序创建出来的原始套接字需要一个统一的管理(内核创建的原始套接字不需要这种统一管理),raw_v4_htable就是这样一个实现统一管理的哈希表,它共有256项,所有的原始套接字,以它们的协议号(IPPROTO_ICMP,IPPROTO_IGMP等)作为索引,被创建后,都自动放到这个哈希表中,被放到哈项表同一项的套接字以链表的形式组织在一起。struct proto raw_prot的成员函数raw_v4_hash, raw_v4_unhash就是完成将套接字插入到哈希表中,和把套接字从哈希表中删除的工作。

以上三个成员函数是在创建原始套接字时就会被调用到的,接下来还有其它的成员函数,后续会有陆续的分析介绍。

19.回顾

首先以socket和send两个系统调用为例,来回顾一下协议栈是如何工作的,在这过程中可以找到如何在协议栈中增加对UDP协议的支持。socket系统调用的原型是

int socket(int domain, int type, int protocol);

domain是协议域,对于ipv4协议来说,其值是PF_INET(ipv4因特网协议),对于我们自己实现的ipv4协议模块,我们为其新增MY_PF_INET。所有的协议域在include/linux/socket.h被定义,如下:

#define AF_UNSPEC 0

#define AF_UNIX 1 // Unix域的socket

#define AF_LOCAL 1 // AF_UNIX的POSIX命名

#define AF_INET 2 // 因特网IP协议

#define AF_AX25 3 // Amateur Radio AX.25

#define AF_IPX 4 // Novell IPX

#define AF_APPLETALK 5 // AppleTalk DDP

#define AF_NETROM 6 // Amateur Radio NET/ROM

#define AF_BRIDGE 7 // Multiprotocol bridge

#define AF_ATMPVC 8 // ATM PVCs

#define AF_X25 9 // Reserved for X.25 project

#define AF_INET6 10 // IP version 6

#define AF_ROSE 11 // Amateur Radio X.25 PLP

#define AF_DECnet 12 // Reserved for DECnet project

#define AF_NETBEUI 13 // Reserved for 802.2LLC project

#define AF_SECURITY 14 // Security callback pseudo AF

#define AF_KEY 15 // PF_KEY key management API

#define AF_NETLINK 16

#define AF_ROUTE AF_NETLINK // Alias to emulate 4.4BSD

#define AF_PACKET 17 // Packet family

#define AF_ASH 18 // Ash

#define AF_ECONET 19 // Acorn Econet

#define AF_ATMSVC 20 // ATM SVCs

#define AF_SNA 22 // Linux SNA Project (nutters!)

#define AF_IRDA 23 // IRDA sockets

#define AF_PPPOX 24 // PPPoX sockets

#define AF_WANPIPE 25 // Wanpipe API Sockets

#define AF_LLC 26 // Linux LLC #define AF_BLUETOOTH 31 // Bluetooth sockets

#define AF_MAX 32 // For now..

可以看到,当前,内核最多支持31个协议域(0为未指定,32为MAX)。而当前的定义中还有27,28,30为空,所以我们定义了MY_PF_INET为28。

在内核中,结构体struct net_proto_family用于表示一个协议域,而全局数组变量static struct net_proto_family *net_families[NPROTO]是一个有32项的数组,用于保存当前内核中所有已注册的协议域,函数sock_register用于把一个协议域注册到内核中,即把一个协议域跟net_families数组

中的某一项相关联。struct net_proto_family的完整定义如下:

struct net_proto_family {

int family;

int (*create)(struct socket *sock, int protocol);

short authentication;

short encryption;

short encrypt_net;

struct module *owner;

};

其中,family为域编号,对于我们的模块即为MY_PF_INET。通过sock_register函数,使net_families[MY_PF_INET]指向需要注册的域。create是该域的socket的创建函数,我们的MY_PF_INET域定义如下:

static struct net_proto_family myinet_family_ops = {

.family = MY_PF_INET,

.create = myinet_create,

.owner = THIS_MODULE,

};

现在回到socket系统调用上来,内核实现socket系统调用的函数是sys_socket。该函数通过调用sock_create进行创建,sock_create调用__sock_create。__sock_create要创建一个struct socket,这是一个普通BSD socket的结构体,其定义如下:

struct socket {

socket_state state;

unsigned long flags;

struct proto_ops *ops;

struct fasync_struct *fasync_list;

struct file *file;

struct sock *sk;

wait_queue_head_t wait;

short type;

};

__sock_create创建的时候,为其type赋上socket系统调用的第二个参数type,最后通过调用net_families[family]->create(sock, protocol)完成socket的创建。对于MY_PF_INET域来说,该create函数即myinet_create。MY_PF_INET域支持的网络层协议是IP协议,在该协议上支持的套接字接口有流套接字(SOCK_STREAM),数据报套接字(SOCK_DGRAM)和原始套接字(SOCK_RAW)。在IP协议上注册一个套接字接口,也即创建一个套接字,需要知道该类型的套接字必需的一些相关信息。结构体struct inet_protosw就是用于在IP协议上注册套接字接口,其完整定义如下:

struct inet_protosw {

struct list_head list;

unsigned short type; //套接字类型,即socket系统调用的第二个参数。

int protocol; //第4层(传输层)协议号

struct proto *prot; //第4层协议的操作函数集

struct proto_ops *ops; //该类型的套接字的操作函数集

int capability;

char no_check;

unsigned char flags;

};

myinet_create函数注册套接字的过程本质上就是为指定套接字类型和第4层协议号的一个socket找到对应的操作函数集,使这个socket随后能真正被操作。全局数组inetsw_array包含了系统当前支持的所有在IP协议上能够注册的套接字接口,在系统初始化的时候,这些结构体以type作为依据,被组织到

static struct list_head inetsw[SOCK_MAX]中。当在inetsw数组中找到对应的socket类型和第4层协议号后,令struct socket->ops的值为struct inet_protosw->ops,即为该类型的套接字指定操作函数集。而struct socket->sk是网络层的套接字接口,其成员sk_prot的值为struct inet_protosw->prot,即为该类型的第4层协议指定操作函数集。套接字的创建工作大致如此。

接下来,再来看send系统调用,它的原型如下:

ssize_t send(int s, const void *buf, size_t len, int flags);

s是文件描述符,在内核中跟一个struct socket结构体建立一一对应的映射关系。buf和len分别为待发送数据的内容和长度,flag是一些标志位。内核实现该系统调用的函数是sys_send。sys_send直接调用sys_sendto,把sys_sendto的最后两个参数addr和addr_len置空。sys_sendto根据文件描述符s找到对应的struct socket,然后建立一个结构体struct msghdr msg用于发送数据内容,该结构体的定义如下:

struct msghdr {

void * msg_name; /* Socket 的名字 */

int msg_namelen; /* 名字的长度 */

struct iovec * msg_iov; /* 数据块 */

__kernel_size_t msg_iovlen; /* 数据块的数量 */

void * msg_control; /* Per protocol magic (eg BSD file descriptor passing) */

__kernel_size_t msg_controllen; /* Length of cmsg list */

unsigned msg_flags;

};

然后,sys_sendto调用sock_sendmsg发送数据,sock_sendmsg调用__sock_sendmsg,__sock_sendmsg调用struct socket->ops->sendmsg,即调用特定套接字类型的操作函数集中的sendmsg成员函数。比如,SOCK_RAW类型的套接字的sendmsg成员函数的实现如下(实际上SOCK_DGRAM类型的套接字的sendmsg成员函数也是这个):

int inet_sendmsg(struct kiocb *iocb, struct socket *sock,

struct msghdr *msg, size_t size)

{

struct sock *sk = sock->sk; if (!inet_sk(sk)->num && inet_autobind(sk))

return -EAGAIN;

return sk->sk_prot->sendmsg(iocb, sk, msg, size);

}

可以看到,在该函数中,调用了具体的第4层协议的操作函数集中的sendmsg成员函数,而该函数真正实现了对应协议的数据报文发送工作。

发布者:admin,转转请注明出处:http://www.yc00.com/news/1689408065a243214.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信