范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文
国学影视

Linux内核网络之网络层接收消息分片组装

  在接收方,一个由发送方发出的原始 IP 数据报,其所有分片将被重新组合,然后才能提交给上层协议。
  而每个将被重新组合的 IP 数据报都用一个 ipq 结构实例来表示。  struct ipq { //用来将ipq_hash散列表链接成双向链表 struct hlist_node list; /*用来将ipq连接到全局链表ipq_lru_list链表中。ipq_lru_list 用于垃圾收集, 当ip组装模块消耗内存大于规定的上限时,会遍历该链表清除符合条件的分片*/ struct list_head lru_list; /* lru list member */ u32 user; //标识分片来源: 来自网络其他主机或是本地环回接口的分片、含有路由警告选项的IP分片  //以下四个字段的值都来源于ip首部,用来唯一确定分片来自哪个ip数据报 __be32 saddr; __be32 daddr; __be16 id; u8 protocol;  u8 last_in; #define COMPLETE 4 //所有分片已到达,可以进行组装; #define FIRST_IN 2 // 第一个分片到达,其特殊之处在于只有第一个分片包含了所有ip选项; #define LAST_IN 1 //最后一个分片已到达,最后一个分片带有原始数据包的长度信息   //用来链接已经接收到的分片 struct sk_buff *fragments; /* linked list of received fragments */ /*当前已接收到分片中offset最大的那个分片的offset值加上其长度值, 即分片末尾处在整个原始数据报中的位置, 因此当收到最后一个分片后该字段值将更新为原始数据报的长度 */  int len; /* total length of original datagram */ //已接收到的所有分片总长度,因此可以用len和meat来判断一个ip数据报的所有分片时否已到齐 int meat; //自旋锁,在smp环境下,处理ipq及分片链时需上锁 spinlock_t lock;  atomic_t refcnt; //引用计数 //组装超时定时器,组装分片非常消耗资源,防止无休止等待分片的到达 struct timer_list timer; /* when will this queue expire? */ //记录最后一个分片的到达时间,在组装数据报时用该值作为时间戳 struct timeval stamp; //接收最后一个分片的网络设备索引号。当分片组装失败时,用该设备发送组装失败icmp出错报文。 int iif; //已接收到分片的计数器,可通过对端信息块peer中的分片计数器和该分片计数器来防止DoS攻击 unsigned int rid; //记录发送方的一些信息 struct inet_peer *peer; };  /* Hash table. */  #define IPQ_HASHSZ 64  /* Per-bucket lock is easy to add now. */ static struct hlist_head ipq_hash[IPQ_HASHSZ];
  每个原始的 IP 数据报的所有分片以链表的形式保存在 ipq 结构 fragments 中。
  在网络层中,会根据每个原始的 IP 数据报首部中的( saddr, daddr, id, protocol, ipfrag_hash_rnd )计算一个 hash 值,然后将 ipq 结构放到对应的 ipq_hash[hash] 散列表中。
  因此,当一个新的分片 skb 到来时,根据( saddr, daddr, id, protocol, ipfrag_hash_rnd )计算出 hash 值,从 ipq_hash[hash] 散列表中找到 ipq 结构,然后把分片存放到 fragments 链表中。
  当 IP 分片到达本地时,先调用 ip_defrag 进行重组。  //ip数据报输入到本地 int ip_local_deliver(struct sk_buff *skb) { /* * Reassemble IP fragments. */ /*若接收到的ip报是分片,则调用ip_defrag进行重组,其标志为IP_DEFRAG_LOCAL_DELIVER*/ if (skb->nh.iph->frag_off & htons(IP_MF|IP_OFFSET)) { /*返回0,表示ip数据包分片还未到齐,重组未完成,直接返回; 非0,返回已完成重组的ip数据报skb缓存的指针,传到传输层进行处理*/ skb = ip_defrag(skb, IP_DEFRAG_LOCAL_DELIVER); if (!skb) return 0; }  /*通过netfilter处理后,调用ip_local_deliver_finish将组装完的ip报文传送到传输层进行处理*/ return NF_HOOK(PF_INET, NF_IP_LOCAL_IN, skb, skb->dev, NULL, ip_local_deliver_finish); }
  分片组装
  分片组装的流程如下:
  ip_defrag
  -->  ip_find  在ipq散列表中查找分片所属的ipq,若找不到则新建一个ipq
  --> ip_frag_queue  将分片插入到ipq分片链表的适当位置
  --> ip_frag_reasm  原始数据报的所有分片全部到达,组装分片
  具体实现细节如下
  ip_defrag  /* Process an incoming IP datagram fragment. */ /* 对分片进行组装 skb : 新接收到的ip数据报 user : 分片来源 */ struct sk_buff *ip_defrag(struct sk_buff *skb, u32 user) { struct iphdr *iph = skb->nh.iph; struct ipq *qp; struct net_device *dev;  IP_INC_STATS_BH(IPSTATS_MIB_REASMREQDS);  /* Start by cleaning up the memory. */ //若iqp散列表消耗的内存大于指定的值,则ip_evictor()清理分片 if (atomic_read(&ip_frag_mem) > sysctl_ipfrag_high_thresh) ip_evictor();  //获取接收数据报的网络设备指针 dev = skb->dev;  /* Lookup (or create) queue header */ /*在ipq散列表中查找分片所属的ipq。若找不到则新建一个ipq,若返回为null,则说明 查找及创建失败*/ if ((qp = ip_find(iph, user)) != NULL) { struct sk_buff *ret = NULL;  spin_lock(&qp->lock); //将分片插入到ipq分片链表的适当位置 ip_frag_queue(qp, skb);  /*当ipq的第一个分片和最后一个分片都已收到,且若已接收数据报的总长度与原始数据报的长度相等, 则说明该原始数据报的所有分片都已到齐*/ if (qp->last_in == (FIRST_IN|LAST_IN) && qp->meat == qp->len) //调用ip_frag_reasm组装分片 ret = ip_frag_reasm(qp, dev);  spin_unlock(&qp->lock);  //删除iqp及其所有分片 ipq_put(qp, NULL);  return ret; }  IP_INC_STATS_BH(IPSTATS_MIB_REASMFAILS);  //释放分片 kfree_skb(skb); return NULL; }
  当一个分片到达后,按照偏移量插入到 ipq 分片链表的适当位置,如下图:
  ip_find
  在 ipq 散列表中查找分片所属的 ipq 结构如下: //根据分片的ip首部以及user标志在ipq散列表中找对应的ipq,若没找到,则为其创建新的ipq  static inline struct ipq *ip_find(struct iphdr *iph, u32 user) { __be16 id = iph->id; __be32 saddr = iph->saddr; __be32 daddr = iph->daddr; __u8 protocol = iph->protocol; unsigned int hash; struct ipq *qp; struct hlist_node *n;  read_lock(&ipfrag_lock); //计算hash值 hash = ipqhashfn(id, saddr, daddr, protocol); /*遍历该桶的ipq链表查找对应的ipq,若找到则返回该ipq,否则说明这是一个新的ip数据报分片, 需要创建一个新的ipq*/ hlist_for_each_entry(qp, n, &ipq_hash[hash], list) { if(qp->id == id && qp->saddr == saddr && qp->daddr == daddr && qp->protocol == protocol && qp->user == user) { atomic_inc(&qp->refcnt); read_unlock(&ipfrag_lock); return qp; } } read_unlock(&ipfrag_lock); //返回新建的ipq return ip_frag_create(iph, user); }
  ip_frag_create
  创建 ipq 结构,并初始化其组装超时定时器。  /*每当接收到一个属于新的ip数据报的分片时,会为其创建对应的iqp,并初始化其组装超时定时器*/ static struct ipq *ip_frag_create(struct iphdr *iph, u32 user) { struct ipq *qp;  if ((qp = frag_alloc_queue()) == NULL) goto out_nomem;  qp->protocol = iph->protocol; qp->last_in = 0; qp->id = iph->id; qp->saddr = iph->saddr; qp->daddr = iph->daddr; qp->user = user; qp->len = 0; qp->meat = 0; qp->fragments = NULL; qp->iif = 0; qp->peer = sysctl_ipfrag_max_dist ? inet_getpeer(iph->saddr, 1) : NULL;  /* Initialize a timer for this entry. */ init_timer(&qp->timer); qp->timer.data = (unsigned long) qp; /* pointer to queue */ qp->timer.function = ip_expire; /* expire function */ spin_lock_init(&qp->lock); atomic_set(&qp->refcnt, 1);  return ip_frag_intern(qp);  out_nomem: LIMIT_NETDEBUG(KERN_ERR "ip_frag_create: no memory left ! "); return NULL; }
  ip_frag_intern  //将新建的ipq插入到ipq散列表中和ipq_lru_list中 static struct ipq *ip_frag_intern(struct ipq *qp_in) { struct ipq *qp; unsigned int hash;  write_lock(&ipfrag_lock); //计算hash值 hash = ipqhashfn(qp_in->id, qp_in->saddr, qp_in->daddr, qp_in->protocol);  qp = qp_in;  /* 安装ipq的组装超时定时器,定时为 sysctl_ipfrag_time */ if (!mod_timer(&qp->timer, jiffies + sysctl_ipfrag_time)) atomic_inc(&qp->refcnt);  //递增ipq的引用计数 atomic_inc(&qp->refcnt); //将ipq插入到ipq散列表和lru_list中 hlist_add_head(&qp->list, &ipq_hash[hash]); INIT_LIST_HEAD(&qp->lru_list); list_add_tail(&qp->lru_list, &ipq_lru_list);  //对ipq的数量进行计数 ip_frag_nqueues++; write_unlock(&ipfrag_lock); return qp; }
  ip_frag_queue  /* Add new segment to existing queue. */ /*将分片skb添加到iqp指定的ipq分片链表中  qp : 将添加ip分片到ipq skb : 接收到 待添加到ipq中的ip分片 */ static void ip_frag_queue(struct ipq *qp, struct sk_buff *skb) { struct sk_buff *prev, *next; int flags, offset; int ihl, end; //对分片已全部接收到的ipq,则释放该分片后返回  if (qp->last_in & COMPLETE) goto err;  /* 若不是有本地生成的分片,则调用ip_frag_too_far检测该分片是否存在DoS攻击嫌疑, 。若受到攻击,则调用ip_frag_reinit释放所有的ipq所有分片*/ if (!(IPCB(skb)->flags & IPSKB_FRAG_COMPLETE) && unlikely(ip_frag_too_far(qp)) && unlikely(ip_frag_reinit(qp))) { ipq_kill(qp); goto err; } //取出ip首部中的标志位、片偏移及首部长度字段,并计算片偏移值和首部长度值 offset = ntohs(skb->nh.iph->frag_off); flags = offset & ~IP_OFFSET; //ip首部中的片偏移字段为13位,表示的是8字节的倍数 offset &= IP_OFFSET; offset <<= 3; /* offset is in 8-byte chunks */ ihl = skb->nh.iph->ihl * 4;  /* Determine the position of this fragment. */ //计算分片末尾处在原始数据报中的位置 end = offset + skb->len - ihl;  /* Is this the final fragment? */ /*若是最后一个分片*/ if ((flags & IP_MF) == 0) { /* If we already have some bits beyond end * or have different end, the segment is corrrupted. */ /* 先对分片进行检查,若其末尾小于原始包长度, 或者ipq已有LAST_IN标志且分片末尾不等于原始包长度,则出错*/  if (end < qp->len ||  ((qp->last_in & LAST_IN) && end != qp->len)) goto err;  //设置LAST_IN标志,将完整数据报长度存储在ipq的len字段中 qp->last_in |= LAST_IN; qp->len = end; } else { //不是最后一个分片,其数据长度又不是8字节对齐,则将其截为8字节对齐 if (end&7) { end &= ~7;  /* 若需要计算校验和,则强制设置有软件来计算校验和,因为截断了ip有效负载,改变了长度, 需重新计算校验和 */ if (skb->ip_summed != CHECKSUM_UNNECESSARY) skb->ip_summed = CHECKSUM_NONE; } /*在最后一个分片没有到达的情况下,若当前分片的末尾在整个数据报中的位置大于ipq中的len值,则更新len。 因为ipq中的len字段始终保持所有已接收到的分片中分片末尾在数据报中的位置的最大值,而只有在收到最后一个分片 后。len值才是整个数据报的长度*/ if (end > qp->len) { /* Some bits beyond end -> corruption. */ //若此数据报有异常,则直接丢弃 if (qp->last_in & LAST_IN) goto err;  qp->len = end; } }  //若分片的数据区长度为0,则该分片异常,直接丢弃 if (end == offset) goto err;  //调用pskb_pull去掉ip首部,只保留数据部分 if (pskb_pull(skb, ihl) == NULL) goto err;  //将skb数据区长度调整为 end-offset, ip有效负载长度 if (pskb_trim_rcsum(skb, end-offset)) goto err;  /* Find out which fragments are in front and at the back of us * in the chain of fragments so far. We must know where to put * this fragment, right? */ /*确定分片在分片链表中的位置。因为各分片很可能不按照顺序到达,而ipq分片链表上的分片 是按照分片偏移值从小到大的顺序连接在一起的*/ prev = NULL; for(next = qp->fragments; next != NULL; next = next->next) { if (FRAG_CB(next)->offset >= offset) break; /* bingo! */ prev = next; }  /* We found where to put this one. Check for overlap with * preceding fragment, and, if needed, align things so that * any overlaps are eliminated. */ /*检测和上一个分片的数量是否有重叠,i是重叠部分的长度,若有重叠,调用pskb_pull去掉重叠的部分*/  if (prev) { int i = (FRAG_CB(prev)->offset + prev->len) - offset;  if (i > 0) { offset += i; if (end <= offset) goto err; if (!pskb_pull(skb, i)) goto err; if (skb->ip_summed != CHECKSUM_UNNECESSARY) skb->ip_summed = CHECKSUM_NONE; } }  /*若和后一个分片有重叠,则还需判断重叠部分的数据长度是否超过下一个分片的数据长度,若没有超过则调整 下一个分片,超过这需要释放下一个分片后再检查与后面第二个分片的数据是否有重叠,如此反复,直到完成后面对 所有分片的检测。调整分片的偏移值、已接收分片总长度等*/ while (next && FRAG_CB(next)->offset < end) { int i = end - FRAG_CB(next)->offset; /* overlap is "i" bytes */  if (i < next->len) { /* Eat head of the next overlapped fragment * and leave the loop. The next ones cannot overlap. */ if (!pskb_pull(next, i)) goto err; FRAG_CB(next)->offset += i; qp->meat -= i; if (next->ip_summed != CHECKSUM_UNNECESSARY) next->ip_summed = CHECKSUM_NONE; break; } else { struct sk_buff *free_it = next;  /* Old fragment is completely overridden with * new one drop it. */ next = next->next;  if (prev) prev->next = next; else qp->fragments = next;  qp->meat -= free_it->len; frag_kfree_skb(free_it, NULL); } }  //记录当前分片的偏移值 FRAG_CB(skb)->offset = offset;  /* Insert this fragment in the chain of fragments. */ //将当前的分片插入到ipq分片队列中的相应位置 skb->next = next; if (prev) prev->next = skb; else qp->fragments = skb;  if (skb->dev) qp->iif = skb->dev->ifindex; skb->dev = NULL; //更新ipq的时间戳 skb_get_timestamp(skb, &qp->stamp); //累计该分片已收到的分片总长度 qp->meat += skb->len;  //累计分片组装模块所占的内存 atomic_add(skb->truesize, &ip_frag_mem);  //若片偏移值为0,说明当前分片为第一个分片,设置FIRST_IN if (offset == 0) qp->last_in |= FIRST_IN;  write_lock(&ipfrag_lock); //调整所属ipq在ipq_lru_list中的位置,这是为了在占用内存超过阈值时可以先释放最久未用的那些分片 list_move_tail(&qp->lru_list, &ipq_lru_list); write_unlock(&ipfrag_lock);  return;  err: kfree_skb(skb); }
  对于其中计算分片末尾处在原始数据报中的位置的地方
  //计算分片末尾处在原始数据报中的位置
  end  = offset  + skb->len  - ihl;
  有关ihl、offset、len 和 end 的关系,如下图:
  ip_frag_reasm  /*组装已到齐的所有分片。当原始数据报所有分片都已到齐时,调用此函数组装分片 qp : 存储待组装分片队列的ipq dev : 输入分片的网络设备 */ static struct sk_buff *ip_frag_reasm(struct ipq *qp, struct net_device *dev) { struct iphdr *iph; struct sk_buff *fp, *head = qp->fragments; int len; int ihlen;  /*组装前,先将此ipq节点从ipq散列表和ipq_lru_list表中断开,并删除定时器*/ ipq_kill(qp);  BUG_TRAP(head != NULL); BUG_TRAP(FRAG_CB(head)->offset == 0);  /* Allocate a new buffer for the datagram. */ //计算原始数据报包括ip首部的总长度, ihlen = head->nh.iph->ihl*4; len = ihlen + qp->len;  //若该长度值超过64k则丢弃 if(len > 65535) goto out_oversize;  /* Head of list must not be cloned. */ /*在组装分片时,所有的分片都会组装到第一个分片上,因此第一个分片不能时克隆的, 若是克隆的,则需为分片组装重新分配一个skb*/ if (skb_cloned(head) && pskb_expand_head(head, 0, 0, GFP_ATOMIC)) goto out_nomem;  /* If the first fragment is fragmented itself, we split * it to two chunks: the first with data and paged part * and the second, holding only fragments. */ /*分片队列的第一个skb不能既带有数据,又带有分片,也即是其frag_list上不能有分片skb, 若有则重新分配一个skb。最终效果时,head自身不包括数据,其frag_list上连接着所有分片的skb。 这也是skb的一种表现形式,不一定是一个连续的数据块,但最终会调用skb_linearize()将这些数据都复制到 一个连续的数据块中*/ if (skb_shinfo(head)->frag_list) { struct sk_buff *clone; int i, plen = 0;  if ((clone = alloc_skb(0, GFP_ATOMIC)) == NULL) goto out_nomem; clone->next = head->next; head->next = clone; skb_shinfo(clone)->frag_list = skb_shinfo(head)->frag_list; skb_shinfo(head)->frag_list = NULL; for (i=0; inr_frags; i++) plen += skb_shinfo(head)->frags[i].size; clone->len = clone->data_len = head->data_len - plen; head->data_len -= clone->len; head->len -= clone->len; clone->csum = 0; clone->ip_summed = head->ip_summed; atomic_add(clone->truesize, &ip_frag_mem); }  /*把所有分片组装起来,即将分片连接到第一个skb中的frag_list上,同时还需要遍历所有分片,重新计算ip数据报长度以及校验和等。*/ skb_shinfo(head)->frag_list = head->next; skb_push(head, head->data - head->nh.raw); atomic_sub(head->truesize, &ip_frag_mem);  for (fp=head->next; fp; fp = fp->next) { head->data_len += fp->len; head->len += fp->len; if (head->ip_summed != fp->ip_summed) head->ip_summed = CHECKSUM_NONE; else if (head->ip_summed == CHECKSUM_COMPLETE) head->csum = csum_add(head->csum, fp->csum); head->truesize += fp->truesize; atomic_sub(fp->truesize, &ip_frag_mem); }  head->next = NULL; head->dev = dev; skb_set_timestamp(head, &qp->stamp);  //重置首部长度、片偏移、标志位和总长度 iph = head->nh.iph; iph->frag_off = 0; iph->tot_len = htons(len); IP_INC_STATS_BH(IPSTATS_MIB_REASMOKS); //既然各分片都已处理完,释放ipq的分片队列 qp->fragments = NULL; return head;  out_nomem: LIMIT_NETDEBUG(KERN_ERR "IP: queue_glue: no memory for gluing " "queue %p ", qp); goto out_fail; out_oversize: if (net_ratelimit()) printk(KERN_INFO "Oversized IP packet from %d.%d.%d.%d. ", NIPQUAD(qp->saddr)); out_fail: IP_INC_STATS_BH(IPSTATS_MIB_REASMFAILS); return NULL; }
  组装的过程就是把所有分片组装起来,即将分片连接到第一个 skb 中的 frag_list 上,并返回组装后的数据报文。
  ipq 散列表的重组
  所有的分片重组都是通过 ipq 散列表进行的。随着后续 ipq 的添加或删除,使得散列表中的 ipq 的分布变得不均匀,处理性能会大大降低,因此需要定时对散列表进行重新组装。这样做同时也是为了防御 DoS 攻击。
  散列表的定时重组是通过 ipfrag_secret_timer定时器实现的,在 ipfrag_init() 中对 ipfrag_secret_timer 定时器的初始化。在该函数中还初始化了 ipfrag_hash_rnd 变量,该变量主要用来与 IP 首部中的源地址、目的地址等构成 ipq 散列表的关键字。每次重组时都会将 ipfrag_hash_rnd 更新为一个新的随机值,并重新设置 ipfrag_secret_timer 定时器,时间跨度为 10 min。
  ipfrag_init  void ipfrag_init(void) { ipfrag_hash_rnd = (u32) ((num_physpages ^ (num_physpages>>7)) ^ (jiffies ^ (jiffies >> 6)));  init_timer(&ipfrag_secret_timer); ipfrag_secret_timer.function = ipfrag_secret_rebuild; ipfrag_secret_timer.expires = jiffies + sysctl_ipfrag_secret_interval; add_timer(&ipfrag_secret_timer); }
  ipfrag_secret_rebuild  //对全局的ipq散列表进行重组 static void ipfrag_secret_rebuild(unsigned long dummy) { unsigned long now = jiffies; int i;  write_lock(&ipfrag_lock); //重新获取ipfrag_hash_rnd随机值 get_random_bytes(&ipfrag_hash_rnd, sizeof(u32)); //遍历ipq散列表中所有ipq,根据新的ipfrag_hash_rnd值把这些ipq重新连接到散列表对应的桶中 for (i = 0; i < IPQ_HASHSZ; i++) { struct ipq *q; struct hlist_node *p, *n;  hlist_for_each_entry_safe(q, p, n, &ipq_hash[i], list) { unsigned int hval = ipqhashfn(q->id, q->saddr, q->daddr, q->protocol);  if (hval != i) { hlist_del(&q->list);  /* Relink to new hash chain. */ hlist_add_head(&q->list, &ipq_hash[hval]); } } } write_unlock(&ipfrag_lock); //重新设置重构定时器的下次到期时间 mod_timer(&ipfrag_secret_timer, now + sysctl_ipfrag_secret_interval); }
  清除超时的 IP 分片
  在复杂的网络环境下,一个 IP 数据报的分片有可能不能全部抵达目的地址,而该数据报已到达的分片会占用大量的资源,此外也为了防止抵御 DoS 攻击,因此需要设置一个时钟,一旦超时,数据报的分片还未全部到达,则将其已到达的分片全部清除。
  每当收到一个属于新的 IP 数据报分片时,在为其创建 ipq 时,会初始化其超时定时器 ip_expire  /*每当接收到一个属于新的ip数据报的分片时,会为其创建对应的iqp,并初始化其组装超时定时器*/ static struct ipq *ip_frag_create(struct iphdr *iph, u32 user) { ...  qp->timer.function = ip_expire; /* expire function */  ... }
  ip_expire  //组装超时定时器例程,当定时器激活时,清除在规定时间内没有完成组装的ipq及其所有分片  static void ip_expire(unsigned long arg) { struct ipq *qp = (struct ipq *) arg;  spin_lock(&qp->lock); //当前已是COMPLETE状态,不做处理,直接跳到释放ipq及其所有的分片处 if (qp->last_in & COMPLETE) goto out;  //将ipq从ipq散列表和ipq_lru_list链表中删除 ipq_kill(qp);  IP_INC_STATS_BH(IPSTATS_MIB_REASMTIMEOUT); IP_INC_STATS_BH(IPSTATS_MIB_REASMFAILS);  //若第一个分片已经到达,则发送分片组装超时ICMP出错报文 if ((qp->last_in&FIRST_IN) && qp->fragments != NULL) { struct sk_buff *head = qp->fragments; /* Send an ICMP "Fragment Reassembly Timeout" message. */ if ((head->dev = dev_get_by_index(qp->iif)) != NULL) { icmp_send(head, ICMP_TIME_EXCEEDED, ICMP_EXC_FRAGTIME, 0); dev_put(head->dev); } } out: spin_unlock(&qp->lock);  //释放ipq及其所有的ip分片 ipq_put(qp, NULL); }
  垃圾收集
  为了控制 IP 组装所占用的内存,设置了两个阈值 ipfrag_high_thresh 和 ipfrag_low_thresh 。当前 ipq 散列表占用的内存量存储在全局变量 ip_frag_mem 中,当 ip_frag_mem 大于 ipfrag_high_thresh 时,需要调用 ip_evictor() 对散列表进行清理,直到 ip_frag_mem 降低到 ipfrag_low_thresh 。这两个阈值可以在系统运行时通过 proc 文件系统修改。
  ip_evictor
  该方法主要对ipq中的分片进行条件性的清理。在所有的ipq中,若分片没有到齐,则被删除。  static void ip_evictor(void) { struct ipq *qp; struct list_head *tmp; int work;  /*在清理前再次对当前消耗的内存量做测试,若少于sysctl_ipfrag_low_thresh,则不进行清理*/ work = atomic_read(&ip_frag_mem) - sysctl_ipfrag_low_thresh; if (work <= 0) return;  while (work > 0) { read_lock(&ipfrag_lock); //若lru链表为空,解锁后返回 if (list_empty(&ipq_lru_list)) { read_unlock(&ipfrag_lock); return; } tmp = ipq_lru_list.next; qp = list_entry(tmp, struct ipq, lru_list); //递增ipq引用计数 atomic_inc(&qp->refcnt); read_unlock(&ipfrag_lock);  //在删除分片前后要做同步保护 spin_lock(&qp->lock); /*若分片还未到齐,则ipq从散列表及lru链表中删除。ipq_kill只删除不释放*/ if (!(qp->last_in&COMPLETE)) ipq_kill(qp); spin_unlock(&qp->lock);  //ipq_put真正删除ipq及其所有分片 ipq_put(qp, &work); IP_INC_STATS_BH(IPSTATS_MIB_REASMFAILS); } }

一周岁的宝宝一般过多久可以自己独立走路呢?我堂姐8个月就会走路了,人们都说她是劳碌命,她是家里的老大,下面还有3个弟弟妹妹。爸爸上班忙,妈妈身体不好,她从小就承担了很多家务。那时候也不流行上学,她就跟着大人们和大人干一样的引产是怎么回事?引产是怎么回事?引产是指女性妊娠14周后,用人工的方法使子宫产生收缩,排除胎儿及其附属物结束妊娠。怀孕1427周,由于各种原因孕妇要求放弃胎儿整个孕期发现胎儿有严重的畸形死胎或当孕你认为肖战的颜值怎么样?说实话,我今年46了,从来没有追过星,当时女儿粉肖战,我还不以为然,娱乐圈小帅哥太多了,哪有统一的标准呢?后来发生了227,我也不了解真相,只知道百家下场黑他,从心里感觉这个孩子挺英雄联盟岚切装备是否是导致ADC衰落的罪魁祸首?你怎么看?英雄联盟的改版一直都对AD不太友好,特别是从8。11版本后ADC的地位遭受了有史以来最大的挑战。8。11版本的到来让下路怪物横行,几乎所有英雄都有打下路的道理,只有ADC不适合在下车牌4个螺丝钉孔,装两个螺丝钉会怎么样?你有没有听说过这样的规定,车牌如果未按照要求悬挂,包括前后车牌没有各装4枚螺丝的,也就是用8枚,一律都要扣12分。对于这个要求,有的人表示,我的车牌全部加起来只有4个螺丝孔,怎么能未来iPhoneSE还值得买吗?下周二凌晨,一年一度的科技春晚就将在苹果总部的乔布斯剧院举行,届时拥有浴霸三摄的iPhone11系列就将与全世界的果粉见面。而在此次活动举行之前,例行的各种爆料自然必不可少,但在关马上就高三了,但是我连初中的英语都看不懂,语法什么的完全不知道,现在想学了要怎么学?如果你连初中英语都看不懂你是怎么考上高中的。幸好,你还有时间补救。给你如下建议1。买一本高考要求的3500个左右单词词汇数。2。买一本新概念二看,网上有视频教学。3。买一套高考真题土豆丝怎么做最好吃?土豆丝咋样做最好吃?这个季节,当然是来一份凉拌土豆丝最好吃,开胃解腻。每到酷夏,太阳下山后温度才能慢慢下降。忙碌了一天的三五好友,袒胸赤膊短裤人字拖,一份凉菜烧烤冰镇啤酒。喧闹的大该如何理解IaaSPaaSSaaS?这里从官方通俗的两个角度解读下IaaSPaaSSaaS官方而言1。基础设施即服务(IaaSInfrastructureasaService)把计算基础(服务器网络技术存储和数据中心是电信300m的宽带好,还是移动500M的宽带好?究竟是电信300M网络好还是移动500M?那么就以我们西安地区,我现在正在使用的电信200M,和移动200M进行对比电信网络通过网络下载速度测试以及网络延迟,包括数据丢包来测试,网踏板摩托车哪款比较适合摩旅?一般人都会选择骑士车摩旅的了,踏板本身先天不足,不是太适合摩旅,如果要骑踏板摩旅,也应该选择路好一点的地方了。一定要选择踏板车,可以找大踏板,大动力大油箱和高离地,水冷的。400公
蒙阴70万亩桃花次第盛开3月25日,蒙阴县在孟良崮举行70万亩桃花从这里胜开崮乡旅游赏花季开幕仪式。蒙阴是中国蜜桃第一县,每到春暖花开时节,70多万亩桃花自南部孟良崮到北部岱崮次第开放,随山势起伏绵延,灿福州光明港将添温泉水乐园配套一批网红游乐设施就在市中心,9米多高的环形温泉滑道空中悬挑无边际温泉泳池观光螺旋楼梯项目沙盘效果图正在建设中的光明港温泉水乐园力争明年元旦开放运营,将给游客带来全新的温泉游乐体验!力争明年元旦开放向春而行,看一路花开!金寨文旅推介会走进鲜美烟台记者秦雪丽通讯员旅宣3月25日,记者从烟台市文化和旅游局获悉,3月24日下午,向春而行看一路花开金寨烟台文旅推介会在烟台百纳瑞汀酒店举办。本次推荐会得到了烟台市芝罘区文化和旅游局烟2023年3月25日IOS的AppStore软件限免8个APP推荐1。解压缩全能王信息原价36元,中文软件,无内购无广告。软件描述一款解压缩文档的应用进程,它提供压缩文件预览,里面的excel,word,ppt,txt等常用文件格式都支持预览,同你的孩子需不需要牙齿矫正,看完这篇就知道了该不该带孩子做牙齿矫正?什么时候去矫正?矫正之后会变怎么样这些问题困扰着那些想给孩子矫正但还在犹豫的家长。牙齿矫正也叫牙齿正畸,主要是通过矫正装置来调整牙齿和颌面骨之间的关系,从而宝妈囤货必备贵阳孕婴童博览会盛大开幕万千商品一站购齐3月24日,由贵阳世纪金源购物中心童兜天地云贵区域主办的2023贵阳孕婴童博览会,在贵阳世纪金源购物中心盛大启幕。活动为期三天,旨在为消费者提供一站式孕婴童全品类商品采购平台,成为CBA新动态!杜峰提前续约周琦被新疆兜售辽篮造谣者被揪出2023年CBA全明星赛重在进行中,广东男篮教练杜峰任南区主教练,广东男篮为表示诚意,给杜峰教练献上一份五年的续约合同,双方完成提前续约事宜。杜峰教练在广东男篮执教已经有十个年头,超燃开跑!深圳巴士集团第二分公司为盐田山海半程马拉松赛事保驾护航来源读特3月26日上午,盐田区成功举办了首届深圳盐田山海半程马拉松。深圳巴士集团第二分公司采取多种措施确保公交服务质量,赛事当天,共抽调80台运力,输送乘客8000余人次,全力做好与苏州结缘40年,华南虎有了专属卡通形象标识昨天上午,一列满载游客的科普小火车,从上方山北部出发,驶向苏州市动物园中国华南虎苏州培育基地,为华南虎举办卡通形象发布会。这个由苏州小学生创作的卡通华南虎,头顶祥云手藏爱心,凝聚了苏州滑稽戏的魅力在哪?著名滑稽戏表演艺术家告诉你今天(3月26日)下午,非遗走进苏图勤为大家乐图书分享会在苏州第二图书馆举行。著名滑稽戏表演艺术家滑稽梅花第一人张克勤携其传记勤为大家乐在此亮相,和大家分享他的艺术生涯和苏州滑稽戏和美乡村莓好未来农安合隆镇第二届草莓文化节开幕春暖三月,草莓飘香。3月25日,农安县合隆镇和美乡村,莓好未来第二届草莓文化节暨亲子研学游活动在陈家店生态园开幕。本届草莓节设置了草莓产品展示展销农副产品展销草莓DIY制作亲子研学