最近阅读了部分Linux5.9源码,添加了一些注释,感兴趣的同学可以看下。
https://github.com/neohope/NeoLinux/
Category Archives: Deployment
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数量
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,然后是四个真正的参数。
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