Linux虚拟化管理

1、内核模块初始化

module_init(vmx_init)->kvm_init
module_init(svm_init)->kvm_init

//其中,kvm_init
->kvm_arch_init
->kvm_irqfd_init
->kvm_arch_hardware_setup
->misc_register(&kvm_dev)

2、从数据结构角度,又可以看到了设备皆为文件的思想

static struct miscdevice kvm_dev = {
    KVM_MINOR,
    "kvm",
    &kvm_chardev_ops,
};

static struct file_operations kvm_chardev_ops = {
    .unlocked_ioctl = kvm_dev_ioctl,
    .llseek     = noop_llseek,
    KVM_COMPAT(kvm_dev_ioctl),
};

//初始化时,通过misc_register,实现了操作的绑定。

3、通过上面的数据结构,我们就可以找到创建虚拟机的方法,并生成控制文件
kvm_dev.kvm_chardev_ops.kvm_dev_ioctl
或者,ioctl系统调用KVM_CREATE_VM,效果也是一样的:

SYSCALL_DEFINE3(ioctl, unsigned int, fd, unsigned int, cmd, unsigned long, arg)
->vfs_ioctl,会用到vfs_ioctl.unlocked_ioctl也就是kvm_dev_ioctl

->case KVM_CREATE_VM:
->        r = kvm_dev_ioctl_create_vm(arg);
->file = anon_inode_getfile("kvm-vm", &kvm_vm_fops, kvm, O_RDWR);

//其中,kvm_dev_ioctl_create_vm
->kvm_create_vm
->->kvm_arch_init_vm
->->hardware_enable_all
->->kvm_arch_post_init_vm
->->list_add(&kvm->vm_list, &vm_list);

4、生成虚拟CPU套路很相似,仍是文件操作

static struct file_operations kvm_vm_fops = {
    .release        = kvm_vm_release,
    .unlocked_ioctl = kvm_vm_ioctl,
    .llseek     = noop_llseek,
    KVM_COMPAT(kvm_vm_compat_ioctl),
};

//创建虚拟机时,通过anon_inode_getfile,实际上就把文件和kvm_vm_fops绑定了起来。
anon_inode_getfile("kvm-vm", &kvm_vm_fops, kvm, O_RDWR)

5、在调用ioctl时

SYSCALL_DEFINE3(ioctl, unsigned int, fd, unsigned int, cmd, unsigned long, arg)
->vfs_ioctl,会用到vfs_ioctl.unlocked_ioctl也就是kvm_vm_ioctl

kvm_vm_ioctl->kvm_vm_ioctl_create_vcpu
->kvm_arch_vcpu_precreate
->kvm_vcpu_init
->kvm_arch_vcpu_create
->kvm_get_kvm
->create_vcpu_fd,生成设备文件inode
->kvm_arch_vcpu_postcreate

//其中,kvm_arch_vcpu_create
->kvm_mmu_create
->vcpu->arch.user_fpu = kmem_cache_zalloc(x86_fpu_cache, GFP_KERNEL_ACCOUNT);
->kvm_pmu_init(vcpu);
->kvm_hv_vcpu_init(vcpu);
->kvm_x86_ops.vcpu_create(vcpu);
->kvm_vcpu_mtrr_init(vcpu);
->vcpu_load(vcpu);
->kvm_vcpu_reset(vcpu, false);
->kvm_init_mmu(vcpu, false);  //包括init_kvm_tdp_mmu和init_kvm_softmmu两种虚拟化方式

6、启动虚拟机,还是文件操作

static struct file_operations kvm_vcpu_fops = {
    .release        = kvm_vcpu_release,
    .unlocked_ioctl = kvm_vcpu_ioctl,
    .mmap           = kvm_vcpu_mmap,
    .llseek     = noop_llseek,
    KVM_COMPAT(kvm_vcpu_compat_ioctl),
};

7、在调用ioctl时KVM_RUN

SYSCALL_DEFINE3(ioctl, unsigned int, fd, unsigned int, cmd, unsigned long, arg)
->vfs_ioctl,会用到vfs_ioctl.unlocked_ioctl也就是kvm_vcpu_ioctl

kvm_vcpu_ioctl->
case KVM_RUN: 
  kvm_arch_vcpu_ioctl_run

//其中,
kvm_arch_vcpu_ioctl_run->vcpu_run->vcpu_enter_guest

8、IO同样有虚拟化和半虚拟化两种
一个处理函数为kvm_fast_pio,另一个为kvm_emulate_instruction

Linux系统调用03

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

本节,给Linux系统,增加一个新系统调用功能,获取cpu数量。

1、新建一个源码编译目录

mkdir kernelbuild

2、下载源码,解压

wget https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.10.59.tar.gz   

tar -xzf linux-5.10.59.tar.gz   

cd linux-5.10.59

3、清理

make mrproper

4、修改文件

4.1、arch/x86/entry/syscalls/syscall_64.tbl
#在440后面增加一行
441  common  get_cpus    sys_get_cpus

4.2、include/linux/syscalls.h
#在最后一个asmlinkage增加一行
asmlinkage long sys_get_cpus(void);

4.3、kernel/sys.c  
#在最后一个SYSCALL_DEFINE0后面增加下面几行
//获取系统中有多少CPU
SYSCALL_DEFINE0(get_cpus)
{
    return num_present_cpus();
}

5、内核配置

make menuconfig
make oldconfig

6、修改.config,去掉一个证书

CONFIG_SYSTEM_TRUSTED_KEYS=“”

7、编译

make -j4

8、安装

sudo make modules_install
sudo make install

9、测试
9.1、新建文件cpus.c

#include <stdio.h>
#include <unistd.h>
#include <sys/syscall.h>
int main(int argc, char const *argv[])
{
    //syscall就是根据系统调用号调用相应的系统调用
    long cpus = syscall(441);
    printf("cpu num is:%d\n", cpus);//输出结果
    return 0;
}

9.2、编译

gcc main.c -o cpus

9.3、运行

./cpus
在没有修改的内核上返回是-1
在修改过的为num_present_cpus数量

极客时间 操作系统实战45讲 Linux系统调用源码

Linux系统调用02

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

二、linux内核部分

1、在make时,会通过syscall_64.tbl生成syscalls_64.h,然后包含到syscall_64.c,进行调用号与函数之间的绑定。
arch/x86/entry/syscalls/syscall_64.tbl
arch/x86/include/generated/asm/syscalls_64.h
arch/x86/entry/syscall_64.c

1.1、以sys_openat为例,在syscall_64.tbl中为

257    common    openat            sys_openat
441    common    get_cpus          sys_get_cpus

1.2、make后,在生成的syscalls_64.h中为

__SYSCALL_COMMON(257, sys_openat)

1.3 在syscall_64.c中,展开__SYSCALL_COMMON

#define __SYSCALL_COMMON(nr, sym) __SYSCALL_64(nr, sym)
//展开就是
__SYSCALL_64(257, sys_openat)

1.4、在syscall_64.c中,第一次展开__SYSCALL_64

#define __SYSCALL_64(nr, sym) extern long __x64_##sym(const struct pt_regs *);
#include <asm/syscalls_64.h>
#undef __SYSCALL_64

//展开就是
extern long __x64_sys_openat(const struct pt_regs *);

//也就是每个__SYSCALL_64都展开成了一个外部函数

1.5、在syscall_64.c中,第二次展开__SYSCALL_64

#define __SYSCALL_64(nr, sym) [nr] = __x64_##sym,

asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
    [0 ... __NR_syscall_max] = &__x64_sys_ni_syscall,
#include <asm/syscalls_64.h>
};

//展开其实就是指向了外部函数
[257]=__x64_sys_openat,

//全部展开结果,都会被包含到sys_call_table中,从而完成了调用号与函数之间的绑定。

2、当产生系统调用时
2.1、应用直接syscall或通过glibc产生了syscall

2.2、cpu会产生类似于中断的效果,开始到entry_SYSCALL_64执行

//文件路径arch/x86/entry/entry_64.S
SYM_CODE_START(entry_SYSCALL_64)
//省略代码
call   do_syscall_64
SYM_CODE_END(entry_SYSCALL_64)

//文件路径arch/x86/entry/entry_64.S,32位兼容模式,过程与64位类似
SYM_CODE_START(entry_SYSCALL_compat)
call   do_fast_syscall_32
SYM_CODE_END(entry_SYSCALL_compat)

2.3、调用do_syscall_64

#ifdef CONFIG_X86_64
__visible noinstr void do_syscall_64(unsigned long nr, struct pt_regs *regs)
{
    nr = syscall_enter_from_user_mode(regs, nr);
    instrumentation_begin();
    if (likely(nr < NR_syscalls)) {
        nr = array_index_nospec(nr, NR_syscalls);
        regs->ax = sys_call_table[nr](regs);
    }
    instrumentation_end();
    syscall_exit_to_user_mode(regs);
}
#endif

2.4、根据sys_call_table调用对应的功能函数

sys_call_table[nr](regs)
如果我们传入257,就会调用__x64_sys_openat
如果我们传入441,就会调用__x64_sys_get_cpus

2.5、但咱们实际写的函数sys_get_cpus,好像和实际调用函数__x64_sys_get_cpus,差了一个__x64,这需要一个wrapper

arch\x86\include\asm\syscall_wrapper.h
#define SYSCALL_DEFINE0(sname)                      \
    SYSCALL_METADATA(_##sname, 0);                  \
    static long __do_sys_##sname(const struct pt_regs *__unused);   \
    __X64_SYS_STUB0(sname)                      \
    __IA32_SYS_STUB0(sname)                     \
    static long __do_sys_##sname(const struct pt_regs *__unused)

#define __X64_SYS_STUB0(name)                       \
    __SYS_STUB0(x64, sys_##name)

#define __SYS_STUB0(abi, name)                      \
    long __##abi##_##name(const struct pt_regs *regs);      \
    ALLOW_ERROR_INJECTION(__##abi##_##name, ERRNO);         \
    long __##abi##_##name(const struct pt_regs *regs)       \
        __alias(__do_##name);

SYSCALL_DEFINE0(get_cpus),会展开成为
__X64_SYS_STUB0(get_cpus) 
//然后
 __SYS_STUB0(x64, sys_get_cpus)
//然后
long __x64_sys_get_cpus(const struct pt_regs *regs);

这样前后就对上了,glibc和linux内核就通了。

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,然后是四个真正的参数。

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的代码