kernel启动流程第二阶段
来源:互联网 发布:达内java速成班 编辑:程序博客网 时间:2024/05/21 05:41
Kernel启动流程中的Tips:
1、 Kernel一般会存在于存储设备上,比如FLASH\EMMC\SDCARD. 因此,需要先将kernel镜像加载到RAM的位置上,CPU才可以去访问到kernel。
但是注意,加载的位置是有要求的,一般是加载到物理RAM偏移0x8000的位置,也就是要在前面预留出32K的RAM。kernel会从加载的位置上开始解压,而kernel前面的32K空闲RAM中,16K作为boot params,16K作为临时页表
2、 Arch/arm/kernel/head.S(kernel的入口函数)
3、 bootloader需要通过设置PC指针到kernel的入口代码处(也就是kernel的加载位置)来实现kernel的跳转。
Start_Kernel():内核启动第二阶段入口 init/main.c
来自:https://www.cnblogs.com/yjf512/p/5999532.html
1、lockdep_init();kernel/kernel3.18/include/linux/lockdep.h
do{}while(0)什么都没做,作用暂未知。
2、set_task_stack_end_magic(&init_task);kernel/kernel3.18/kernel/fork.c
init_task:
1)为init_task_union结构体的成员,为系统的第一个进行,PID为0,唯一一个不用fork()创建的进程
2) 由宏’INIT_TASK(tsk)’初始化,方式:为task_struct结构体中的每个成员直接赋值
该函数主要用于指定init_task线程堆栈的结尾,设置标记STACK_END_MAGIC(防溢出操作)
3、smp_setup_processor_id();kernel/kernel3.18/arch/arm/kernel/setup.c
设置smp模型的处理器ID;SMP(多对称处理模型),指多个CPU之间地位平等,共享所有总线、内存、I/O,但也因此会造成抢占资源的问题。
4、boot_init_stack_canary();kernel/kernel3.18/arch/arm/asm/stackprotector.h
该函数也用于防止堆栈溢出,该函数在堆和栈之间设置一个标记位(canaryword)。当该位被修改后,即可探测到溢出。
参考:https://www.ibm.com/developerworks/cn/linux/l-cn-gccstack/
上图2中的EBP(堆栈帧指针,ESP为栈顶指针)和返回地址已经被覆盖,如果能在运行时检测出这种破坏,就有可能对函数栈进行保护。目前的堆栈保护实现大多使用基于 “Canaries”的探测技术来完成对这种破坏的检测。详细见上述http地址。
5、cgroup_init_early();kernel/kernel3.18/kernel/cgoup.c
参考:http://www.cnblogs.com/yjf512/p/6003094.html描述了Linux系统中的cgroup机制,部分截选如下:
我们把每种资源叫做子系统,比如CPU子系统,内存子系统。为什么叫做子系统呢,因为它是从整个操作系统的资源衍生出来的。然后我们创建一种虚拟的节点,叫做cgroup(controlgroup,一组进程的行为控制)。
进程分组css_set,不同层级中的节点cgroup也都有了。那么,就要把节点cgroup和层级进行关联,和数据库中关系表一样。这个事一个多对多的关系。为什么呢?首先,一个节点可以隶属于多个css_set,这就代表这这批css_set中的进程都拥有这个cgroup所代表的资源。其次,一个css_set需要多个cgroup。因为一个层级的cgroup只代表一种或者几种资源,而一般进程是需要多种资源的集合体。
Cgroup机制结构图如下:
结构体css_set中最重要的成员就是cgroup_subsys_statesubsys[]数组以及structcgroup_subsys_state *subsys[CGROUP_SUBSYS_COUNT];
task_struct->css_set->cgroup_subsys_state->cgroup反映了进程与cgroup之间的连接关系。
即该函数就是初始化这个cgroup机制。
6、local_irq_enable();关闭中断(底层调用汇编指令)
7、boot_cpu_init();初始化第一个CPU,并将当前CPU设置为激活状态
8、page_address_init();
页地址初始化的作用,未定义高端内存,代码为do{}while(0),不需要做任何操作,具体参考上述网址。
9、setup_arch(&command_line);
内核启动最重要的部分
参考:http://blog.csdn.net/qing_ping/article/details/17351541
http://www.it165.net/os/html/201409/9264.html
主要完成四个工作:
A)取得machine和processor的信息,然后赋值给kernel相应的全局变量
B)对boot_command_line和tags进行解析
C)memory和cache的初始化
D)为kernel后续运行请求资源
1. /*内核通过machine_desc结构体来控制系统体系架构相关部分的初始化。
2. machine_desc结构体通过MACHINE_START宏来初始化,在代码中,
3. 通过在start_kernel->setup_arch中调用setup_machine_fdt来获取。*/
每种体系结构都有自己的setup_arch()函数,是体系结构相关的,具体编译哪个 体系结构的setup_arch()函数,由源码树顶层目录下的Makefile中的ARCH变量决定
{
1)setup_processor();
首先从寄存器中读取cpuid,之后调用lookup_processor_type来取得proc_info_list
2)setup_machine_fdt();
获取machine_desc结构体
3)
1. 通过连接脚本中得到的Linux代码位置数据来初始化一个mm_struct结构体init_mm中的部分数据
2. ps:每一个任务都有一个mm_struct结构以管理内存空间,init_mm是内核自身的mm_struct
后续暂留
}
10、mm_init_cpumask(&init_mm)
初始化CPU屏蔽字
mm.owner = &init_task
11、setup_command_line();
对cmdline进行备份和保存:保存未改变的comand_line到字符数组static_command_line[] 中。
保存 boot_command_line到字符数组saved_command_line[]中
初始化initcall_command_line变量用于后续使用
12、setup_nr_cpu_ids();
参考:http://blog.csdn.net/yin262/article/details/46778013
nr_cpu_ids全局变量被声明为__read_mostly属性。
nr_cpu_ids保存的是所有可处于联机状态的CPU总数。
nr_cpu_ids具有当前系统能具备的CPU数的信息,默认值为NR_CPUS值,NR_CPUS是编译时用户可设置的常量值。NR_CPUS并非当前系统内存在的CPU的数值,而是Linux内核能支持的最大CPU数的最大值。
三种系统中UP(Uni-Processor)中是1,32位SMP中具有2~32的值,64位内核中具有2~64位的值。
其中:
cpu_possible_mask:系统内可安装的最多CPU的位图
cpu_online_mask:系统内安装的CPU中,正在使用的CPU的位图
cpu_present_mask:系统内安装的CPU的位图
cpu_active_mask:处于联机状态且可以动的(migration)的CPU的位图
13、setup_per_cpu_areas();
setup_per_cpu_areas是为了对内核的内存管理(mm)进行初始化而调用的函数之一。只在SMP系统中调用,UP中不执行任何操作。
setup_per_cpu_areas函数为SMP的每个处理器生成per-cpu数据。
per-cpu数据按照不同的CPU类别使用,以将性能低下引发的缓存一致性(cachecoherency)问题减小到最小。per-cpu数据由各cpu独立使用,即使不锁也可访问,十分有效。
系统中维护的per_cpu数据是一个数据结构数组,其中每一个元素即每一个数据结构仅供一个CPU使用,不用担心竞争。
__per_cpu_offset数组中记录了每个CPU的percpu区域的开始地址。我们访问每CPU变量就要依靠__per_cpu_offset中的地址。
14、smp_prepare_boot_cpu();donothing
15、build_all_zonelists();
建立系统内存页区(zone)链表
是是是
16、page_alloc_init();
内存页初始化
Here
17、打印boot_command_line
18、parse_early_param();
解析早期格式的内核参数
19、parse_args("Bootingkernel", static_command_line, __start___param,
__stop___param - __start___param,
-1, -1, &unknown_bootoption);
参考:http://blog.csdn.net/funkunho/article/details/51967137
函数对Linux启动命令行参数进行再分析和处理,
当不能够识别前面的命令时,所调用的函数。
参考:http://blog.csdn.net/tommy_wxie/article/details/8041487
系统的所有参数都是由__setup(str,fn)和early_param(str,fn)宏定义的,两个宏都调用一个宏:
#define __setup(str, fn) /
__setup_param(str, fn, fn, 0)
#define early_param(str,fn) /
__setup_param(str, fn, fn, 1)
可见唯一区别为最后的early标记是0还是1。为1则为early param,其表示需要在其他内核选项之前被处理(18parse_early_param),若为0则由parse_args处理。
__setup()宏用于定义一个结构体(obs_kernel_param),并指定其放入“.init.setup”段,段头段尾分别为(__setup_start和__setup_end),因此解析参数的时候即可以通过从头遍历到尾,将其中的标记提取出来用来判断是否是early参数,之后对符合的参数,提取其结构体中的函数(定义时传入的fn)直接调用。
20、jump_label_init();
处理静态定义在跳转标号
21、setup_log_buf(0);
使用memblock_alloc分配一个启动时log缓冲区
参考:http://blog.csdn.net/ffmxnjm/article/details/70230958
22、pidhash_init(); //初始化pid散列表
参考:http://blog.csdn.net/ongoingcre/article/details/50405356
设定Pid散列表的原因是:通过遍历进程双向链表来找到指定的进程效率太低,因此再维护一个hash表来提高查找效率
第一:分配系统支持最大的hash_table
,以及长度,在此期间内核动态的为4个散列表分配空间,分别是PID(进程ID),TGID(线程组领头进程的PID),PGID(进程组领头进程的PID),SID(会话领头进程的PID)。
第二:根据得到的长度初始化pid_hash.
第三:hash表可能存在冲突的问题,因此在得到pid_hash后,初始化链表来解决冲突的问题。
23、vfs_caches_init_early(); //初始化dentry和inode的hashtable
两个hash表的初始化的方式和pidhash_init()如出一辙,都是先分配空间,初始化hash表,再讲对应的链表初始化。此函数为前期步骤,后续初始化为函数68。
24、sort_main_extable(); //对内核异常向量表进行排序
25、trap_init(); //对内核陷阱异常进行初始化,arm没有用到
26、mm_init(); // 初始化内核内存分配器,过度到伙伴系统,启动slab机制,初始化
// 非连续内存区
参考:http://blog.csdn.net/sunlei0625/article/details/58594542
Mem_map:描述所有的物理内存采用的struct page结构的数组的基指针。比如说,对于4GB的内存(2^32)来说,如果一个页定义为4KB,即2^12字节。那么可想而知,总共这个mem_map数组大小为2^20个。
这些页都有一个具体的页帧号与之对应。页帧号一般用pfn来表示,那么由于每个页都有一个页帧号,那最小的页帧号和最大的页帧号为多少呢?需要特别注意的是,页帧号也是与mem_map数组的index相对应。我们一般认为pfn_min为0,而最大pfn_max为mem_map数组下标的最大值,这个最大值也就是max_pfn,这个值跟内核的max_mapnr相对应。
函数set_max_mapnr()就是用于计算max_mapnr。max_mapnr是在setup_arch的paging_init()中调用bootmem_init()来设置的。在成功设置max_mapnr后,我们要把启动过程时所有的空闲内存释放到伙伴系统,这里需要注意三点:
一. bootmem内存管理或者nobootmem管理
二. memblock内存管理
三. 伙伴系统
27、sched_init(); //初始化进程调度器
根据系统的配置情况,分配空间并初始化root_task_group中的调度实体或调度队列指针。
将root_task_group添加到task_groups链表
。。。一系列初始化
init_idle(current,smp_processor_id()); //将当前进程,即init_task设置为idle进程
current->sched_class = &fair_sched_class; //设置当前进程,即init_task进程采用CFS调度策略
28、preempt_disable(); //禁止内核抢占
if (WARN(!irqs_disabled(), "Interrupts wereenabled *very* early, fixing it\n"))
local_irq_disable(); // 关闭本地中断
29、idr_init_cache(); //创建idr(整数id管理机制)高速缓存
IDR机制原理:
IDR机制适用在那些需要把某个整数和特定指针关联在一起的地方。例如,在IIC总线中,每个设备都有自己的地址,要想在总线上找到特定的设备,就必须要先发送设备的地址。当适配器要访问总线上的IIC设备时,首先要知道它们的ID号,同时要在内核中建立一个用于描述该设备的结构体,和驱动程序
将ID号和设备结构体结合起来,如果使用数组进行索引,一旦ID号很大,则用数组索引会占据大量内存空间。这显然不可能。或者用链表,但是,如果总线中实际存在的设备很多,则链表的查询效率会很低。
此时,IDR机制应运而生。该机制内部采用红黑树实现,可以很方便的将整数和指针关联起来,并且具有很高的搜索效率
30、rcu_init(); //初始化rcu机制(读-写-拷贝)
31、trace_init(); //初始化系统的trace功能
32、context_tracking_init();//
33、radix_tree_init(); //初始化内核基数树
/* init some links before init_ISA_irqs() */
Linuxradix树最广泛的用途是用于内存管理,结构address_space通过radix树跟踪绑定到地址映射上的核心页,该radix树允许内存管理代码快速查找标识为dirty或writeback的页。
Linux基数树(radix tree)是将指针与long整数键值相关联的机制,它存储有效率,并且可快速查询,用于指针与整数值的映射(如:IDR机制)、内存管理等。
34、early_irq_init(); // arm64没有用到
35、init_IRQ(); //初始化中断
参考:http://blog.chinaunix.net/uid-12567959-id-160975.html
start_kernel()函数调用trap_init()、early_irq_init()和init_IRQ()三个函数来初始化中断管理系统。
1)trap_init()在arm平台下为空。
2)early_irq_init()函数在kernel/handle.c文件中根据内核配置时是否选择了CONFIG_SPARSE_IRQ,而可以选择两个不同版本的该函数中的一个进行编译。CONFIG_SPARSE_IRQ配置项,用于支持稀疏irq号,对于发行版的内核很有用,它允许定义一个高CONFIG_NR_CPUS值,但仍然不希望消耗太多内存的情况。
主要工作即为初始化用于管理中断的irq_desc[NR_IRQS]数组的每个元素,它主要设置数组中每一个成员的中断号,使得数组中每一个元素的kstat_irqs字段(irq stats per cpu),指向定义的二维数组中的对应的行。
alloc_desc_masks(&desc[i], 0, true)和init_desc_masks(&desc[i])函数在非SMP平台上为空函数。arch_early_irq_init()在主要用于x86平台和PPC平台,其他平台上为空函数。
3)init_IRQ(void)函数是一个特定于体系结构的函数,这个函数将irq_desc[NR_IRQS]结构数组各个元素的状态字段设置为IRQ_NOREQUEST| IRQ_NOPROBE,也就是未请求和未探测状态。然后调用特定机器平台的中断初始化init_arch_irq()函数。
36、tick_init(); //初始化时钟滴答控制器
内部调用两个函数:tick_broadcast_init()和tick_nohz_init()
1)Linux的broadcasttimer机制:防止系统休眠时(CPU深度休眠也会关闭local timer),localtimer也休眠导致无法唤醒本CPU,所以添加了一个broadcast timer机制,用于在cpu休眠时负责唤醒。
详细参考:http://www.wowotech.net/timer_subsystem/tick-broadcast-framework.html
2)nohz时钟机制:
参考:http://blog.csdn.net/droidphone/article/details/8112948
Linux中的时钟事件都是由一个周期时钟提供,不管系统中的clock_event_device是工作于周期触发模式,还是工作于单触发模式,也不管定时器系统是工作于低分辨率模式,还是高精度模式,内核都竭尽所能,用不同的方式提供周期时钟,以产生定期的tick事件,tick事件或者用于全局的时间管理(jiffies和时间的更新),或者用于本地cpu的进程统计、时间轮定时器框架等等。周期性时钟虽然简单有效,但是也带来了一些缺点,尤其在系统的功耗上,因为就算系统目前无事可做,也必须定期地发出时钟事件,激活系统。为此,内核的开发者提出了动态时钟这一概念,我们可以通过内核的配置项CONFIG_NO_HZ来激活特性。有时候这一特性也被叫做tickless,不过还是把它称呼为动态时钟比较合适,因为并不是真的没有tick事件了,只是在系统无事所做的idle阶段,我们可以通过停止周期时钟来达到降低系统功耗的目的,只要有进程处于活动状态,时钟事件依然会被周期性地发出。
36+ rcu_init_nohz();
37、init_timers(); //初始化内核定时器
参考:http://blog.csdn.net/sunnybeike/article/details/7016322
(1)初始化本 CPU 上的软件时钟相关的数据结构;
(2)向 cpu_chain 通知链注册元素 timers_nb ,该元素的回调函数用于初始化
指定 CPU 上的软件时钟相关的数据结构;
(3)初始化时钟的软中断处理函数。(会注册中断处理函数run_timer_softirq)
38、hrtimers_init(); //初始化高精度时钟
39、softirq_init(); //初始化软中断
软件中断的资源是有限的,内核目前只实现了10种类型的软件中断。定义在 include/linux/interrupt.h
该函数会调用open_softirq()初始化softirq_vet数组中的两个中断类型(每种中断类型对应一个数组元素,故该数组共有10个元素),分别为TASKLET_SOFTIRQ和HI_SOFTIRQ,指定softirq_vet[中断类型]中的action即中断处理函数。
补充:。内核为每个cpu都管理着一个待决软中断变量(pending),它就是irq_cpustat_t,每一位对应一个中断是否触发。疑问:变量为unsigned int类型,如何指定10种中断类型?
40、timekeeping_init(); // 初始化了大量的时钟相关全局变量
详细见:http://www.wowotech.net/timer_subsystem/timekeeping.html
41、time_init(); //时钟初始化
与架构相关,其实就是调用板级初始化文件(arch/arm/mach-*/board-*.c)中定义“设备描述结构体”中的timer成员的初始化函数。
41+、sched_clock_postinit();
41++、perf_event_init();//软件性能分析工具初始化
42、profile_init(); // 初始化内核profile子系统,内核性能调试工具
43、call_function_init(); // smp下跨cpu的函数传递初始化
44、WARN(!irqs_disabled(), "Interrupts were enabledearly\n");
45、early_boot_irqs_disabled = false;
46、local_irq_enable(); //使能当前cpu中断
47、kmem_cache_init_late();//完善slab分配器的缓存机制,对应于mm_init中的
//kmem_cache_init
后续可参考:http://blog.csdn.net/zdy0_2004/article/details/48852147
/*
* HACK ALERT! This is early. We're enablingthe console before
* we've done PCI setups etc, andconsole_init() must be aware of
* this. But we do want output early, in casesomething goes wrong.
*/
48、console_init();//初始化终端
依次调用__con_initcall_start到__con_initcall_end之间的函数,通过vmlinux.lds可以找到宏console_initcall(fn),该宏指定的函数调用register_console()接口实现console的初始化。
if (panic_later)
panic(panic_later, panic_param);
Linux内核在发生kernelpanic时会打印出Oops信息(哎呦),会将寄存器状态、堆栈内容和完整的Call trace都打印出来。
Oops信息的具体含义参考:https://www.cnblogs.com/wwang/archive/2010/11/14/1876735.html
后续均来自:http://www.360doc.com/content/15/0129/20/14530056_444817012.shtml
49、lockdep_info();//打印当前锁的依赖信息
/*
* Need to run this when irqs are enabled,because it wants
* to self-test [hard/soft]-irqs on/off lockinversion bugs
* too:
*/
50、locking_selftest();
#ifdef CONFIG_BLK_DEV_INITRD
if (initrd_start && !initrd_below_start_ok&&
page_to_pfn(virt_to_page((void*)initrd_start)) < min_low_pfn) {
pr_crit("initrd overwritten(0x%08lx < 0x%08lx) - disabling it.\n",
page_to_pfn(virt_to_page((void *)initrd_start)),
min_low_pfn);
initrd_start = 0;
}
#endif
检查initrd的位置是否符合要求。min_low_pfn是系统可用的最小的页帧号,即判断传递进来的initrd_start对应的物理地址是否正常,如果有误就清零。
51、page_cgroup_init();
52、debug_objects_mem_init();//Debug_objetcs机制的内存分配初始化
53、kmemleak_init();//内核内存泄漏检测机制初始化
54、setup_per_cpu_pageset();//设置每个CPU的页组,并初始化。此前只有启动页
//组
55、numa_policy_init(); //非一致性内存访问(NUMA)初始化
if (late_time_init)
late_time_init();
56、sched_clock_init();//对每个CPU,初始化调度时钟
57、calibrate_delay(); //计算BogoMIPS值,他是衡量一个CPU性能的标识
58、pidmap_init(); //PID分配映射初始化
59、anon_vma_init(); //匿名虚拟内存域(anonymousVMA)初始化
60、acpi_early_init();
#ifdef CONFIG_X86
if (efi_enabled(EFI_RUNTIME_SERVICES))
efi_enter_virtual_mode();
#endif
61、thread_info_cache_init();//获取thread_info缓存空间,arm为空
62、cred_init();//任务信用系统初始化。详见:Documentation/credentials.txt
63、fork_init(totalram_pages);//进程创建初始化。为内核“task_struct”分配
//空间,计算最大任务数
64、proc_caches_init();//初始化进程创建机制所需的其他数据结构,为其申请空间。
65、buffer_init();//缓存系统初始化,创建缓存头空间,并检查其大小限时。
66、key_init(); //内核密钥管理系统初始化
67、security_init();//内核安全框架初始化
68、dbg_late_init();//内核调试系统后期初始化
69、vfs_caches_init(totalram_pages);//虚拟文件系统缓存初始化
70、signals_init();//信号管理系统初始化
/* rootfs populating might need page-writeback */
page_writeback_init();//页回写机制初始化
#ifdef CONFIG_PROC_FS
proc_root_init();//proc文件系统初始化
#endif
71、cgroup_init();//control group的正式初始化
72、cpuset_init();//cpuset初始化
73、taskstats_init_early();//任务状态早起初始化函数:为结构体获取高速缓存,
//并初始化互斥机制
74、delayacct_init();//任务延迟机制初始化
75、check_bugs();//检查CPU BUG的函数,通过软件规避BUG
76、acpi_subsystem_init();//ACPI(高级配置和电源接口)
77、sfi_init_late();
if (efi_enabled(EFI_RUNTIME_SERVICES)) {
efi_late_init();
efi_free_boot_services();
}
78、ftrace_init();
/* Do the rest non-__init'ed, we're now alive */
79、rest_init();
{
1)start_sheduler_starting();打开RCU,从该函数后所有RCU事件都讲被相应
2)调用kernel_thread()创建kernel_init线程(线程号为1),kernel线程中会调用wait接口,等待一个条件触发,条件就是kthreadd_done。
3)numa_default_policy();设定NUMA系统的内存访问策略:意图未知
4)再次调用kernel_thread()创建kthreadd线程,其作用是管理和调度其他的内核线程。
Kthreadd:调度其他线程步骤如下
设置内核环境;
在for循环内,查看kthread_create_list全局链表(维护内核线程)是否为空,若不为空则先将链表中的下一个元素摘除,然后创建一个kthread线程,传入的参数为这个链表元素指针。然后调用schedule函数
进行调度(猜测:使链表中的内核线程得以运行)。最后退出kthread线程。
5)主线程在创建了kernel_init和kthreadd进程后会将kernel_init线程等待的条件kthreadd_done置位。
6)执行一次schedule函数
再:init线程等待的条件满足后,接下来的做如下操作:
参考:http://blog.chinaunix.net/uid-20543672-id-3172321.html
1)执行kernel_init_freeable();
初始化可在任何node分配到内存页
初始化可在任何CPU运行
设置init线程可被任何CPU运行,除非在位掩码中删除该CPU位
为smp系统做准备,激活所有的CPU
Do_basic_setup()初始化设备驱动程序
打开根文件系统中的/dev/console
2)释放内存前完成所有的异步__init代码
3)释放所有init.*段中的内存
4)设置系统为运行态
5)设置当前进程状态为不可杀(SIGNAL_UNKILLABLE)
6)查看ramdisk_execute_command和execute_command是否有指定的init进程,若有则执行,没有就执行后续系统的默认程序。
7)执行默认的程序:/sbin/init /etc/init /bin/init /bin/sh
}
重要的点:后续接着学习
1、内核启动参数的获取和处理;
2、setup_arch(&command_line)函数;
3、内存管理的初化(从bootmem到slab);
4、rest_init()函数
- kernel启动流程第二阶段
- uboot第二阶段启动流程
- uboot - 启动流程分析【第二阶段】
- 嵌入式 arm平台kernel启动第二阶段分析
- uboot启动流程webee210启动第二阶段
- uboot 启动流程分析(二) — 第二阶段
- uboot 启动流程分析(二) — 第二阶段
- ARM Linux启动流程-汇编第二阶段
- u-boot第二阶段启动流程分析
- kernel启动流程概要
- Kernel启动流程
- MTk kernel启动流程
- kernel启动流程概要
- kernel启动流程
- kernel启动流程
- kernel 启动流程
- linux kernel启动流程
- LiteOS Kernel启动流程
- 14-CSS3提高 重点部分 盒模型 浮动和定位
- MOOC清华《VC++面向对象与可视化程序设计》第4章:鼠标实例程序(光标九宫格)
- 详解DNS域名解析全过程
- leetcode 457. Circular Array Loop 双指针 + 循环判断
- android 代码怎么实时监控连接当前wifi热点的设备的连接或断开事件?
- kernel启动流程第二阶段
- css居中思路整理
- JAVA的数组
- zabbix 从部署到快速上手
- Python3.6爬虫练习之爬取全国大学省份数据
- Qt状态机场景模拟-续
- HTML 依赖注入 内置服务
- 使用canvas实现图片压缩
- js+jQuery+ajax,处理数据和功能的实现