Linux网络编程
API函数初步剖析
今天我们来分析一下前几篇博文中提到的网络编程中几个核心的
API,探究一下当我们调用每个
API时,内核中具体做了哪些准备和初始化工作。
1
、socket(family,type,protocol)
当我们在开发网络应用程序时,使用该系统调用来创建一个套接字。该
API所做的工作如下所示:
该系统调用主要完成两个任务:“创建套接字”和“为套接字绑定文件句柄”。
socket{}<include/linux/net.h>结构定义如下:
struct socket {
socket_state state; //socket状态
unsigned long flags; //标识,如
SOCK_ASYNC_NOSAPCE
const struct proto_ops *ops; //协议特定的
socket操作集
struct fasync_struct *fasync_list; //异步唤醒队列
struct file *file; //指向文件的指针
struct sock *sk; //指向下一层中的
sock结构
wait_queue_head_t wait; //等待在这个
socket上的任务列表
short type; //数据包的类型
};
在创建
socket套接字时,就是要完成
ops、
file和
sk等这些成员的初始化。
1). 创建套接字:
sock_create()
根据
family参数值在全局数组
struct net_proto_family net_families[]里找到我们所指定的地址簇。不同类型的地址簇都有一个
struct net_proto_family{}类型的对象,例如我们常见的
IPv4的
inet_family_ops,
IPv6的
inet6_family_ops,
X25协议的
ax25_family_ops等。在内核是初始化时,这些模块会在自己的初始化函数内部调用
sock_register()接口将各自的地址簇对象注册到
net_families
[]数组里。
我们分析的焦点集中在
IPv4协议簇,即
inet_family_ops对象上。重点是
inet_create函数,该函数的主要任务就是创建一个
socket套接字,并对其中相关结构体成员进行必要的初始化。至于它创建套接字时的依据和原理等到我们讲协议栈时大家就明白了,这里主要是让大家对其流程执行流程有个感性的把握。
sock_alloc()函数中我们创建一个
struct socket{}类型的对象,假如叫做
A,将
socket()系统调用的第二参数
type字段赋值给
A->type。
在
inet_create()函数中,我们根据
type的值,在全局数组
struct inet_protosw inetsw[]里找到我们对应的协议转换开关。而
inetsw[]数组是在
inet_init()函数里被初始化的:
其中
inetsw_array[]是一个比较重要的数据结构,定义在
af_inet.c文件中:
根据
type的值,就可以确定
struct socket{}
->ops,到底是
inet_stream_ops、
inet_dgram_ops或者
inet_sockraw_ops
。然后,对应地,就以
tcp_prot、
udp_prot或
raw_prot
为输入参数,实例化一个
struct sock{}
对象
sk=sk_alloc()
。紧接着建立
socket{}
和
sock{}
的关联,最后将
socket()
系统调用的第三个参数
protocol
付给
sock{}
对象中的属性
sk_protocol
。
看不懂别着急,我说过,这里只是给大家梳理整体流程,等到我们讲了协议栈章节,然后再回头看本篇,就感觉这些东西就太小儿科了。
2). 为套接字绑定文件句柄:
sock_map_fd()
我们都知道网络套接字也是一种系统
IO,所以不可避免的要与文件系统打交道。每个套接字都对应一个已打开的文件标识符,所以在套接字初始化完成后,就要将其和本地一个唯一的文件标识符关联起来,即建立
socket{}和
file{}之间的关联关系。
2
、bind (sockfd, sockaddr, addrlen)
该系统调用在内核中的执行过程如下:
重点是
socket->ops->bind()回调接口。我们现在已经知道了,针对
IPv4而言,这里的
ops无非就是
inet_stream_ops、
inet_dgram_ops或
inet_sockraw_ops对象。碰巧的是,这三个对象中的
bind函数指针均指向
inet_bind()函数。只有原始套接字的情况,这里会去调用
raw_prot对象的
bind回调函数,即
raw_bind()。
3
、listen(sockfd, backlog)
这里我们可以看到面向无连接的套接字和原始套接字是不用
listen的,只有流式套接字才有效。
4
、connect(sockfd, sockaddr, addrlen)
从这幅图中我们确实看到,
connect()系统调用不但可以面向连接的套接字,也可用于无连接及原始套接字。
5
、accept(sockfd, sockaddr, addrlen)
同样地,我们看到只有面向连接的流式套接字调用
accept()才有意义。最终调用的是
tcp_prot对象的
accept成员函数。
本篇主要进一步分析了网络编程中常见的几个
API函数内部的调用流程,一方面可以使大家对这些
API的有了更深的认识,不是仅仅停留在形而上的层面;另一方面为后面分析协议栈的实现原理,奠定坚实的基础。
未完,待续
…