目录

一、概述

二、connect

2.1 connect流程

2.1.1 路由查找

2.1.2 connect端口分配

2.1.3 再次查找路由

2.1.4 初始化seq

2.1.5 构造并发送SYN

三、三次握手主要流程


一、概述

主要关注流程,其他如滑动窗口变化等后续文章统一分析。

二、connect

connect的函数原型是:

  • int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

connect执行主动打开,发起三次握手,函数参数前面已经说过。

2.1 connect流程

2.1.1 路由查找

从inet_stream_connect->tcp_v4_connect,内容较多,分段来看

[net/ipv4/tcp_ipv4.c]

int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{struct sockaddr_in *usin = (struct sockaddr_in *)uaddr;struct inet_sock *inet = inet_sk(sk);struct tcp_sock *tp = tcp_sk(sk);__be16 orig_sport, orig_dport;__be32 daddr, nexthop;struct flowi4 *fl4;struct rtable *rt;int err;struct ip_options_rcu *inet_opt;struct inet_timewait_death_row *tcp_death_row = &sock_net(sk)->ipv4.tcp_death_row;nexthop = daddr = usin->sin_addr.s_addr;inet_opt = rcu_dereference_protected(inet->inet_opt,lockdep_sock_is_held(sk));...orig_sport = inet->inet_sport;orig_dport = usin->sin_port;fl4 = &inet->cork.fl.u.ip4;rt = ip_route_connect(fl4, nexthop, inet->inet_saddr,RT_CONN_FLAGS(sk), sk->sk_bound_dev_if,IPPROTO_TCP,orig_sport, orig_dport, sk);if (!inet->inet_saddr)inet->inet_saddr = fl4->saddr;sk_rcv_saddr_set(sk, inet->inet_saddr);if (tp->rx_opt.ts_recent_stamp && inet->inet_daddr != daddr) {/* Reset inherited state */tp->rx_opt.ts_recent      = 0;tp->rx_opt.ts_recent_stamp = 0;if (likely(!tp->repair))tp->write_seq     = 0;}inet->inet_dport = usin->sin_port;sk_daddr_set(sk, daddr);inet_csk(sk)->icsk_ext_hdr_len = 0;if (inet_opt)inet_csk(sk)->icsk_ext_hdr_len = inet_opt->opt.optlen;tp->rx_opt.mss_clamp = TCP_MSS_DEFAULT;
  • 上面的片段就是做一些基本的配置,为建立连接做准备,这里先不考虑ip_option的情况
  • 首先构造flowi,并通过ip_route_connect查路由
  • 如果没有bind的话,在路由查找后填充一下源IP: inet->inet_saddr
  • 此时dport和daddr也填充一下

2.1.2 connect端口分配

一般情况下,用户不指定sport,这时候要自动分配,这中情况下主要考虑sport和bind时的冲突,以及sport是否和ehash有冲突

tcp_set_state(sk, TCP_SYN_SENT);
err = inet_hash_connect(tcp_death_row, sk);
if (err)goto failure;
sk_set_txhash(sk);

[net/ipv4/inet_hashtables.c]

int inet_hash_connect(struct inet_timewait_death_row *death_row,struct sock *sk)
{u32 port_offset = 0;if (!inet_sk(sk)->inet_num)port_offset = inet_sk_port_offset(sk);return __inet_hash_connect(death_row, sk, port_offset,__inet_check_established);
}
  • 其中port_offset是一个搜索端口号的偏移

下面分段看一下核心的__inet_hash_connect函数,先来看已经bind的情形:

int __inet_hash_connect(struct inet_timewait_death_row *death_row,struct sock *sk, u32 port_offset,int (*check_established)(struct inet_timewait_death_row *,struct sock *, __u16, struct inet_timewait_sock **))
{struct inet_hashinfo *hinfo = death_row->hashinfo;struct inet_timewait_sock *tw = NULL;struct inet_bind_hashbucket *head;int port = inet_sk(sk)->inet_num;struct net *net = sock_net(sk);struct inet_bind_bucket *tb;u32 remaining, offset;int ret, i, low, high;static u32 hint;if (port) {head = &hinfo->bhash[inet_bhashfn(net, port,hinfo->bhash_size)];tb = inet_csk(sk)->icsk_bind_hash;spin_lock_bh(&head->lock);if (sk_head(&tb->owners) == sk && !sk->sk_bind_node.next) {inet_ehash_nolisten(sk, NULL);spin_unlock_bh(&head->lock);return 0;}spin_unlock(&head->lock);/* No definite answer... Walk to established hash table */ret = check_established(death_row, sk, port, NULL);local_bh_enable();return ret;}
  • 由于已经bind过了,所以一定能找到对应的tb,如果当前只有一个sk,那么直接使用inet_ehash_nolisten,这意味着在ehash的四元组检测中一定不会有冲突,因为bhash的二元组检测是比ehash的四元组检测更为严格的。
  • 如果tb->owner承载不止一个sk,意味着sk的二元组是存在多个的,这时候要进一步检测ehash,这意味着另一个sk可能已经使用ehash建立了连接,这部分放到后面说。

inet_ehash_nolisten就是将sk插到ehash中,同时删除冲突的osk,当然上面这种情况下,只有sk一个socket,因此第二个参数为NULL,看一下其核心函数inet_ehash_insert

bool inet_ehash_insert(struct sock *sk, struct sock *osk)
{struct inet_hashinfo *hashinfo = sk->sk_prot->h.hashinfo;struct hlist_nulls_head *list;struct inet_ehash_bucket *head;spinlock_t *lock;bool ret = true;WARN_ON_ONCE(!sk_unhashed(sk));sk->sk_hash = sk_ehashfn(sk);head = inet_ehash_bucket(hashinfo, sk->sk_hash);list = &head->chain;lock = inet_ehash_lockp(hashinfo, sk->sk_hash);spin_lock(lock);if (osk) {WARN_ON_ONCE(sk->sk_hash != osk->sk_hash);ret = sk_nulls_del_node_init_rcu(osk);}if (ret)__sk_nulls_add_node_rcu(sk, list);spin_unlock(lock);return ret;
}
  • 流程上没有什么特别的,这里需要注意一下hash函数,hash key是四元组(saddr, sport, daddr, dport)
static u32 sk_ehashfn(const struct sock *sk)
{...return inet_ehashfn(sock_net(sk),sk->sk_rcv_saddr, sk->sk_num,sk->sk_daddr, sk->sk_dport);
}

下面来看一下未bind情况下分配端口号的情况,和bind时情况类似,只不过优先分配偶数端口号:

 inet_get_local_port_range(net, &low, &high);high++; /* [32768, 60999] -> [32768, 61000[ */remaining = high - low;if (likely(remaining > 1))remaining &= ~1U;offset = (hint + port_offset) % remaining;/* In first pass we try ports of @low parity.* inet_csk_get_port() does the opposite choice.*/offset &= ~1U;

搜索上也是类似的

other_parity_scan:port = low + offset;for (i = 0; i < remaining; i += 2, port += 2) {if (unlikely(port >= high))port -= remaining;if (inet_is_local_reserved_port(net, port))continue;head = &hinfo->bhash[inet_bhashfn(net, port,hinfo->bhash_size)];spin_lock_bh(&head->lock);/* Does not bother with rcv_saddr checks, because* the established check is already unique enough.*/inet_bind_bucket_for_each(tb, &head->chain) {if (net_eq(ib_net(tb), net) && tb->port == port) {if (tb->fastreuse >= 0 ||tb->fastreuseport >= 0)goto next_port;WARN_ON(hlist_empty(&tb->owners));if (!check_established(death_row, sk,port, &tw))goto ok;goto next_port;}}
  • connect的时候还是要先检查一下bhash,一般情况下,如果选择的sport在ehash中可以找到,就尝试下一个,即不要和bind及可能bind的socket产生冲突

可以看到:

             if (tb->fastreuse >= 0 ||tb->fastreuseport >= 0)goto next_port;

我们在bind是看过,fastreuse,fastreuseport总是>=0的,所以这里的逻辑是connect时sprot选择只要和bhash port相同,就尝试下一个。如果是connect自己的sport产生冲突,就将fastreuse = -1,这样就进入establish状态的冲突的检测:

static int __inet_check_established(struct inet_timewait_death_row *death_row,struct sock *sk, __u16 lport,struct inet_timewait_sock **twp)
{...sk_nulls_for_each(sk2, node, &head->chain) {if (sk2->sk_hash != hash)continue;if (likely(INET_MATCH(sk2, net, acookie,saddr, daddr, ports, dif, sdif))) {if (sk2->sk_state == TCP_TIME_WAIT) {tw = inet_twsk(sk2);if (twsk_unique(sk, sk2, twp))break;}goto not_unique;}}
}

在不考虑TIME_WAIT状态下,使用四元组唯一的确定connect是否冲突:

比较的方式:

#define INET_MATCH(__sk, __net, __cookie, __saddr, __daddr, __ports, __dif, __sdif) \(((__sk)->sk_portpair == (__ports))            &&  \((__sk)->sk_addrpair == (__cookie))           &&  \(!(__sk)->sk_bound_dev_if   ||              \((__sk)->sk_bound_dev_if == (__dif))          ||  \((__sk)->sk_bound_dev_if == (__sdif)))        &&  \net_eq(sock_net(__sk), (__net)))

另外还要对tw状态的sk进行检测

static inline int twsk_unique(struct sock *sk, struct sock *sktw, void *twp)
{if (sk->sk_prot->twsk_prot->twsk_unique != NULL)return sk->sk_prot->twsk_prot->twsk_unique(sk, sktw, twp);return 0;
}

如果ehash检测没有问题,就加入ehash,此时inet的源端口已经是确定了。

 inet->inet_num = lport;inet->inet_sport = htons(lport);sk->sk_hash = hash;WARN_ON(!sk_unhashed(sk));__sk_nulls_add_node_rcu(sk, &head->chain);if (tw) {sk_nulls_del_node_init_rcu((struct sock *)tw);__NET_INC_STATS(net, LINUX_MIB_TIMEWAITRECYCLED);}

ehash检测通过后,将sk加入bhash

ok:hint += i + 2;/* Head lock still held and bh's disabled */inet_bind_hash(sk, tb, port);if (sk_unhashed(sk)) {inet_sk(sk)->inet_sport = htons(port);inet_ehash_nolisten(sk, (struct sock *)tw);}if (tw)inet_twsk_bind_unhash(tw, hinfo);spin_unlock(&head->lock);if (tw)inet_twsk_deschedule_put(tw);
  • sk_unhashed(sk) 检测有没有加入ehash,在bhash检测时,如果port没有冲突,会走到这部分,此时可以直接加入到ehash中。

2.1.3 再次查找路由

 rt = ip_route_newports(fl4, rt, orig_sport, orig_dport,inet->inet_sport, inet->inet_dport, sk);if (IS_ERR(rt)) {err = PTR_ERR(rt);rt = NULL;goto failure;}/* OK, now commit destination to socket.  */sk->sk_gso_type = SKB_GSO_TCPV4;sk_setup_caps(sk, &rt->dst);
  • 如果源端口分配后,新旧源端口或者目的端口有变化,还要重新查找一下路由
  • 设置路由缓存并确定设备的SG特性,这在tcp发包的时候需要
void sk_setup_caps(struct sock *sk, struct dst_entry *dst)
{u32 max_segs = 1;sk_dst_set(sk, dst);sk->sk_route_caps = dst->dev->features | sk->sk_route_forced_caps;if (sk->sk_route_caps & NETIF_F_GSO)sk->sk_route_caps |= NETIF_F_GSO_SOFTWARE;sk->sk_route_caps &= ~sk->sk_route_nocaps;if (sk_can_gso(sk)) {if (dst->header_len && !xfrm_dst_offload_ok(dst)) {sk->sk_route_caps &= ~NETIF_F_GSO_MASK;} else {sk->sk_route_caps |= NETIF_F_SG | NETIF_F_HW_CSUM;sk->sk_gso_max_size = dst->dev->gso_max_size;max_segs = max_t(u32, dst->dev->gso_max_segs, 1);}}sk->sk_gso_max_segs = max_segs;
}

2.1.4 初始化seq

 if (likely(!tp->repair)) {if (!tp->write_seq)tp->write_seq = secure_tcp_seq(inet->inet_saddr,inet->inet_daddr,inet->inet_sport,usin->sin_port);tp->tsoffset = secure_tcp_ts_off(sock_net(sk),inet->inet_saddr,inet->inet_daddr);}inet->inet_id = tp->write_seq ^ jiffies;

2.1.5 构造并发送SYN

主要的函数是tcp_connect

[net/ipv4/tcp_output.c]

int tcp_connect(struct sock *sk)
{struct tcp_sock *tp = tcp_sk(sk);struct sk_buff *buff;int err;tcp_call_bpf(sk, BPF_SOCK_OPS_TCP_CONNECT_CB, 0, NULL);if (inet_csk(sk)->icsk_af_ops->rebuild_header(sk))return -EHOSTUNREACH; /* Routing failure or similar. */tcp_connect_init(sk);if (unlikely(tp->repair)) {tcp_finish_connect(sk, NULL);return 0;}buff = sk_stream_alloc_skb(sk, 0, sk->sk_allocation, true);if (unlikely(!buff))return -ENOBUFS;tcp_init_nondata_skb(buff, tp->write_seq++, TCPHDR_SYN);tcp_mstamp_refresh(tp);tp->retrans_stamp = tcp_time_stamp(tp);tcp_connect_queue_skb(sk, buff);tcp_ecn_send_syn(sk, buff);tcp_rbtree_insert(&sk->tcp_rtx_queue, buff);/* Send off SYN; include data in Fast Open. */err = tp->fastopen_req ? tcp_send_syn_data(sk, buff) :tcp_transmit_skb(sk, buff, 1, sk->sk_allocation);if (err == -ECONNREFUSED)return err;/* We change tp->snd_nxt after the tcp_transmit_skb() call* in order to make this packet get counted in tcpOutSegs.*/tp->snd_nxt = tp->write_seq;tp->pushed_seq = tp->write_seq;buff = tcp_send_head(sk);if (unlikely(buff)) {tp->snd_nxt    = TCP_SKB_CB(buff)->seq;tp->pushed_seq   = TCP_SKB_CB(buff)->seq;}TCP_INC_STATS(sock_net(sk), TCP_MIB_ACTIVEOPENS);/* Timer for repeating the SYN until an answer. */inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,inet_csk(sk)->icsk_rto, TCP_RTO_MAX);return 0;
}

还是一段一段来看,tcp_connect首先使用tcp_connect_init对sk进行初始化,接着分配skb,最终调用tcp_connect_queue_skb发送SYN。

三、三次握手主要流程

三次握手流程对照TCP状态图很好理解,这里主要结合源码分析一下,tcp接收入口是tcp_v4_rcv,我把主要流程贴在下面:

int tcp_v4_rcv(struct sk_buff *skb)
{
...lookup:sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,th->dest, &refcounted);if (!sk)goto no_tcp_socket;process:if (sk->sk_state == TCP_TIME_WAIT)goto do_time_wait;...if (sk->sk_state == TCP_LISTEN) {ret = tcp_v4_do_rcv(sk, skb);goto put_and_return;}if (!sock_owned_by_user(sk)) {if (!tcp_prequeue(sk, skb))ret = tcp_v4_do_rcv(sk, skb);} else if (tcp_add_backlog(sk, skb)) {goto discard_and_relse;}
}

收到一个报文,要先看其和那个sk是关联的,这里有两种情况,报文在ehash中,说明三次握手已经建立完成,此时的报文是正常的通信报文;报文在listen hash中,说明报文在握手过程中:

static inline struct sock *__inet_lookup(struct net *net,struct inet_hashinfo *hashinfo,struct sk_buff *skb, int doff,const __be32 saddr, const __be16 sport,const __be32 daddr, const __be16 dport,const int dif,bool *refcounted)
{u16 hnum = ntohs(dport);struct sock *sk;sk = __inet_lookup_established(net, hashinfo, saddr, sport,daddr, hnum, dif);*refcounted = true;if (sk)return sk;*refcounted = false;return __inet_lookup_listener(net, hashinfo, skb, doff, saddr,sport, daddr, hnum, dif);
}

我们知道server经过bind,listen进入TCP_LISTEN状态,在该状态收到的报文进入三次握手处理流程:

int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{struct sock *rsk;if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */...return 0;}if (sk->sk_state == TCP_LISTEN) {struct sock *nsk = tcp_v4_cookie_check(sk, skb);if (!nsk)goto discard;if (nsk != sk) {if (tcp_child_process(sk, nsk, skb)) {rsk = nsk;goto reset;}return 0;}} elsesock_rps_save_rxhash(sk, skb);if (tcp_rcv_state_process(sk, skb)) {rsk = sk;goto reset;}return 0;
}

上述函数最重要的是tcp_rcv_state_process,这个函数是主要的状态机转换流程(不包括establish,time_wait),看一下LISTEN状态是如何处理的:

 case TCP_LISTEN:if (th->ack)return 1;if (th->rst)goto discard;if (th->syn) {if (th->fin)goto discard;/* It is possible that we process SYN packets from backlog,* so we need to make sure to disable BH right there.*/local_bh_disable();acceptable = icsk->icsk_af_ops->conn_request(sk, skb) >= 0;local_bh_enable();if (!acceptable)return 1;consume_skb(skb);return 0;}goto discard;

跟踪一下tcp_v4_conn_request->tcp_conn_request,这里还是只关注主要流程:

int tcp_conn_request(struct request_sock_ops *rsk_ops,const struct tcp_request_sock_ops *af_ops,struct sock *sk, struct sk_buff *skb)
{if (sk_acceptq_is_full(sk)) {NET_INC_STATS(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);goto drop;}req = inet_reqsk_alloc(rsk_ops, sk, !want_cookie);if (want_cookie && !tmp_opt.saw_tstamp)tcp_clear_options(&tmp_opt);af_ops->init_req(req, sk, skb);...if (!dst) {dst = af_ops->route_req(sk, &fl, req);if (!dst)goto drop_and_free;}tcp_ecn_create_request(req, skb, sk, dst);if (want_cookie) {isn = cookie_init_sequence(af_ops, sk, skb, &req->mss);req->cookie_ts = tmp_opt.tstamp_ok;if (!tmp_opt.tstamp_ok)inet_rsk(req)->ecn_ok = 0;}if (!want_cookie) {tcp_reqsk_record_syn(sk, req, skb);fastopen_sk = tcp_try_fastopen(sk, skb, req, &foc, dst);}if (fastopen_sk) {...} else {tcp_rsk(req)->tfo_listener = false;if (!want_cookie)inet_csk_reqsk_queue_hash_add(sk, req, TCP_TIMEOUT_INIT);af_ops->send_synack(sk, dst, &fl, req, &foc,!want_cookie ? TCP_SYNACK_NORMAL :TCP_SYNACK_COOKIE);if (want_cookie) {reqsk_free(req);return 0;}}reqsk_put(req);return 0;
}

这里涉及到所谓半连接队列和全连接队列。一般将SYN请求放入半连接队列,并发送SYN/ACK,当再次收到ACK将连接从半连接队列放入全连接队列。

static inline bool sk_acceptq_is_full(const struct sock *sk)
{return sk->sk_ack_backlog > sk->sk_max_ack_backlog;
}

我们看到当前全连接队列的大小sk->sk_max_ack_backlog就是listen的第二个参数,sk->sk_ack_backlog即是当前全连接队列大小。

客户端收到SYN/ACK

 case TCP_SYN_SENT:tp->rx_opt.saw_tstamp = 0;skb_mstamp_get(&tp->tcp_mstamp);queued = tcp_rcv_synsent_state_process(sk, skb, th);if (queued >= 0)return queued;/* Do step6 onward by hand. */tcp_urg(sk, skb, th);__kfree_skb(skb);tcp_data_snd_check(sk);return 0;

四、accept

accept本身没有什么好说的

struct sock *inet_csk_accept(struct sock *sk, int flags, int *err, bool kern)
{struct inet_connection_sock *icsk = inet_csk(sk);struct request_sock_queue *queue = &icsk->icsk_accept_queue;struct request_sock *req;struct sock *newsk;int error;lock_sock(sk);/* We need to make sure that this socket is listening,* and that it has something pending.*/error = -EINVAL;if (sk->sk_state != TCP_LISTEN)goto out_err;/* Find already established connection */if (reqsk_queue_empty(queue)) {long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK);/* If this is a non blocking socket don't sleep */error = -EAGAIN;if (!timeo)goto out_err;error = inet_csk_wait_for_connect(sk, timeo);if (error)goto out_err;}req = reqsk_queue_remove(queue, sk);newsk = req->sk;...
}

就是 阻塞等待

static int inet_csk_wait_for_connect(struct sock *sk, long timeo)
{struct inet_connection_sock *icsk = inet_csk(sk);DEFINE_WAIT(wait);int err;/** True wake-one mechanism for incoming connections: only* one process gets woken up, not the 'whole herd'.* Since we do not 'race & poll' for established sockets* anymore, the common case will execute the loop only once.** Subtle issue: "add_wait_queue_exclusive()" will be added* after any current non-exclusive waiters, and we know that* it will always _stay_ after any new non-exclusive waiters* because all non-exclusive waiters are added at the* beginning of the wait-queue. As such, it's ok to "drop"* our exclusiveness temporarily when we get woken up without* having to remove and re-insert us on the wait queue.*/for (;;) {prepare_to_wait_exclusive(sk_sleep(sk), &wait,TASK_INTERRUPTIBLE);release_sock(sk);if (reqsk_queue_empty(&icsk->icsk_accept_queue))timeo = schedule_timeout(timeo);sched_annotate_sleep();lock_sock(sk);err = 0;if (!reqsk_queue_empty(&icsk->icsk_accept_queue))break;err = -EINVAL;if (sk->sk_state != TCP_LISTEN)break;err = sock_intr_errno(timeo);if (signal_pending(current))break;err = -EAGAIN;if (!timeo)break;}finish_wait(sk_sleep(sk), &wait);return err;
}

linux网络子系统分析(四)—— INET连接建立API分析之connect/accept相关推荐

  1. Linux网络子系统

    今天分享一篇经典Linux协议栈文章,主要讲解Linux网络子系统,看完相信大家对协议栈又会加深不少,不光可以了解协议栈处理流程,方便定位问题,还可以学习一下怎么去设计一个可扩展的子系统,屏蔽不同层次 ...

  2. 一文搞定 | Linux 网络子系统

    今天分享一篇经典Linux协议栈文章,主要讲解Linux网络子系统,看完相信大家对协议栈又会加深不少,不光可以了解协议栈处理流程,方便定位问题,还可以学习一下怎么去设计一个可扩展的子系统,屏蔽不同层次 ...

  3. Linux中断子系统(四)之中断申请注册

    Linux中断子系统(四)之中断申请注册 备注:   1. Kernel版本:5.4   2. 使用工具:Source Insight 4.0   3. 参考博客: Linux中断子系统(一)中断控制 ...

  4. linux网络编程(四)线程池

    linux网络编程(四)线程池 为什么会有线程池? 实现简单的线程池 为什么会有线程池? 大多数的服务器可能都有这样一种情况,就是会在单位时间内接收到大量客户端请求,我们可以采取接受到客户端请求创建一 ...

  5. linux 驱动 (2)---Linux input子系统最清晰、详尽的分析

    Linux input子系统最清晰.详尽的分析 Linux input分析之二:解构input_handler.input_core.input_device 输入输出是用户和产品交互的手段,因此输入 ...

  6. linux网络子系统研究:数据收发简略流程图

    Linux网络子系统十分庞大复杂,总想着等自己全部弄明白后再动笔写些笔记,但实在太耗时.后来想通了,先从宏观上掌握大体框图,然后再研究细节. 本文先给出一张自己画的网络数据收发简略流程图,每个路径都可 ...

  7. Linux协议栈:基于ping流程窥探Linux网络子系统,及常用优化方法

    初识 Linux 网络栈及常用优化方法 RToax 2020年9月 初识 Linux 网络栈及常用优化方法 1. 文章简介 基于 ping 流程窥探 Linux 网络子系统,同时介绍各个模块的优化方法 ...

  8. 四年级计算机期末质量分析,四年级期末质量分析

    <四年级期末质量分析>由会员分享,可在线阅读,更多相关<四年级期末质量分析(2页珍藏版)>请在人人文库网上搜索. 1.四年级上学期英语期末测试质量分析一.试卷基本情况分析试题依 ...

  9. Android 8.1 PowerManagerService分析(四)——亮屏流程分析

    欢迎大家关注我的掘金帐号 我会在那里定期更新最新版本的Android Framework源码分析! 相关文章: [Android Framework] 8.1 PowerManagerService分 ...

  10. 【Linux从青铜到王者】第二十三篇:Linux网络基础第四篇之kcp协议

    系列文章目录 文章目录 系列文章目录 前言 一.kcp协议简介 二.kcp技术特性 1.RTO翻倍vs不翻倍 2.选择性重传 vs 全部重传 3.快速重传 4.延迟ACK vs 非延迟ACK 5.UN ...

最新文章

  1. matlab处理图像位置,MATLAB图像处理:我的直方图的最后一个位置出现了
  2. Shell主要逻辑源码级分析(1)——SHELL运行流程
  3. python爬虫框架排行榜-Python爬虫框架--pyspider初体验
  4. 理解SetWindowOrg,SetViewportOrg,SetWindowExt,SetViewportExt
  5. 源码学习【HashMap第一篇】HashMap到底是怎么put的?
  6. Activity生命周期的学习和验证
  7. leetcode(3)---寻找最大字符串
  8. led内部结构_科普PCB,DPC,陶瓷PCB对于LED封装有哪些差异? - led显示屏_高清led显示屏价格_led显示屏生产厂家...
  9. python 抓取微博评论破亿_一篇文章教会你使用Python定时抓取微博评论
  10. mysql、oracle知识点总结
  11. 机器学习实用指南_机器学习方法:实用指南
  12. html页面text固定长度,HTML中input type=text和type=password 显示的长度不一样
  13. Mac做深度学习开发【从无到有】
  14. LeetCode 852. Peak Index in a Mountain Array
  15. 2020 快手 被吊打面经
  16. 【2022年江西省研究生数学建模】水汽过饱和的核化除霾 31页论文解析
  17. HP服务器清除bios方法
  18. 移动app性能测试工具:Emmagee使用介绍
  19. Exp8 Web基础 20154328 常城
  20. excel中看着是空单元格,使用Ctrl+G定位空值却定位不到

热门文章

  1. Delphi 中的 procedure of object
  2. BZOJ 1715: [Usaco2006 Dec]Wormholes 虫洞
  3. UIBlock 按钮图片设置
  4. C# 只允许运行一个实例
  5. java 快速排序流程图_java简单快速排序实例解析
  6. SharePoint 使用ECMAscript对象模型来读取帖子列表
  7. log4j.properties中log4j.rootLogger 与log4j.rootCategory 有什么区别 .
  8. 算法导论9.1-1习题解答(二叉树)
  9. redis持久化方法
  10. maven:Java heap space内存不足错误的解决方法