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);

Linux启动过程02【通用】

操作系统的启动分为两个阶段:引导boot和启动startup,本文主要描述startup过程。

接上文Grub在/boot目录下找到的linux内核,是bzImage格式
1、bzImage格式生成:
1.1、head_64.S+其他源文件->编译-> vmlinux【A】
1.2、objcopy工具拷贝【 拷贝时,删除了文件中“.comment”段,符号表和重定位表】->vmlinux.bin【A】
1.3、gzib压缩->vmlinux.bin.gz
1.4、piggy打包,附加解压信息->piggy.o->其他.o文件一起链接->vmlinux【B】
1.5、objcopy工具拷贝【 拷贝时,删除了文件中“.comment”段,符号表和重定位表】->vmlinux【B】
1.6、head.S +main.c+其他->setup.bin
1.7、setup.bin+vmlinux.bin【B】->bzImage合并->bzImage

2、GRUB加载bzImage文件
2.1、会将bzImage的setup.bin加载到内存地址0x90000 处
2.2、把vmlinuz中的vmlinux.bin部分,加载到1MB 开始的内存地址

3、GRUB会继续执行setup.bin代码,入口在header.S【arch/x86/boot/header.S】
GRUB会填充linux内核的一个setup_header结构,将内核启动需要的信息,写入到内核中对应位置,而且GRUB自身也维护了一个相似的结构。
Header.S文件中从start_of_setup开始,其实就是这个setup_header的结构。
此外, bootparam.h有这个结构的C语言定义,会从Header.S中把数据拷贝到结构体中,方便后续使用。

4、GRUB然后会跳转到 0x90200开始执行【恰好跳过了最开始512 字节的 bootsector】,正好是head.S的_start这个位置;

5、在head.S最后,调用main函数继续执行

6、main函数【 arch/x86/boot/main.c】【16 位实模式】
6.1、拷贝header.S中setup_header结构,到boot_params【arch\x86\include\uapi\asm\bootparam.h】
6.2、调用BIOS中断,进行初始化设置,包括console、堆、CPU模式、内存、键盘、APM、显卡模式等
6.3、调用go_to_protected_mode进入保护模式

7、 go_to_protected_mode函数【 arch/x86/boot/pm.c】
7.1、安装实模式切换钩子
7.2、启用1M以上内存
7.3、设置中断描述符表IDT
7.4、设置全局描述符表GDT
7.4、protected_mode_jump,跳转到boot_params.hdr.code32_start【保护模式下,长跳转,地址为 0x100000】

8、恰好是vmlinux.bin在内存中的位置,通过这一跳转,正式进入vmlinux.bin

9、startup_32【arch/x86/boot/compressed/head64.S】
全局描述符GDT
加载段描述符
设置栈
检查CPU是否支持长模式
开启PAE
建立MMU【4级,4G】
开启长模式
段描述符和startup_64地址入栈
开启分页和保护模式
弹出段描述符和startup_64地址到CS:RIP中,进入长模式

10、 startup_64【arch/x86/boot/compressed/head64.S】
初始化寄存器
初始化栈
调准给MMU级别
压缩内核移动到Buffer最后
调用.Lrelocated

11、.Lrelocated
申请内存
被解压数据开始地址
被解压数据长度
解压数据开始地址
解压后数据长度
调用 extract_kernel解压内核

12、extract_kernel解压内核【arch/x86/boot/compressed/misc.c】
保存boot_params
解压内核
解析ELF,处理重定向, 把 vmlinux 中的指令段、数据段、BSS 段,根据 elf 中信息和要求放入特定的内存空间
返回了解压后内核地址,保存到%rax

13、返回到.Lrelocated继续执行
跳转到%rax【解压后内核地址】,继续执行
解压后的内核文件,入口函数为【arch/x86/kernel/head_64.S】

14、SYM_CODE_START_NOALIGN(startup_64)【arch/x86/kernel/head_64.S】
SMP 系统加电之后,总线仲裁机制会选出多个 CPU 中的一个 CPU,称为 BSP,也叫第一个 CPU。它负责让 BSP CPU 先启动,其它 CPU 则等待 BSP CPU 的唤醒。
第一个启动的 CPU,会跳转 secondary_startup_64 函数中 1 标号处,对于其它被唤醒的 CPU 则会直接执行 secondary_startup_64 函数。

15、secondary_startup_64 函数【arch/x86/kernel/head_64.S】
各类初始化工作,gdt、描述符等
跳转到initial_code,也就是x86_64_start_kernel

16、 x86_64_start_kernel【arch/x86/kernel/head64.c】
各类初始化工作,清理bss段,清理页目录,复制引导信息等
调用x86_64_start_reservations

17、x86_64_start_reservations【arch/x86/kernel/head64.c】
调用start_kernel();

18、start_kernel【init/main.c】
各类初始化:ARCH、日志、陷阱门、内存、调度器、工作队列、RCU锁、Trace事件、IRQ中断、定时器、软中断、ACPI、fork、缓存、安全、pagecache、信号量、cpuset、cgroup等等
调用 arch_call_rest_init,调用到rest_init

19、rest_init【init/main.c】
kernel_thread,调用_do_fork,创建了kernel_init进程,pid=1 . 是系统中所有其它用户进程的祖先
kernel_thread,调用_do_fork,创建了 kernel_thread进程,pid=2, 负责所有内核线程的调度和管理
当前的进程, 最后会变成idle进程,pid=0

20、kernel_init
根据内核启动参数,调用run_init_process,创建对应进程
调用try_to_run_init_process函数,尝试以 /sbin/init、/etc/init、/bin/init、/bin/sh 这些文件为可执行文件建立init进程,只要其中之一成功就可以

调用链如下:

try_to_run_init_process
run_init_process
kernel_execve
bprm_execve
exec_binprm
search_binary_handler-》依次尝试按各种可执行文件格式进行加载,而ELF的处理函数为 load_elf_binary
load_elf_binary
start_thread
start_thread_common,会将寄存器地址,设置为ELF启动地址
当从系统调用返回用户态时,init进程【1号进程】,就从ELF执行了

到此为止,系统的启动过程结束。

Linux启动过程01【UEFI】

操作系统的启动分为两个阶段:引导boot和启动startup,本节主要还是boot过程:

UEFI->GRUB->Linux内核【硬盘引导、UEFI】

1、按开机键,系统加电

2、主板通电

3、UEFI开始执行【UEFI功能比BIOS强大很多,支持命令行,有简单图形界面,也支持文件系统】
3.1、UEFI会检测硬件,并对设备执行简单的初始化工作
3.2、UEFI会判断启动模式,是UEFI还是Legacy【Legacy模式下,UEFI通过CSM模块支持MBR方式启动】
3.3、如果是UEFI模式启动,UEFI会读取硬盘分区表,查找并挂载ESP分区【 EFI System Partition,VFAT格式】
GPT分区下有特殊GUID: C12A7328-F81F-11D2-BA4B-00A0C93EC93B;
MBR分区下有 标识为 0xEF
3.4、各操作系统引的导程序按规则存放到/boot/efi目录下【可以操作文件而不需操作扇区,文件大小限制也宽松了很多】
比如Ubuntu,/boot/efi/ubuntu/grubx64.efi
【可以先引导grub,然后引导Linux】
【也可以直接启动系统内核,包括Windows和Linux,但他们也都需要一个efi文件用于引导系统】

4、UEFI加载efi文件并启动
如果用grub,EFI boot manager会加载/EFI/ubuntu/boot/grubx64.efi,移交控制权,会进入到grub2阶段【grub.cfg也在这个目录下】
如果用ubuntu,EFI boot manager会加载/EFI/ubuntu/boot/ubuntu.efi,移交控制权,可以直接启动linux内核【编译时打开EFI Boot Stub】
如果用windows,EFI boot manager会加载/EFI/Mirosoft/Boot/bootmgr.efi
如果采用默认启动,会使用/EFI/Boot/bootx64.efi

5、后续过程,和BIOS流程就比较相似了

Linux启动过程01【BIOS】

操作系统的启动分为两个阶段:引导boot和启动startup,本文主要描述boot过程。

BIOS->GRUG1->GRUB1.5->GRUB2->Linux内核【环境硬盘引导、MBR分区】

1、按开机键,系统加电

2、主板通电
CPU加电时,会默认设置[CS:IP]为[0XF000:0XFFF0],根据实模式下寻址规则,CPU指向0XFFFF0
这个地址正是BIOS启动程序位置,而BIOS访问方式与内存一致,所以CPU可以直接读取命令并执行

3、BIOS执行
3.1、BIOS首先执行POST自检,包括主板、内存、外设等,遇到问题则报警并停止引导

3.2、BIOS对设备执行简单的初始化工作

3.3、BIOS 会在内存中:
建立中断表(0x00000~0x003FF)
构建 BIOS 数据区(0x00400~0x004FF)
加载了中断服务程序(0x0e05b~0x1005A)

3.4、BIOS根据设备启动顺序,依次判断是否可以启动
比如先检查光驱能否启动
然后依次检查硬盘是否可以启动【硬盘分区的时候,设置为活动分区】

4、硬盘引导
4.1、先说下寻址方式,与扇区编号的事情
最传统的磁盘寻址方式为CHS,由三个参数决定读取哪个扇区:磁头(Heads)、柱面(Cylinder)、扇区(Sector)
磁头数【8位】,从0开始,最大255【微软DOS系统,只能用255个】,决定了读取哪个盘片的哪个面【一盘两面】
柱面数【10位】,从0开始,最大1023【决定了读取哪个磁道,磁道无论长短都会划分为相同扇区数】
扇区数【6位】,从1开始,最大数 63【CHS中扇区从1开始,而逻辑划分中扇区从0开始,经常会造成很多误解】
每个扇区为512字节

4.2、然后说下引导方式
BIOS在发现硬盘启动标志后,BIOS会引发INT 19H中断
这个操作,会将MBR【逻辑0扇区】,也就是磁盘CHS【磁头0,柱面0,扇区1】,读取到内存[0:7C00h],然后执行其代码【GRUB1阶段】,至此BIOS把主动权交给了GRUB1阶段代码
MBR扇区为512字节,扇区最后分区表至少需要66字节【64字节DPT+2字节引导标志】,所以这段代码最多只能有446字节,grub中对应的就是引导镜像boot.img
boot.img的任务就是,定位,并通过BIOS INT13中断读取1.5阶段代码,并运行

5、Grub1.5阶段
5.1、先说一下MBR GAP
据说微软DOS系统原因,第一个分区的起始逻辑扇区是63扇区,在MBR【0扇区】和分布表之间【63扇区】,存在62个空白扇区,共 31KB。
Grub1.5阶段代码就安装在这里。

5.2、上面提到,boot.img主要功能就是找到并加载Grub1.5阶段代码,并切换执行。
Grub1.5阶段代码是core.img,其主要功能就是加载文件系统驱动,挂载文件系统, 位加载并运行GRUB2阶段代码。
core.img包括多个映像和模块:
diskboot.img【1.5阶段引导程序】,存在于MBR GAP第一个扇区;【这里是硬盘启动的情况,如果是cd启动就会是cdboot.img】
lzma_decompress.img【解压程序】
kernel.img【grub核心代码】,会【压缩存放】
biosdisk.mod【磁盘驱动】、Part_msdos.mod【MBR分区支持】、Ext2.mod【EXT文件系统】等,会【压缩存放】

其实boot.img只加载了core.img的第一个扇区【存放diskboot.img】,然后控制权就交出去了,grub阶段1代码使命结束。
diskboot.img知道后续每个文件的位置,会继续通过BIOS中断读取扇区,加载余下的部分并转交控制权,包括:
加载lzma_decompress.img,从而可以解压被压缩的模块
加载kernel.img,并转交控制权给kernel.img
kernel.img的grub_main函数会调用grub_load_modules函数加载各个mod模块
加载各个mod后,grub就支持文件系统了,访问磁盘不需要再依靠BIOS的中断以扇区为单位读取了,终于可以使用文件系统了

6、GRUB2阶段
现在grub就能访问boot/grub及其子目录了
kernel.img接着调用grub_load_normal_mode加载normal模块
normal模块读取解析文件grub.cfg,查看有哪些命令,比如发现了linux、initrd这几个命令,需要linux模块
normal模块会根据command.lst,定位并加载用到的linux模块【一般在/boot/grub2/i386-pc目录】
当然,同时需要完成初始化显示、载入字体等工作
接下来Grub就会给咱们展示启动菜单了

7、选择启动菜单
7.1、引导协议
引导程序加载内核,前提是确定好数据交换方式,叫做引导协议,内核中引导协议相关部分的代码在arch/x86/boot/header.S中,内核会在这个文件中标明自己的对齐要求、是否可以重定位以及希望的加载地址等信息。同时也会预留空位,由引导加载程序在加载内核时填充,比如initramfs的加载位置和大小等信息。
引导加载程序和内核均为此定义了一个结构体linux_kernel_params,称为引导参数,用于参数设定。Grub会在把控制权移交给内核之前,填充好linux_kernel_params结构体。如果用户要通过grub向内核传递启动参数,即grub.cfg中linux后面的命令行参数。Grub也会把这部分信息关联到引导参数结构体中。

#结构体对照
#grub源码
linux_i386_kernel_header
linux_kernel_params

#linux源码
arch/x86/boot/header.S
arch/x86/boot/boot_params.h    boot_params
arch/x86/boot/boot_params.h    setup_header

7.2、开始引导
Linux内核的相关文件位于/boot 目录下,文件名均带有前缀 vmlinuz。
咱们选择对应的菜单后,Grub会开始执行对应命令,定位、加载、初始化内核,并移交到内核继续执行。
调用linux模块中的linux命令,加载linux内核
调用linux模块中的initrd命令,填充initramfs信息,然后Grub会把控制权移交给内核。
内核此时开始执行,同时也就可以读取linux_kernel_params结构体的数据了
boot阶段结束,开始进入startup阶段。

一次MacOS升级引发的灾难

一、上周帮老婆大人把MacOS从Mojave升级到了Catalina,然后悲剧发生了。
任何需要截屏、屏幕共享或屏幕录制的应用都不能使用了,具体表现为:
1、配置-》安全与隐私里,多了一个选项”屏幕录制/Screen Recording”,里面列表是空的。
2、快捷键录屏是可以的
3、打开QuickPlayer,录制屏幕,提示之前禁用过屏幕录制,要求去修改权限;但由于列表是空的,根本无法做任何操作
4、其他APP,如微信、腾讯会议什么的,提示要增加权限,但“屏幕录制”里还是空的。

二、网上找了一下,无论国内国外,都有一些用户遇到了这个问题。尝试了一些建议的方法,搞不定。

#正常模式下,显示失败
sudo tccutil reset ScreenCapture

#恢复模式下,显示成功,重启后无效
tccutil reset ScreenCapture

三、于是打电话给Apple,一位妹子远程视频帮忙处理了很久,把能重置配置的方式都用了,还是不行。最后建议我重装系统。

四、重装,没任何变化。

五、把系统升级到了Big Sur,好歹有了一点儿进展:
1、配置-》安全与隐私里,”屏幕录制/Screen Recording”,里面列表是空的,但是新增任何APP都无效
2、快捷键录屏是可以的
3、QuickPlayer录制屏幕是可以的
4、其他APP,如微信、腾讯会议什么的,提示要增加权限,但“屏幕录制”里还是空的。

六、找了好长时间,最后终于解决了,步骤如下:
1、重启系统,听到提示音时,按下Command+R,进入恢复模式
2、恢复模式下,开启Terminal,禁用SIP服务 (system integrity protection)

csrutil disable

3、重启系统,进入正常模式,重命名tcc.db文件

sudo mv /Library/Application\ Support/com.apple.TCC/TCC.db /Library/Application\ Support/com.apple.TCC/TCC.db.bak

4、重启系统,进入恢复模式
5、恢复模式下,开启Terminal,启用SIP服务 (system integrity protection)

csrutil enable

6、重启系统,进入正常模式
7、你会发现问题解决了

整体来说,这应该是MacOS升级的一个Bug,出现频率并不高,而且横跨两个大版本至今没有解决。
(也可以这么说,Big Sur解决问题的方式只是把QuickTime加白名单了,根本没有解决根本问题;或者说解决问题的Apple程序员可能根本没有定位到问题产生的原因)

我自己解决这个问题花费了4个小时以上,如果没有编程经验的普通用户遇到,估计只能格式化重装了,希望Apple可以出个工具,解决此类问题,节约大家的时间。