Linux系统调用01

Linux系统调用的整体流程为:
1、应用程序【用户态】通过syscall或glibc进行内核功能调用,这一部分在glibc源码中进行的
2、CPU收到syscall,Linux内核响应syscall调用【内核态】,这一部分在linux源码中进行的
3、返回结果到应用程序【用户态】
本节先处理第一部分:

一、glibc部分
1、应用程序调用open函数

//glibc/intl/loadmsgcat.c
# define open(name, flags)  __open_nocancel (name, flags)

2、展开后实际上调用了

__open_nocancel(name, flags)

3、而__open_nocancel 最终调用了INLINE_SYSCALL_CALL

//glibc/sysdeps/unix/sysv/linux/open_nocancel.c
__open_nocancel(name, flags)
->return INLINE_SYSCALL_CALL (openat, AT_FDCWD, file, oflag, mode);

4、宏展开【理解就好,不保证顺序】

4.1、初始为

INLINE_SYSCALL_CALL (openat, AT_FDCWD, file, oflag, mode);

4.2、第1次展开INLINE_SYSCALL_CALL
[code lang="c"]
#define INLINE_SYSCALL_CALL(...) \
  __INLINE_SYSCALL_DISP (__INLINE_SYSCALL, __VA_ARGS__)

//展开得到:
__INLINE_SYSCALL_DISP(__INLINE_SYSCALL, __VA_ARGS__【openat, AT_FDCWD, file, oflag, mode】)

4.3、第2次展开__INLINE_SYSCALL_DISP
[code lang="c"]
#define __INLINE_SYSCALL_DISP(b,...) \
  __SYSCALL_CONCAT (b,__INLINE_SYSCALL_NARGS(__VA_ARGS__))(__VA_ARGS__)

//展开得到:
__SYSCALL_CONCAT(b【__INLINE_SYSCALL】,__INLINE_SYSCALL_NARGS(__VA_ARGS__【openat, AT_FDCWD, file, oflag, mode】))(__VA_ARGS__【openat, AT_FDCWD, file, oflag, mode】)

4.4、第3次展开__INLINE_SYSCALL_NARGS

__INLINE_SYSCALL_NARGS(__VA_ARGS__【openat, AT_FDCWD, file, oflag, mode】)

#define __INLINE_SYSCALL_NARGS(...) \
  __INLINE_SYSCALL_NARGS_X (__VA_ARGS__,7,6,5,4,3,2,1,0,)

//展开得到:
__INLINE_SYSCALL_NARGS_X(openat, AT_FDCWD, file, oflag, mode,7,6,5,4,3,2,1,0,)

//然后展开__INLINE_SYSCALL_NARGS_X
#define __INLINE_SYSCALL_NARGS_X(a,b,c,d,e,f,g,h,n,...) n

//展开得到参数个数:
4

//从而4.4的结果为
__SYSCALL_CONCAT(__INLINE_SYSCALL,4)(__VA_ARGS__【openat, AT_FDCWD, file, oflag, mode】)

4.5、然后展开__SYSCALL_CONCAT,其实就是字符拼接

__SYSCALL_CONCAT(__INLINE_SYSCALL,4)

#define __SYSCALL_CONCAT_X(a,b)     a##b
#define __SYSCALL_CONCAT(a,b)       __SYSCALL_CONCAT_X (a, b)

//展开得到:
__INLINE_SYSCALL4

//从而4.5的结果为
__INLINE_SYSCALL4(openat, AT_FDCWD, file, oflag, mode)

4.6、然后展开INTERNAL_SYSCALL4

#define __INLINE_SYSCALL4(name, a1, a2, a3, a4) \
  INLINE_SYSCALL (name, 4, a1, a2, a3, a4)

//展开得到:
INLINE_SYSCALL(openat, 4, AT_FDCWD, file, oflag, mode)

4.7、展开INLINE_SYSCALL

//glibc/sysdeps/unix/sysv/linux/sysdep.h
#define INLINE_SYSCALL(name, nr, args...)       \
  ({                  \
    long int sc_ret = INTERNAL_SYSCALL (name, nr, args);    \
    __glibc_unlikely (INTERNAL_SYSCALL_ERROR_P (sc_ret))    \
    ? SYSCALL_ERROR_LABEL (INTERNAL_SYSCALL_ERRNO (sc_ret))   \
    : sc_ret;               \
  })

//展开得到
INTERNAL_SYSCALL (openat, 4, args【AT_FDCWD, file, oflag, mode】);  

4.8、展开INTERNAL_SYSCALL

#define INTERNAL_SYSCALL(name, nr, args...)       \
  internal_syscall##nr (SYS_ify (name), args)

//展开得到
internal_syscall4(SYS_ify(openat), args【AT_FDCWD, file, oflag, mode】)

//展开 SYS_ify(openat)
#define SYS_ify(syscall_name) __NR_##syscall_name
//得到
__NR_openat

//从而得到
internal_syscall4(__NR_openat, args【AT_FDCWD, file, oflag, mode】)

4.9、最后internal_syscall4中,汇编调用了syscall

glibc\sysdeps\unix\sysv\linux\x86_64\64\arch-syscall.h
#define __NR_openat 257

最终,syscall时,先传入调用号257,然后是四个真正的参数。

Mininet+ONOS网络模拟环境搭建

1、下载mininet虚拟机:

http://mininet.org/download/#option-1-mininet-vm-installation-easy-recommended

2、导入并运行虚拟机

用户名/密码:mininet/mininet

3、安装docker

sudo apt-get update
sudo apt install curl ssh
curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun

4、拉取onos镜像

sudo docker pull onosproject/onos

5、运行ones容器

#运行
sudo docker run -t -d -p 8181:8181 --name onos1 onosproject/onos

#查看容器运行情况
sudo docker ps

6、连接容器,启用服务

#查看ip地址
sudo docker inspect --format '{{ .NetworkSettings.IPAddress }}' onos1

#ssh连接到容器
ssh -p 8101 karaf@172.17.0.2

#启用OpenFlow Provider Suite
app activate org.onosproject.openflow

#启用 Reactive Forwarding
app activate org.onosproject.fwd

#退出
ctrl+d

7、创建网络

#创建临时网络
sudo mn --topo tree,2 --controller remote,ip=172.17.0.2 --switch=ovsk,protocols=OpenFlow13
*** Creating network
*** Adding controller
Connecting to remote controller at 172.17.0.2:6653
*** Adding hosts:
h1 h2 h3 h4
*** Adding switches:
s1 s2 s3
*** Adding links:
(s1, s2) (s1, s3) (s2, h1) (s2, h2) (s3, h3) (s3, h4)
*** Configuring hosts
h1 h2 h3 h4
*** Starting controller
c0
*** Starting 3 switches
s1 s2 s3 ...
*** Starting CLI:

mininet> nodes
available nodes are:
c0 h1 h2 h3 h4 s1 s2 s3

mininet> links
s1-eth1<->s2-eth3 (OK OK)
s1-eth2<->s3-eth3 (OK OK)
s2-eth1<->h1-eth0 (OK OK)
s2-eth2<->h2-eth0 (OK OK)
s3-eth1<->h3-eth0 (OK OK)
s3-eth2<->h4-eth0 (OK OK)

mininet> net
h1 h1-eth0:s2-eth1
h2 h2-eth0:s2-eth2
h3 h3-eth0:s3-eth1
h4 h4-eth0:s3-eth2
s1 lo: s1-eth1:s2-eth3 s1-eth2:s3-eth3
s2 lo: s2-eth1:h1-eth0 s2-eth2:h2-eth0 s2-eth3:s1-eth1
s3 lo: s3-eth1:h3-eth0 s3-eth2:h4-eth0 s3-eth3:s1-eth2
c0

mininet> h1 ping h2
PING 10.0.0.2 (10.0.0.2) 56(84) bytes of data.
64 bytes from 10.0.0.2: icmp_seq=1 ttl=64 time=43.2 ms

mininet> pingall
*** Ping: testing ping reachability
h1 -> h2 h3 h4
h2 -> h1 h3 h4
h3 -> h1 h2 h4
h4 -> h1 h2 h3
*** Results: 0% dropped (12/12 received)

#退出
ctrl+d

8、网络访问

http://172.17.0.2:8181/onos/ui/login.html
账号/密码:karaf/karaf

然后就可以看到拓扑图了

Linux内存管理

一、整理一下思路
NUMA体系下,每个CPU都有自己直接管理的一部分内存,叫做内存节点【node】,CPU访问自己的内存节点速度,快于访问其他CPU的内存节点;
每个内存节点,按内存的迁移类型,被划分为多个内存区域【zone】;迁移类型包括ZONE_DMA、ZONE_DMA32、ZONE_NORMAL 、ZONE_HIGHMEM、ZONE_MOVABLE、ZONE_DEVICE等;
每个内存区域中,是一段逻辑上的连续内存,包括多个可用页面;但在这个连续内存中,同样有不能使用的地方,叫做内存空洞;在处理内存操作时,要避免掉到洞里;

二、整理一下结构
每个内存节点由一个pg_data_t结构来描述其内存布局;
每个pg_data_t有一个zone数组,包括了内存节点下的全部内存区域zone;
每个zone里有一个free_area数组【0-10】,其序号n的元素下面,挂载了全部的连续2^n页面【page】,也就是free_area【0-10】分别挂载了【1个页面,2个页面,直到1024个页面】
每个free_area,都有一个链表数组,按不同迁移类型,对所属页面【page】再次进行了划分

三、分配内存【只能按2^n页面申请】

alloc_pages->alloc_pages_current->__alloc_pages_nodemask
->get_page_from_freelist,快速分配路径,尝试直接分配内存
->__alloc_pages_slowpath,慢速分配路径,尝试回收、压缩后,再分配内存,如果有OOM风险则杀死进程->实际分配时仍会调用get_page_from_freelist
->->所以无论快慢路径,都会到rmqueue
->->->如果申请一个页面rmqueue_pcplist->__rmqueue_pcplist
1、如果pcplist不为空,则返回一个页面
2、如果pcplist为空,则申请一块内存后,再返回一个页面
->->->如果申请多个页面__rmqueue_smallest
1、首先要取得 current_order【指定页面长度】 对应的 free_area 区中 page
2、若没有,就继续增加 current_order【去找一个更大的页面】,直到最大的 MAX_ORDER
3、要是得到一组连续 page 的首地址,就对其脱链,然后调用expand函数对内存进行分割
->->->->expand 函数分割内存
1、expand分割内存时,也是从大到小的顺序去分割的
2、每一次都对半分割,挂载到对应的free_area,也就加入了伙伴系统
3、直到得到所需大小的页面,就是我们申请到的页面了

四、此外
1、在整个过程中,有一个水位_watermark的概念,其实就是用于控制内存区是否需要进行内存回收
2、申请内存时,会先按请求的 migratetype 从对应类型的page结构块中寻找,如果不成功,才会从其他 migratetype 的 page 结构块中分配, 降低内存碎片【rmqueue->__rmqueue->__rmqueue_fallback】
3、申请内存时,一般先在CPU所属内存节点申请;如果失败,再去其他内存节点申请;具体顺序,和NUMA memory policy有关;

Linux进程管理

一、进程数据结构
每个CPU有一个rq结构,描述进程运行队列,其中:
A、cfs_rq、rt_rq、dl_rq,分别包含了公平调度、实时调度、最早截至时间调度算法相关的队列
B、记录了当前CPU的,正在运行的进程、空转进程、停止进程等;
C、每个进程用一个task_struct结构描述;

task_struct结构包括:
sched_entity结构,描述调度实体;
files_struct 结构,描述进程打开的文件;
mm_struct结构,描述一个进程的地址空间的数据结构;其中包括,vm_area_struct 结构,描述一段虚拟地址空间

二、fork创建一个进程

调用fork
->_do_fork
->->_do_fork首先调用复制进程copy_process
->->->调用了一系列的copy和初始化函数:dup_task_struct、copy_creds、copy_semundo、copy_files、copy_fs、copy_sighand、copy_signal、copy_mm、copy_namespaces、copy_io、copy_thread、copy_seccomp
->->_do_fork然后调用wake_up_new_task,初始化并准备好第一次启动,进入runqueue

其中,_do_fork->copy_process->dup_task_struct
A、alloc_task_struct_node,分配结构体
alloc_task_struct_node->kmem_cache_alloc_node->kmem_cache_alloc->slab_alloc->接上了之前的内容
B、alloc_thread_stack_node,分配内核栈
alloc_thread_stack_node->alloc_pages_node->__alloc_pages_node->__alloc_pages->__alloc_pages_nodemask->接上了之前的内容
C、arch_dup_task_struct复制task_struct
D、setup_thread_stack设置内核栈

其中,_do_fork->copy_process->copy_mm->dup_mm
A、allocate_mm,分配内存
B、memcpy,结构拷贝
C、mm_init,mm初始化
D、dup_mmap,mmap拷贝

其中,_do_fork->copy_files->dup_fd
kmem_cache_alloc,分配内存
copy_fd_bitmaps,拷贝fd位图数据

三、调度器数据结构
sched_class结构,通过一组函数指针描述了调度器;
__end_sched_classes,优先级最高
stop_sched_class,停止调度类
dl_sched_class,最早截至时间调度类
rt_sched_class,实时调度类
fair_sched_class,公平调度调度类
idle_sched_class,空转调度类
__begin_sched_classes,优先级最低

调度器的优先级,是编译时指定的,通过__begin_sched_classes和__end_sched_classes进行定位;

四、CFS调度
cfs调度算法,调度队列为cfs_rq,其整体是一个红黑树,树根记录在tasks_timeline中;
cfs调度器,根据一个进程权重占总体权重的比例,确定每个进程的CPU时间分配比例;而这个权重,开放给程序员的是一个nice值,数值越小,权重越大;

同时,即不能让进程切换过于频繁,也不能让进程长期饥饿,需要保证调度时间:
当进程数小于8个时,进程调度延迟为6ms,也就是每6ms保证每个进程至少运行一次;
当进程数大于8个时,进程延迟无法保证,需要确保程序至少运行一段时间才被调度,这个时间称为最小调度粒度时间,默认为0.75ms;

cfs中,由于每个进程的权重不同,所以无法单纯的通过进程运行时间来对进程优先级进行排序。所以将进程运行时间,通过权重换算,得到了一个进程运行的虚拟时间,然后通过虚拟时间,来对进程优先级进行排序。此时,红黑树的排序特性就充分发挥了,哪个进程的虚拟时间最小,就会来到红黑树的最左子节点,进行调度时,从左到右进行判断就好了。

这个时间又是如何刷新呢:

Linux会有一个scheduler_tick定时器,给调度器提供机会,刷新CFS队列虚拟时间
scheduler_tick->rq.curr.sched_class.task_tick,对应到CFS调度器,就是task_tick_fair
task_tick_fair->entity_tick
->update_curr,更新当前进程调度时间
->check_preempt_tick,根据实际运行时间、最小调度时间、虚拟时间是否最小等,判断是否要进行调度,如果需要调度则打标记

Linux进行进程调度时,调用schedule->__schedule
->pick_next_task
A、首先尝试pick_next_task_fair,获取下一个进程
B、如果获取失败,就按调取器优先级,依次尝试获取下一个进程
C、如果全部获取失败,就返回idel进程
->context_switch,如果获取到了新的进程,进行进程切换

其中,pick_next_task_fair->pick_next_entity,其实就是按红黑树从左到右尝试反馈优先级最高的进程;
然后,当前进程被切换时,也会更新虚拟时间,会在CFS红黑数中比较右侧的地方找到自己的位置,然后一直向左,向左,直到再次被调度。

Linux设备管理

关于数据结构
一、目录组织相关结构
kobject结构表示sysfs一个目录或者文件节点,同时提供了引用计数或生命周期管理相关功能;
kset结构,可以看作一类特殊的kobject,可以作为kobject的集合;同时承担了发送用户消息的功能;

Linux通过kobject和 kset来组织sysfs下的目录结构。但两者之间关系,却并非简单的文件和目录的关系。每个kobject的父节点,需要通过parent和kset两个属性来决定:
A、无parent、无kset,则将在sysfs的根目录(即/sys/)下创建目录;
B、无parent、有kset,则将在kset下创建目录;并将kobj加入kset.list;
C、有parent、无kset,则将在parent下创建目录;
D、有parent、有kset,则将在parent下创建目录,并将kobj加入kset.list;

kobject和kset并不会单独被使用,而是嵌入到其他结构中发挥作用。

二、总线与设备结构
bus_type结构,表示一个总线,其中 subsys_private中包括了kset;
device结构,表示一个设备,包括驱动指针、总线指针和kobject;
device_driver结构,表示一个驱动,其中 driver_private包括了kobject;
上面说的kset和kobject的目录组织关系,起始就是存在于这些数据结构中的;
通过kset和kobject就可以实现总线查找、设备查找等功能;

三、初始化
全局kset指针devices_kset管理所有设备
全局kset指针bus_kset管理所有总线

初始化调用链路:

kernel_init->kernel_init_freeable->do_basic_setup->driver_init
->devices_init设备初始化
->buses_init总线初始化

四、设备功能函数调用
miscdevice结构,表示一个杂项设备;
其中 file_operations包含了全部功能函数指针;

以打开一个设备文件为例,其调用链路为:

filp_open->file_open_name->do_filp_open->path_openat->do_o_path->vfs_open->do_dentry_open
通过file_operations获取了open函数指针,并进行了调用

关于驱动程序Demo
极客时间 操作系统实战45讲 miscdrv源码

一、miscdrv是一个内核模块
1、四个操作函数,封装在file_operations结构中,包括:
misc_open在打开设备文件时执行
misc_release在关闭设备文件时执行
misc_read在读取设备时执行
misc_write在写入设备时执行
file_operations又被封装在miscdevice中,在注册设备时传入

2、devicesinfo_bus_match函数用于总线设备的过滤,被封装在bus_type结构中
bus_type描述了总线结构,在总线注册时传入

3、module_init和module_exit声明入口和出口函数:
miscdrv_init注册设备和总线,在安装内核模块时执行
miscdrv_exit反注册设备和总线,在卸载内核模块时执行

4、只有misc_read比较复杂:
A、通过注册时的devicesinfo_bus获取kset,枚举kset中的每一个kobj
B、对于每个kobj,通过container_of转换为subsys_private
C、对于每个subsys_private,枚举其bus中每个设备,并通过misc_find_match函数进行处理
D、misc_find_match会在kmsg中输出设备名称

二、app.c
就是打开设备,写一下,读一下,关闭设备,主要是触发设备输出

三、执行顺序,需要两个Terminal,T1和T2

1、T1:make
2、T1:sudo insmod miscdrv.ko
3、T2:sudo cat /proc/kmsg
4、T1:sudo ./app
5、T2:ctrl+c
6、T1:sudo rmmod miscdrv.ko

Linux文件管理

一、数据结构
1、四大基本结构
A、超级块管理为super_block,用于描述存储设备上的文件系统,可以从super_block出发把存储设备上的内容读取出来
B、目录结构管理为dentry,通过其来组织整个目录结构
C、文件索引节点管理为inode,可以先把它看作是存储设备上的具体对象,一个inode可以对应多个dentry【比如link】
D、文件管理为file,描述进程中的某个文件对象

2、Linux在挂载文件系统时,会读取文件系统超级块super_block,然后从超级块出发读取并构造全部dentry目录结构;dentry目录结构指向存储设备文件时,是一个个的inode结构。

3、应用程序在打开文件时,在进程结构task_struct->fs_struct中,记录进程相关的文件系统信息,这样就可以对文件系统,进行新增、删除、打开、关闭等相关操作。

4、同时,在进程结构task_struct->files_struct->fdtable->file,保存全部打开的文件指针,文件指针file结构中,会保存inode指针,从而可以获取文件权限、文件访问记录、文件数据块号的信息,进一步可以从文件读取文件信息。

二、trfs demo
极客时间 操作系统实战45讲 trfs源码

1、除上面的结构外,内部使用了两个结构:文件描述fileinfo,目录描述dir_entry
A、fileinfo记录在了inode的私有数据中,这样通过inode就可以方便的找到fileinfo
B、如果是文件,fileinfo.data中记录的就是文件内容
C、如果是文件夹,fileinfo.data记录的就是一个个dir_entry

2、trfs基于非连续内存
A、由MAX_FILES+1个fileinfo组成,记录在全局变量finfo_arr中,但第0和第MAX_FILES个好像没有使用
B、每个fileinfo中包含一个文件块,大小为MAX_BLOCKSIZE
C、并没有使用单独的位图,而是通过每个fileinfo来记录其使用情况的

3、初始化

A、初始化了finfo_arr结构
trfs_init->init_fileinfo

B、超级块创建,占用了finfo_arr[1]
trfs_mount->mount_nodev->trfs_fill_super

4、使用
A、每次新建文件或文件夹,就占用一个空闲的fileinfo
B、删除文件或文件夹,就将一个fileinfo设置为可用
C、读写文件就是通过file找到fileinfo.data
D、查找和枚举就是通过file找到fileinfo.data,然后访问其中的每个dir_entry

Linux四次挥手源码分析

TCP_STATES

四次挥手过程分析【V5.8,正常流程】
1、客户端主动断开连接,状态从TCP_ESTABLISHED变为TCP_FIN_WAIT1,发送FIN包给服务端

A、状态变为TCP_FIN_WAIT1
tcp_close->tcp_close_state
->tcp_set_state(sk, new_state[TCP_ESTABLISHED]),也就是TCP_FIN_WAIT1

B、发送FIN包
tcp_close->tcp_close_state
->tcp_send_fin

2、服务端收到FIN包,状态从TCP_ESTABLISHED变为TCP_CLOSE_WAIT,并返回ACK包

A、状态变为TCP_CLOSE_WAIT
【tcp_protocol.handler】tcp_v4_rcv->tcp_v4_do_rcv->tcp_rcv_established
->tcp_data_queue
->->tcp_fin
->->->inet_csk_schedule_ack; 安排ack
->->->sk->sk_shutdown |= RCV_SHUTDOWN; 模拟了close
->->->sock_set_flag(sk, SOCK_DONE);
->->->case TCP_ESTABLISHED:
->->->tcp_set_state(sk, TCP_CLOSE_WAIT); 修改状态
->->inet_csk(sk)->icsk_ack.pending |= ICSK_ACK_NOW;  ACS是否立即发送

B、发送ACK包
【tcp_protocol.handler】tcp_v4_rcv->tcp_v4_do_rcv->tcp_rcv_established【接上面】
->tcp_ack_snd_check->__tcp_ack_snd_check->tcp_send_ack

3、客户端收到ACK包,状态从TCP_FIN_WAIT1变为TCP_FIN_WAIT2,然后被替换为状态TCP_TIME_WAIT,子状态TCP_FIN_WAIT2

【tcp_protocol.handler】tcp_v4_rcv->tcp_v4_do_rcv->tcp_rcv_state_process
->case TCP_FIN_WAIT1:
->tcp_set_state(sk, TCP_FIN_WAIT2);
->tcp_time_wait(sk, TCP_FIN_WAIT2, tmo);
->->tw = inet_twsk_alloc(sk, tcp_death_row, state);
->->->tw->tw_state = TCP_TIME_WAIT;   
->->->tw->tw_substate = TCP_FIN_WAIT2;
->->->timer_setup(&tw->tw_timer, tw_timer_handler, TIMER_PINNED);

4、服务端状态从TCP_CLOSE_WAIT变为TCP_LAST_ACK,发送FIN包

A、状态变为TCP_LAST_ACK
tcp_close->tcp_close_state
->tcp_set_state(sk, new_state[TCP_CLOSE_WAIT]),也就是TCP_LAST_ACK

B、发送FIN包
tcp_close->tcp_close_state
->tcp_send_fin

5、客户端收到FIN包,子状态从TCP_FIN_WAIT2变为TCP_TIME_WAIT,返回ACK包

A、状态和子状态都为TCP_TIME_WAIT
【tcp_protocol.handler】tcp_v4_rcv->
->if (sk->sk_state == TCP_TIME_WAIT) goto do_time_wait;
->do_time_wait:
->tcp_timewait_state_process
->->if (tw->tw_substate == TCP_FIN_WAIT2)
->->tw->tw_substate = TCP_TIME_WAIT;
->->inet_twsk_reschedule,重新设置回调时间
->->return TCP_TW_ACK;

B、返回ACK
->case TCP_TW_ACK:
->tcp_v4_timewait_ack(sk, skb);

6、服务端收到ACK包,状态从TCP_LAST_ACK变为TCP_CLOSE

【tcp_protocol.handler】tcp_v4_rcv->tcp_v4_do_rcv->tcp_rcv_state_process
->case TCP_LAST_ACK:
->tcp_done
->->tcp_set_state(sk, TCP_CLOSE);

7、客户端超时回调

A、超时时间定义
#define TCP_TIMEWAIT_LEN (60*HZ)
#define TCP_FIN_TIMEOUT TCP_TIMEWAIT_LEN

B、超时后,回调tw_timer_handler->inet_twsk_kill,进行inet_timewait_sock清理工作

C、没有找到状态变从TCP_TIME_WAIT变为TCP_CLOSE的代码

Linux三次握手源码分析

TCP_STATES

三次握手过程分析【V5.8,正常流程】
1、客户端发起第一次握手,状态调变为TCP_SYN_SENT,发送SYN包

connect->__sys_connect->__sys_connect_file->【sock->ops->connect】tcp_v4_connect
A、状态变化
->tcp_set_state(sk, TCP_SYN_SENT);
B、发送SYN
->tcp_connect->tcp_send_syn_data

2、服务端收到客户端的SYN包,初始化socket,状态从TCP_LISTEN变为TCP_NEW_SYN_RECV,发送第二次握手SYN_ACK包

A、收到连接,初始化socket
accept->__sys_accept4->__sys_accept4_file->【sock->ops->accept】inet_csk_accept

B、收到SYN,改变状态
【tcp_protocol.handler】tcp_v4_rcv->tcp_v4_do_rcv->tcp_rcv_state_process->
->case TCP_LISTEN:
->[sock->ops->conn_request]tcp_v4_conn_request->tcp_conn_request
->->inet_reqsk_alloc
->->->ireq->ireq_state = TCP_NEW_SYN_RECV;

C、发送SYN_ACK包
->[sock->ops->conn_request]tcp_v4_conn_request->tcp_conn_request【和B路径一样】
->->【af_ops->send_synack】tcp_v4_send_synack
->->->tcp_make_synack
->->->__tcp_v4_send_check

3、客户端收到SYN_ACK包,状态从TCP_SYN_SENT变为TCP_ESTABLISHED,并发送ACK包

A、收到SYN_ACK包
【tcp_protocol.handler】tcp_v4_rcv->tcp_v4_do_rcv->tcp_rcv_state_process
->case TCP_SYN_SENT:
->tcp_rcv_synsent_state_process->tcp_finish_connect
->->tcp_set_state(sk, TCP_ESTABLISHED);

B、发送ACK包
->tcp_rcv_synsent_state_process->tcp_send_ack->__tcp_send_ack

4、服务端收到ACK包,状态从TCP_NEW_SYN_RECV变为TCP_SYN_RECV【实际上是新建了一个sock】

【tcp_protocol.handler】tcp_v4_rcv->
->if (sk->sk_state == TCP_NEW_SYN_RECV)
->tcp_check_req
->->【inet_csk(sk)->icsk_af_ops->syn_recv_sock】tcp_v4_syn_recv_sock->tcp_create_openreq_child->inet_csk_clone_lock
->->->inet_sk_set_state(newsk, TCP_SYN_RECV);

5、服务端状态从TCP_SYN_RECV变为TCP_ESTABLISHED

【tcp_protocol.handler】tcp_v4_rcv->tcp_v4_do_rcv->tcp_rcv_state_process
->case TCP_SYN_RECV:
->tcp_set_state(sk, TCP_ESTABLISHED);

如何解决内存碎片问题


如何解决内存碎片问题?
————NEOHOPE的内存碎片优化手册

其实无论采用哪种分配方式,内存的碎片化都是难以彻底避免的。无论是操作系统、虚拟机还是应用,都要面对这个问题。
业界有多种思路来解决或缓解此问题:

一、操作系统层面
1、把不可移动内存集中管理,内存分区其实在一定程度上解决了这些问题
从操作系统的角度来说,有些内存页面是可以移动的,有些是无法移动的。如果无法移动的页面过于分散,整个系统内存的连续性就很差,碎片化就更严重,而且更加不容易管理。所以在操作系统设计的时候,一般会根据功能用途对内存进行分区,一定程度上避免此类问题。

2、linux采用了buddy system来缓解内存碎片问题
buddy system是一个很巧妙的设计,将内存连续页面按2的n次方进行分组。
申请时会匹配第一个大于等于所需页面的2的n次方连续页面,并将多余页面拆分后挂载到对应的2的x方组。
释放内存页面时,仍按2的n次方进行归还,并尝试将内存进行合并。

3、linux中为了处理特别小的内存请求,引入了slab技术,来辅助buddy system
类似于对象池的概念,系统先申请一部分页面用于对象池。
申请时划分一部分出来给应用使用,如果内存不足会进行主动扩容。
归还时对象还给对象池,如果空闲对象比较多时,会主动释放部分内存。

4、windows有一种LFH(Low Fragmentation Heap)技术,缓解内存碎片问题
在应用程序启动时,操作系统会额外分配一定的连续内存LFH给这个进程备用
如果应用需要使用内存,会优先从LFH中申请,从而降低系统层面的内存管理负担

5、windows在进程退出后,不会立即释放dll文件内存
一方面提速,如果关闭一个应用,再开启,就会感觉很快
另一方面也缓解了操作系统内存管理负担。
其实,看下你手机的APP,切换到后台时,就是这个效果

6、内存整理服务
无论是linux还是windows都有低优先级线程,在后台默默做着内存规整工作,类似于磁盘碎片清理
比如linux内核中的kcompactd线程。

7、类似与LFH,可以考虑在内存分配时额外预留一部分,下次分配在预留的地方继续分配
windows在xp时代,需要应用自行开启LFH功能,但vista之后,操作系统会同一进行管理

8、为了内存规整方便,可以考虑靠近应用已分配内存区域进行分配
其实可操作性不高,还不如上一条可行性好一些

9、还有一种思路,就是将不连续的内存,转换为逻辑上连续的内存,来绕过碎片化问题
但一些情况下性能难以保证

二、虚拟机层面
1、JVM整体内存会被划分为多个部分,类似于做了分区:
一部分是虚拟机共有的【方法区、堆】,一部分是线程私有的【虚拟机栈、本地栈、程序计数器】

2、同时,JVM虚拟机也会根据对象的生命周期,类似进一步做了分区,而且不同分区采用不同GC策略
最常用的就是年代划分法,新生代、老年代、永久代【后来的Metaspace】

3、JVM虚拟机,GC时会通过标记-整理(比如CMS)或复制-清除(比如G1)的方法来解决部分碎片问题

三、应用层面
1、redis在处理内存的时候,申请时会额外申请一部分先备着【记得是jemalloc】

2、redis释放时也不会立即释放,有单独的线程进行处理,在应用层面去降低系统内存管理负担

3、redis在数据结构上也做了很多努力

4、在写程序时,如果需要频繁创建和释放对象,可以尝试使用对象池

5、在写程序的时候,尽量不要零零散散的去申请大量小内存;

6、除了标准库以外,可以试一下 jemalloc或Doug Lea’s malloc

7、感兴趣可以看下redis内存管理的代码

如何写出让CPU跑得更快的代码


如何写出让 CPU 跑得更快的代码?
————NEOHOPE的代码优化手册

一、需求阶段

1、过早优化是魔鬼。业务能运行,比业务响应速度更重要。能用的慢代码,比不能用的快代码好的多

2、搞清楚业务类型,是重IO,还是重CPU,还是重GPU,明确具体需求

3、这个需求,确定是要通过代码优化解决吗?
是不是一个业务问题无法解决,想通过系统去限制业务呢?

4、这个需求,是否可以通过花钱或其他方式解决,比如增加硬件

5、搞清楚为何要优化代码,不优化问题是什么?优化代码的风险是什么?时间周期大概是剁手?代价和受益如何?

6、对比4、5的各种方案,确定用哪个

二、分析阶段

1、一旦明确了要进行优化,不要一头扎入细节。考虑整个系统的架构是否合理,看下全局,看下上下游的影响。

2、遵从Ahmdal定律,确定优化范围;

3、咱们要优化的东西,业界是否已有解决方案,不要重复造轮子

4、如果涉及面比较广,别急着动手,做好分析,制定方案,再动手

三、架构阶段

1、调整不合理的架构

2、调整不合理的数据流

3、进行必要的组件替换

四、算法提升阶段

1、遵从80-20法则,程序80%的时间在运行20%或更少的代码,针对热代码进行优化,才容易产出效果;

2、评估当前算法,是否有高效的算法
比如:用DP或贪婪算法替代暴力算法,将算法的时间复杂度尽量降低

3、使用合理的数据结构
比如哈希表、红黑树

4、使用数学公式,替代低效计算

5、缓存重复出现的中间结果,空间换时间

五、编码提升阶段

1、遵从数据访问的局部性法则,按数据存放顺序访问内存效率远高于乱序访问内存效率,也就是尽量帮助CPU做好数据Cache的预测工作。同样根据Cache大小,做好数据结构的优化工作,进行数据压缩或数据填充,也是提升Cache效率的好方式;

2、遵从指令访问的局部性法则,减少跳转指令,同样是尽量帮助CPU做好数据Cache的预测工作;现代CPU都有一些预测功能【如分支预测】,利用好CPU的这些功能,也会提升Cache命中率;

3、减少内存的重复申请和释放

4、 减少函数调用层数
使用INLINE等关键字,同样是减少调用层数
使用宏展开,也有类似效果

5、减少数据传递个数,合理封装

6、数据传递,以引用为主,减少传值

7、减少数据类型转换

10、调整运算顺序,减少CPU低效运算。
比如除法运算。

11、调整运算顺序,尽早结束循环,避免无效操作

12、 少用高级语言的多重继承等特性

13、使用汇编和C语言,而不是更高级的语言,很多时候可以提速运算效率;

14、直接使用昂贵的寄存器作为变量,可以变相提供加速效果;

15、采用性能高的函数,批量处理数据,如memset等

16、能在编译阶段完成的任务,不要留到运行阶段完成。
比如,CPP的泛型是编译阶段完成的,比运行时再做处理的语言效率更高
比如,不要用反射,不要动态类型绑定
比如,初始化工作尽早完成

六、编译阶段

1、开启编译器优化,速度最大化

2、使用Intel自家的编译器,开启性能优化,很多时候可以提速运算效率;

3、考虑指令级并行性(IPL)

七、运行阶段

1、避免计算线程在多个核心之间漂移,避免缓存重复加载,可以绑定核心【物理核即可,不用到逻辑核】,提高效率;

2、去除伪共享缓存:在多核环境下,减少多个核心对同一区域内存的读写并发操作,减少内存失效的情况的发生;

3、合理提高进程优先级,减少进程间切换,可以变相提供Cache提速的效果;

4、关闭Swap,可以变相提供内存提速、Cache提速的效果;

八、测试阶段

1、合适的手段及工具进行性能评估

2、要符合实际情况,不要片面追求指标

先总结这些吧。