内核中的TCP的追踪分析-8-TCP(IPV4)的socket连接

来源:互联网 发布:移动手机信号测试软件 编辑:程序博客网 时间:2024/06/05 15:17

我们继续探讨socket的连接,同样象在unixsocket章节一样,我们还是先从练习中的例子看起

connect(sockfd, (struct sockaddr *)&address, len)

在这个练习很明显是客户端的socket发起的,中间的过程我们不详细叙述了,需要了解的朋友请看我在博客中的关于unixsocket连接那篇文章

这里我们还是从err = sock->ops->connect(sock, (struct sockaddr *)address, addrlen,sock->file->f_flags); 看起,其余部分请朋友们参阅以前内容,那些函数都在前面分析过了,在地址设备那节中我们看到是将socket的ops通过answer结构变量转接入了inet_stream_ops,所以这里会跳入这个钩子结构去执行,我们再看一下这个结构中相关的钩子函数

const struct proto_ops inet_stream_ops = {

......
    .connect     = inet_stream_connect,

......
    };

结构中其他的部分我们暂且不要关心,也就是说会执行钩子函数inet_stream_connect。

 

sys_socketcall()-->sys_connect()-->inet_stream_connect()

int inet_stream_connect(struct socket *sock, struct sockaddr *uaddr,
            int addr_len, int flags)
{
    struct sock *sk = sock->sk;
    int err;
    long timeo;

    lock_sock(sk);

    if (uaddr->sa_family == AF_UNSPEC) {
        err = sk->sk_prot->disconnect(sk, flags);
        sock->state = err ? SS_DISCONNECTING : SS_UNCONNECTED;
        goto out;
    }

    switch (sock->state) {
    default:
        err = -EINVAL;
        goto out;
    case SS_CONNECTED:
        err = -EISCONN;
        goto out;
    case SS_CONNECTING:
        err = -EALREADY;
        /* Fall out of switch with err, set for this state */
        break;
    case SS_UNCONNECTED:
        err = -EISCONN;
        if (sk->sk_state != TCP_CLOSE)
            goto out;

        err = sk->sk_prot->connect(sk, uaddr, addr_len);
        if (err < 0)
            goto out;

        sock->state = SS_CONNECTING;

        
/* Just entered SS_CONNECTING state; the only
         * difference is that return value in non-blocking
         * case is EINPROGRESS, rather than EALREADY.
         */

        err = -EINPROGRESS;
        break;
    }

    timeo = sock_sndtimeo(sk, flags & O_NONBLOCK);

    if ((<< sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {
        /* Error code is set above */
        if (!timeo || !inet_wait_for_connect(sk, timeo))
            goto out;

        err = sock_intr_errno(timeo);
        if (signal_pending(current))
            goto out;
    }

    
/* Connection was closed by RST, timeout, ICMP error
     * or another process disconnected us.
     */

    if (sk->sk_state == TCP_CLOSE)
        goto sock_error;

    
/* sk->sk_err may be not zero now, if RECVERR was ordered by user
     * and error was received after socket entered established state.
     * Hence, it is handled normally after connect() return successfully.
     */


    sock->state = SS_CONNECTED;
    err = 0;
out:
    release_sock(sk);
    return err;

sock_error:
    err = sock_error(sk) ? : -ECONNABORTED;
    sock->state = SS_UNCONNECTED;
    if (sk->sk_prot->disconnect(sk, flags))
        sock->state = SS_DISCONNECTING;
    goto out;
}

这个函数先是对sock结构加锁,然后判断我们在练习中设置的地址是否符合要求,接下来要根据sock的状态来执行switch语句代码,如果回忆一下在创建socket的过程中,朋友们可以看socket创建那篇文章也可以参阅我的博客中的http://blog.chinaunix.net/u2/64681/showart.php?id=1685658,在那里我们看到会通过inet_create()函数将新创建的sock结构的state状态标志设为了SS_UNCONNECTED,意思是还没有连接,显然,会执行SS_UNCONNECTED相关的语句,我们看到使用sock中的sk_prot钩子结构来调用connect,而sk_prot则在创建socket时我们说到了他是如何挂钩的,这里直接进入挂入的tcp_prot结构变量中的钩子函数,它是运输层的结构体

struct proto tcp_prot = {
    。。。。。。
    .connect        = tcp_v4_connect,
    。。。。。。
};

显然是进入tcp_v4_connect()函数中,我们分段来看

int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
    struct inet_sock *inet = inet_sk(sk);
    struct tcp_sock *tp = tcp_sk(sk);
    struct sockaddr_in *usin = (struct sockaddr_in *)uaddr;
    struct rtable *rt;
    __be32 daddr, nexthop;
    int tmp;
    int err;

    if (addr_len < sizeof(struct sockaddr_in))
        return -EINVAL;

    if (usin->sin_family != AF_INET)
        return -EAFNOSUPPORT;

    nexthop = daddr = usin->sin_addr.s_addr;
    if (inet->opt && inet->opt->srr) {
        if (!daddr)
            return -EINVAL;
        nexthop = inet->opt->faddr;
    }

    tmp = ip_route_connect(&rt, nexthop, inet->saddr,
             RT_CONN_FLAGS(sk), sk->sk_bound_dev_if,
             IPPROTO_TCP,
             inet->sport, usin->sin_port, sk, 1);
    if (tmp < 0) {
        if (tmp == -ENETUNREACH)
            IP_INC_STATS_BH(IPSTATS_MIB_OUTNOROUTES);
        return tmp;
    }

    if (rt->rt_flags & (RTCF_MULTICAST | RTCF_BROADCAST)) {
        ip_rt_put(rt);
        return -ENETUNREACH;
    }

    if (!inet->opt || !inet->opt->srr)
        daddr = rt->rt_dst;

    if (!inet->saddr)
        inet->saddr = rt->rt_src;
    inet->rcv_saddr = inet->saddr;

    if (tp->rx_opt.ts_recent_stamp && inet->daddr != daddr) {
        /* Reset inherited state */
        tp->rx_opt.ts_recent     = 0;
        tp->rx_opt.ts_recent_stamp = 0;
        tp->write_seq         = 0;
    }

我们知道参数uaddr是从用户空间传递过来的地址结构变量,这里转变成IP地址结构struct sockaddr_in,这种结构我们已经在以前看过了,首先是对这个地址结构的检测是否是IP协议,此后是检测地址类型,接着进入ip_route_connect()函数这是与路由相关的函数

static inline int ip_route_connect(struct rtable **rp, __be32 dst,
                 __be32 src, u32 tos, int oif, u8 protocol,
                 __be16 sport, __be16 dport, struct sock *sk,
                 int flags)
{
    struct flowi fl = { .oif = oif,
             .mark = sk->sk_mark,
             .nl_u = { .ip4_u = { .daddr = dst,
                         .saddr = src,
                         .tos = tos } },
             .proto = protocol,
             .uli_u = { .ports =
                 { .sport = sport,
                     .dport = dport } } };

    int err;
    struct net *net = sock_net(sk);
    if (!dst || !src) {
        err = __ip_route_output_key(net, rp, &fl);
        if (err)
            return err;
        fl.fl4_dst = (*rp)->rt_dst;
        fl.fl4_src = (*rp)->rt_src;
        ip_rt_put(*rp);
        *rp = NULL;
    }
    security_sk_classify_flow(sk, &fl);
    return ip_route_output_flow(net, rp, &fl, sk, flags);
}

我们在函数头部看到一个数据结构struct flowi这是个专门用于路由的键值,我们先大概的看一下,在使用的过程中可以对照

struct flowi {
    int    oif;
    int    iif;
    __u32    mark;

    union {
        struct {
            __be32            daddr;
            __be32            saddr;
            __u8            tos;
            __u8            scope;
        } ip4_u;
        
        struct {
            struct in6_addr        daddr;
            struct in6_addr        saddr;
            __be32            flowlabel;
        } ip6_u;

        struct {
            __le16            daddr;
            __le16            saddr;
            __u8            scope;
        } dn_u;
    } nl_u;
#define fld_dst        nl_u.dn_u.daddr
#define fld_src        nl_u.dn_u.saddr
#define fld_scope    nl_u.dn_u.scope
#define fl6_dst        nl_u.ip6_u.daddr
#define fl6_src        nl_u.ip6_u.saddr
#define fl6_flowlabel    nl_u.ip6_u.flowlabel
#define fl4_dst        nl_u.ip4_u.daddr
#define fl4_src        nl_u.ip4_u.saddr
#define fl4_tos        nl_u.ip4_u.tos
#define fl4_scope    nl_u.ip4_u.scope

    __u8    proto;
    __u8    flags;
    union {
        struct {
            __be16    sport;
            __be16    dport;
        } ports;

        struct {
            __u8    type;
            __u8    code;
        } icmpt;

        struct {
            __le16    sport;
            __le16    dport;
        } dnports;

        __be32        spi;

        struct {
            __u8    type;
        } mht;
    } uli_u;
#define fl_ip_sport    uli_u.ports.sport
#define fl_ip_dport    uli_u.ports.dport
#define fl_icmp_type    uli_u.icmpt.type
#define fl_icmp_code    uli_u.icmpt.code
#define fl_ipsec_spi    uli_u.spi
#define fl_mh_type    uli_u.mht.type
    __u32 secid;    /* used by xfrm; see secid.txt */
} __attribute__((__aligned__(BITS_PER_LONG/8)));

这里很显然我们需要先看参数才能搞明白路由函数的过程,从tcp_v4_connect函数传递的参数来看分别是rt,它是一个查询路由表使用的结构

struct rtable
{
    union
    {
        struct dst_entry    dst;
    } u;

    /* Cache lookup keys */
    struct flowi        fl;

    struct in_device    *idev;
    
    int            rt_genid;
    unsigned        rt_flags;
    __u16            rt_type;

    __be32            rt_dst;    /* Path destination    */
    __be32            rt_src;    /* Path source        */
    int            rt_iif;

    /* Info on neighbour */
    __be32            rt_gateway;

    /* Miscellaneous cached information */
    __be32            rt_spec_dst; /* RFC1122 specific destination */
    struct inet_peer    *peer; /* long-living peer info */
};

然后nexthop是要服务器的地址,即目标地址,inet->saddr是我们绑定地址那节中看到的绑定的127.0.0.1这个地址,然后根据sock中的tosSOCK_LOCALROUTE状态位来确定最终的tostos就是type of service的意思,即服务类型。sk->sk_bound_dev_ifsocket绑定到的设备,IPPROTO_TCP宏是确定使用的传输控制协议,inet->sport是客户端的socket的端口,而usin->sin_port则是服务器端口即目标端口。上面这些参数一一对应ip_route_connect()函数的参数。我们看到将这些参数设置到了路由的键值结构变量fl中了。然后取得网络命名空间结构,在我们这里取得的是全局变量init_net,如果地址类型是多播或者是广播的话那么我们这里源地址就是0,这个是在绑定地址章节中看到过的,http://blog.chinaunix.net/u2/64681/showart_1387214.html 所以会进入__ip_route_output_key()函数中,这个函数是根据我们上面设置的路由键值查找适用的路由表

int __ip_route_output_key(struct net *net, struct rtable **rp,
             const struct flowi *flp)
{
    unsigned hash;
    struct rtable *rth;

    hash = rt_hash(flp->fl4_dst, flp->fl4_src, flp->oif);

    rcu_read_lock_bh();
    for (rth = rcu_dereference(rt_hash_table[hash].chain); rth;
        rth = rcu_dereference(rth->u.dst.rt_next)) {
        if (rth->fl.fl4_dst == flp->fl4_dst &&
         rth->fl.fl4_src == flp->fl4_src &&
         rth->fl.iif == 0 &&
         rth->fl.oif == flp->oif &&
         rth->fl.mark == flp->mark &&
         !((rth->fl.fl4_tos ^ flp->fl4_tos) &
             (IPTOS_RT_MASK | RTO_ONLINK)) &&
         net_eq(dev_net(rth->u.dst.dev), net) &&
         rth->rt_genid == atomic_read(&rt_genid)) {
            dst_use(&rth->u.dst, jiffies);
            RT_CACHE_STAT_INC(out_hit);
            rcu_read_unlock_bh();
            *rp = rth;
            return 0;
        }
        RT_CACHE_STAT_INC(out_hlist_search);
    }
    rcu_read_unlock_bh();

    return ip_route_output_slow(net, rp, flp);
}

函数首先根据路由键值确定一个hash值,然后在rt_hash_table的杂凑表中找到适用的路由表,朋友们可以看已阅读这段代码,如果找到了就会返回0,如果没有找到就会进入ip_route_output_slow()函数,这个函数总的来说就是再建立一个新的路由键值,然后根据我们这里的键值进行一系列的初始化操作,最后根据这个键值进入fib_lookup()函数在我们全局的网络空间依次查找适用的路由表,最后找到路由表并建立缓存后通过rp参数得到了路由表。这个过程我们暂时放一放了。我们继续看ip_route_connect()函数中得到了这个rp路由表后则根据路由表中提供的目标地址和源地址调整这里的路由键值fl,最后函数进入ip_route_output_flow()返回

int ip_route_output_flow(struct net *net, struct rtable **rp, struct flowi *flp,
             struct sock *sk, int flags)
{
    int err;

    if ((err = __ip_route_output_key(net, rp, flp)) != 0)
        return err;

    if (flp->proto) {
        if (!flp->fl4_src)
            flp->fl4_src = (*rp)->rt_src;
        if (!flp->fl4_dst)
            flp->fl4_dst = (*rp)->rt_dst;
        err = __xfrm_lookup((struct dst_entry **)rp, flp, sk,
                 flags ? XFRM_LOOKUP_WAIT : 0);
        if (err == -EREMOTE)
            err = ipv4_dst_blackhole(rp, flp);

        return err;
    }

    return 0;
}

我们看到在这个函数中再次通过__ip_route_output_key()函数确定一下是否能在路由的杂凑缓存表中找到我们的路由表,然后通过__xfrm_lookup()函数查找策略,这个函数__xfrm_lookup()非常大主要是ipsec的过程,我们也暂时放一放。这里暂时用网上的一句简短的描述概括一下函数的主要过程:下面摘取linuxforum论坛中的一段文字

事实上无论是本机的包还是转发的包,都必须经过xfrm_lookup这个函数的处理。 
而每个skbuff在xfrm_lookup处理前dst_output 已经有了一个默认的设置,比如说是ip_output 
而在经过xfrm_lookup处理过程中首先要检查的是策略,也就是policy,策略无非就是绕过、处理和丢弃。如果是绕过,则不作任何的处理,直接返回,然后根据原有的dst_output将数据包发出。这个过程和Linux-2.4内核的处理过程基本相同。关键区别是在加入ipsec处理上 
如果策略决定数据包需要经过ipsec处理,那在xfrm_lookup中就有一个复杂的处理过程了,大概顺序是 
1、查策略 
2、找sa(xfrm_state),xfrm_find_bundle 
3、没有找到就创建一个xfrm_bundle_create 
4、创建完以后还要添加sa(xfrm_state) 
5、最后关键的一步是在dst_output的修改上,经过处理后的skbuff的dst_output已经不再是原有的ip_output,而是根据查找的xfrm_state设置成具体的ah_output或者是esp_output。 
而这些所有的dst_output都连成了一个链,往往需要进行ipsec处理的都放在链头最先获得处理,处理完后又被修改为ip_output这样的正常处理,又重新挂到链上处理。 
这样需要ipsec处理的包可以得到不同的处理。

到此我们返回到tcp_v4_connect()函数中继续往下看,我们看到除了一些对函数返回值的检查外还对inet的地址进行了调整。由于时间关系我们明天继续。

转自:http://blog.chinaunix.net/uid-7960587-id-2035554.html

原创粉丝点击