分类
linux file system

字节招聘







字节跳动系统技术与工程团队招聘

字节跳动系统技术与工程团队招聘


Linux内核高级研发工程师

工作地点:北京/上海/杭州

投递地址:

https://job.toutiao.com/job/detail/50971


职位描述:

1. 针对业务需求定制Linux内核,结合业务需求开发内核新功能;

2. 结合服务特点对服务器底层/Linux内核进行性能调优;

3. 负责操作系统/内核前沿新技术的研究和应用;

职位要求:

1. 精通Linux内核,至少阅读过2-3个主要模块(调度,文件系统,网络,I/O,内存管理等)的源代码;

2. 熟悉Linux平台上的C语言编程,熟悉多进程多线程编程,熟悉socket编程;

3. 具有复杂系统软件的设计、开发和调优能力;

4. 有丰富内核故障调试或内核社区补丁提交经验者优先;

5. 有多平台(x86/ARM/RISC-V)内核与系统研发经验者优先

6. 了解主流虚拟化技术(Xen/KVM等)的实现,阅读过相关源代码者优先

7. 良好的团队合作精神,较强的沟通能力

8. 优秀的分析问题和解决问题的能力,对解决具有挑战性 问题充满激情

Linux内核研发工程师

工作地点:北京/上海/杭州

投递地址:

https://job.toutiao.com/job/detail/50977


职位描述:

1. 针对业务需求定制Linux内核,结合业务需求开发内核新功能;

2. 结合服务特点对服务器底层/Linux内核进行性能调优;

3. 负责改进,维护公司内部内核,保障内核稳定且高效。

职位要求:

1. 精通Linux内核,至少阅读过1个主要模块(调度,文件系统,网络,I/O,内存管理等)的源代码;

2. 熟悉Linux平台上的C语言编程,熟悉多进程多线程编程,熟悉socket编程;

3. 善于学习新的知识,动手能力强,有进取心;

4. 有内核社区补丁提交经验者优先;

5. 有丰富内核故障调试经验者优先。

网络高级研发工程师

工作地点:北京/上海/杭州

投递地址:

https://job.toutiao.com/job/detail/50988


职位描述:

1. 结合业务对Linux内核协议栈/用户态协议栈/RDMA等进行优化;

2. 负责公司内部相关 SDN、NFV 的设计、优化和开发;

3. 参与高性能网络/网络虚拟化研发;

4. 参与分布式网关/ACL/网络QoS 研发。

职位要求:

1. 3~5年相关工作经验,熟悉Linux操作系统下开发,熟悉Linux操作系统工作机制,熟悉Linux常见网络模型;

2. 熟悉Linux操作系统下C/C++开发, 能运用常见工具定位调试问题代码;

3. 熟悉了解 P4 语言,了解 openflow 协议,有 barefoot P4 相关开发经验优先;

4. 熟悉Linux kernel 或 FreeBSD 相关网络协议栈优先;

5. 熟悉路由器/交换机工作原理,熟悉NAT、VPN、负载均衡、SDN、NFV、overlay等网络技术

6. 熟悉 openstack neutron、openvswitch 等开源技术优先。

网络研发工程师

工作地点:北京/上海/杭州

投递地址:

https://job.toutiao.com/job/detail/50991


职位描述:

1. 结合业务对Linux内核协议栈等进行优化;

2. 参与高性能网络/网络虚拟化研发;

3. 参与分布式网关/ACL 研发。

职位要求:

1. 熟悉Linux操作系统下开发,熟悉Linux操作系统工作机制,熟悉Linux常见网络模型;

2. 熟悉Linux操作系统下C/C++开发, 能运用常见工具定位调试问题代码;

3. 熟悉Linux kernel 或 FreeBSD 相关网络协议栈优先;

4. 熟悉路由器/交换机工作原理,熟悉NAT、VPN等网络技术。

虚拟化高级研发工程师

工作地点:北京/上海/杭州

投递地址:

https://job.toutiao.com/job/detail/51032


职位描述:

1. 负责优化虚拟化技术的性能和稳定性;

2. 负责系统通用虚拟化技术前沿探索,不断满足业务需求;

3. 负责研究轻量虚拟化/安全容器等serverless场景下单机系统技术,满足业务需求。

岗位要求:

1. 精通C/C++编程和多线程性能优化,对系统编程有深入的理解;

2. 熟悉x86体系架构,深入理解操作系统原理及Linux内核,熟悉内核开发;

3. 有KVM/Xen,QEMU,Libvirt相关开发经验者优先;

4. 对Kata Containers、firecracker、crosvm、rust-vmm等有深入研究者优先;

5. 有较强硬件虚拟化研发经验者优先。

虚拟化研发工程师

工作地点:北京/上海/杭州

投递地址:

https://job.toutiao.com/job/detail/51039


职位描述:

1. 负责优化虚拟化技术的性能和稳定性;

2. 开发虚拟化性能分析工具和监控工具;

3. 参与系统通用虚拟化技术前沿探索,不断满足业务需求。

岗位要求:

1. 精通C/C++编程和多线程性能优化,对系统编程有深入的理解;

2. 熟悉x86体系架构,深入理解操作系统原理及Linux内核;

3. 有KVM/Xen,QEMU,Libvirt相关开发经验者优先;

4. 对Kata Containers、firecracker、crosvm、rust-vmm等有深入研究者优先;

5. 有较强硬件虚拟化研发经验者优先。

系统软件工程师

工作地点:北京

投递地址:

https://job.toutiao.com/job/detail/51040


职位描述:

1. 优化系统级基础软件设施,包括操作系统及其组件、基础库性能等;

2. 改进和优化系统监控、软件布署及其升级的自动化运维实现;

3. 优化巨量服务器的自动化管理及其维护方式;

4. 基本的系统软硬件性能瓶颈分析、故障trouble shooting排查。

职位要求:

1. 熟悉操作系统其及组件,对Linux内核有基本的了解和认识;

2. 熟悉C或者C++,并掌握其它语言如Python/Golang/Rust/Java的至少一种;

3. 熟悉常用的数据结构和算法、熟悉多线程编程及多线程程序性能优化;

4. 能够团队合作完成中大型系统软件的设计实现,代码健壮性好,有初步的代码性能优化经验;

5. 至少熟悉网络、存储、内存管理、进程调度、或者服务器硬件架构的其中一项。

系统数据平台研发工程师

工作地点:北京

投递地址:

https://job.toutiao.com/job/detail/51041


职位描述:

1. 负责超大规模数据中心系统和网络监控数据平台的设计和研发;

2. 设计和构建系统网络和监控大数据分析技术架构;

3. 应用机器学习技术提升系统和网络核心监控能力。

职位要求:

https://job.toutiao.com/job/detail/51094


1. 熟悉操作系统原理,有良好的数据结构和算法基础;

2. 熟悉至少一个数据处理平台,如 Map Reduce、Flink、Spark, 熟悉至少一种大数据数据库如;TSDB, Druid, ClickHouse,InfluxDB。PB级或以上数据处理经验优先;

3. 熟练掌握至少 Python/Go/Java 等编程语言之一,具备优秀的编码能力;

4. 自我驱动,具备良好的沟通技能,对新技术有强烈的学习热情;

5. 有统计或机器学习(TensorFlow/pytorch/MxNet)工作背景者优先;

6. 有相关开源社区工作经验者优先。

SRE工程师-系统技术方向

工作地点:北京

投递地址:

岗位职责:

1. 推进系统技术现有平台化服务的迭代和优化(系统压测平台、发布平台、系统滚动平台等服务/平台);

2. 推进新系统技术服务化、平台化、自动化研发落地;

3. 对海量数据进行数据清洗和整合,并对相关数据进行分析,构建健康预测能力和监控指标;

4. 构建底层技术的 CI/CD 机制,保证系统稳定性。

岗位要求:

1. 掌握常用开发语言 C/C++/Python/Golang/Shell;

2. 熟悉 Linux 操作系统,熟悉各种网络协议,对 linux kernel 有了解或者对底层技术有浓厚兴趣;

3. 有较强的学习能力,能够熟练阅读涉及产品和技术的英文文档;

4. 能够独立完成工作,具有较强的综合分析问题及解决问题的能力;

5. 有良好的工作文档习惯,及时按要求撰写更新工作流程及技术文档;

6. 有 AIOPS 项目经验者优先;有开源项目贡献者或开源项目领导者优先;

编译器高级研发工程师

工作地点:北京

投递地址:

https://job.toutiao.com/job/detail/51042


工作职责:

1. 负责开发与维护公司编译器工具链;

2. 负责对大型工程的代码生成以及编译器优化分析调优;

3. 深入研究编译器,优化落地并贡献开源社区。

任职要求:

1. 熟悉编译器架构与算法,有设计与实现编译各阶段的经验,包括语言处理,编译器优化和代码生成等;

2. 较强的C/C++开发能力,有丰富的问题分析定位与调试经验;

3. 熟悉业界主流的编译器的设计与开发,如LLVM,GCC;

4. 对编译器中间语言表达机制有深入的理解,有相关设计与优化经验;熟练阅读,解析,修改,优化IR指令;

5. 熟悉ARM汇编优先;对Buck、Bazel、Blade Build等编译构建系统有深入研究或者深度使用者优先。

编译器研发工程师

工作地点:北京

投递地址:

https://job.toutiao.com/job/detail/51043


工作职责:

1. 参与开发与维护公司编译器工具链;

2. 针对大型工程的代码生成以及编译器优化分析调优;

3. 深入研究编译器新技术,优化落地并贡献社区。

任职要求:

1. 熟练使用Linux;熟练使用git;熟练使用make/cmake等开发工具;

2. 较强的C/C++开发能力,有丰富的问题分析定位与调试经验;

3. 熟悉数据结构及算法;

4. 深入掌握基本的编译原理知识;

5. 熟悉业界至少1个主流的编译器的设计与开发,如LLVM/GCC等。

服务器固件研发工程师

工作地点:北京/上海/杭州

投递地址:

https://job.toutiao.com/job/detail/51044


职位描述:

1. 负责海量服务器硬件组件的各固件统一化定制(与OEM、ODM协同研发);

2. 负责下一代服务器、及自研硬件的固件软件开发及上线。

职位要求:

1. 两年以上相关工作经验,有熟练阅读英文技术规范、硬件手册的能力;

2. 熟悉C或者C++开发、具有良好的数据结构算法能力;

3. 符合以下要求其中两条或以上:

4. 熟悉Intel微处理器架构、或者Intel PCH芯片架构(或者ARM服务器对应的技术),并有相关固件代码经验;

5. 熟悉Legacy BIOS、UEFI、EDK II至少其一,熟悉ACPI标准,并有相关组件的代码开发经验;

6. 熟悉商用BMC或者OpenBMC的实现,并在此方向上有丰富的嵌入式C/C++编程经验;

7. 熟悉硬件故障处理的OS与固件的交互逻辑,并有相关代码开发经验;

8. 有TPU、GPU、高速网卡、SSL / Zip加速器其中之一的固件开发经验。

分类
未分类

字节跳动系统技术与工程团队招聘

Linux内核高级研发工程师

工作地点:北京/上海/杭州
投递地址:https://job.toutiao.com/job/detail/50971
职位描述:
1. 针对业务需求定制Linux内核,结合业务需求开发内核新功能;
2. 结合服务特点对服务器底层/Linux内核进行性能调优;
3. 负责操作系统/内核前沿新技术的研究和应用;
职位要求:
1. 精通Linux内核,至少阅读过2-3个主要模块(调度,文件系统,网络,I/O,内存管理等)的源代码;
2. 熟悉Linux平台上的C语言编程,熟悉多进程多线程编程,熟悉socket编程;
3. 具有复杂系统软件的设计、开发和调优能力;
4. 有丰富内核故障调试或内核社区补丁提交经验者优先;
5. 有多平台(x86/ARM/RISC-V)内核与系统研发经验者优先
6. 了解主流虚拟化技术(Xen/KVM等)的实现,阅读过相关源代码者优先
7. 良好的团队合作精神,较强的沟通能力
8. 优秀的分析问题和解决问题的能力,对解决具有挑战性 问题充满激情

Linux内核研发工程师

工作地点: 北京/上海/杭州
投递地址:https://job.toutiao.com/job/detail/50977
职位描述:
1. 针对业务需求定制Linux内核,结合业务需求开发内核新功能;
2. 结合服务特点对服务器底层/Linux内核进行性能调优;
3. 负责改进,维护公司内部内核,保障内核稳定且高效。
职位要求:
1. 精通Linux内核,至少阅读过1个主要模块(调度,文件系统,网络,I/O,内存管理等)的源代码;
2. 熟悉Linux平台上的C语言编程,熟悉多进程多线程编程,熟悉socket编程;
3. 善于学习新的知识,动手能力强,有进取心;
4. 有内核社区补丁提交经验者优先;
5. 有丰富内核故障调试经验者优先。

网络高级研发工程师

工作地点:北京/上海/杭州
投递地址:https://job.toutiao.com/job/detail/50988
职位描述:
1. 结合业务对Linux内核协议栈/用户态协议栈/RDMA等进行优化;
2. 负责公司内部相关 SDN、NFV 的设计、优化和开发;
3. 参与高性能网络/网络虚拟化研发;
4. 参与分布式网关/ACL/网络QoS 研发。
职位要求:
1. 3~5年相关工作经验,熟悉Linux操作系统下开发,熟悉Linux操作系统工作机制,熟悉Linux常见网络模型;
2. 熟悉Linux操作系统下C/C++开发, 能运用常见工具定位调试问题代码;
3. 熟悉了解 P4 语言,了解 openflow 协议,有 barefoot P4 相关开发经验优先;
4. 熟悉Linux kernel 或 FreeBSD 相关网络协议栈优先;
5. 熟悉路由器/交换机工作原理,熟悉NAT、VPN、负载均衡、SDN、NFV、overlay等网络技术
6. 熟悉 openstack neutron、openvswitch 等开源技术优先。

网络研发工程师

工作地点:北京/上海/杭州
投递地址:https://job.toutiao.com/job/detail/50991
职位描述:
1. 结合业务对Linux内核协议栈等进行优化;
2. 参与高性能网络/网络虚拟化研发;
3. 参与分布式网关/ACL 研发。
职位要求:
1. 熟悉Linux操作系统下开发,熟悉Linux操作系统工作机制,熟悉Linux常见网络模型;
2. 熟悉Linux操作系统下C/C++开发, 能运用常见工具定位调试问题代码;
3. 熟悉Linux kernel 或 FreeBSD 相关网络协议栈优先;
4. 熟悉路由器/交换机工作原理,熟悉NAT、VPN等网络技术。

虚拟化高级研发工程师

工作地点:北京/上海/杭州
投递地址:https://job.toutiao.com/job/detail/51032
职位描述:
1. 负责优化虚拟化技术的性能和稳定性;
2. 负责系统通用虚拟化技术前沿探索,不断满足业务需求;
3. 负责研究轻量虚拟化/安全容器等serverless场景下单机系统技术,满足业务需求。
岗位要求:
1. 精通C/C++编程和多线程性能优化,对系统编程有深入的理解;
2. 熟悉x86体系架构,深入理解操作系统原理及Linux内核,熟悉内核开发;
3. 有KVM/Xen,QEMU,Libvirt相关开发经验者优先;
4. 对Kata Containers、firecracker、crosvm、rust-vmm等有深入研究者优先;
5. 有较强硬件虚拟化研发经验者优先。

虚拟化研发工程师

工作地点:北京/上海/杭州
投递地址:https://job.toutiao.com/job/detail/51039
职位描述:
1. 负责优化虚拟化技术的性能和稳定性;
2. 开发虚拟化性能分析工具和监控工具;
3. 参与系统通用虚拟化技术前沿探索,不断满足业务需求。
岗位要求:
1. 精通C/C++编程和多线程性能优化,对系统编程有深入的理解;
2. 熟悉x86体系架构,深入理解操作系统原理及Linux内核;
3. 有KVM/Xen,QEMU,Libvirt相关开发经验者优先;
4. 对Kata Containers、firecracker、crosvm、rust-vmm等有深入研究者优先;
5. 有较强硬件虚拟化研发经验者优先。

系统软件工程师

工作地点:北京
投递地址:https://job.toutiao.com/job/detail/51040
职位描述:
1. 优化系统级基础软件设施,包括操作系统及其组件、基础库性能等;
2. 改进和优化系统监控、软件布署及其升级的自动化运维实现;
3. 优化巨量服务器的自动化管理及其维护方式;
4. 基本的系统软硬件性能瓶颈分析、故障trouble shooting排查。
职位要求:
1. 熟悉操作系统其及组件,对Linux内核有基本的了解和认识;
2. 熟悉C或者C++,并掌握其它语言如Python/Golang/Rust/Java的至少一种;
3. 熟悉常用的数据结构和算法、熟悉多线程编程及多线程程序性能优化;
4. 能够团队合作完成中大型系统软件的设计实现,代码健壮性好,有初步的代码性能优化经验;
5. 至少熟悉网络、存储、内存管理、进程调度、或者服务器硬件架构的其中一项。

系统数据平台研发工程师

工作地点:北京
投递地址:https://job.toutiao.com/job/detail/51041
职位描述:
1. 负责超大规模数据中心系统和网络监控数据平台的设计和研发;
2. 设计和构建系统网络和监控大数据分析技术架构;
3. 应用机器学习技术提升系统和网络核心监控能力。
职位要求:https://job.toutiao.com/job/detail/51094
1. 熟悉操作系统原理,有良好的数据结构和算法基础;
2. 熟悉至少一个数据处理平台,如 Map Reduce、Flink、Spark, 熟悉至少一种大数据数据库如;TSDB, Druid, ClickHouse,InfluxDB。PB级或以上数据处理经验优先;
3. 熟练掌握至少 Python/Go/Java 等编程语言之一,具备优秀的编码能力;
4. 自我驱动,具备良好的沟通技能,对新技术有强烈的学习热情;
5. 有统计或机器学习(TensorFlow/pytorch/MxNet)工作背景者优先;
6. 有相关开源社区工作经验者优先。

SRE工程师-系统技术方向

工作地点:北京
投递地址:
岗位职责:
1. 推进系统技术现有平台化服务的迭代和优化(系统压测平台、发布平台、系统滚动平台等服务/平台);
2. 推进新系统技术服务化、平台化、自动化研发落地;
3. 对海量数据进行数据清洗和整合,并对相关数据进行分析,构建健康预测能力和监控指标;
4. 构建底层技术的 CI/CD 机制,保证系统稳定性。
岗位要求:
1. 掌握常用开发语言 C/C++/Python/Golang/Shell;
2. 熟悉 Linux 操作系统,熟悉各种网络协议,对 linux kernel 有了解或者对底层技术有浓厚兴趣;
3. 有较强的学习能力,能够熟练阅读涉及产品和技术的英文文档;
4. 能够独立完成工作,具有较强的综合分析问题及解决问题的能力;
5. 有良好的工作文档习惯,及时按要求撰写更新工作流程及技术文档;
6. 有 AIOPS 项目经验者优先;有开源项目贡献者或开源项目领导者优先;

编译器高级研发工程师

工作地点:北京
投递地址:https://job.toutiao.com/job/detail/51042
工作职责:
1. 负责开发与维护公司编译器工具链;
2. 负责对大型工程的代码生成以及编译器优化分析调优;
3. 深入研究编译器,优化落地并贡献开源社区。
任职要求:
1. 熟悉编译器架构与算法,有设计与实现编译各阶段的经验,包括语言处理,编译器优化和代码生成等;
2. 较强的C/C++开发能力,有丰富的问题分析定位与调试经验;
3. 熟悉业界主流的编译器的设计与开发,如LLVM,GCC;
4. 对编译器中间语言表达机制有深入的理解,有相关设计与优化经验;熟练阅读,解析,修改,优化IR指令;
5. 熟悉ARM汇编优先;对Buck、Bazel、Blade Build等编译构建系统有深入研究或者深度使用者优先。

编译器研发工程师

工作地点:北京
投递地址:https://job.toutiao.com/job/detail/51043
工作职责:
1. 参与开发与维护公司编译器工具链;
2. 针对大型工程的代码生成以及编译器优化分析调优;
3. 深入研究编译器新技术,优化落地并贡献社区。
任职要求:
1. 熟练使用Linux;熟练使用git;熟练使用make/cmake等开发工具;
2. 较强的C/C++开发能力,有丰富的问题分析定位与调试经验;
3. 熟悉数据结构及算法;
4. 深入掌握基本的编译原理知识;
5. 熟悉业界至少1个主流的编译器的设计与开发,如LLVM/GCC等。

服务器固件研发工程师

工作地点:北京/上海/杭州
投递地址:https://job.toutiao.com/job/detail/51044
职位描述:
1. 负责海量服务器硬件组件的各固件统一化定制(与OEM、ODM协同研发);
2. 负责下一代服务器、及自研硬件的固件软件开发及上线。
职位要求:
1. 两年以上相关工作经验,有熟练阅读英文技术规范、硬件手册的能力;
2. 熟悉C或者C++开发、具有良好的数据结构算法能力;
3. 符合以下要求其中两条或以上:
4. 熟悉Intel微处理器架构、或者Intel PCH芯片架构(或者ARM服务器对应的技术),并有相关固件代码经验;
5. 熟悉Legacy BIOS、UEFI、EDK II至少其一,熟悉ACPI标准,并有相关组件的代码开发经验;
6. 熟悉商用BMC或者OpenBMC的实现,并在此方向上有丰富的嵌入式C/C++编程经验;
7. 熟悉硬件故障处理的OS与固件的交互逻辑,并有相关代码开发经验;
8. 有TPU、GPU、高速网卡、SSL / Zip加速器其中之一的固件开发经验。

分类
未分类

trace_event 注册以及使能

上一篇文章我们看到,内核展开一个 trace_point的宏以后,定义了一个trace_event_call 结构,并把这个结构放在”_ftrace_events” 段中。这一篇文章我们将解析 内核是如何加载 这些结构并使能的.
如果是内核vmlinux中的trace将在这个函数中 直接初始化,如果是模块中的调用trace_module_add_events 初始化。 这里只节选一个函数。

 static __init int event_trace_enable(void)
 {
 >-------struct trace_array *tr = top_trace_array();
 >-------struct trace_event_call **iter, *call;
 >-------int ret;

 >-------if (!tr)
 >------->-------return -ENODEV;

 >-------for_each_event(iter, __start_ftrace_events, __stop_ftrace_events) {

 >------->-------call = *iter;
 >------->-------ret = event_init(call);  \\关键函数,对event进行初始化,
 >------->-------if (!ret)
 >------->------->-------list_add(&call->list, &ftrace_events);
 >-------}

 >-------/*
 >------- * We need the top trace array to have a working set of trace
 >------- * points at early init, before the debug files and directories
 >------- * are created. Create the file entries now, and attach them
 >------- * to the actual file dentries later.
 >------- */
 >-------__trace_early_add_events(tr);

 >-------early_enable_events(tr, false);

 >-------trace_printk_start_comm();

 >-------register_event_cmds();

 >-------register_trigger_cmds();

 >-------return 0;
 }

event_init 函数如下,

 static int event_init(struct trace_event_call *call)
 {
 >-------int ret = 0;
 >-------const char *name;

 >-------name = trace_event_name(call);
 >-------if (WARN_ON(!name))
 >------->-------return -EINVAL;

 >-------if (call->class->raw_init) {
 >------->-------ret = call->class->raw_init(call);         \\ trace_event_raw_init函数
 >------->-------if (ret < 0 && ret != -ENOSYS)
 >------->------->-------pr_warn("Could not initialize trace events/%s\n", name);
 >-------}

 >-------return ret;
 }

这个函数只是调用trace_event_raw_init函数,这个函数调用了 register_trace_event 函数。这个register_trace_event 函数只是将trace_event 注册到全局结构中。

注册完成以后 会调用 __add_event_to_tracers 将trace_event 加入到各个tracer中,并在 /sys/kernel/debug/tracing/events/ 对应的目录下创建控制文件。到这一步 注册就完成了。

使能操作,使能时时给enable 文件中输入1
调用链条如下

system_enable_write --> __ftrace_set_clr_event --> __ftrace_set_clr_event_nolock -->  ftrace_event_enable_disable --> __ftrace_event_enable_disable -->trace_event_reg --> tracepoint_probe_register --> tracepoint_probe_register_prio -->tracepoint_add_func

当这一串函数执行玩意后,在trace_出会执行 trace_event_rawevent##call 对应的函数。 到这儿使能完毕。

raw_trace_point 简介
基于 bpf的raw trace_point 逻辑非常简单, 宏展开如下

 #define DECLARE_EVENT_CLASS(call, proto, args, tstruct, assign, print)>-\
 static notrace void>---->------->------->------->------->------->-------\
 __bpf_trace_##call(void *__data, proto)>>------->------->------->-------\
 {>------>------->------->------->------->------->------->------->-------\
 >-------struct bpf_prog *prog = __data;>>------->------->------->-------\
 >-------CONCATENATE(bpf_trace_run, COUNT_ARGS(args))(prog, CAST_TO_U64(args));>-\
 }

 /*
  * This part is compiled out, it is only here as a build time check
  * to make sure that if the tracepoint handling changes, the
  * bpf probe will fail to compile unless it too is updated.
  */
 #undef DEFINE_EVENT
 #define DEFINE_EVENT(template, call, proto, args)>------>------->-------\
 static inline void bpf_test_probe_##call(void)>->------->------->-------\
 {>------>------->------->------->------->------->------->------->-------\
 >-------check_trace_callback_type_##call(__bpf_trace_##template);>------\
 }>------>------->------->------->------->------->------->------->-------\
 static struct bpf_raw_event_map>__used>->------->------->------->-------\
 >-------__attribute__((section("__bpf_raw_tp_map")))>--->------->-------\
 __bpf_trace_tp_map_##call = {>-->------->------->------->------->-------\
 >-------.tp>---->-------= &__tracepoint_##call,>>------->------->-------\
 >-------.bpf_func>------= (void *)__bpf_trace_##template,>------>-------\
 >-------.num_args>------= COUNT_ARGS(args),>---->------->------->-------\
 };


 #undef DEFINE_EVENT_PRINT
 #define DEFINE_EVENT_PRINT(template, name, proto, args, print)>-\
 >-------DEFINE_EVENT(template, name, PARAMS(proto), PARAMS(args))

在4.19内核里面 还不支持 module 添加raw_tracepoint .通过bpf 可以在raw trace_point中添加任何执行,输入参数是trace_point直接输入。 有兴趣的可以看看bcc 是如何用raw_tracepoint 抓 一些数据的

注意事项:
trace_event 在复值并把 数据放到buffer的时候并没有格式化成字符串,只有当调用 读trace_pipe 或则trace文件时才会正真的将变量格式化成字符串输入。因此如果在写trace_point的时候 赋值指针的。去读的时候可能得到的不是当时的值。

分类
未分类

SACK 分析

首先我们要看 sack 和dsack的 rfc 链接如下 sack https://tools.ietf.org/html/rfc2018 dsack https://tools.ietf.org/html/rfc2883 要理解 kernel 中是怎么实现 sack和dsack。需要知道以下几个关键点

  1. kernel 发送数据包是以 skb为基础单元。发送完成以后会议skb的seq number作为key ,将这些发送完成的skb 插入到红黑树中,数据结构是(sk->tcp_rtx_queue)

  2. 当发送端接收到ack后,会遍历 sk->tcp_rtx_queue 中的skb。如果这个skb整个都在ack的seq范围内,则会删除释放这个skb。 否则 只释放skb前部分内存,调整skb的seq。

  3. 当接收到sack 时,内核会做会根据sack的分段,查找红黑树,sack最多可以包含4个分段,每个分段分别包含start_seq 和 end_seq。

    • 内核会首先使用start_seq在红黑树中找到包含 这个seq的skb即(skb start_seq < start_seq < skb end_seq)

    • 找到这个skb后,内核会将这个skb,列分为两个skb,将两个skb都插入红黑树中,第一部分不标记TCPCB_SACKED_CACKED.第二部分标记为TCPCB_SACKED_CACKED,标记在TCP_SKB_CB的sacked结构中。

    • 内核继续沿着红黑树查找下一个skb.如果这个skb的 end_seq 小于 sack的end_seq, 则会将这个skb 与上一个skb 合并。

    • 直到找到一个skb 他的 end_seq 大于 sack的end_seq。这时候,内核会将这个skb 以sack中的end_seq为分析界限,分成两部分。 第一部分的数据合并到上一个skb。后面的数据继续保持独立。

  4. 当重传时,会从这个红黑树中找skb。如果skb的sacked 已经标记为 TCPCB_SACKED_ACKED|TCPCB_SACKED_RETRANS 则不会重传,否则重传。

  5. 虽然红黑树中是以skb为单元记录。但是列分时是以mss 为最小单元的,即一个skb 的长度是mss的倍数,列分时,只能列分出去mss的倍数的长度。skb中包含多少个mss的长度记录在TCP_SKB_CB数据结构中的tcp_gso_segs成员中,注意这个成员是16位的。

在理解以上几点的基础上我们看代码。 首先看数据包发送的代码,在tcp_event_new_data_sent 函数中将发送出去的skb 插入红黑树。在tso_fragment函数中会计算tcp_gso_segs的值。

tcp_sendmsg 
    --&gt; tcp_sendmsg_locked 
        --&gt;tcp_push
            --&gt;__tcp_push_pending_frames 
                --&gt;tcp_write_xmit 
                    --&gt;tso_fragment
                    --&gt;tcp_event_new_data_sent

在看快重传逻辑。在tcp_xmit_retransmit_queue函数中对过滤掉已经有 TCPCB_SACKED_ACKED标记的skb。

tcp_ack
    --&gt;tcp_fastretrans_alert
        --&gt;tcp_simple_retransmit
            --&gt;tcp_xmit_retransmit_queue
                --&gt;tcp_retransmit_skb

最后,我们看 sack 包的处理逻辑。 tcp_clean_rtx_queue 以后的函数调用是收到ack以后清理 红黑树上的skb的逻辑, tcp_clean_rtx_queue以前的逻辑是处理sack的。结合上面几点,我们可以清晰的看到 内核是怎么处理sack。

tcp_v4_rcv
    --&gt;tcp_v4_fill_cb
    --&gt;tcp_v4_do_rcv
        tcp_rcv_established
            --&gt;tcp_validate_incoming
                --&gt;tcp_fast_parse_options
                    --&gt;tcp_parse_options
            --&gt;tcp_ack
                --&gt;tcp_sacktag_write_queue
                    --&gt;tcp_sacktag_skip
                    --&gt;tcp_sacktag_walk
                        --&gt;tcp_shift_skb_data
                            --&gt;tcp_shift_skb_data
                                --&gt;skb_shift
                                --&gt;tcp_shifted_skb
                                    --&gt;tcp_sacktag_one
                        --&gt;tcp_match_skb_to_sack
                            --&gt;tcp_fragment
                        --&gt;tcp_sacktag_one
                --&gt;tcp_clean_rtx_queue
                    --&gt;tcp_tso_acked
                        --&gt;tcp_trim_head
                    --&gt;tcp_rtx_queue_unlink_and_free
分类
未分类

vmbus 分析二

上一篇写到vmbus 怎么发现 vmbus上的设备,这一篇讲vmbus的网络设备怎么probe.

当有新的vmbus设备注册 进来时,vmbus 会调用注册在上面的驱动的match函数去匹配是否可以用这个驱动来驱起这个设备,如果能驱起来则会调用probe函数来处理。
在vmbus 的网络设备上会调用 netvsc_probe函数来处理。 这里面的其他操作跟普通的网卡驱动没有什么区别,有意思的是他调用rndis_filter_device_add这个函数,这个函数里面有两个地方比较有意思 第一个是调用了netvsc_device_add函数。该函数如下。

 struct netvsc_device *netvsc_device_add(struct hv_device *device,
 >------->------->------->-------const struct netvsc_device_info *device_info)
 {
 >-------int i, ret = 0;
 >-------struct netvsc_device *net_device;
 >-------struct net_device *ndev = hv_get_drvdata(device);
 >-------struct net_device_context *net_device_ctx = netdev_priv(ndev);

 >-------net_device = alloc_net_device();
 >-------if (!net_device)
 >------->-------return ERR_PTR(-ENOMEM);

 >-------for (i = 0; i < VRSS_SEND_TAB_SIZE; i++)
 >------->-------net_device_ctx->tx_table[i] = 0;

 >-------/* Because the device uses NAPI, all the interrupt batching and
 >------- * control is done via Net softirq, not the channel handling
 >------- */
 >-------set_channel_read_mode(device->channel, HV_CALL_ISR);

 >-------/* If we're reopening the device we may have multiple queues, fill the
 >------- * chn_table with the default channel to use it before subchannels are
 >------- * opened.
 >------- * Initialize the channel state before we open;
 >------- * we can be interrupted as soon as we open the channel.
 >------- */

 >-------for (i = 0; i < VRSS_CHANNEL_MAX; i++) {
 >------->-------struct netvsc_channel *nvchan = &net_device->chan_table[i];

 >------->-------nvchan->channel = device->channel;
 >------->-------nvchan->net_device = net_device;
 >------->-------u64_stats_init(&nvchan->tx_stats.syncp);
 >------->-------u64_stats_init(&nvchan->rx_stats.syncp);
 >-------}

 >-------/* Enable NAPI handler before init callbacks */
 >-------netif_napi_add(ndev, &net_device->chan_table[0].napi,
 >------->-------       netvsc_poll, NAPI_POLL_WEIGHT);

 >-------/* Open the channel */
 >-------ret = vmbus_open(device->channel, netvsc_ring_bytes,
 >------->------->------- netvsc_ring_bytes,  NULL, 0,
 >------->------->------- netvsc_channel_cb, net_device->chan_table);

 >-------if (ret != 0) {
 >------->-------netdev_err(ndev, "unable to open channel: %d\n", ret);
 >------->-------goto cleanup;
 >-------}

 >-------/* Channel is opened */
 >-------netdev_dbg(ndev, "hv_netvsc channel opened successfully\n");

 >-------napi_enable(&net_device->chan_table[0].napi);

 >-------/* Connect with the NetVsp */
 >-------ret = netvsc_connect_vsp(device, net_device, device_info);
 >-------if (ret != 0) {
 >------->-------netdev_err(ndev,
 >------->------->-------"unable to connect to NetVSP - %d\n", ret);
 >------->-------goto close;
 >-------}

 >-------/* Writing nvdev pointer unlocks netvsc_send(), make sure chn_table is
 >------- * populated.
 >------- */
 >-------rcu_assign_pointer(net_device_ctx->nvdev, net_device);

 >-------return net_device;

 close:
 >-------RCU_INIT_POINTER(net_device_ctx->nvdev, NULL);
 >-------napi_disable(&net_device->chan_table[0].napi);

 >-------/* Now, we can close the channel safely */
 >-------vmbus_close(device->channel);

 cleanup:
 >-------netif_napi_del(&net_device->chan_table[0].napi);
 >-------free_netvsc_device(&net_device->rcu);

 >-------return ERR_PTR(ret);
 }

这个函数里面调用了 vmbus_open ,vmbus_open里面调用了两个函数 vmbus_alloc_ring和__vmbus_open函数,第一个函数是用来创建接受数据的缓冲区,第二个是设置 channel的onchannel_callback函数的, 而onchannel_callback函数会在 vmbus_on_event函数中调用,或者在vmbus_channel_isr函数中调用 或者在vmbus_on_event函数中调用,vmbus_on_event是一个tasklet函数,这个tasklet 是channel->callback_event。 vmbus_on_event代码如下

 void vmbus_on_event(unsigned long data)
 {
 >-------struct vmbus_channel *channel = (void *) data;
 >-------unsigned long time_limit = jiffies + 2;

 >-------trace_vmbus_on_event(channel);

 >-------do {
 >------->-------void (*callback_fn)(void *);

 >------->-------/* A channel once created is persistent even when
 >------->------- * there is no driver handling the device. An
 >------->------- * unloading driver sets the onchannel_callback to NULL.
 >------->------- */
 >------->-------callback_fn = READ_ONCE(channel->onchannel_callback);
 >------->-------if (unlikely(callback_fn == NULL))
 >------->------->-------return;

 >------->-------(*callback_fn)(channel->channel_callback_context);

 >------->-------if (channel->callback_mode != HV_CALL_BATCHED)
 >------->------->-------return;

 >------->-------if (likely(hv_end_read(&channel->inbound) == 0))
 >------->------->-------return;

 >------->-------hv_begin_read(&channel->inbound);
 >-------} while (likely(time_before(jiffies, time_limit)));

 >-------/* The time limit (2 jiffies) has been reached */
 >-------tasklet_schedule(&channel->callback_event);
 }
 static void vmbus_channel_isr(struct vmbus_channel *channel)
 {
 >-------void (*callback_fn)(void *);

 >-------callback_fn = READ_ONCE(channel->onchannel_callback);
 >-------if (likely(callback_fn != NULL))
 >------->-------(*callback_fn)(channel->channel_callback_context);
 }

到这里我们跟上一篇的 vmbus_isr函数对上, vmbus_isr函数会调用vmbus_chan_sched 函数.vmbus_chan_sched 会根据 callback_mode的类型选择是挂起tasklet或者直接调用onchannel_callback函数,很不巧网卡刚刚好是HV_CALL_ISR类型,因此会直接调用onchannel_callback函数。调用的函数netvsc_channel_cb 。 netvsc_channel_cb函数如下。这个函数只是简单的挂起这个channel的napi函数,最后在napi中收包。

 void netvsc_channel_cb(void *context)
 {
 >-------struct netvsc_channel *nvchan = context;
 >-------struct vmbus_channel *channel = nvchan->channel;
 >-------struct hv_ring_buffer_info *rbi = &channel->inbound;

 >-------/* preload first vmpacket descriptor */
 >-------prefetch(hv_get_ring_buffer(rbi) + rbi->priv_read_index);

 >-------if (napi_schedule_prep(&nvchan->napi)) {
 >------->-------/* disable interrupts from host */
 >------->-------hv_begin_read(rbi);

 >------->-------__napi_schedule_irqoff(&nvchan->napi);
 >-------}
 }

如果网卡只有一个队列,对着vmbus的设备来说只有一个channel,到这里已经完事, 但是如果网卡有多个队列,也就是多个channel,这个时候 netvsc_sc_open函数会在下一个channel被探测到的时候被执行,主要是因为在rndis_filter_device_add函数中执行 vmbus_set_sc_create_callback函数,该函数如下,看到这个函数我们就想到上一篇说 sub channel 被探测到以后,不会在vmbus中注册但是会执行primary channel的sc_creation_callback函数。这边会执行的就是netvsc_sc_open函数。

 void vmbus_set_sc_create_callback(struct vmbus_channel *primary_channel,
 >------->------->------->-------void (*sc_cr_cb)(struct vmbus_channel *new_sc))
 {
 >-------primary_channel->sc_creation_callback = sc_cr_cb;
 }

netvsc_sc_open函数如下。

 static void netvsc_sc_open(struct vmbus_channel *new_sc)
 {
 >-------struct net_device *ndev =
 >------->-------hv_get_drvdata(new_sc->primary_channel->device_obj);
 >-------struct net_device_context *ndev_ctx = netdev_priv(ndev);
 >-------struct netvsc_device *nvscdev;
 >-------u16 chn_index = new_sc->offermsg.offer.sub_channel_index;
 >-------struct netvsc_channel *nvchan;
 >-------int ret;

 >-------/* This is safe because this callback only happens when
 >------- * new device is being setup and waiting on the channel_init_wait.
 >------- */
 >-------nvscdev = rcu_dereference_raw(ndev_ctx->nvdev);
 >-------if (!nvscdev || chn_index >= nvscdev->num_chn)
 >------->-------return;

 >-------nvchan = nvscdev->chan_table + chn_index;

 >-------/* Because the device uses NAPI, all the interrupt batching and
 >------- * control is done via Net softirq, not the channel handling
 >------- */
 >-------set_channel_read_mode(new_sc, HV_CALL_ISR);

 >-------/* Set the channel before opening.*/
 >-------nvchan->channel = new_sc;

 >-------ret = vmbus_open(new_sc, netvsc_ring_bytes,
 >------->------->------- netvsc_ring_bytes, NULL, 0,
 >------->------->------- netvsc_channel_cb, nvchan);
 >-------if (ret == 0)
 >------->-------napi_enable(&nvchan->napi);
 >-------else
 >------->-------netdev_notice(ndev, "sub channel open failed: %d\n", ret);

 >-------if (atomic_inc_return(&nvscdev->open_chn) == nvscdev->num_chn)
 >------->-------wake_up(&nvscdev->subchan_open);
 }

在netvsc_sc_open 我们看到网卡设备打开了更多的队列,当队列数量等于channel数量后,网卡的其他设置功能可用。

vmbus 这块我觉得比较有创意的地方在于,把0xf3 这个 irq当作了一个percpu的irq,然后讲 所有的channel都共享irq。减少了irq的数量。 这个没有pci那么多 枚举的过程。把 外设当作一个主体,这种思想非常有借鉴意义。

分类
未分类

vmbus 分析 一

vmbus device 发现
1.在创建vmbus时又一个hv_setup_vmbus_irq函数内容如下,注册进去的函数是 vmbus_isr函数

static void (*vmbus_handler)(void);
 __visible void __irq_entry hyperv_vector_handler(struct pt_regs *regs)
 {
 >-------struct pt_regs *old_regs = set_irq_regs(regs);

 >-------entering_irq();
 >-------inc_irq_stat(irq_hv_callback_count);
 >-------if (vmbus_handler)
 >------->-------vmbus_handler();

 >-------if (ms_hyperv.hints & HV_DEPRECATING_AEOI_RECOMMENDED)
 >------->-------ack_APIC_irq();

 >-------exiting_irq();
 >-------set_irq_regs(old_regs);
 }

 void hv_setup_vmbus_irq(void (*handler)(void))
 {
 >-------vmbus_handler = handler;
 }
 void hv_remove_vmbus_irq(void)
 {
 >-------/* We have no way to deallocate the interrupt gate */
 >-------vmbus_handler = NULL;
 }

hyperv_vector_handler函数是一个中断处理函数,这个中断处理函数处理的是0xf3中断
当有这个中断的时候 会调用vmbus_isr进行中断处理。

vmbus_isr函数中,主要做了两件事情,第一是调用vmbus_chan_sched 函数,第二是挂起 hv_cpu->msg_dpc这个tasklet。而vmbus_chan_sched 函数中会遍历当前cpu的chan_list链表,并挂起channel->callback_event tasklet。这个tasklet最后会发现是触发网络的napi操作。 先讲 hv_cpu->msg_dpc这个tasklet

 static void vmbus_isr(void)
 {
 >-------struct hv_per_cpu_context *hv_cpu
 >------->-------= this_cpu_ptr(hv_context.cpu_context);
 >-------void *page_addr = hv_cpu->synic_event_page;
 >-------struct hv_message *msg;
 >-------union hv_synic_event_flags *event;
 >-------bool handled = false;

 >-------if (unlikely(page_addr == NULL))
 >------->-------return;

 >-------event = (union hv_synic_event_flags *)page_addr +
 >------->------->------->------->------- VMBUS_MESSAGE_SINT;
 >-------/*
 >------- * Check for events before checking for messages. This is the order
 >------- * in which events and messages are checked in Windows guests on
 >------- * Hyper-V, and the Windows team suggested we do the same.
 >------- */

 >-------if ((vmbus_proto_version == VERSION_WS2008) ||
 >------->-------(vmbus_proto_version == VERSION_WIN7)) {

 >------->-------/* Since we are a child, we only need to check bit 0 */
 >------->-------if (sync_test_and_clear_bit(0, event->flags))
 >------->------->-------handled = true;
 >-------} else {
 >------->-------/*
 >------->------- * Our host is win8 or above. The signaling mechanism
 >------->------- * has changed and we can directly look at the event page.
 >------->------- * If bit n is set then we have an interrup on the channel
 >------->------- * whose id is n.
 >------->------- */
 >------->-------handled = true;
 >-------}

 >-------if (handled)
 >------->-------vmbus_chan_sched(hv_cpu);

 >-------page_addr = hv_cpu->synic_message_page;
 >-------msg = (struct hv_message *)page_addr + VMBUS_MESSAGE_SINT;

 >-------/* Check if there are actual msgs to be processed */
 >-------if (msg->header.message_type != HVMSG_NONE) {
 >------->-------if (msg->header.message_type == HVMSG_TIMER_EXPIRED)
 >------->------->-------hv_process_timer_expiration(msg, hv_cpu);
 >------->-------else
 >------->------->-------tasklet_schedule(&hv_cpu->msg_dpc);
 >-------}

 >-------add_interrupt_randomness(HYPERVISOR_CALLBACK_VECTOR, 0);
 }

hv_cpu->msg_dpc这个tasklet是在hv_synic_alloc函数中初始化的。这个函数对hv_context进行了初始化.我们可以看到msg_dpc 这个tasklet最后将要执行的函数是 vmbus_on_msg_dpc 函数,需要注意这里又一个percpu的tasklet,表示这个类型的tasklet可以并发执行,相当于搞了一个软中断号。可以看到这里创建了一个workqueue , workqueue里面执行的是vmbus_onmessage_work函数,vmbus_onmessage_work函数里面直接调用 vmbus_onmessage函数。

 void vmbus_on_msg_dpc(unsigned long data)
 {
 >-------struct hv_per_cpu_context *hv_cpu = (void *)data;
 >-------void *page_addr = hv_cpu->synic_message_page;
 >-------struct hv_message *msg = (struct hv_message *)page_addr +
 >------->------->------->-------  VMBUS_MESSAGE_SINT;
 >-------struct vmbus_channel_message_header *hdr;
 >-------const struct vmbus_channel_message_table_entry *entry;
 >-------struct onmessage_work_context *ctx;
 >-------u32 message_type = msg->header.message_type;

 >-------if (message_type == HVMSG_NONE)
 >------->-------/* no msg */
 >------->-------return;

 >-------hdr = (struct vmbus_channel_message_header *)msg->u.payload;

 >-------trace_vmbus_on_msg_dpc(hdr);

 >-------if (hdr->msgtype >= CHANNELMSG_COUNT) {
 >------->-------WARN_ONCE(1, "unknown msgtype=%d\n", hdr->msgtype);
 >------->-------goto msg_handled;
 >-------}

 >-------entry = &channel_message_table[hdr->msgtype];
 >-------if (entry->handler_type>== VMHT_BLOCKING) {
 >------->-------ctx = kmalloc(sizeof(*ctx), GFP_ATOMIC);
 >------->-------if (ctx == NULL)
 >------->------->-------return;

 >------->-------INIT_WORK(&ctx->work, vmbus_onmessage_work);
 >------->-------memcpy(&ctx->msg, msg, sizeof(*msg));

 >------->-------/*
 >------->------- * The host can generate a rescind message while we
 >------->------- * may still be handling the original offer. We deal with
 >------->------- * this condition by ensuring the processing is done on the
 >------->------- * same CPU.
 >------->------- */
 >------->-------switch (hdr->msgtype) {
 >------->-------case CHANNELMSG_RESCIND_CHANNELOFFER:
 >------->------->-------/*
 >------->------->------- * If we are handling the rescind message;
 >------->------->------- * schedule the work on the global work queue.
 >------->------->------- */
 >------->------->-------schedule_work_on(vmbus_connection.connect_cpu,
 >------->------->------->------->------- &ctx->work);
 >------->------->-------break;

 >------->-------case CHANNELMSG_OFFERCHANNEL:
 >------->------->-------atomic_inc(&vmbus_connection.offer_in_progress);
 >------->------->-------queue_work_on(vmbus_connection.connect_cpu,
 >------->------->------->-------      vmbus_connection.work_queue,
 >------->------->------->-------      &ctx->work);
 >------->------->-------break;

 >------->-------default:
 >------->------->-------queue_work(vmbus_connection.work_queue, &ctx->work);
 >------->-------}
 >-------} else
 >------->-------entry->message_handler(hdr);

 msg_handled:
 >-------vmbus_signal_eom(msg, message_type);
 }

vmbus_onmessage函数内容如下,这个函数前面又一个message table 用来路由消息的处理函数。

const struct vmbus_channel_message_table_entry
 channel_message_table[CHANNELMSG_COUNT] = {
 >-------{ CHANNELMSG_INVALID,>-->------->-------0, NULL },
 >-------{ CHANNELMSG_OFFERCHANNEL,>----->-------0, vmbus_onoffer },
 >-------{ CHANNELMSG_RESCIND_CHANNELOFFER,>-----0, vmbus_onoffer_rescind },
 >-------{ CHANNELMSG_REQUESTOFFERS,>---->-------0, NULL },
 >-------{ CHANNELMSG_ALLOFFERS_DELIVERED,>------1, vmbus_onoffers_delivered },
 >-------{ CHANNELMSG_OPENCHANNEL,>------>-------0, NULL },
 >-------{ CHANNELMSG_OPENCHANNEL_RESULT,>-------1, vmbus_onopen_result },
 >-------{ CHANNELMSG_CLOSECHANNEL,>----->-------0, NULL },
 >-------{ CHANNELMSG_GPADL_HEADER,>----->-------0, NULL },
 >-------{ CHANNELMSG_GPADL_BODY,>------->-------0, NULL },
 >-------{ CHANNELMSG_GPADL_CREATED,>---->-------1, vmbus_ongpadl_created },
 >-------{ CHANNELMSG_GPADL_TEARDOWN,>--->-------0, NULL },
 >-------{ CHANNELMSG_GPADL_TORNDOWN,>--->-------1, vmbus_ongpadl_torndown },
 >-------{ CHANNELMSG_RELID_RELEASED,>--->-------0, NULL },
 >-------{ CHANNELMSG_INITIATE_CONTACT,>->-------0, NULL },
 >-------{ CHANNELMSG_VERSION_RESPONSE,>->-------1, vmbus_onversion_response },
 >-------{ CHANNELMSG_UNLOAD,>--->------->-------0, NULL },
 >-------{ CHANNELMSG_UNLOAD_RESPONSE,>-->-------1, vmbus_unload_response },
 >-------{ CHANNELMSG_18,>------->------->-------0, NULL },
 >-------{ CHANNELMSG_19,>------->------->-------0, NULL },
 >-------{ CHANNELMSG_20,>------->------->-------0, NULL },
 >-------{ CHANNELMSG_TL_CONNECT_REQUEST,>-------0, NULL },
 };

 /*
  * vmbus_onmessage - Handler for channel protocol messages.
  *
  * This is invoked in the vmbus worker thread context.
  */
 void vmbus_onmessage(void *context)
 {
 >-------struct hv_message *msg = context;
 >-------struct vmbus_channel_message_header *hdr;
 >-------int size;

 >-------hdr = (struct vmbus_channel_message_header *)msg->u.payload;
 >-------size = msg->header.payload_size;

 >-------trace_vmbus_on_message(hdr);

 >-------if (hdr->msgtype >= CHANNELMSG_COUNT) {
 >------->-------pr_err("Received invalid channel message type %d size %d\n",
 >------->------->-------   hdr->msgtype, size);
 >------->-------print_hex_dump_bytes("", DUMP_PREFIX_NONE,
 >------->------->------->-------     (unsigned char *)msg->u.payload, size);
 >------->-------return;
 >-------}

 >-------if (channel_message_table[hdr->msgtype].message_handler)
 >------->-------channel_message_table[hdr->msgtype].message_handler(hdr);
 >-------else
 >------->-------pr_err("Unhandled channel message type %d\n", hdr->msgtype);
 }

在这些消息处理函数里面又一个vmbus_onoffer处理函数,这个函数会创建新的channel,并且调用vmbus_process_offer函数,这个函数会判断当前channel是primary还是sub channel,并设置相应的关系。最后会创建workqueue 调用 vmbus_add_channel_work 函数。vmbus_add_channel函数如下

 static void vmbus_add_channel_work(struct work_struct *work)
 {
 >-------struct vmbus_channel *newchannel =
 >------->-------container_of(work, struct vmbus_channel, add_channel_work);
 >-------struct vmbus_channel *primary_channel = newchannel->primary_channel;
 >-------unsigned long flags;
 >-------u16 dev_type;
 >-------int ret;

 >-------dev_type = hv_get_dev_type(newchannel);

 >-------init_vp_index(newchannel, dev_type);

 >-------if (newchannel->target_cpu != get_cpu()) {
 >------->-------put_cpu();
 >------->-------smp_call_function_single(newchannel->target_cpu,
 >------->------->------->------->------- percpu_channel_enq,
 >------->------->------->------->------- newchannel, true);
 >-------} else {
 >------->-------percpu_channel_enq(newchannel);
 >------->-------put_cpu();
 >-------}

 >-------/*
 >------- * This state is used to indicate a successful open
 >------- * so that when we do close the channel normally, we
 >------- * can cleanup properly.
 >------- */
 >-------newchannel->state = CHANNEL_OPEN_STATE;

 >-------if (primary_channel != NULL) {
 >------->-------/* newchannel is a sub-channel. */
 >------->-------struct hv_device *dev = primary_channel->device_obj;

 >------->-------if (vmbus_add_channel_kobj(dev, newchannel))
 >------->------->-------goto err_deq_chan;

 >------->-------if (primary_channel->sc_creation_callback != NULL)
 >------->------->-------primary_channel->sc_creation_callback(newchannel);

 >------->-------newchannel->probe_done = true;
 >------->-------return;
 >-------}

 >-------/*
 >------- * Start the process of binding the primary channel to the driver
 >------- */
 >-------newchannel->device_obj = vmbus_device_create(
 >------->-------&newchannel->offermsg.offer.if_type,
 >------->-------&newchannel->offermsg.offer.if_instance,
 >------->-------newchannel);
 >-------if (!newchannel->device_obj)
 >------->-------goto err_deq_chan;

 >-------newchannel->device_obj->device_id = dev_type;
 >-------/*
 >------- * Add the new device to the bus. This will kick off device-driver
 >------- * binding which eventually invokes the device driver's AddDevice()
 >------- * method.
 >------- */
 >-------ret = vmbus_device_register(newchannel->device_obj);

 >-------if (ret != 0) {
 >------->-------pr_err("unable to add child device object (relid %d)\n",
 >------->------->-------newchannel->offermsg.child_relid);
 >------->-------kfree(newchannel->device_obj);
 >------->-------goto err_deq_chan;
 >-------}

 >-------newchannel->probe_done = true;
 >-------return;

 err_deq_chan:
 >-------mutex_lock(&vmbus_connection.channel_mutex);

 >-------/*
 >------- * We need to set the flag, otherwise
 >------- * vmbus_onoffer_rescind() can be blocked.
 >------- */
 >-------newchannel->probe_done = true;

 >-------if (primary_channel == NULL) {
 >------->-------list_del(&newchannel->listentry);
 >-------} else {
 >------->-------spin_lock_irqsave(&primary_channel->lock, flags);
 >------->-------list_del(&newchannel->sc_list);
 >------->-------spin_unlock_irqrestore(&primary_channel->lock, flags);
 >-------}

 >-------mutex_unlock(&vmbus_connection.channel_mutex);

 >-------if (newchannel->target_cpu != get_cpu()) {
 >------->-------put_cpu();
 >------->-------smp_call_function_single(newchannel->target_cpu,
 >------->------->------->------->------- percpu_channel_deq,
 >------->------->------->------->------- newchannel, true);
 >-------} else {
 >------->-------percpu_channel_deq(newchannel);
 >------->-------put_cpu();
 >-------}

 >-------vmbus_release_relid(newchannel->offermsg.child_relid);

 >-------free_channel(newchannel);
 }

vmbus_add_channel_work函数的 对channel进行操作,不同的类型的channel操作不一样,对于primary的channel 会调用 vmbus_device_register注册一个新的vmbus 设备,对于sub channel则会调用 sc_creation_callback函数 设置任务。这块会在网卡注册的时候讲。注意一个小细节 如果当前的cpu不等于 channel的target_cpu则会调用ipi 把当前channel 加到target cpu的hv_cpu_context链表中.target_cpu是在init_vp_index中计算出来的。

vmbus_device_rgister注册后,会触发 match 以及调用 vmbus上的驱动操作,驱动虚拟设备。

有上面我们发现 vmbus上的设备创建是由 hyper-V 给虚拟机发送中断,然后发消息,然后虚拟机中的vmbus 创建 这个设备。 这个方式非常新颖。相对于pci枚举或者其他方式,这种方式把外设当作一个主体,相当于可编程的外设。个人感觉学到了

分类
未分类

TCP listen 过程分析

TCP listen 系统调用过程
__sys__listen –> inet_listen –>inet_csk_listen_start–>inet_hash–>__inet_hash
1.在 inet_listen函数中,检查sock状态是否为 TCPF_CLOSE或者TCPF_LISTEN状态,只有这两个状态的sock才能listen。如果已经处于listen状态,则只修改backlog,否则需要执行inet_csk_listen_start函数将sock添加到listen hash table中
2.inet_csk_listen_start函数会改变sock状态,改为TCP_LISTEN状态。
3.inet_hash函数将sock 添加到listen hash table中,备查。如果设置了sk_reuseport标志位,则会调用inet_reuseport_add_sock函数处理reuseport
具体细节可以沿着这个调用栈看代码。

TCP listen 收到 syn包后的执行的动作

tcp_v4_rcv –> tcp_v4_do_rcv –> tcp_rcv_state_process –> tcp_v4_conn_request –> tcp_conn_request –>tcp_v4_send_synack

1.tcp_v4_rcv 函数在listen hash table中找到对应的 sock。调用 tcp_v4_do_rcv函数
2.tcp_v4_do_rcv 函数判断有没有开启tcp cookie。在收到syn 包的过程中 cookie开启不开启没有影响
3.tcp_rcv_state_process函数根据sock状态及 来包状态,决定调用tcp_connect_request函数。
4.tcp_conn_request 函数
a.如果没有开启tcp cookie和fastopen的情况下,会创建一个request_sock的sock,并将sk_state设置为 TCP_NEW_SYN_RECV状态。
b.调用inet_csk_reqsk_queue_hash_add将 req sock 添加到 establish hash table中,并发送 synack。

TCP listen 收到 ack执行动作。
tcp_v4_rcv –> tcp_check_req –> tcp_v4_syn_recv_sock–>tcp_create_openreq_child–>inet_csk_clone_lock
1.tcp_check_req 函数检查ack包是否有问题,并将tcp_v4_syn_recv_sock创建的sock 通过inet_csk_reqsk_queue_add放到 accept队列中。
2.tcp_v4_syn_recv_sock 函数创建child sock。
3. inet_csk_cloen_lock函数会设置child sock 状态为TCP_SYN_RECV
4.最后调用 tcp_child_process,对ack 进行处理,当sock正在被调用时 把包放到backlog里面,否则唤醒listen的进程。
我分析的过程中 忽略了 tcp cookie 和 fastopen。不过主线流程情况以后,这些附加功能可以慢慢通过看代码了解。

分类
未分类

tcp bind 端口分析

bind系统调用 是指定源端口的。 大多数情况下 我们在 listen之前调用,但是在connect之前也可以调用这个系统调用,指定发起连接的源端口。
在TCP bind系统调用中,会创建一个 inet_bind_bucket的实体。系统会把这个结构记录在一个hash表中,后续bind时使用它来判定是否可以bind。
在以下四种情况下 可以共享本地端口
1. sockets bound 到了不同的 接口上。
2.sockets 设置了 REUSEADDR,且没有sockets处于listen状态。
3.sockets 设置了REUSEADDR,且有sockets处于listen状态,如果还设置了REUSEPORT也可以共享。
4.所有的sockets 绑定在不同的地址上。
判断是否有冲突的函数是 inet_csk_bind_conflict,有兴趣可以进去仔细看。

需要注意的是,除了在bind 函数中会创建inet_bind_bucket的实体外,如果在connect前没有进行bind,同样会创建inet_bind_bucket实体,并将该实体插入 bhash hash表中。只是在connect判断端口是否可以复用采用了更加简单地判定方法。具体可以看__inet_hash_connect函数。
bind 系统调用的调用链如下

__sys_bind –> inet_bind –> __inet_bind –> inet_csk_get_port(get_port) –> inet_csk_bind_conflict

bind 关键点在于判定 端口是否可用。判定条件是前文中提到的四个。

分类
未分类

TCP connect 收到 syn/ack 处理流程

在上章中分析了connect流程。这一章分析,服务端发送syn/ack后,客户端的处理流程。
这个流程在IPV4中的入口函数是tcp_v4_rcv.当ip层收到tcp包后,会将这种包上送到tcp_v4_rcv函数中。
这个函数首先检查数据包的合法性(数据包校验和是否正确,数据包是否完整)
然后在tcp 连接表中查找 sock。首先在established表中查找,如果在established表中差找不到,则在listener表中查找。由于我们分析的是syn/ack收包过程,这个sk应该会在established表中查找到。且sock 状态为TCP_SYN_SENT。

        if (!sock_owned_by_user(sk)) {
                ret = tcp_v4_do_rcv(sk, skb);   //tcp_v4_rcv函数最后调用这个函数处理syn/ack报文
        } else if (tcp_add_backlog(sk, skb)) {
                goto discard_and_relse;
        }

在tcp_v4_do_rcv函数中,会根据sk状态执行不同的操作,由于当前sk状态为TCP_SYN_SENT,因此直接执行tcp_rcv_state_process函数。

        if (tcp_rcv_state_process(sk, skb)) { //改变tcp sk状态函数。这个函数根据当前状态以及skb包中标志位,改变sk状态
                rsk = sk;
                goto reset;
        }

在tcp_rcv_state_process函数中,根据当前TCP sk 状态 为TCP_SYN_SENT,执行的代码为

        case TCP_SYN_SENT:
                tp->rx_opt.saw_tstamp = 0; 
                tcp_mstamp_refresh(tp);
                queued = tcp_rcv_synsent_state_process(sk, skb, th);  //执行这个函数改变tcp状态
                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;
        } 

tcp_rcv_synsent_state_process首先判断是否有 ack报文,然后检查报文的合法性,(seq,tstamp)。然后检查是否有rst,如果有rst 则直接关闭socket,并通知用户态程序 该sock 出错。

        if (th->ack) {
                /* rfc793:
                 * "If the state is SYN-SENT then
                 *    first check the ACK bit
                 *      If the ACK bit is set
                 *        If SEG.ACK =< ISS, or SEG.ACK > SND.NXT, send
                 *        a reset (unless the RST bit is set, if so drop
                 *        the segment and return)"
                 */
                if (!after(TCP_SKB_CB(skb)->ack_seq, tp->snd_una) ||
                    after(TCP_SKB_CB(skb)->ack_seq, tp->snd_nxt))
                        goto reset_and_undo;

                if (tp->rx_opt.saw_tstamp && tp->rx_opt.rcv_tsecr &&
                    !between(tp->rx_opt.rcv_tsecr, tp->retrans_stamp,
                             tcp_time_stamp(tp))) {
                        NET_INC_STATS(sock_net(sk),
                                        LINUX_MIB_PAWSACTIVEREJECTED);
                        goto reset_and_undo;
                }

                /* Now ACK is acceptable.
                 *
                 * "If the RST bit is set
                 *    If the ACK was acceptable then signal the user "error:
                 *    connection reset", drop the segment, enter CLOSED state,
                 *    delete TCB, and return."
                 */

                if (th->rst) {
                        tcp_reset(sk);
                        goto discard;
                }

                /* rfc793:
                 *   "fifth, if neither of the SYN or RST bits is set then
                 *    drop the segment and return."
                 *
                 *    See note below!
                 *                                        --ANK(990513)
                 */
                if (!th->syn)
                        goto discard_and_undo;

                /* rfc793:
                 *   "If the SYN bit is on ...
                 *    are acceptable then ...
                 *    (our SYN has been ACKed), change the connection
                 *    state to ESTABLISHED..."
                 */

                tcp_ecn_rcv_synack(tp, th);

                tcp_init_wl(tp, TCP_SKB_CB(skb)->seq);
                tcp_ack(sk, skb, FLAG_SLOWPATH);

                /* Ok.. it's good. Set up sequence numbers and
                 * move to established.
                 */
                tp->rcv_nxt = TCP_SKB_CB(skb)->seq + 1;
                tp->rcv_wup = TCP_SKB_CB(skb)->seq + 1;

                /* RFC1323: The window in SYN & SYN/ACK segments is
                 * never scaled.
                 */
                tp->snd_wnd = ntohs(th->window);

如果整个过程中一切都ok,则调用 tcp_finish_connect函数,这个函数将tcp sk 状态改为 TCP_ESTABLISH状态,如果开启了keepalive则设置keepalive定时器。代码如下

void tcp_finish_connect(struct sock *sk, struct sk_buff *skb)
{
        struct tcp_sock *tp = tcp_sk(sk);
        struct inet_connection_sock *icsk = inet_csk(sk);

        tcp_set_state(sk, TCP_ESTABLISHED);
        icsk->icsk_ack.lrcvtime = tcp_jiffies32;
        if (skb) {
                icsk->icsk_af_ops->sk_rx_dst_set(sk, skb);
                security_inet_conn_established(sk, skb);
                sk_mark_napi_id(sk, skb);
        }
        tcp_init_transfer(sk, BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB);

        /* Prevent spurious tcp_cwnd_restart() on first data
         * packet.
         */
        tp->lsndtime = tcp_jiffies32;

        if (sock_flag(sk, SOCK_KEEPOPEN))
                inet_csk_reset_keepalive_timer(sk, keepalive_time_when(tp));

        if (!tp->rx_opt.snd_wscale)
                __tcp_fast_path_on(tp, tp->snd_wnd);
        else
                tp->pred_flags = 0;
}

如果收到syn/rst 则会调用 tcp_reset函数。函数如下

void tcp_reset(struct sock *sk)
{
        trace_tcp_receive_reset(sk);
        /* We want the right error as BSD sees it (and indeed as we do). */
        switch (sk->sk_state) {
        case TCP_SYN_SENT:
                sk->sk_err = ECONNREFUSED;
                break;
        case TCP_CLOSE_WAIT:
                sk->sk_err = EPIPE;
                break;
        case TCP_CLOSE:
                return;
        default:
                sk->sk_err = ECONNRESET;
        }
        /* This barrier is coupled with smp_rmb() in tcp_poll() */
        smp_wmb();

        tcp_write_queue_purge(sk);
        tcp_done(sk);

        if (!sock_flag(sk, SOCK_DEAD))
                sk->sk_error_report(sk);  //通知用户态程序出错,具体函数是sock_def_error_report。 在net/core/sock.c文件中
}

sock_def_error_report函数如下:

static void sock_def_error_report(struct sock *sk)
{
        struct socket_wq *wq;

        rcu_read_lock();
        wq = rcu_dereference(sk->sk_wq);
        if (skwq_has_sleeper(wq))
                wake_up_interruptible_poll(&wq->wait, EPOLLERR);
        sk_wake_async(sk, SOCK_WAKE_IO, POLL_ERR); //正常情况下返回POLL_ERR,但是我之前遇到过既返回POLL_ERR又返回POLL_IN POLL_OUT的情况,可能是因为有其他事件同时发生导致。
        rcu_read_unlock();
}

最后调用 sk_wake_async唤醒阻塞在sk上的进程。

                if (!sock_flag(sk, SOCK_DEAD)) {
                        sk->sk_state_change(sk);
                        sk_wake_async(sk, SOCK_WAKE_IO, POLL_OUT);
                }

最后调用 tcp_send_ack 发送 ack报文到服务端,整个tcp 接受syn/ack 过程完成。
后面的函数都是一些异常处理。

分类
linux network

TCP socket connect

TCP 在作为客户端时,创建完socket一般都会立即向服务端发起connect请求。如果用户没有事先调用bind 绑定源端口和源ip。则内核会选取一个源端口,并根据路由选择源IP。
TCP connect 的大致流程是,调用inet的connect函数,调用TCP的connect函数,在TCP的connect函数中,设置socket状态。发送syn报文,设置超时重发,将socket 添加到establish hash 表中,阻塞等待服务端发送syn/ack报文。等到syn/ack报文,回ack报文,并返回,或超时返回。
系统调用入口函数 __sys_connect 如下

int __sys_connect(int fd, struct sockaddr __user *uservaddr, int addrlen)
{
        struct socket *sock;
        struct sockaddr_storage address;
        int err, fput_needed;

        sock = sockfd_lookup_light(fd, &err, &fput_needed); //通过fd获取sock结构
        if (!sock)
                goto out;
        err = move_addr_to_kernel(uservaddr, addrlen, &address); //拷贝服务端地址
        if (err < 0)
                goto out_put;

        err =
            security_socket_connect(sock, (struct sockaddr *)&address, addrlen); //感觉像是selinux的hook。没仔细研究过,这块好多东西
        if (err)
                goto out_put;

        err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen,
                                 sock->file->f_flags); //调用 inet_stream_ops  inet_stream_connect。进行connect
out_put:
        fput_light(sock->file, fput_needed);
out:
        return err;
}

SYSCALL_DEFINE3(connect, int, fd, struct sockaddr __user *, uservaddr,
                int, addrlen)
{
        return __sys_connect(fd, uservaddr, addrlen);
}

最后调用 __inet_stream_connect 这个函数。connect最后阻塞在这个函数里面。函数重要代码如下

int __inet_stream_connect(struct socket *sock, struct sockaddr *uaddr,
                          int addr_len, int flags, int is_sendmsg)
{
    ......
            case SS_UNCONNECTED:
                err = -EISCONN;
                if (sk->sk_state != TCP_CLOSE)
                        goto out;

                if (BPF_CGROUP_PRE_CONNECT_ENABLED(sk)) { //连接前的 bpf操作,进去看了一下 如果没有bpf prog 很快出来
                        err = sk->sk_prot->pre_connect(sk, uaddr, addr_len);
                        if (err)
                                goto out;
                }

                err = sk->sk_prot->connect(sk, uaddr, addr_len); //执行具体协议的connect函数
                if (err < 0)
                        goto out;

                sock->state = SS_CONNECTING; //修改socket状态为connecting

                if (!err && inet_sk(sk)->defer_connect)
                        goto out;

                /* 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 ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {
                int writebias = (sk->sk_protocol == IPPROTO_TCP) &&
                                tcp_sk(sk)->fastopen_req &&
                                tcp_sk(sk)->fastopen_req->data ? 1 : 0;

                /* Error code is set above */
                if (!timeo || !inet_wait_for_connect(sk, timeo, writebias))
                    /*等待连接,连接超时(这个超时是TCP设置的)或者收到syn/ack。
                    *如果没有设置O_NOBLOCK则进程被阻塞。否则直接返回,直接返回后通过
                    *epoll等待连接事件。在某些版本的内核里面超时以后,这块行为会变成可读
                    *可写加超时error,因此用户态需要注意处理方式。踩过坑
                    */
                        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) //查看tcp连接状态。如果TCP状态没有改变说明没有收到 syn/ack报文
                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:
        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;
}

sk->sk_prot->connect 在tcp中调用的是tcp_v4_connect。 这个函数的作用,按照代码顺序分为以下几个:
1.如果用户已经设置源IP,则直接使用设置的源IP
2.如果没有设置源IP则根据目的ip和路由查找源IP
3.如果用户已经设置源port,则直接使用源Port
4.如果没有设置源PORT 根据目的IP port 和当前系统的establish tcp hash表查找sport。
5.查找目的路由
6.将TCP sock 状态设置为TCP_SYN_SENT。
7.根据已经查找到的路由及路由出口设备的能力设置一些GSO TSO标志等。sk_setup_caps。
这个函数比较简单,这里就不列出来了。需要注意的是查找可用源PORT的函数,会在查找到源port后将sock添加到establish hash表中。这个时候sock状态还没有establish。个人认为 主要是为了区分listen。listen 五元组不全, establish 是全五元组。函数是inet_hash_connect。有兴趣可以进去看看。
tcp connect 关键函数是tcp_connect 函数体如下

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); //设置超时重发,使用的retrans机制。
        return 0;
}

在tcp连接中有三个定时器。比较重要
1.icsk_retransmit_timer 超时重发,handler tcp_write_timer
2.icsk_delack_timer 延迟回复ack。减少ack数量handler tcp_delack_timer
3.sk_timer keepalive_handler 这个不是tcp专有,连接保活用的。handler tcp_keepalive_timer

如果超时没有收到 服务器端返回的syn/ack 报文,当前设置为1秒,则会进入超时重发逻辑,进入tcp_write_timer_handler。 这个函数根据超时事件,进行不同的处。如果是超时重发则进入 tcp_retransmit_timer中处理,这个函数将重试发包。如果超过重试次数,则会进入 tcp_write_err函数,唤醒阻塞的实体处理,调用sk->sk_error_report (sock_def_error_report)。
tcp connect到这儿发送部分基本就完了,下节分析一下 收到syn/ack后,修改TCP状态到ESTABLISH。