对于用户态编程的第一个函数是
fd=socket(AF_INET,SOCK_STREAM,0);
该函数会通过系统调用进入内核,内核的实现如下:
kernel\net\Socket.c
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
int retval;
struct socket *sock;
int flags;
...
基本类型:由低8位设置共8种类型
特殊类型:由其他bit位设置
下面2句的意思就是判断socket创建的类型只能是基本类型(SOCK_STREAM,SOCK_DGRAM等)或者(SOCK_CLOEXEC,SOCK_NONBLOCK))如果还包含其他的高位地址的类型,则返回错误的参数
flags = type & ~SOCK_TYPE_MASK;
if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
return -EINVAL;
type &= SOCK_TYPE_MASK;
对于类似alpha或者mips架构有可能会将SOCK_NONBLOCK设置为不等于O_NONBLOCK,因此针对我们的ARM架构可以忽略这条
if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;
下面就是创建一个socket文件,注意这里的sock指针作为传入参数在sock_create完成赋值
retval = sock_create(family, type, protocol, &sock);
if (retval < 0)
goto out;
通过nlmsg_multicast将socket创建的消息发送出去
if (retval == 0)
sockev_notify(SOCKEV_SOCKET, sock);
将这个新建的socket文件关联到一个文件中这个文件的fops为socket_file_ops,并返回这个关联的文件描述符
retval = sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
if (retval < 0)
goto out_release;
out:
/* It may be already another descriptor 8) Not kernel problem. */
return retval;
out_release:
sock_release(sock);
return retval;
}
sock_create
->__sock_create(current->nsproxy->net_ns,...) 这里的第一个参数代表的是当前进程的网络命名空间
struct socket *sock;
const struct net_proto_family *pf;
...
sock = sock_alloc();
sock->type = type; ----SOCK_STREAM/SOCK_DGRAM
//针对我们当前的sock而言,socket的family是AF_INET,同时AF_INET是PF_INET的别称,因此返回的pf就是inet_family_ops
pf = rcu_dereference(net_families[family]);
...
//调用的自然就是inet_create
err = pf->create(net, sock, protocol, kern);//针对当前的调用情况,这个值kern目前为0
*res = sock; //返回新建的sock文件,这里的*res就是sock_create传入的最后一个参数
这里要说明的是net_families这是一个指针数组定义如下:
static const struct net_proto_family __rcu *net_families[NPROTO] __read_mostly;
数组中的每一个元素都是一个指向结构体net_proto_family的指针,那么问题来了这些指针是怎么被放进去的呢,在sock.c文件中提供了一个函数sock_register用于填充这个数组。如下:
kernel\net\ipv4\Af_inet.c
static const struct net_proto_family inet_family_ops = {
.family = PF_INET,
.create = inet_create,
.owner = THIS_MODULE,
};
inet_init
(void)sock_register(&inet_family_ops);
因此针对socket的创建最终就回来到inet_create这个函数
inet_create
这里inet_ehash_secret是一个4字节的整型数代表的是一个加密字串,如果这个字串还没有设置,同时报文既不是SOCK_RAW也不是SOCK_DGRAM,那么我们先设置加密字串,回到我们应用程序中,我们设置的类型是SOCK_STREAM,因此这里是需要设置这个加密字串的,但是只会设置一次
if (unlikely(!inet_ehash_secret))
if (sock->type != SOCK_RAW && sock->type != SOCK_DGRAM)
build_ehash_secret();
设置当前socket状态为未连接
sock->state = SS_UNCONNECTED;
lookup_protocol:
这里的inetsw是一个静态链表类型的数组,保存的是像tcp或者udp协议对应的链表元素,因此下面这句的意思就是通过sock->type,具体到我当前的应用而言就是SOCK_STREAM,通过这个下标找到tcp对应的inet_protosw类型的answer指针
list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {
....
}
更新相关字段,这里面sock->ops这个字段很关键
sock->ops = answer->ops;
answer_prot = answer->prot;
answer_no_check = answer->no_check;
answer_flags = answer->flags;
创建sock文件,这里的sk是struct sock *sk;
sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot);
sk->sk_no_check = answer_no_check;
因为inet内嵌的第一个元素就是sock结构体,因此这里将sk强制转换为inet
inet = inet_sk(sk);
对于TCP中inet->is_icsk这个字段为true,其他协议为false
inet->is_icsk = (INET_PROTOSW_ICSK & answer_flags) != 0;
下面是是否使能MTU的路径发现机制,可以通过"/proc/sys/net/ipv4/ip_no_pmtu_disc"去设置
所谓的MTU的路径发现机制:
我们知道如果应用层的数据报文过大,IP会分片,如果使能了MTU路径发现机制,那么应用层会逐步增大报文的长度尝试向对端发送报文,一旦路径中的主机发现报文超过了它设置的MTU值,就会丢弃报文,同时向报文的源端发送“报文过大”的ICMP消息。从而源端就可以知道在不分片的情况下链路最大可以设置的MTU
if (ipv4_config.no_pmtu_disc)
inet->pmtudisc = IP_PMTUDISC_DONT;
else
inet->pmtudisc = IP_PMTUDISC_WANT;
inet->inet_id = 0;
利用struct socket进一步初始化struct sock结构体,这里也会设置sock的发送和接收的缓冲区的大小
sock_init_data(sock, sk);
...
if (sk->sk_prot->init) {
err = sk->sk_prot->init(sk);
if (err)
sk_common_release(sk);
}
这里的sk->sk_prot->init对应是TCP协议的tcp_prot中的init函数tcp_v4_init_sock
这里详细说明下inetsw中的元素通过inet_register_protosw的方式将下面协议逐个加进去的
static struct inet_protosw inetsw_array[] =
{
{
.type = SOCK_STREAM,
.protocol = IPPROTO_TCP,
.prot = &tcp_prot,
.ops = &inet_stream_ops,
.no_check = 0,
.flags = INET_PROTOSW_PERMANENT |
INET_PROTOSW_ICSK,
},
{
.type = SOCK_DGRAM,
.protocol = IPPROTO_UDP,
.prot = &udp_prot,
.ops = &inet_dgram_ops,
.no_check = UDP_CSUM_DEFAULT,
.flags = INET_PROTOSW_PERMANENT,
},
{
.type = SOCK_DGRAM,
.protocol = IPPROTO_ICMP,
.prot = &ping_prot,
.ops = &inet_dgram_ops,
.no_check = UDP_CSUM_DEFAULT,
.flags = INET_PROTOSW_REUSE,
},
{
.type = SOCK_RAW,
.protocol = IPPROTO_IP, /* wild card */
.prot = &raw_prot,
.ops = &inet_sockraw_ops,
.no_check = UDP_CSUM_DEFAULT,
.flags = INET_PROTOSW_REUSE,
}
};
再说下最后一个函数,之前我们有提到的tcp_v4_init_sock
kernel\net\ipv4\Tcp_ipv4.c
static int tcp_v4_init_sock(struct sock *sk)
{
struct inet_connection_sock *icsk = inet_csk(sk);
tcp_init_sock(sk);
icsk->icsk_af_ops = &ipv4_specific;
#ifdef CONFIG_TCP_MD5SIG
tcp_sk(sk)->af_specific = &tcp_sock_ipv4_specific;
#endif
return 0;
}
kernel\net\ipv4\Tcp.c
void tcp_init_sock(struct sock *sk)
{
struct inet_connection_sock *icsk = inet_csk(sk);
struct tcp_sock *tp = tcp_sk(sk);
skb_queue_head_init(&tp->out_of_order_queue);
tcp_init_xmit_timers(sk);
tcp_prequeue_init(tp);
INIT_LIST_HEAD(&tp->tsq_node);
icsk->icsk_rto = TCP_TIMEOUT_INIT;
tp->mdev = TCP_TIMEOUT_INIT;
/* So many TCP implementations out there (incorrectly) count the
* initial SYN frame in their delayed-ACK and congestion control
* algorithms that we must have the following bandaid to talk
* efficiently to them. -DaveM
*/
tp->snd_cwnd = TCP_INIT_CWND;
/* See draft-stevens-tcpca-spec-01 for discussion of the
* initialization of these values.
*/
tp->snd_ssthresh = TCP_INFINITE_SSTHRESH;
tp->snd_cwnd_clamp = ~0;
tp->mss_cache = TCP_MSS_DEFAULT;
tp->reordering = sysctl_tcp_reordering;
tcp_enable_early_retrans(tp);
icsk->icsk_ca_ops = &tcp_init_congestion_ops;
tp->tsoffset = 0;
sk->sk_state = TCP_CLOSE;
sk->sk_write_space = sk_stream_write_space;
sock_set_flag(sk, SOCK_USE_WRITE_QUEUE);
icsk->icsk_sync_mss = tcp_sync_mss;
/* Presumed zeroed, in order of appearance:
* cookie_in_always, cookie_out_never,
* s_data_constant, s_data_in, s_data_out
*/
这里会再次更新发送和接收缓冲区的大小,这个值可以通过下面的方式查看和修改:
"/sys/kernel/ipv4/tcp_wmem_def"
sk->sk_sndbuf = sysctl_tcp_wmem[1];
sk->sk_rcvbuf = sysctl_tcp_rmem[1];
local_bh_disable();
sock_update_memcg(sk);
sk_sockets_allocated_inc(sk);
local_bh_enable();
}