TCP/IP协议栈在Linux内核中的运行时序分析
本次报告主要分为两个部分,第一部分介绍Linux内核任务调度的基本概念及过程第二部分详细分析send和receive调用过程的源码以及运行时环境。TCP/IP协议栈运行的时序图进行总结和概括。
@
Linux基础
Linux内核任务调度
CFS是的实现,分为4个主要的部分来分析。

时间记账
所有的调度器都必须对进程的运行时间记账,换句话说就是要知道当前调度周期内,进程还剩下多少个时间片可用(这将会是抢占的一个重要标准)
1. 调度器实体结构
CFS中用于记录进程运行时间的数据结构为“调度实体”,这个结构体被定义在<linux/sched.h>中:
struct sched_entity {
/* 用于进行调度均衡的相关变量,主要跟红黑树有关 */
struct load_weight load; // 权重,跟优先级有关
unsigned long runnable_weight; // 在所有可运行进程中所占的权重
struct rb_node run_node; // 红黑树的节点
struct list_head group_node; // 所在进程组
unsigned int on_rq; // 标记是否处于红黑树运行队列中
u64 exec_start; // 进程开始执行的时间
u64 sum_exec_runtime; // 进程总运行时间
u64 vruntime; // 虚拟运行时间,下面会给出详细解释
u64 prev_sum_exec_runtime; // 进程在切换CPU时的sum_exec_runtime,简单说就是上个调度周期中运行的总时间
u64 nr_migrations;
struct sched_statistics statistics;
// 以下省略了一些在特定宏条件下才会启用的变量
}`
2. 虚拟实时 (vruntime)
现在我们来谈谈上面结构体中的vruntime变量所表示的意义。我们称它为“虚拟运行时间”,该运行时间的计算是经过了所有可运行进程总数的标准化(简单说就是加权的)。它以ns为单位,与定时器节拍不再相关。
可以认为这是CFS为了能够实现理想多任务处理而不得不虚拟的一个新的时钟,具体地讲,一个进程的vruntime会随着运行时间的增加而增加,但这个增加的速度由它所占的权重load来决定。
结果就是权重越高,增长越慢:所得到的调度时间也就越小 —— CFS用它来记录一个程序到底运行了多长时间以及还应该运行多久。
下面我们来看一下这个记账功能的实现源码(kernel/sched/fair.c)
`/*
* Update the current task's runtime statistics.
*/
static void update_curr(struct cfs_rq *cfs_rq)
{
struct sched_entity *curr = cfs_rq->curr;
u64 now = rq_clock_task(rq_of(cfs_rq));
u64 delta_exec;
if (unlikely(!curr))
return;
// 获得从最后一次修改负载后当前任务所占用的运行总时间
delta_exec = now - curr->exec_start;
if (unlikely((s64)delta_exec <= 0))
return;
// 更新执行开始时间
curr->exec_start = now;
schedstat_set(curr->statistics.exec_max,
max(delta_exec, curr->statistics.exec_max));
curr->sum_exec_runtime += delta_exec;
schedstat_add(cfs_rq->exec_clock, delta_exec);
// 计算虚拟时间,具体的转换算法写在clac_delta_fair函数中
curr->vruntime += calc_delta_fair(delta_exec, curr);
update_min_vruntime(cfs_rq);
if (entity_is_task(curr)) {
struct task_struct *curtask = task_of(curr);
trace_sched_stat_runtime(curtask, delta_exec, curr->vruntime);
cgroup_account_cputime(curtask, delta_exec);
account_group_exec_runtime(curtask, delta_exec);
}
account_cfs_rq_runtime(cfs_rq, delta_exec);
}`
该函数计算了当前进程的执行时间,将其存放在delta_exec变量中,然后使用clac_delta_fair函数计算对应的虚拟运行时间,并更新vruntime值。
这个函数是由系统定时器周期性调用的(无论进程的状态是什么),因此vruntime可以准确地测量给定进程的运行时间,并以此为依据推断出下一个要运行的进程是什么。
进程选择
这里便是调度的核心部分,用一句话来梗概CFS算法的核心就是选择具有最小vruntime的进程作为下一个需要调度的进程。
为了实现选择,当然要维护一个可运行的进程队列(教科书上常说的ready队列),CFS使用了红黑树来组织这个队列。
红黑树是一种非常著名的数据结构,但这里我们不讨论它的实现和诸多特性(过于复杂),我们记住:红黑树是一种自平衡二叉树,再简单一点,它是一种以树节点方式储存数据的结构,每个节点对应了一个键值,利用这个键值可以快速索引树上的数据,并且它可以按照一定的规则自动调整每个节点的位置,使得通过键值检索到对应节点的速度和整个树节点的规模呈指数比关系。
1. 找到下一个任务节点
先假设一个红黑树储存了系统中所有的可运行进程,节点的键值就是它们的vruntime,CFS现在要找到下一个需要调度的进程,那么就是要找到这棵红黑树上键值最小的那个节点:就是最左叶子节点。
实现此过程的源码如下(kernel/sched/fair.c):
static struct sched_entity *
pick_next_entity(struct cfs_rq *cfs_rq, struct sched_entity *curr)
{
struct sched_entity *left = __pick_first_entity(cfs_rq);
struct sched_entity *se;
/*
* If curr is set we have to see if its left of the leftmost entity
* still in the tree, provided there was anything in the tree at all.
*/
if (!left || (curr && entity_before(curr, left)))
left = curr;
se = left; /* ideally we run the leftmost entity */
/*
* 下面的过程主要针对一些特殊情况,我们在此不做讨论
*/
if (cfs_rq->skip == se) {
struct sched_entity *second;
if (se == curr) {
second = __pick_first_entity(cfs_rq);
} else {
second = __pick_next_entity(se);
if (!second || (curr && entity_before(curr, second)))
second = curr;
}
if (second && wakeup_preempt_entity(second, left) < 1)
se = second;
}
if (cfs_rq->last && wakeup_preempt_entity(cfs_rq->last, left) < 1)
se = cfs_rq->last;
if (cfs_rq->next && wakeup_preempt_entity(cfs_rq->next, left) < 1)
se = cfs_rq->next;
clear_buddies(cfs_rq, se);
return se;
}
复制代码
2. 向队列中加入新的进程
向可运行队列中插入一个新的节点,意味着有一个新的进程状态转换为可运行,这会发生在两种情况下:一是当进程由阻塞态被唤醒,二是fork产生新的进程时。
将其加入队列的过程本质上来说就是红黑树插入新节点的过程:
static void
enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
bool renorm = !(flags & ENQUEUE_WAKEUP) || (flags & ENQUEUE_MIGRATED);
bool curr = cfs_rq->curr == se;
/*
* 如果要加入的进程就是当前正在运行的进程,重新规范化vruntime
* 然后更新当前任务的运行时统计数据
*/
if (renorm && curr)
se->vruntime += cfs_rq->min_vruntime;
update_curr(cfs_rq);
/*
* Otherwise, renormalise after, such that we're placed at the current
* moment in time, instead of some random moment in the past. Being
* placed in the past could significantly boost this task to the
* fairness detriment of existing tasks.
*/
if (renorm && !curr)
se->vruntime += cfs_rq->min_vruntime;
/*
* 更新对应调度器实体的各种记录值
*/
update_load_avg(cfs_rq, se, UPDATE_TG | DO_ATTACH);
update_cfs_group(se);
enqueue_runnable_load_avg(cfs_rq, se);
account_entity_enqueue(cfs_rq, se);
if (flags & ENQUEUE_WAKEUP)
place_entity(cfs_rq, se, 0);
check_schedstat_required();
update_stats_enqueue(cfs_rq, se, flags);
check_spread(cfs_rq, se);
if (!curr)
__enqueue_entity(cfs_rq, se); // 真正的插入过程
se->on_rq = 1;
if (cfs_rq->nr_running == 1) {
list_add_leaf_cfs_rq(cfs_rq);
check_enqueue_throttle(cfs_rq);
}
}
复制代码
上面的函数主要用来更新运行时间和各类统计数据,然后调用__enqueue_entity()来把数据真正插入红黑树中:
static void __enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
struct rb_node **link = &cfs_rq->tasks_timeline.rb_root.rb_node;
struct rb_node *parent = NULL;
struct sched_entity *entry;
bool leftmost = true;
/*
* 在红黑树中搜索合适的位置
*/
while (*link) {
parent = *link;
entry = rb_entry(parent, struct sched_entity, run_node);
/*
* 具有相同键值的节点会被放在一起
*/
if (entity_before(se, entry)) {
link = &parent->rb_left;
} else {
link = &parent->rb_right;
leftmost = false;
}
}
rb_link_node(&se->run_node, parent, link);
rb_insert_color_cached(&se->run_node,
&cfs_rq->tasks_timeline, leftmost);
}
复制代码
while()循环是遍历树以寻找匹配键值的过程,也就是搜索一颗平衡树的过程。找到后我们对要插入位置的父节点执行rb_link_node()来将节点插入其中,然后更新红黑树的自平衡相关属性。
3. 从队列中移除进程
从队列中删除一个节点有两种可能:一是进程执行完毕退出,而是进程受到了阻塞。
static void
dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
/*
* 更新“当前进程”的运行统计数据
*/
update_curr(cfs_rq);
/*
* When dequeuing a sched_entity, we must:
* - Update loads to have both entity and cfs_rq synced with now.
* - Substract its load from the cfs_rq->runnable_avg.
* - Substract its previous weight from cfs_rq->load.weight.
* - For group entity, update its weight to reflect the new share
* of its group cfs_rq.
*/
update_load_avg(cfs_rq, se, UPDATE_TG);
dequeue_runnable_load_avg(cfs_rq, se);
update_stats_dequeue(cfs_rq, se, flags);
clear_buddies(cfs_rq, se);
if (se != cfs_rq->curr)
__dequeue_entity(cfs_rq, se);
se->on_rq = 0;
account_entity_dequeue(cfs_rq, se);
/*
* 重新规范化vruntime
*/
if (!(flags & DEQUEUE_SLEEP))
se->vruntime -= cfs_rq->min_vruntime;
/* return excess runtime on last dequeue */
return_cfs_rq_runtime(cfs_rq);
update_cfs_group(se);
/*
* Now advance min_vruntime if @se was the entity holding it back,
* except when: DEQUEUE_SAVE && !DEQUEUE_MOVE, in this case we'll be
* put back on, and if we advance min_vruntime, we'll be placed back
* further than we started -- ie. we'll be penalized.
*/
if ((flags & (DEQUEUE_SAVE | DEQUEUE_MOVE)) == DEQUEUE_SAVE)
update_min_vruntime(cfs_rq);
}
和插入一样,实际对树节点操作的工作由__dequeue_entity()实现:
static void __dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
rb_erase_cached(&se->run_node, &cfs_rq->tasks_timeline);
}
可以看到删除一个节点要比插入简单的多,这得益于红黑树本身实现的rb_erase()函数。
调度器入口
正如上文所述,每当要发生进程的调度时,是有一个统一的入口,从该入口选择真正需要调用的调度类。
这个入口是内核中一个名为schedule()的函数,它会找到一个最高优先级的调度类,这个调度类拥有自己的可运行队列,然后向其询问下一个要运行的进程是谁。
这个函数中唯一重要的事情是执行了pick_next_task()这个函数(定义在kenerl/sched/core.c中),它以优先级为顺序,依次检查每一个调度类,并且从最高优先级的调度类中选择最高优先级的进程。
static inline struct task_struct *
pick_next_task(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
const struct sched_class *class;
struct task_struct *p;
/*
* 优化:如果当前所有要调度的进程都是普通进程,那么就直接采用普通进程的调度类(CFS)
*/
if (likely((prev->sched_class == &idle_sched_class ||
prev->sched_class == &fair_sched_class) &&
rq->nr_running == rq->cfs.h_nr_running)) {
p = fair_sched_class.pick_next_task(rq, prev, rf);
if (unlikely(p == RETRY_TASK))
goto again;
/* Assumes fair_sched_class->next == idle_sched_class */
if (unlikely(!p))
p = idle_sched_class.pick_next_task(rq, prev, rf);
return p;
}
// 遍历调度类
again:
for_each_class(class) {
p = class->pick_next_task(rq, prev, rf);
if (p) {
if (unlikely(p == RETRY_TASK))
goto again;
return p;
}
}
/* The idle class should always have a runnable task: */
BUG();
}
复制代码
每个调度类都实现了pick_next_task()方法,它会返回下一个可运行进程的指针,没有则返回NULL。调度器入口从第一个返回非NULL的类中选择下一个可运行进程。
睡眠和唤醒
睡眠和唤醒的流程在linux中是这样的:
- 睡眠:进程将自己标记成休眠状态,然后从可执行红黑树中移除,放入等待队列,然后调用
schedule()选择和执行一个其他进程。 - 唤醒:进程被设置为可执行状态,然后从等待队列移到可执行红黑树中去。
休眠在Linux中有两种状态,一种会忽略信号,一种则会在收到信号的时候被唤醒并响应。不过这两种状态的进程是处于同一个等待队列上的。
1.等待队列
和可运行队列的复杂结构不同,等待队列在linux中的实现只是一个简单的链表。所有有关等待队列的数据结构被定义在include/linux/wait.h中,具体的实现代码则被定义在kernel/sched/wait.c中。
内核使用wait_queue_head_t结构来表示一个等待队列,它其实就是一个链表的头节点,但是加入了一个自旋锁来保持一致性(等待队列在中断时可以被随时修改)
struct wait_queue_head {
spinlock_t lock;
struct list_head head;
};
typedef struct wait_queue_head wait_queue_head_t;
复制代码
而休眠的过程需要进程自己把自己加入到一个等待队列中,这可以使用内核所提供的、推荐的函数来实现。
一个可能的流程如下:
- 调用宏
DEFINE_WAIT()创建一个等待队列的项(链表的节点) - 调用
add_wait_queue()把自己加到队列中去。该队列会在进程等待的条件满足时唤醒它,当然唤醒的具体操作需要进程自己定义好(你可以理解为一个回调) - 调用
prepare_to_wait()方法把自己的状态变更为上面说到的两种休眠状态中的其中一种。
下面是上述提到的方法的源码:
void add_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
unsigned long flags;
wq_entry->flags &= ~WQ_FLAG_EXCLUSIVE;
spin_lock_irqsave(&wq_head->lock, flags);
__add_wait_queue(wq_head, wq_entry);
spin_unlock_irqrestore(&wq_head->lock, flags);
}
static inline void __add_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
list_add(&wq_entry->entry, &wq_head->head);
}
void prepare_to_wait(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry, int state)
{
unsigned long flags;
wq_entry->flags &= ~WQ_FLAG_EXCLUSIVE;
spin_lock_irqsave(&wq_head->lock, flags);
if (list_empty(&wq_entry->entry))
__add_wait_queue(wq_head, wq_entry);
// 标记自己的进程状态
set_current_state(state);
spin_unlock_irqrestore(&wq_head->lock, flags);
}
2.唤醒
唤醒操作主要通过wake_up()实现,它会唤醒指定等待队列上的所有进程。内部由try_to_wake_up()函数将对应的进程标记为TASK_RUNNING状态,接着调用enqueue_task()将进程加入红黑树中。
wake_up()系函数由宏定义,一般具体内部由下面这个函数实现:
/*
* The core wakeup function. Non-exclusive wakeups (nr_exclusive == 0) just
* wake everything up. If it's an exclusive wakeup (nr_exclusive == small +ve
* number) then we wake all the non-exclusive tasks and one exclusive task.
*
* There are circumstances in which we can try to wake a task which has already
* started to run but is not in state TASK_RUNNING. try_to_wake_up() returns
* zero in this (rare) case, and we handle it by continuing to scan the queue.
*/
static int __wake_up_common(struct wait_queue_head *wq_head, unsigned int mode,
int nr_exclusive, int wake_flags, void *key,
wait_queue_entry_t *bookmark)
{
wait_queue_entry_t *curr, *next;
int cnt = 0;
if (bookmark && (bookmark->flags & WQ_FLAG_BOOKMARK)) {
curr = list_next_entry(bookmark, entry);
list_del(&bookmark->entry);
bookmark->flags = 0;
} else
curr = list_first_entry(&wq_head->head, wait_queue_entry_t, entry);
if (&curr->entry == &wq_head->head)
return nr_exclusive;
list_for_each_entry_safe_from(curr, next, &wq_head->head, entry) {
unsigned flags = curr->flags;
int ret;
if (flags & WQ_FLAG_BOOKMARK)
continue;
ret = curr->func(curr, mode, wake_flags, key);
if (ret < 0)
break;
if (ret && (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
break;
if (bookmark && (++cnt > WAITQUEUE_WALK_BREAK_CNT) &&
(&next->entry != &wq_head->head)) {
bookmark->flags = WQ_FLAG_BOOKMARK;
list_add_tail(&bookmark->entry, &next->entry);
break;
}
}
return nr_exclusive;
}
复制代码
抢占与上下文切换
上下文切换
上下文切换是指从一个可执行进程切换到另一个可执行进程。由定义在kernel/sched/core.c中context_switch()实现:
static __always_inline struct rq *
context_switch(struct rq *rq, struct task_struct *prev,
struct task_struct *next, struct rq_flags *rf)
{
struct mm_struct *mm, *oldmm;
prepare_task_switch(rq, prev, next);
mm = next->mm;
oldmm = prev->active_mm;
/*
* For paravirt, this is coupled with an exit in switch_to to
* combine the page table reload and the switch backend into
* one hypercall.
*/
arch_start_context_switch(prev);
// 把虚拟内存从上一个内存映射切换到新进程中
if (!mm) {
next->active_mm = oldmm;
mmgrab(oldmm);
enter_lazy_tlb(oldmm, next);
} else
switch_mm_irqs_off(oldmm, mm, next);
if (!prev->mm) {
prev->active_mm = NULL;
rq->prev_mm = oldmm;
}
rq->clock_update_flags &= ~(RQCF_ACT_SKIP|RQCF_REQ_SKIP);
/*
* Since the runqueue lock will be released by the next
* task (which is an invalid locking op but in the case
* of the scheduler it's an obvious special-case), so we
* do an early lockdep release here:
*/
rq_unpin_lock(rq, rf);
spin_release(&rq->lock.dep_map, 1, _THIS_IP_);
/* Here we just switch the register state and the stack. */
// 切换处理器状态到新进程,这包括保存、恢复寄存器和栈的相关信息
switch_to(prev, next, prev);
barrier();
return finish_task_switch(prev);
}
复制代码
上下文切换由schedule()函数在切换进程时调用。但是内核必须知道什么时候调用schedule(),如果只靠用户代码显式地调用,代码可能会永远地执行下去。
为此,内核为每个进程设置了一个need_resched标志来表明是否需要重新执行一次调度,当某个进程应该被抢占时,scheduler_tick()会设置这个标志,当一个优先级高的进程进入可执行状态的时候,try_to_wake_up()也会设置这个标志位,内核检查到此标志位就会调用schedule()重新进行调度。
用户抢占
内核即将返回用户空间的时候,如果need_reshced标志位被设置,会导致schedule()被调用,此时就发生了用户抢占。意思是说,既然要重新进行调度,那么可以继续执行进入内核态之前的那个进程,也完全可以重新选择另一个进程来运行,所以如果设置了need_resched,内核就会选择一个更合适的进程投入运行。
简单来说有以下两种情况会发生用户抢占:
- 从系统调用返回用户空间
- 从中断处理程序返回用户空间
内核抢占
Linux和其他大部分的Unix变体操作系统不同的是,它支持完整的内核抢占。
不支持内核抢占的系统意味着:内核代码可以一直执行直到它完成为止,内核级的任务执行时无法重新调度,各个任务是以协作方式工作的,并不存在抢占的可能性。
在Linux中,只要重新调度是安全的,内核就可以在任何时间抢占正在执行的任务,这个安全是指,只要没有持有锁,就可以进行抢占。
为了支持内核抢占,Linux做出了如下的变动:
- 为每个进程的
thread_info引入了preempt_count计数器,用于记录持有锁的数量,当它为0的时候就意味着这个进程是可以被抢占的。 - 从中断返回内核空间的时候,会检查
need_resched和preempt_count的值,如果need_resched被标记,并且preempt_count为0,就意味着有一个更需要调度的进程需要被调度,而且当前情况是安全的,可以进行抢占,那么此时调度程序就会被调用。
除了响应中断后返回,还有一种情况会发生内核抢占,那就是内核中的进程由于阻塞等原因显式地调用schedule()来进行显式地内核抢占:当然,这个进程显式地调用调度进程,就意味着它明白自己是可以安全地被抢占的,因此我们不用任何额外的逻辑去检查安全性问题。
下面罗列可能的内核抢占情况:
- 中断处理正在执行,且返回内核空间之前
- 内核代码再一次具有可抢占性时
- 内核中的任务显式地调用
schedule() - 内核中的任务被阻塞
中断处理过程
大多数异常是通过向导致异常的进程发送Linux信号来处理的。因此,将要采取的措施推迟到该过程接收到信号为止,内核能够快速处理异常。
此方法不适用于中断,因为中断通常在中断所涉及的进程(例如,请求数据传输的进程)被挂起并且正在运行完全不相关的进程之后很长时间到达。因此,将Unix信号发送到当前进程毫无意义。
中断处理取决于中断的类型。为了我们的目的,linux中有三种主要的中断:
I / O中断
I / O设备需要引起注意;相应的中断处理程序必须查询设备以确定正确的操作过程。我们将在下一部分“ I / O中断处理”中介绍这种类型的中断。
I / O中断处理时,通常,I / O中断处理程序必须足够灵活以同时服务多个设备。例如,在PCI总线体系结构中,几个设备可以共享同一IRQ线。这意味着仅中断向量并不能说明全部情况。在表4-3所示的示例中,相同的向量43被分配给USB端口和声卡。但是,如果旧的PC体系结构(例如ISA)中的某些硬件设备的IRQ线与其他设备共享,则它们将无法可靠地运行。
中断处理程序的灵活性以两种不同的方式实现,如下表所示。

定时器中断
某些计时器(本地APIC计时器或外部计时器)已发出中断;这种中断告诉内核已经过固定时间间隔。
处理器中断
一个CPU向多处理器系统的另一个CPU发出了中断。
内核线程
为什么需要内核线程
Linux内核可以看作一个服务进程(管理软硬件资源,响应用户进程的种种合理以及不合理的请求)。
内核需要多个执行流并行,为了防止可能的阻塞,支持多线程是必要的。
内核线程就是内核的分身,一个分身可以处理一件特定事情。内核线程的调度由内核负责,一个内核线程处于阻塞状态时不影响其他的内核线程,因为其是调度的基本单位。
这与用户线程是不一样的。因为内核线程只运行在内核态
因此,它只能使用大于PAGE_OFFSET(传统的x86_32上是3G)的地址空间。
内核线程概述
内核线程是直接由内核本身启动的进程。内核线程实际上是将内核函数委托给独立的进程,它与内核中的其他进程”并行”执行。内核线程经常被称之为内核守护进程。
他们执行下列任务
- 周期性地将修改的内存页与页来源块设备同步
- 如果内存页很少使用,则写入交换区
- 管理延时动作, 如2号进程接手内核进程的创建
- 实现文件系统的事务日志
内核线程主要有两种类型
- 线程启动后一直等待,直至内核请求线程执行某一特定操作。
- 线程启动后按周期性间隔运行,检测特定资源的使用,在用量超出或低于预置的限制时采取行动。
内核线程由内核自身生成,其特点在于
- 它们在CPU的管态执行,而不是用户态。
- 它们只可以访问虚拟地址空间的内核部分(高于TASK_SIZE的所有地址),但不能访问用户空间
内核线程的进程描述符task_struct
task_struct进程描述符中包含两个跟进程地址空间相关的字段mm, active_mm,
struct task_struct { // ... struct mm_struct *mm; struct mm_struct *avtive_mm; //... };
大多数计算机上系统的全部虚拟地址空间分为两个部分: 供用户态程序访问的虚拟地址空间和供内核访问的内核空间。每当内核执行上下文切换时, 虚拟地址空间的用户层部分都会切换, 以便当前运行的进程匹配, 而内核空间不会放生切换。
对于普通用户进程来说,mm指向虚拟地址空间的用户空间部分,而对于内核线程,mm为NULL。
这位优化提供了一些余地, 可遵循所谓的惰性TLB处理(lazy TLB handing)。active_mm主要用于优化,由于内核线程不与任何特定的用户层进程相关,内核并不需要倒换虚拟地址空间的用户层部分,保留旧设置即可。由于内核线程之前可能是任何用户层进程在执行,故用户空间部分的内容本质上是随机的,内核线程决不能修改其内容,故将mm设置为NULL,同时如果切换出去的是用户进程,内核将原来进程的mm存放在新内核线程的active_mm中,因为某些时候内核必须知道用户空间当前包含了什么。
为什么没有mm指针的进程称为惰性TLB进程?假如内核线程之后运行的进程与之前是同一个, 在这种情况下, 内核并不需要修改用户空间地址表。地址转换后备缓冲器(即TLB)中的信息仍然有效。只有在内核线程之后, 执行的进程是与此前不同的用户层进程时, 才需要切换(并对应清除TLB数据)。
内核线程和普通的进程间的区别在于内核线程没有独立的地址空间,mm指针被设置为NULL;它只在 内核空间运行,从来不切换到用户空间去;并且和普通进程一样,可以被调度,也可以被抢占。
系统调用过程
大致下图所示:

软中断
软中断类型
软中断的实现难机制很多,比如bottom half(下半部)、task queue,不过这两者已经被废弃,下面三种实现机制是现在kernel还支持的:
软中断(softirq):内核2.3引入,是最基本、最优先的软中断处理形式,为了避免名字冲突,本文中将这种子类型的软中断叫softirq。
tasklet:其底层使用softirq机制实现,提供了一种用户方便使用的软中方式,为软中断提供了很好的扩展性。
work queue:前两种软中断执行时是禁止抢占的(softirq的ksoftirq除外),对于用户进程不友好。如果在softirq执行时间过长,会继续推后到work queue中执行,work queue执行处于进程上下文,其可被抢占,也可以被调度,如果软中断需要执行睡眠、阻塞,直接选择work queue。
软中断的数据结构
struct tasklet_head
{
struct tasklet_struct *head;
struct tasklet_struct *tail;
};
static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec);
static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec);
kernel2.6支持以下6个软中断,其优先级和在softirq_vec的下标相等。其中HI_SOFTIRQ和TASKLET_SOFTIRQ就是用于实现tasklet机制,其他的基本都是外设专用的软中断。
tasklet
由于软中断必须使用可重入函数,这就导致设计上的复杂度变高,作为设备驱动程序的开发者来说,增加了负担。而如果某种应用并不需要在多个CPU上并行执行,那么软中断其实是没有必要的。因此诞生了弥补以上两个要求的tasklet。它具有以下特性:
a)一种特定类型的tasklet只能运行在一个CPU上,不能并行,只能串行执行。
b)多个不同类型的tasklet可以并行在多个CPU上。
c)软中断是静态分配的,在内核编译好之后,就不能改变。但tasklet就灵活许多,可以在运行时改变(比如添加模块时)。
tasklet是在两种软中断类型的基础上实现的,因此如果不需要软中断的并行特性,tasklet就是最好的选择。也就是说tasklet是软中断的一种特殊用法,即延迟情况下的串行执行。
相关数据结构
- tasklet描述符
void tasklet_init(struct tasklet_struct *t,void (*func)(unsigned long), unsigned long data)
{
t->next = NULL;
t->state = 0;
atomic_set(&t->count, 0);
t->func = func;
t->data = data;
}
- tasklet链表
static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec);//低优先级
static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec);//高优先级• 1 • 2
相关API
- 定义tasklet
#define DECLARE_TASKLET(name, func, data)
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }
//定义名字为name的非激活tasklet
#define DECLARE_TASKLET_DISABLED(name, func, data)
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data }
//定义名字为name的激活tasklet
void tasklet_init(struct tasklet_struct *t,void (*func)(unsigned long), unsigned long data)
//动态初始化tasklet
- tasklet操作
static inline void tasklet_disable(struct tasklet_struct *t)
//函数暂时禁止给定的tasklet被tasklet_schedule调度,直到这个tasklet被再次被enable;若这个tasklet当前在运行, 这个函数忙等待直到这个tasklet退出
static inline void tasklet_enable(struct tasklet_struct *t)
//使能一个之前被disable的tasklet;若这个tasklet已经被调度, 它会很快运行。tasklet_enable和tasklet_disable必须匹配调用, 因为内核跟踪每个tasklet的"禁止次数"
static inline void tasklet_schedule(struct tasklet_struct *t)
//调度 tasklet 执行,如果tasklet在运行中被调度, 它在完成后会再次运行; 这保证了在其他事件被处理当中发生的事件受到应有的注意. 这个做法也允许一个 tasklet 重新调度它自己
tasklet_hi_schedule(struct tasklet_struct *t)
//和tasklet_schedule类似,只是在更高优先级执行。当软中断处理运行时, 它处理高优先级 tasklet 在其他软中断之前,只有具有低响应周期要求的驱动才应使用这个函数, 可避免其他软件中断处理引入的附加周期.
tasklet_kill(struct tasklet_struct *t)
//确保了 tasklet 不会被再次调度来运行,通常当一个设备正被关闭或者模块卸载时被调用。如果 tasklet 正在运行, 这个函数等待直到它执行完毕。若 tasklet 重新调度它自己,则必须阻止在调用 tasklet_kill 前它重新调度它自己,如同使用 del_timer_sync
实现原理
- 调度原理
static inline void tasklet_schedule(struct tasklet_struct *t)
{ if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) __tasklet_schedule(t);
}
void __tasklet_schedule(struct tasklet_struct *t)
{ unsigned long flags; local_irq_save(flags); t->next = NULL; *__get_cpu_var(tasklet_vec).tail = t; __get_cpu_var(tasklet_vec).tail = &(t->next);//加入低优先级列表 raise_softirq_irqoff(TASKLET_SOFTIRQ);//触发软中断 local_irq_restore(flags);
}
- tasklet执行过程TASKLET_SOFTIRQ对应执行函数为tasklet_action,HI_SOFTIRQ为tasklet_hi_action,以tasklet_action为例说明,tasklet_hi_action大同小异。
static void tasklet_action(struct softirq_action *a) {
struct tasklet_struct *list; local_irq_disable();
list = __get_cpu_var(tasklet_vec).head;
__get_cpu_var(tasklet_vec).head = NULL;
__get_cpu_var(tasklet_vec).tail = &__get_cpu_var(tasklet_vec).head;//取得tasklet链表 local_irq_enable();
while (list) { struct tasklet_struct *t = list; list = list->next; if (tasklet_trylock(t)) {
if (!atomic_read(&t->count)) { //执行tasklet if (!test_and_clear_bit(TASKLET_STATE_SCHED, &t->state)) BUG();
t->func(t->data); tasklet_unlock(t);
continue;
}
tasklet_unlock(t); } //如果t->count的值不等于0,说明这个tasklet在调度之后,被disable掉了,所以会将tasklet结构体重新放回到tasklet_vec链表,并重新调度TASKLET_SOFTIRQ软中断,在之后enable这个tasklet之后重新再执行它
local_irq_disable(); t->next = NULL;
*__get_cpu_var(tasklet_vec).tail = t;
__get_cpu_var(tasklet_vec).tail = &(t->next);
__raise_softirq_irqoff(TASKLET_SOFTIRQ);
local_irq_enable();
}
}`
Send和Recv调用过程
传输层
send和receive的调用在初始化的时候可以选择先从传输层作为分析的切入点。在传输层中主要包括了tcp发送数据和tcp接受数据两个部分,其发送与接受过程中所涉及的结构体关系如下所示
tcp 发送数据
tcp_sendmsg 实际上调用的 tcp_sendmsg_locked(struct sock sk, struct msghdr msg, size_t size),而在 tcp_sendmsg_locked 中,所做的工作是将应用层及上层传来的数据信息。组织组织成一个队列,每一个队列中的元素称作一个skb。其中最主要的内容便是待发送的数据。
在 tcp 协议的头部有几个标志字段:URG、ACK、RSH、RST、SYN、FIN,tcp_push 中会判断这个 skb 的元素是否需要 push,如果需要就将 tcp 头部字段的 push 置一,置一的过程如下:
然后,tcp_push 调用了 tcp_push_pending_frames(sk, mss_now, nonagle);函数发送数据:
随后又调用了 tcp_write_xmit 来发送数据:
若发送队列未满,则准备发送报文
检查发送窗口的大小
tcp_write_xmit 位于 tcpoutput.c 中, 它实现了 tcp 的拥塞控制, 然后调用了tcp_transmit_skb(sk, skb, 1, gfp)传输数据,实际上调用的是 tcp_transmit_skb
构建 TCP 头部和校验和
tcp_transmit_skb 是 tcp 发送数据位于传输层的最后一步,这里首先对 TCP 数据段的头部进行了处理,然后调用了网络层提供的发送接口 icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl);实现了数据的发送,自此,数据离开了传输层,传输层的任务也就结束了。
调试验证:


tcp 接收数据
接收函数比发送函数要复杂得多,因为数据接收不仅仅只是接收,tcp 的三次握手也是在接收函数实现的,所以收到数据后要判断当前的状态,是否正在建立连接等,根据发来的 信息考虑状态是否要改变,在这里仅仅考虑在连接建立后数据的接收。
首先从上向下分析,即上一层中调用了 tcp_recvmsg。
该函数完成从接收队列中读取数据复制到用户空间的任务;函数在执行过程中会锁定控 制块,避免软中断在 tcp 层的影响;函数会涉及从接收队列 receive_queue 和后备队列
backlog 中读取数据;其中从 backlog 中读取的数据,还需要经过 sk_backlog_rcv 回调,
该回调的实现为 tcp_v4_do_rcv,实际上是先缓存到队列中,然后需要读取的时候,才进入协议栈处理,此时,是在进程上下文执行的,因为会设置 tp->ucopy.task=current,在协议栈处理过程中,会直接将数据复制到用户空间。
这里的 copied 表示已经复制了多少字节,target 表示目标是多少字节。
在连接建立后,若没有数据到来,接收队列为空,进程会在 sk_busy_loop 函数内循环等待。Lock_sock()传输层上锁,避免软中断影响 。
获得数据后,遍历接收队列,找到满足读取的 skb
并调用函数 skb_copy_datagram_msg 将接收到的数据拷贝到用户态,实际调用的是
skb_datagram_iter,这里同样用了 struct msghdr *msg 来实现。
以上是对数据进行拷贝。
如果 copied>0,即读取到数据则继续,否则的话,也就是没有读到想要的数据,[当设置了 nonblock 时,(表现在 timeo=0)],就返回-EAGAIN,也就是非阻塞方式。
如果目标数据读取完,则处理后备队列。但是如果没有设置 nonblock,同时也没有出现 copied >= target 的情况,也就是没有读到足够多的数据,则调用 sk_wait_data 将当前
进程等待。也就是我们希望的阻塞方式。阻塞函数 sk_wait_data 所做的事情就是让出 CPU, 等数据来了或者设定超时之后再恢复运行。
然后从下向上分析,即 tcp 层是如何接收来自 ip 的数据并且插入相应队列的。
tcp_v4_rcv 函数为 TCP 的总入口,数据包从 IP 层传递上来,进入该函数;其协议操作函数结构如下所示, 其中 handler 即为 IP 层向 TCP 传递数据包的回调函数,以下为tcp_v4_rcv 的具体函数。

int tcp_v4_rcv(struct sk_buff *skb)
{
const struct iphdr *iph;
struct tcphdr *th;
struct sock *sk;
int ret;
//如果不是发往本地的数据包,则直接丢弃
if (skb->pkt_type != PACKET_HOST)
goto discard_it;
TCP_INC_STATS_BH(TCP_MIB_INSEGS);
//包长是否大于TCP头的长度
if (!pskb_may_pull(skb, sizeof(struct tcphdr)))
goto discard_it;
//取得TCP首部
th = tcp_hdr(skb);
//检查TCP首部的长度和TCP首部中的doff字段是否匹配
if (th->doff < sizeof(struct tcphdr) / 4)
goto bad_packet;
//检查TCP首部到TCP数据之间的偏移是否越界
if (!pskb_may_pull(skb, th->doff * 4))
goto discard_it;
if (!skb_csum_unnecessary(skb) && tcp_v4_checksum_init(skb))
goto bad_packet;
th = tcp_hdr(skb);
iph = ip_hdr(skb);
TCP_SKB_CB(skb)->seq = ntohl(th->seq);
//计算end_seq,实际上,end_seq是数据包的结束序列号,实际上是期待TCP确认
//包中ACK的数值,在数据传输过程中,确认包ACK的数值等于本次数据包SEQ
//号加上本数据包的有效载荷,即skb->len - th->doff * 4,但是在处理SYN报文或者
//FIN报文的时候,确认包的ACK等于本次处理数据包的SEQ+1,考虑到这种情况,
//期待下一个数据包的ACK就变成了TCP_SKB_CB(skb)->seq + th->syn + th->fin +
//skb->len - th->doff * 4
// TCP_SKB_CB宏会返回skb->cb[0],一个类型为tcp_skb_cb的结构指针,这个结
//构保存了TCP首部选项和其他的一些状态信息
TCP_SKB_CB(skb)->end_seq = (TCP_SKB_CB(skb)->seq + th->syn + th->fin +
skb->len - th->doff * 4);
TCP_SKB_CB(skb)->ack_seq = ntohl(th->ack_seq);
TCP_SKB_CB(skb)->when = 0;
TCP_SKB_CB(skb)->flags = iph->tos;
TCP_SKB_CB(skb)->sacked = 0;
//根据四元组查找相应连接的sock结构,大体有两个步骤,
//首先用__inet_lookup_established函数查找已经处于establish状态的连接,
//如果查找不到的话,就调用__inet_lookup_listener函数查找是否存在四元组相
//匹配的处于listen状态的sock,这个时候实际上是被动的接收来自其他主机的连接
//请求
//如果查找不到匹配的sock,则直接丢弃数据包
sk = __inet_lookup(&tcp_hashinfo, iph->saddr, th->source,
iph->daddr, th->dest, inet_iif(skb));
if (!sk)
goto no_tcp_socket;
//检查sock是否处于半关闭状态
process:
if (sk->sk_state == TCP_TIME_WAIT)
goto do_time_wait;
//检查IPSEC规则
if (!xfrm4_policy_check(sk, XFRM_POLICY_IN, skb))
goto discard_and_relse;
nf_reset(skb);
//检查BPF规则
if (sk_filter(sk, skb))
goto discard_and_relse;
skb->dev = NULL;
//这里主要是和release_sock函数实现互斥,release_sock中调用了
// spin_lock_bh(&sk->sk_lock.slock);
bh_lock_sock_nested(sk);
ret = 0;
//查看是否有用户态进程对该sock进行了锁定
//如果sock_owned_by_user为真,则sock的状态不能进行更改
if (!sock_owned_by_user(sk)) {
#ifdef CONFIG_NET_DMA
struct tcp_sock *tp = tcp_sk(sk);
if (!tp->ucopy.dma_chan && tp->ucopy.pinned_list)
tp->ucopy.dma_chan = get_softnet_dma();
if (tp->ucopy.dma_chan)
ret = tcp_v4_do_rcv(sk, skb);
else
#endif
{
//进入预备处理队列
if (!tcp_prequeue(sk, skb))
ret = tcp_v4_do_rcv(sk, skb);
}
} else
//如果数据包被用户进程锁定,则数据包进入后备处理队列,并且该进程进入
//套接字的后备处理等待队列sk->lock.wq
sk_add_backlog(sk, skb);
bh_unlock_sock(sk);
sock_put(sk);
return ret;
no_tcp_socket:
if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb))
goto discard_it;
if (skb->len < (th->doff << 2) || tcp_checksum_complete(skb)) {
bad_packet:
TCP_INC_STATS_BH(TCP_MIB_INERRS);
} else {
tcp_v4_send_reset(NULL, skb);
}
discard_it:
kfree_skb(skb);
return 0;
discard_and_relse:
sock_put(sk);
goto discard_it;
do_time_wait:
if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
inet_twsk_put(inet_twsk(sk));
goto discard_it;
}
if (skb->len < (th->doff << 2) || tcp_checksum_complete(skb)) {
TCP_INC_STATS_BH(TCP_MIB_INERRS);
inet_twsk_put(inet_twsk(sk));
goto discard_it;
}
switch (tcp_timewait_state_process(inet_twsk(sk), skb, th)) {
case TCP_TW_SYN: {
struct sock *sk2 = inet_lookup_listener(&tcp_hashinfo,
iph->daddr, th->dest,
inet_iif(skb));
if (sk2) {
inet_twsk_deschedule(inet_twsk(sk), &tcp_death_row);
inet_twsk_put(inet_twsk(sk));
sk = sk2;
goto process;
}
}
case TCP_TW_ACK:
tcp_v4_timewait_ack(sk, skb);
break;
case TCP_TW_RST:
goto no_tcp_socket;
case TCP_TW_SUCCESS:;
}
goto discard_it;
}
在 IP 层处理本地数据包时,会获取到上述结构的实例,并且调用实例的 handler 回调, 也就是调用了 tcp_v4_rcv;
tcp_v4_rcv 函数主要完成了以下的任务:(1) 设置 TCP_CB (2) 查找控制块 (3)根据控制块状态做不同处理, 包括 TCP_TIME_WAIT 状态处理, TCP_NEW_SYN_RECV 状态处理,
TCP_LISTEN 状态处理 (4) 接收 TCP 段;
tcp_v4_rcv 判断状态为 listen 时会直接调用 tcp_v4_do_rcv;如果是其他状态,将 TCP 包投递到目的套接字进行接收处理。如果套接字未被上锁则调用 tcp_v4_do_rcv。当套接字正被用户锁定,TCP 包将暂时排入该套接字的后备队列(sk_add_backlog)。
Tcp_v4_do_ecv 检查状态如果是 established,就调用 tcp_rcv_established 函数。
tcp_rcv_established 用于处理已连接状态下的输入,处理过程根据首部预测字段分为快速路径和慢速路径。
在快路中,若无数据,则处理输入 ack,释放该 skb,检查是否有数据发送,有则发送;
若有数据,则使用 tcp_queue_rcv()将数据加入到接收队列中。
加入方式包括合并到已有数据段,或者加入队列尾部。
回到快路中继续进行 tcp_ack()处理 ack , tcp_data_snd_check(sk)检查是否有数据要发送,需要则发送, tcp_ack_snd_check(sk, 0)检查是否有 ack 要发送,需要则发送.
kfree_skb_partial(skb, fragstolen) skb 已经复制到用户空间,则释放之。
唤醒用户进程通知有数据可读。
在慢路中,会进行更详细的校验,然后处理 ack,处理紧急数据,接收数据段。
其中数据段可能包含乱序的情况,如果他是有序的就调用 tcp_queue_rcv()将数据加入到接收队列中,无序的就放入无序队列中 tcp_ofo_queue。最后 tcp_data_ready 唤醒用户进程通知有数据可读。
调试验证:
网络层
ip 发送数据
ip_queue_xmit 是 ip 层提供给 tcp 层发送回调,大多数 tcp 发送都会使用这个回调,
tcp 层使用 tcp_transmit_skb 封装了 tcp 头之后调用该函数。
Ip_queue_xmit 实际上是调用 ip_queue_xmit。
以下是源码
/*
* 在TCP中,将TCP段打包成IP数据包的方法根据TCP段类型
* 的不同而有多种接口。其中最常用的就是ip_queue_xmit(),
* 而ip_build_and_send_pkt()和ip_send_reply()只有在发送特定段时
* 才会被调用。
* @skb: 待封装成IP数据包的TCP段。
* @ipfragok: 标识待输出的数据是否已经完成分片。由于
* 在调用函数时ipfragok参数总为0,因此输出的IP数据包
* 是否分片取决于是否启用PMTU发现。
*/ //TCP发送的时候从tcp_transmit_skb函数里面跳转过来
int ip_queue_xmit(struct sk_buff *skb, int ipfragok)
{
struct sock *sk = skb->sk;
struct inet_sock *inet = inet_sk(sk);
struct ip_options *opt = inet->opt;
struct rtable *rt;
struct iphdr *iph;
/* Skip all of this if the packet is already routed,
* f.e. by something like SCTP.
*/
/*
* 如果待输出的数据包已准备好路由缓存,
* 则无需再查找路由,直接跳转到packet_routed
* 处作处理。
*/
rt = skb_rtable(skb);
if (rt != NULL)
goto packet_routed;
/* Make sure we can route this packet. */
/*
* 如果输出该数据包的传输控制块中
* 缓存了输出路由缓存项,则需检测
* 该路由缓存项是否过期。
* 如果过期,重新通过输出网络设备、
* 目的地址、源地址等信息查找输出
* 路由缓存项。如果查找到对应的路
* 由缓存项,则将其缓存到传输控制
* 块中,否则丢弃该数据包。
* 如果未过期,则直接使用缓存在
* 传输控制块中的路由缓存项。
*/
rt = (struct rtable *)__sk_dst_check(sk, 0);
if (rt == NULL) {
__be32 daddr;
/* Use correct destination address if we have options. */
daddr = inet->daddr;
if(opt && opt->srr)
daddr = opt->faddr;
{
struct flowi fl = { .oif = sk->sk_bound_dev_if,
.mark = sk->sk_mark,
.nl_u = { .ip4_u =
{ .daddr = daddr,
.saddr = inet->saddr,
.tos = RT_CONN_FLAGS(sk) } },
.proto = sk->sk_protocol,
.flags = inet_sk_flowi_flags(sk),
.uli_u = { .ports =
{ .sport = inet->sport,
.dport = inet->dport } } };
/* If this fails, retransmit mechanism of transport layer will
* keep trying until route appears or the connection times
* itself out.
*/
security_sk_classify_flow(sk, &fl);
if (ip_route_output_flow(sock_net(sk), &rt, &fl, sk, 0))
goto no_route;
}
sk_setup_caps(sk, &rt->u.dst);
}
skb_dst_set(skb, dst_clone(&rt->u.dst));
packet_routed:
/*
* 查找到输出路由后,先进行严格源路由
* 选项的处理。如果存在严格源路由选项,
* 并且数据包的下一跳地址和网关地址不
* 一致,则丢弃该数据包。
*/
if (opt && opt->is_strictroute && rt->rt_dst != rt->rt_gateway)
goto no_route;
/* OK, we know where to send it, allocate and build IP header. */
/*
* 设置IP首部中各字段的值。如果存在IP选项,
* 则在IP数据包首部中构建IP选项。
*/
skb_push(skb, sizeof(struct iphdr) + (opt ? opt->optlen : 0));
skb_reset_network_header(skb);
iph = ip_hdr(skb);
*((__be16 *)iph) = htons((4 << 12) | (5 << 8) | (inet->tos & 0xff));
if (ip_dont_fragment(sk, &rt->u.dst) && !ipfragok)
iph->frag_off = htons(IP_DF);
else
iph->frag_off = 0;
iph->ttl = ip_select_ttl(inet, &rt->u.dst);
iph->protocol = sk->sk_protocol;
iph->saddr = rt->rt_src; //这里为什么是路由的src和dst ??????????????????????????????????????????//
iph->daddr = rt->rt_dst;
/* Transport layer set skb->h.foo itself. */
if (opt && opt->optlen) {
iph->ihl += opt->optlen >> 2;
ip_options_build(skb, opt, inet->daddr, rt, 0);
}
ip_select_ident_more(iph, &rt->u.dst, sk,
(skb_shinfo(skb)->gso_segs ?: 1) - 1);
/*
* 设置输出数据包的QoS类型。
*/
skb->priority = sk->sk_priority;
skb->mark = sk->sk_mark;
return ip_local_out(skb);
no_route:
/*
* 如果查找不到对应的路由缓存项,
* 在此处理,将该数据包丢弃。
*/
IP_INC_STATS(sock_net(sk), IPSTATS_MIB_OUTNOROUTES);
kfree_skb(skb);
return -EHOSTUNREACH;
}
static void ip_copy_metadata(struct sk_buff *to, struct sk_buff *from)
{
to->pkt_type = from->pkt_type;
to->priority = from->priority;
to->protocol = from->protocol;
skb_dst_drop(to);
skb_dst_set(to, dst_clone(skb_dst(from)));
to->dev = from->dev;
to->mark = from->mark;
/* Copy the flags to each fragment. */
IPCB(to)->flags = IPCB(from)->flags;
#ifdef CONFIG_NET_SCHED
to->tc_index = from->tc_index;
#endif
nf_copy(to, from);
#if defined(CONFIG_NETFILTER_XT_TARGET_TRACE) || \\
defined(CONFIG_NETFILTER_XT_TARGET_TRACE_MODULE)
to->nf_trace = from->nf_trace;
#endif
#if defined(CONFIG_IP_VS) || defined(CONFIG_IP_VS_MODULE)
to->ipvs_property = from->ipvs_property;
#endif
skb_copy_secmark(to, from);
}
Skb_rtable(skb)获取 skb 中的路由缓存,然后判断是否有缓存,如果有缓存就直接进行 packet_routed。
如果没有路由缓存就 ip_route_output_ports 查找路由缓存,在之后封装 ip 头和 ip 选项的功能。
最后调用 ip_local_out 发送数据包
调用 ip_local_out。
经过 netfilter 的 LOCAL_OUT 钩子点进行检查过滤,如果通过,则调用 dst_output 函数,实际上调用的是 ip 数据包输出函数 ip_output。
里面调用 ip_finish_output。
实际上调用的是
ip_finish_output,如果需要分片就调用 ip_fragment,否则直接调用 ip_finish_output2。
在构造好 ip 头,检查完分片之后,会调用邻居子系统的输出函数 neigh_output 进行输
出。
输出分为有二层头缓存和没有两种情况,有缓存时调用 neigh_hh_output 进行快速输出,没有缓存时,则调用邻居子系统的输出回调函数进行慢速输出。
最后调用 dev_queue_xmit 向链路层发送数据包。

/*
* 网络接口口核心层向网络协议层提供的统一
* 的发送接口,无论IP,还是ARP协议,以及其它
* 各种底层协议,通过这个函数把要发送的数据
* 传递给网络接口核心层
*
* update:
* 若支持流量控制,则将待输出的数据包根据规则
* 加入到输出网络队列中排队,并在合适的时机激活
* 网络设备输出软中断,依次将报文从队列中取出通过
* 网络设备输出。若不支持流量控制,则直接将数据包
* 从网络设备输出。
* 如果提交失败,则返回相应的错误码,然而返回
* 成功也并不能确保数据包被成功发送,因为有可能
* 由于拥塞而导致流量控制机制将数据包丢弃。
* 调用dev_queue_xmit()函数输出数据包,前提是必须启用
* 中断,只有启用中断之后才能激活下半部。
*/ //到这里的skb可能有以下三种:支持GSO(FRAGLIST类型的聚合分散I/O数据包, 对于SG类型的聚合分散I/O数据包), 或者是非GSO的SKB,但这里的skb是在ip_finish_output中分片后的skb
int dev_queue_xmit(struct sk_buff *skb) //通过ip_local_out走到这里,走到这里的SKB起IP层及其以上各层已经封装完毕。
{
struct net_device *dev = skb->dev;
struct netdev_queue *txq;
struct Qdisc *q;
int rc = -ENOMEM;
/* GSO will handle the following emulations directly. */
/*
* 如果是GSO数据包,且网络设备支持
* GSO数据包的处理,则跳转到
* gso标签处对GSO数据包直接处理。
*/
if (netif_needs_gso(dev, skb))
goto gso;
/*
* 对于FRAGLIST类型的聚合分散I/O数据包,
* 如果输出网络设备不支持FRAGLIST类型的
* 聚合分散I/O(目前只有回环设备支持),
* 则需将其线性化。若线性化失败,则
* 丢弃数据包,发送失败。
//如果发送的数据包是分片 但网卡不支持skb的碎片列表,则需要调用函数__skb_linearize把这些碎片重组到一个完整的skb中
*/
if (skb_has_frags(skb) &&
!(dev->features & NETIF_F_FRAGLIST) &&
__skb_linearize(skb))
goto out_kfree_skb;
/* Fragmented skb is linearized if device does not support SG,
* or if at least one of fragments is in highmem and device
* does not support DMA from it.
*/
/*
* 对于SG类型的聚合分散I/O数据包,如果
* 输出网络设备不支持SG类型的聚合分散I/O,
* 则需将其线性化。如果网络设备不支持
* 在高端内存使用DMA,但高端内存中有分片,
* 此时也需要将数据包线性化。若线性化失败,
* 则丢弃该数据包,发送失败。
//如果要发送的数据包使用了分散/聚合i/o 但网卡不支持或分片中至少有一个在高端内存中,并且网卡不支持dma,则同样需要调用函数__skb_linearize
进行线性化处理
*/
if (skb_shinfo(skb)->nr_frags &&
(!(dev->features & NETIF_F_SG) || illegal_highdma(dev, skb)) &&
__skb_linearize(skb))
goto out_kfree_skb;
/* If packet is not checksummed and device does not support
* checksumming for this protocol, complete checksumming here.
*/
/*
* 如果待输出的数据包由硬件来执行校验和
* (尚未执行校验和),但网络设备不支持
* 硬件执行校验和,不支持对IP报文执行
* 校验和,则在此处计算校验和。若
* 校验和失败,则丢弃数据包,发送失败。
*/
if (skb->ip_summed == CHECKSUM_PARTIAL) {
skb_set_transport_header(skb, skb->csum_start -
skb_headroom(skb));
if (!dev_can_checksum(dev, skb) && skb_checksum_help(skb))
goto out_kfree_skb;
}
gso:
/* Disable soft irqs for various locks below. Also
* stops preemption for RCU.
*/
rcu_read_lock_bh();
/* 获取dev设备上的排队规程,如果执行了tc qdisc add dev eth0 就会找到对应的Qdisc */
txq = dev_pick_tx(dev, skb);
/*
* 获取输出网络设备的排队规程。rcu_dereference()在
* RCU读临界部分中取出一个RCU保护的指针。在
* 需要内存屏障的体系中进行内存屏障,目前
* 只有Alpha体系需要。
*/
q = rcu_dereference(txq->qdisc); //实际上就是获取net_device -> netdev_queue 也就是该dev设备的跟qdisc
#ifdef CONFIG_NET_CLS_ACT
/*
* 与包分类器相关
*/
skb->tc_verd = SET_TC_AT(skb->tc_verd, AT_EGRESS);
#endif
/*
* 如果获取的排队规程定义了"入队"操作,
* 说明启用了QoS。
*/ /*如果这个设备启动了TC,那么把数据包压入队列 见tc_modify_qdisc中的qdisc_graft*/
if (q->enqueue) {//则对这个数据包进行QoS处理。 /* qos源码分析参考<TC流速流量控制分析> */ //alloc_netdev_mq可以看出开辟的q的空间为空的,如果不赋值的话
//进入出口流控的函数为dev_queue_xmit(); 如果是入口流控, 数据只是刚从网卡设备中收到, 还未交到网络上层处理,
//不过网卡的入口流控不是必须的, 缺省情况下并不进行流控,进入入口流控函数为ing_filter()函数,该函数被skb_receive_skb()调用。
/*
* 将待发送的数据包按排队规则插入到
* 队列,然后进行流量控制,调度队列
* 输出数据包,完成后返回。
*/
rc = __dev_xmit_skb(skb, q, dev, txq);
goto out;//数据包入队后,整个入队流程就结束了
}
/* The device has no queue. Common case for software devices:
loopback, all the sorts of tunnels...
Really, it is unlikely that netif_tx_lock protection is necessary
here. (f.e. loopback and IP tunnels are clean ignoring statistics
counters.)
However, it is possible, that they rely on protection
made by us here.
Check this and shot the lock. It is not prone from deadlocks.
Either shot noqueue qdisc, it is even simpler 8)
*/
/*
* 如果设备已打开但未启用QoS,则直接输出
* 数据包。
*/
if (dev->flags & IFF_UP) {
int cpu = smp_processor_id(); /* ok because BHs are off */
/*
* HARD_TX_LOCK/HARD_TX_UNLOCK是一对操作,
* 在这两个操作之间不能再次调用
* dev_queue_xmit接口。因此如果正在用
* 该网络设备发送数据包的CPU又
* 调用dev_queue_xmit()输出数据包,则
* 说明代码有bug,需输出警告信息。
* 否则,首先需加锁,以防止其他CPU
* 的并发操作,然后在网络设备处于开启
* 状态时,调用dev_hard_start_xmit()输出数据包
* 到网络设备。
*/
if (txq->xmit_lock_owner != cpu) {
HARD_TX_LOCK(dev, txq, cpu);
if (!netif_tx_queue_stopped(txq)) {
rc = NET_XMIT_SUCCESS;
if (!dev_hard_start_xmit(skb, dev, txq)) {
HARD_TX_UNLOCK(dev, txq);
goto out;
}
}
HARD_TX_UNLOCK(dev, txq);
if (net_ratelimit())
printk(KERN_CRIT "Virtual device %s asks to "
"queue packet!\\n", dev->name);
} else {
/* Recursion is detected! It is possible,
* unfortunately */
if (net_ratelimit())
printk(KERN_CRIT "Dead loop on virtual device "
"%s, fix it urgently!\\n", dev->name);
}
}
/*
* 如果网络设备处于关闭状态,则返回
* 相应的错误码。
*/
rc = -ENETDOWN;
rcu_read_unlock_bh();
/*
* 凡跳转到此处的都是输出数据包时出现错误的,
* 如聚合分散I/O数据包线性化失败,丢弃数据包。
*/
out_kfree_skb:
kfree_skb(skb);
return rc;
out:
/*
* 完成数据包输出后,返回相应结果。
*/
rcu_read_unlock_bh();
return rc;
}
调试验证:


ip 接收数据
IP 层的入口函数在 ip_rcv 函数。
然后调用已经注册的 Pre-routing netfilter hook ,完成后最终到达 ip_rcv_finish 函数。
如果是发到本机就调用 dst_input,里面由 ip_local_deliver 函数。
判断是否分片,如果有分片就 ip_defrag()进行合并多个数据包的操作,没有分片就调用 ip_local_deliver_finish()。
进一步调用 ip_protocol_deliver_rcu, 该函数根据 package 的下一个处理层的protocal number,调用下一层接口,包括 tcp_v4_rcv (TCP), udp_rcv (UDP)。对于 TCP 来说,函数 tcp_v4_rcv 函数会被调用,从而处理流程进入 TCP 栈。
调试验证:

链路层和物理层
发送数据
上层调用 dev_queue_xmit 进入链路层的处理流程,实际上调用的是 dev_queue_xmit
调用 dev_hard_start_xmit
然后调用 xmit_one
调用 netdev_start_xmit,实际上是调用 netdev_start_xmit,以下是源码分析
/*
* dev_hard_start_xmit()将待输出的数据包提交给网络设备的
* 输出接口,完成数据包的输出。
*/ //走到这里的SKB,通过ip_local_out走到这里,走到这里的SKB在ip_local_out中已经把IP层及其以上各层已经封装完毕。该函数后开始走二层封装
int dev_hard_start_xmit(struct sk_buff *skb, struct net_device *dev,
struct netdev_queue *txq)
{
const struct net_device_ops *ops = dev->netdev_ops;
int rc;
/*
* 如果输出是单个数据包,通常情况下都是
* 输出单独数据包。
*/
if (likely(!skb->next)) {
/*
* 如果应用层通过socket(AF_PACKET,SOCK_RAW,htons(ETH_P_ALL))
* 创建的原始套接字,则需发送一份数据包给这样
* 的套接字。
*/
if (!list_empty(&ptype_all))
dev_queue_xmit_nit(skb, dev);
/*
* 如果待输出数据包是GSO数据包,但网络设备
* 不支持相应的特性,则调用dev_gso_segment()对
* GSO数据包进行软分割。如果经分割后仍是
* 一个数据包,则直接调用网络设备的hard_start_xmit
* 接口输出数据包。然而,通常一个GSO数据包经
* 软分割,会生成多个链接起来的数据包,如果
* 是这样的话就需跳转到gso标签处,逐个处理
* 数据包。
*/ //见dev_gso_segment
if (netif_needs_gso(dev, skb)) {
if (unlikely(dev_gso_segment(skb)))
goto out_kfree_skb;
if (skb->next)
goto gso;
}
/*
* If device doesnt need skb->dst, release it right now while
* its hot in this cpu cache
*/
if (dev->priv_flags & IFF_XMIT_DST_RELEASE)
skb_dst_drop(skb);
/*
* e1000网络设备驱动中为e1000_xmit_frame()
*/
rc = ops->ndo_start_xmit(skb, dev); //在该函数中封装MAC层
if (rc == NETDEV_TX_OK)
txq_trans_update(txq);
/*
* TODO: if skb_orphan() was called by
* dev->hard_start_xmit() (for example, the unmodified
* igb driver does that; bnx2 doesn't), then
* skb_tx_software_timestamp() will be unable to send
* back the time stamp.
*
* How can this be prevented? Always create another
* reference to the socket before calling
* dev->hard_start_xmit()? Prevent that skb_orphan()
* does anything in dev->hard_start_xmit() by clearing
* the skb destructor before the call and restoring it
* afterwards, then doing the skb_orphan() ourselves?
*/
return rc;
}
gso:
/*
* 当一个GSO数据包经过软分割,生成
* 多个链接起来的数据包后,需逐个
* 处理数据包。调用网络设备的ndo_start_xmit
* 接口(e100网络设备驱动中为e100_xmit_frame())
* 输出数据包,如果发生错误,则返回
* 相应错误码。
*/
do {
struct sk_buff *nskb = skb->next;
skb->next = nskb->next;
nskb->next = NULL;
rc = ops->ndo_start_xmit(nskb, dev);
if (unlikely(rc != NETDEV_TX_OK)) {
nskb->next = skb->next;
skb->next = nskb;
return rc;
}
txq_trans_update(txq);
if (unlikely(netif_tx_queue_stopped(txq) && skb->next))
return NETDEV_TX_BUSY;
} while (skb->next);
/*
* 成功发送了所有的数据包,需恢复
* SKB原先的析构函数。
*/
skb->destructor = DEV_GSO_CB(skb)->destructor;
/*
* 如果调用dev_gso_segment()对GSO数据包进行
* 软分割失败,会跳转到此丢弃数据包。
*/
out_kfree_skb:
kfree_skb(skb);
return NETDEV_TX_OK;
}

调用各网络设备实现的 ndo_start_xmit 回调函数,从而把数据发送给网卡,物理层在收到发送请求之后,通过 DMA 将该主存中的数据拷贝至内部 RAM(buffer)之中。在数据拷贝中,同时加入符合以太网协议的相关 header,IFG、前导符和 CRC。对于以太网网络, 物理层发送采用 CSMA/CD,即在发送过程中侦听链路冲突。
一旦网卡完成报文发送,将产生中断通知 CPU,然后驱动层中的中断处理程序就可以删除保存的 skb 了。
调试验证:

接收数据
这层的数据接收要涉及到一些中断和硬件层面的东西。
1: 数据包从外面的网络进入物理网卡。如果目的地址不是该网卡,且该网卡没有开启混杂模式,该包会被网卡丢弃。
2: 网卡将数据包通过 DMA 的方式写入到指定的内存地址,该地址由网卡驱动分配并初始化。注: 老的网卡可能不支持 DMA,不过新的网卡一般都支持。
3: 网卡通过硬件中断(IRQ)通知 CPU,告诉它有数据来了
4: CPU 根据中断表,调用已经注册的中断函数,这个中断函数会调到驱动程序(NIC
Driver)中相应的函数
5: 驱动先禁用网卡的中断,表示驱动程序已经知道内存中有数据了,告诉网卡下次再收到数据包直接写内存就可以了,不要再通知 CPU 了,这样可以提高效率,避免 CPU 不停的被中断。
6: 启动软中断。这步结束后,硬件中断处理函数就结束返回了。由于硬中断处理程序执行的过程中不能被中断,所以如果它执行时间过长,会导致 CPU 没法响应其它硬件的中断, 于是内核引入软中断,这样可以将硬中断处理函数中耗时的部分移到软中断处理函数里面来慢慢处理。
软中断会触发内核网络模块中的软中断处理函数,内核中的 ksoftirqd 进程专门负责软中断的处理,当它收到软中断后,就会调用相应软中断所对应的处理函数,对于上面第 6 步中是网卡驱动模块抛出的软中断,ksoftirqd 会调用网络模块的 net_rx_action 函数。
net_rx_action 调用网卡驱动里的 naqi_poll 函数来一个一个的处理数据包。在 poll 函数中,驱动会一个接一个的读取网卡写到内存中的数据包,内存中数据包的格式只有驱动 知道。驱动程序将内存中的数据包转换成内核网络模块能识别的 skb 格式,然后调用
napi_gro_receive 函数。
napi_gro_receive 会直接调用 netif_receive_skb_core。
netif_receive_skb_core 调用 netif_receive_skb_one_core,将数据包交给上层
ip_rcv 进行处理。
/*
非NAPI方式 NAPI方式
IRQ
|
_______________________|_____________________________
| |
netif_rx napi_schedule
上半部 | |
enqueue_to_backlog __napi_schedule
| |
skb加入input_pkt_queuem中 napi_struct加入poll_list中
backlog加入poll_list中 |
|____________________________________________________|
|
net_rx_action
下半部 |
_______________________|_____________________________
| |
porcess_backlog->__netif_receive_skb 驱动poll方法->napi_gro_receive->netif_receive_skb->__netif_receive_skb
*/
gro_result_t napi_gro_receive(struct napi_struct *napi, struct sk_buff *skb)
{
skb_gro_reset_offset(skb);
return napi_skb_finish(__napi_gro_receive(napi, skb), skb);
}
待内存中的所有数据包被处理完成后(即 poll 函数执行完成),启用网卡的硬中断, 这样下次网卡再收到数据的时候就会通知 CPU。
**TCP/IP协议栈运行时序总结*。
通过对TCP/IP协议栈的源码分析,使得对linux的网络组成和运行原理有了更进一步的理解和掌握。尤其是在传输层以及网络层部分的分析,使得对网络协议的代码实现有了相对清晰和准确的理解,并通过对网络协议的实现代码中,各个字段的解读,加深了对tcp\ip细节实现上的掌握,基于此,以图片的形式概括整个网络传输过程的流程:
如下图所示:


浙公网安备 33010602011771号