驱动静态加载过程:(编译进内核)
#define module_init(x) __initcall(x); //include/linux/module.h
#define __initcall(fn) device_initcall(fn)
#define device_initcall(fn) __define_initcall(fn, 6)
#define __define_initcall(fn, id) ___define_initcall(fn, id, .initcall##id)
typedef int (*initcall_t)(void);
#define ___define_initcall(fn, id, __sec) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__section__(#__sec ".init"))) = fn;
module_init(test_init) static initcall_t __initcall_test_init6 __used __attribute__((__section__(".initcall6" ".init"))) =test_init
将test_init放在.initcall6.init段中
数字id 0~7代表的是不同的优先级(0最高,module_init对应的优先级为6,所以一般我们注册的驱动程序优先级为6)
vmlinux.lds __initcall6_start = .; *(.initcall6.init) *(.initcall6s.init)
在kernel启动过程中,会调用do_initcalls函数一次调用我们通过xxx_initcall注册的各种函数,优先级高的先执行
#define module_exit(x) __exitcall(x);
typedef void (*exitcall_t)(void);
#define __section(S) __attribute__((__section__(#S)))
#define __exit_call __used __section(.exitcall.exit)
#define __exitcall(fn) \
static exitcall_t __exitcall_##fn __exit_call = fn
驱动动态加载过程:(insmod加载)
/* Each module must use one module_init(). */
#define module_init(initfn) \
static inline initcall_t __maybe_unused __inittest(void) \
{ return initfn; } \
int init_module(void) __copy(initfn) __attribute__((alias(#initfn)));
/* This is only required if you want to be unloadable. */
#define module_exit(exitfn) \
static inline exitcall_t __maybe_unused __exittest(void) \
{ return exitfn; } \
void cleanup_module(void) __copy(exitfn) __attribute__((alias(#exitfn)));
static inline initcall_t __maybe_unused __inittest(void){ return initfn; }检测定义的函数是否符合 initcall_t 类型
alias属性是gcc的特有属性,将定义init_module为函数initfn的别名
module_init(hello_init)的作用就是定义一个变量名init_module,其地址和hello_init是一样的
编译驱动时,会生成xxx.mod.c文件,记录了驱动的相关信息
#include <linux/module.h>
#include <linux/vermagic.h>
#include <linux/compiler.h>
MODULE_INFO(vermagic, VERMAGIC_STRING);
struct module __this_module
__attribute__((section(".gnu.linkonce.this_module"))) = {
.name = KBUILD_MODNAME,
.init = init_module,
#ifdef CONFIG_MODULE_UNLOAD
.exit = cleanup_module,
#endif
.arch = MODULE_ARCH_INIT,
};
static const char __module_depends[]
__used
__attribute__((section(".modinfo"))) =
"depends=";
定义了一个类型为module的全局变量__this_module,成员init为init_module(即 hello_init),且该变量链接到.gnu.linkonce.this_module段中
insmod命令在busybox的代码中可查到:
insmod_main //busybox/modutils/insmod.c
-->bb_init_module
-->try_to_mmap_module //busybox/modutils/modutils.c
-->init_module
#define init_module(mod, len, opts) syscall(__NR_init_module, mod, len, opts)
#define __NR_init_module 105 //include/uapi/asm-generic/unistd.h
__SYSCALL(__NR_init_module, sys_init_module) //系统调用对应的软中断,执行sys_init_module
asmlinkage long sys_init_module(void __user *umod, unsigned long len,
const char __user *uargs);
asmlinkage long sys_delete_module(const char __user *name_user,
unsigned int flags);
|
-->load_module
|
--> do_init_module(mod)
|
--> do_one_initcall(mod->init);
SYSCALL_DEFINE3(init_module, ...)
-->SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
-->SYSCALL_DEFINEx(x, sname, ...)
-->__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)
-->asmlinkage long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))
-->sys_init_module
创建文件-->分配节点,分配全局唯一的节点号(这是存在于磁盘的节点,存在于内存的节点是inode)-->初始化节点(如果是设备文件还需初始化设备号)-->写入磁盘(还需在文件所在目录下添加一个目录项)
struct ext2_inode {
__le16 i_mode; /* File mode */
__le16 i_uid; /* Low 16 bits of Owner Uid */
__le32 i_size; /* Size in bytes */
__le32 i_atime; /* Access time */
__le32 i_ctime; /* Creation time */
__le32 i_mtime; /* Modification time */
__le16 i_gid; /* Low 16 bits of Group Id */
__le16 i_links_count; /* Links count */
__le32 i_blocks; /* Blocks count */
__le32 i_flags; /* File flags */
__le32 i_block[EXT2_N_BLOCKS];/* Pointers to blocks */
};
如果是普通文件,则i_block指向真正文件数据所在的块号,如果是设备文件则存放主次设备号
static int __ext2_write_inode(struct inode *inode, int do_sync)
{
struct ext2_inode_info *ei = EXT2_I(inode);
struct super_block *sb = inode->i_sb;
ino_t ino = inode->i_ino;
uid_t uid = i_uid_read(inode);
gid_t gid = i_gid_read(inode);
struct buffer_head * bh;
struct ext2_inode * raw_inode = ext2_get_inode(sb, ino, &bh); //分配节点
int n;
int err = 0;
raw_inode->i_mode = cpu_to_le16(inode->i_mode);
if (S_ISCHR(inode->i_mode) || S_ISBLK(inode->i_mode)) { //判断是否是字符设备或块设备
if (old_valid_dev(inode->i_rdev)) {
raw_inode->i_block[0] =
cpu_to_le32(old_encode_dev(inode->i_rdev));
raw_inode->i_block[1] = 0;
} else {
raw_inode->i_block[0] = 0;
raw_inode->i_block[1] =
cpu_to_le32(new_encode_dev(inode->i_rdev)); //inode->i_rdev即为设备号
raw_inode->i_block[2] = 0;
}
} else for (n = 0; n < EXT2_N_BLOCKS; n++)
raw_inode->i_block[n] = ei->i_data[n];
}
目录项
struct ext2_dir_entry {
__le32 inode; /* Inode number */
__le16 rec_len; /* Directory entry length */
__le16 name_len; /* Name length */
char name[]; /* File name, up to EXT2_NAME_LEN */
};
文件io调用框架:
task_struct -->struct files_struct *files
-->files_struct -->struct file __rcu * fd_array[NR_OPEN_DEFAULT]
-->struct file -->struct file_operations *f_op
-->struct file_operations
-->struct thread_info thread_info
open
-->sys_open
-->do_sys_open
-->getname
-->get_unused_fd_flags
-->__alloc_fd //查看fd_array哪一个元素未被使用,返回其下标,即为文件描述符fd
-->do_filp_open
-->path_openat //分配一个struct file结构体,并初始化这个结构体,将f_op指向对应的file_operations
-->fd_install //将fd_array[fd]指针指向struct file,并返回fd
其他文件io操作直接根据文件描述符索引fd_array,来操作对应的file结构体,从而调用对应的方法
进程:
编译时进行代码优化:
if(unlikely(error)) 认为error绝少发生
if(likely(success)) 认为success通常为真
同一个进程中,每个线程都拥有虚拟的处理器,但共享虚拟内存
fork、vfork、__clone
-->clone
-->do_fork
-->copy_process
-->dup_task_struct //创建内核栈、thread_info、task_struct,子进程和父进程的描述符完全相同
-->alloc_task_struct_node //slab分配,可见于笔记slab分配环节
-->kmem_cache_alloc_node
//检查确保当前未超出当前用户进程数限制
子进程清除统计量等信息
子进程状态设置为不可中断,设置task_struct的flags
-->copy_mm //复制父进程的内存描述符给子进程,若指定CLONE_VM标志,则不分配内存描述符地址空间,将子进程的task_struct.mm
指向父进程,共享地址空间
-->dup_mm
-->allocate_mm
-->kmem_cache_alloc //从mm_cachep这个缓存的slab中分配内存描述符内存空间
-->alloc_pid //分配pid
对打开的文件,信号,地址空间等进行拷贝或共享
返回子进程描述符
子进程先执行,如执行exec则能避免父进程的向地址空间写入数据导致的写时拷贝
getpid
getppid
fork:在父进程中返回子进程pid,在子进程中返回0。
全部拷贝的数据也许并不共享,或者,立即执行新的程序,会导致拷贝的数据白费,因此,写时拷贝页,在新进程立即执行exec时,就无需拷贝父进程的数据了
exec
exit:终结进程并释放资源,进程退出为僵死态,若没有显式调用这个函数,c编译器默认会在main函数的返回点放置调用exit的代码
-->do_exit
-->do_task_dead
-->exit_notify //向父进程发送信号,给子进程找养父,为线程组其他线程或者init进程
-->schedule
wait:查询子进程是否结束
-->sys_wait
-->sys_waitpid
-->sys_wait4
-->my_syscall4
-->__NR_wait4
-->SYSCALL_DEFINE4 == sys_wait4(与前三步的sys_wait4不是同一个)
-->kernel_wait4
-->do_wait
-->do_wait_thread
-->wait_consider_task
-->wait_task_zombie
-->release_task
-->__exit_signal //从任务列表移除该进程
-->put_task_struct_rcu_user
-->delayed_put_task_struct
-->put_task_struct
-->__put_task_struct //释放进程的内核栈、thread_info所占的页以及task_struct所占的slab高速缓存
孤儿进程将被init进程(进程号为1)所收养,并由init进程例行调用wait检查其子进程对它们完成状态收集工作,清除与其相关的僵死进程
任务队列(task list):存放进程列表的双向循环链表,成员为进程描述符task_struct
struct thread_info放在进程的内核栈的栈顶,且为task_struct的第一个成员,其指针成员task指向task_struct,stack指向内核栈
struct task_struct的thread_info成员即为thread_info,stack指向内核栈
current_thread_info通过地址对齐,current_stack_pointer & ~(THREAD_SIZE - 1)指向当前进程的thread_info,进而指向task_struct
state:进程状态:运行,可中断,不可中断,被跟踪,停止,调度程序中,schedule调用context_switch切换任务
parent:指向父进程
children:子进程指针链表
调整进程的状态:
set_current_state
set_task_state
内核中并没有专门区实现线程,仅把它视为与某些进程共享资源的进程,也有自己的task_struct,在clone中传递不同的flag
纯粹的内核线程没有地址空间,不会切换到用户空间区,仅为内核服务,由kthreadd衍生
kthread_create:创建内核线程
wake_up_process-->try_to_wake_up:唤醒内核来运行
kthread_get_run:调用kthread_create和wake_up_process
do_exit:内核线程内部退出
kthread_stop:参数为kthread_create返回的task_struct,外部退出内核线程
修改、proc/sys/kernel/pid_max可以修改pid最大值的上限
进程调度:
多任务操作系统:分为抢占式和非抢占式
linux:抢占式(preempt),时间片用完后,强制挂起正在运行的进程,让其他程序得到运行
O(1)调度程序:适用于服务器,对有交互程序因而对响应时间敏感的桌面系统不适用
完全公平调度算法(CFS):根据各个进程的权重分配运行时间,分配给进程的运行时间 = 调度周期 * 进程权重 / 所有进程权重之和
进程:分为I/O消耗型和处理器消耗型,I/O消耗型倾向于更优先调用
I/O消耗型:大部分时间都在提交或者等待I/O请求,经常处于可运行状态,但运行时间短,调度频率高,因为它随时会被I/O请求阻塞
处理器消耗型:大部分时间用在执行代码上,没有太多I/O需求,调度频率低,运行时间长
调度策略的目的:进程响应迅速和最大系统利用率
进程优先级:
优先级高的先执行,优先级低的后执行
实时进程的优先级 > 普通进程的优先级
nice优先级: 普通进程拥有nice优先级(-20到19),ps -el查看命令NI列,nice值更低的优先级越高
实时优先级: 实时进程具有实时优先级(0到99),ps -eo state,uid,pid,ppid,rtprio,time,comm查看命令RTPRIO列,数字越大优先级越高
时间片:进程被抢占前可以持续运行的时间
时间片过长会影响系统对交互的响应性能,时间片过短会增加进程切换的时间开销,同时进程能够运行的时间片却过短
I/O消耗型不需要长的时间片,处理器消耗型希望时间片越长越好
linux系统将处理器的使用比划分给进程,进程获得的处理器时间和系统负载密切相关,nice值高优先级进程获得高权重占用更多时间比,低优先级进程获得低权重占用更少时间比
假设a和b nice值相同,各自占50%时间比,a每次消耗不到50%,b每次可能超过50%。为了兑现公平,a进入可运行状态时,会立即抢占处理器运行,b则在剩下的时间里运行
可运行进程消耗的时间比小于当前进程,则抢占当前进程,否则推迟
Linux调度器以模块方式提供,这种模块化结构被称为调度器类,允许不同类型的进程可以有针对性地选择调度算法,允许多种不同的可动态添加的调度算法并存,调度属于自己范畴的进程
每个调度器都有一个优先级,内核会按照优先级顺序遍历调度类,拥有一个可执行进程的,最高优先级的调度器类胜出,去选择下面要执行的那一个程序
系统对不同类型的进程有不同的调度队列,由struct rq统一管理,其成员有struct cfs_rq、struct rt_rq、struct dl_rq,代表不同的调度队列。不同的调度队列使用不同的调度器处理。
sys_sched_setscheduler
sys_sched_setparam
-->do_sched_setscheduler
-->find_process_by_pid
-->get_task_struct
-->sched_setscheduler
-->_sched_setscheduler
-->__sched_setscheduler
-->task_current
-->put_prev_task
-->__setscheduler
-->__setscheduler_uclamp
-->set_next_task
-->check_class_changed
-->put_task_struct
这里提一下时间片分配的问题:分配绝对的时间片给公平造成了变数
完全公平调度(CFS)是一个针对普通进程的调度类:通过nice值对进程进行相对加权计算,权重越大的进程占的时间周期内的处理器时间比越多,最小粒度为1ms
struct cfs_rq:普通进程的调度队列,有curr,next,last等调度实体指针struct sched_entity
struct sched_entity:调度实体,是task_struct的成员se
时间记账:记录进程已经运行了多长时间,还要运行多长时间
虚拟实时:由sched_entity的vruntime成员进行时间记账
update_curr //更新当前进程的虚拟运行时间,由系统定时器周期性调用,优先级越高的进程,虚拟实时增长越慢。
进程选择:使用红黑树rbtree组织进程队列,迅速搜索到vruntime最小的进程(即最左边的叶子节点),cfs会选择vruntime最小的进程来运行
__pick_next_entity:查找下一个运行的进程
enqueue_entity //向树中加入进程
-->update_curr
-->__enqueue_entity //树中插入调度节点
dequeue_entity //从树中删除节点
-->update_curr
-->__dequeue_entity
sched_init //调度器初始化
schedule //调度器主要入口,选择拥有自己调度队列的最高优先级的调度类,查找其下一个运行的进程
-->current
-->__schedule
-->pick_next_task //依次从最高优先级调度类开始遍历,在对应调度类中查找下一个运行的进程
-->fair_sched_class.pick_next_task_fair
-->pick_next_entity
-->context_switch //进程上下文切换
休眠:由于等待I/O事件等原因,进程从可执行红黑树移出,放入等待队列,进入休眠状态。休眠有两种进程状态:TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE
TASK_INTERRUPTIBLE:接收到信号会被提前唤醒并响应信号
TASK_UNINTERRUPTIBLE:忽略信号
唤醒:通过函数wake_up,唤醒指定等待队列上的所有进程。
等待队列:用wait_queue_head_t表示的简单链表
创建:
1. DECLARE_WAITQUEUE
2. init_waitqueue_head
内核中进程休眠:
DEFINE_WAIT(wait); //创建等待队列的项
for (;;) {
prepare_to_wait(sk_sleep(sk), &wait, TASK_INTERRUPTIBLE); //将自己加入到等待队列sk_sleep(sk),并设置进程状态为TASK_INTERRUPTIBLE
if (sk->sk_state != TCP_SYN_SENT) //判断不满足条件就跳出去唤醒
break;
if (!signal_pending(current)) { //信号唤醒进程(伪唤醒),检查并处理信号
release_sock(sk);
schedule(); //未出现信号,调走
lock_sock(sk);
continue;
}
err = -ERESTARTSYS;
break; //出现信号,if不成立,跳出
}
finish_wait(sk_sleep(sk), &wait); //进程把自己设置为TASK_RUNNING,并移出等待队列
wake_up
-->__wake_up
-->__wake_up_common_lock
-->__wake_up_common
-->default_wake_function
-->try_to_wake_up //将进程设置为TASK_RUNNING
-->ttwu_queue
-->ttwu_do_activate
-->enqueue_task
唤醒是在条件达成处被调用的,信号的虚假唤醒需要检查
上下文切换:
schedule
-->__schedule
if (!preempt && prev->state) //preempt传的false,进程正在运行则走else移出
-->signal_pending_state
else
-->deactivate_task //移出红黑树
-->dequeue_task
-->pick_next_task //查找下一个进程
-->context_switch //进程上下文切换
-->switch_mm_irqs_off //进程虚拟内存映射切换
-->switch_mm
-->switch_to //进程栈和寄存器状态切换
进程抢占:
need_resched:thread_info.flags的第三位,表明是否需要重新执行调度
scheduler_tick:当某个进程需要被抢占的时候会设置need_resched标志
try_to_wake_up:也会设置need_resched标志,让出资源
用户抢占:内核在中断处理或者系统调用后返回(此时是安全的,返回的时候锁都会被释放掉),都会检查need_resched,若
设置,会调用调度程序选择更合适的进程去执行
内核抢占:必须确保抢占是安全的,也就是没有持有锁。thread_info.preempt_count抢占计数器,初值为0,使用锁加1,释
放锁减1。当preempt_count为0,表示当前进程未持有锁,可以安全的被抢占
从中断返回内核的时候,会检查need_resched和preempt_count,如果满足,会调用调度进程
释放锁的时候,如果preempt_count为0,释放锁的代码会检查need_resched是否被设置,如果是,就会调用调度程序
实时调度策略:SCHED_FIFO和SCHED_RR,linux提供的软实时,对实时不做严格保证,尽力满足实时
SCHED_FIFO:先进先出,不基于时间片,除非被更高优先级的实时进程抢占,否则同优先级的轮流执行,并且只有它愿意让出处理器时才会退出,
否则可以一直执行下去。直到退出,其它低优先级进程才能执行
SCHED_RR :与SCHED_FIFO的区别在于,SCHED_RR在同级别之间基于时间片执行,当然,高级的可以抢占低级的
实时优先级:0~MAX_RT_PRIO(100),即0到99,nice值-20~19对应100~139
task_nice: 获取进程的nice值
sched_setscheduler: 设置进程调度策略和实时优先级,task_struct.policy和task_struct.rt_priority
sched_getscheduler: 获取进程调度策略和实时优先级,task_struct.policy和task_struct.rt_priority
sched_setparam: 设置进程实时优先级
sched_getparam: 获取进程实时优先级
sched_get_priority_max: 获取实时优先级的最大值
sched_get_priority_min: 获取实时优先级的最小值
sched_rr_get_interval: 获取进程的时间片值
sched_setaffinity: 设置进程强制在某个处理器运行,task_struct.cpus_mask,每位对应一个可用的系统处理器,默认所有的位都被设置
sched_getaffinity: 获取进程的处理器亲和力
sched_yield: 暂时让出处理器,放入执行队列的最后面,同时放入过期队列(实时进程不会过期)
系统调用:用户空间和内核空间的中间层
一个系统调用可以实现一组应用编程接口(API),一个应用编程接口也可以由一个或多个系统调用实现,甚至一个应用编程接口不使用系统调用也可以
API编程比系统调用便于移植
系统调用出现错误时,C库会把错误码写入errno全局变量。调用perror()函数可以打印对应的字符串
sys_call_table:系统调用表,每个系统调用都有一个系统调用号
应用程序-->软中断(引发异常,执行异常处理程序:即系统调用处理程序)-->系统调用
软中断中断号128
vector_swi:软中断处理程序,由中断向量表跳转过来
/arch/arm/kernel/entry_header.S
scno .req r7 @ syscall number //调用号
tbl .req r8 @ syscall table pointer //调用表
/arch/arm/kernel/entry_common.S
addne scno, r7, #__NR_SYSCALL_BASE @ put OS number in
adr tbl, sys_call_table @ load syscall table pointer
invoke_syscall tbl, scno, r10, __ret_fast_syscall //系统调用处理
syscall_table_start sys_call_table //定义系统调用表
#define COMPAT(nr, native, compat) syscall nr, native
#ifdef CONFIG_AEABI
#include <calls-eabi.S>
#else
#include <calls-oabi.S>
#endif
#undef COMPAT
syscall_table_end sys_call_table
内核不应通过系统调用接收用户空间的指针,会有安全隐患,一旦用户空间给的假指针,内核可能读取进程无权限读取的数据
copy_from_user和copy_to_user用于内核空间和用户空间的数据交互
添加系统调用的步骤:
1.添加函数到系统调用表
2.unistd.h添加系统调用号宏定义
#define __NR_foo 283
__syscall0(long,foo) //第一个值为返回值类型,第二个为系统调用名
int main()
{
long ret = foo;
}
与内核的交互可以使用一个设备节点ioctl来实现,应尽量避免增添系统调用
内核数据结构:
链表:
struct list_head {
struct list_head *next, *prev;
};
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
struct fox { //内核的链表,将指针作为
int data;
struct list_head list;
};
#define list_entry(ptr, type, member) \
container_of(ptr, type, member)
static inline void INIT_LIST_HEAD(struct list_head *list)
{
list->next = list;
list->prev = list;
}
#define LIST_HEAD_INIT(name) { &(name), &(name) }
#define LIST_HEAD(name) \
struct list_head name = LIST_HEAD_INIT(name)
struct node *node;
struct list_head *p;
list_for_each(p,list_head)
{
node = list_entry(p,struct node,node->list_head);
}
映射:键值对
通过散列表或者自平衡二叉搜索树实现实现
二叉树:
二叉搜索树:简称BST
1.左分支所有节点小于根节点
2.右分支节点都大于根节点
3.子树都是二叉搜索树
平衡二叉搜索树:所有叶节点深度差不超过1
自平衡二叉搜索树:其操作都试图维持平衡的二叉搜索树
红黑树:
1.所有节点分红黑两色
2.根节点为黑色,叶节点为黑色
红色节点不能连续
3.叶节点不包含数据
4.所有非叶节点都有两个子节点
5.如果一个节点为红色,那它的子节点都是黑色
6.一个节点到其叶子节点的路径中,如果总是包含相同数目的黑色节点,则该路径相比其它路径是最短的
红黑一行一行交替出现
如果一个树的左边或右边特别长,显得不平衡,势必影响查找的效率,体现不出树的优势,因此平衡很重要
rbtree:
struct rb_root root = RB_ROOT;
rb_node
中断和中断处理:
中断值:即中断号,称为中断(IRQ)请求线
中断处理程序:也叫中断服务例程(ISR),运行于中断上下文中,中断上下文会关闭调度,不可睡眠,也称为原子上下文,没有调度程序像调度进程那样来调度中断。
要完成中断任务,同时要清除中断标志,告诉硬件中断已被接收
中断上下文:
1.中断上文:硬件通过中断触发信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的一些变量和参数也要传递给内核,内核通过这些参数进行中断处理。
中断上文可以看作就是硬件传递过来的这些参数和内核需要保存的一些其他环境(主要是当前被中断的进程环境。
2.中断下文:执行在内核空间的中断服务程序。
上半部:做有严格时限的事情,如应答中断,复位硬件,此时,本地的中断被禁止,其它中断无法响应,因此必须快速
下半部:为了迅速响应中断,能稍后执行的工作都尽量推迟到下半部
注册中断处理程序:在设备驱动程序中,可以通过request_irq注册一个中断处理程序,并激活给定的中断线
request_irq //参数:中断号 中断服务函数 flags name dev
-->request_threaded_irq
-->__setup_irq
如果是共享中断
-->disable_irq
-->local_irq_save
-->handler
-->local_irq_restore
-->enable_irq
flags:
IRQF_TIMER //为系统定时器的中断处理准备的
IRQF_SHARED //在同一个给定线上注册的每个中断处理程序都必须指定这个标志,用于共享中断线
name //中断相关设备名,会被/proc/irq和/proc/interrupts使用
dev //空指针,共享中断线时,释放中断服务程序的唯一标志信息(因为此时中断号是共享的),如果不共享中断线时,填NULL即可。
内核每次调用中断时,都会传递这个指针参数
注销中断处理程序:卸载驱动程序时,需要通过free_irq注销相应的中断处理程序,并释放中断线,如果中断不是共享的,同时也会禁止中断线
free_irq //中断号 dev
linux中断发生时,相应中断线在所有处理器上会被屏蔽,以防止同一中断线上接收另一个中断,被重入。通常其它中断线都是打开的,可以处理的。总之,同一个中断不会被嵌套处理
共享中断处理:
1.共享中断的flags必须设置IRQF_SHARED标志
2.dev必须唯一
3.程序必须有相关逻辑知道它的设备是否产生了中断,还是共享中断线的其它设备产生了中断。内核有一个共享中断服务程序链表,会依次调用中断线上的每个处理程序。
处理程序则检查对应的设备状态寄存器,以便中断处理程序进行检查
中断上下文:切记!!!不可在中断中使用会睡眠的函数,因为不是在进程上下文中,没有调度序列,调度出队列就会报错。假设中断能睡眠,那又该怎么重新调度它呢???
中断有自己的栈空间,每个处理器一个
中断过程:硬件-->中断控制器-->处理器-->处理器中断内核-->do_IRQ-->查找中断处理程序-->handle_IRQ_event-->运行该线上所有中断处理程序-->ret_from_intr-->返回继续执行进程
内核的中断向量表:/arch/arm/kernel/entry-armv.S
vector_irq
-->vector_stub
-->__irq_usr/__irq_svc
-->usr_entry //保存现场
-->kuser_cmpxchg_check
-->irq_handler //处理中断
-->handle_arch_irq
-->desc->handle_irq //由set_handle_irq设置
-->get_thread_info tsk
-->mov why, #0
-->ret_to_user_from_irq //恢复现场
-->slow_work_pending
-->do_work_pending //检测_TIF_NEED_RESCHED,是否需要重新调度schedule
-->no_work_pending //无需调度,返回
-->restore_user_regs //恢复寄存器
handle_level_irq
-->irqd_set
-->handle_irq_event
-->handle_irq_event_percpu
-->__handle_irq_event_percpu
-->for_each_action_of_desc //循环,如不是共享,则第一次执行后就退出
-->action->handler(irq, action->dev_id)
-->add_interrupt_randomness //随机熵池?使用中断间隔时间为随机数产生器产生熵
-->irqd_clear
/proc/interrupts:存放与中断相关的统计信息
procfs:虚拟文件系统
第一列是中断号 cpu对应的列是cpu上的中断次数 处理中断的控制器 中断相关的设备名字(request_irq的dev参数,如果是共享的,会全部列出来)
show_interrupts() //提供/proc/interrupts的函数
-->arch_show_interrupts
-->show_ipi_list
-->seq_printf
禁止与激活中断:
local_irq_enable //用于禁止或激活本地cpu上的中断,是无条件的执行的,无法知道本片段之前的中断状态,执行完之后无条件禁止,会导致状态有可能不匹配
的潜在问题,只有我们知道中断并未在其他地方被禁用的情况下,才能使用
local_irq_disable
local_irq_save(flags) //把当前中断状态保存到flags中,然后禁用当前处理器上的中断发送,flags 被直接传递, 而不是通过指针来传递。
flags参数无法传递给另一个函数,因此,这两个要在同一个函数中使用
local_irq_restore(flags)
临界资源:需要互斥访问的资源
最初采用全局中断互斥访问资源,但会导致全局的其它中断都被阻塞住
结合内部代码区使用自旋锁,用于互斥的访问临界资源,本地中断禁止确保临界访问不会被本地中断嵌套访问,自旋锁确保其它处理器上的中断来竞争资源的时候有锁,无法抢夺临界资源
禁止中断线(在所有处理器上):注意,中断线禁止了多少次就需要打开多少次,否则无法生效
disable_irq(irq) //会等待尚未处理完的中断申请处理完,不能在中断处理程序中使用,容易导致死锁,因为在等待中断都处理完,但是当前中断获得了临界资源
时,其它中断无法获取,就不会执行完,其它中断不执行完,当前disable_irq就不会返回,于是死锁发生
disable_irq_nosync(irq) //立即返回
enable_irq(irq)
synchronize_irq(irq) //等待特定的中断处理程序退出
irqs_disabled //当前处理器上的中断是否被禁止,被禁止则返回非0,即true,否则,返回0,即false
in_interrupt //判断是否在中断处理程序,或者下半部处理程序,即中断上文和下文
in_irq //只判断是否在中断的上文中
下半部:
上半部:中断处理程序,完成对硬件的响应,处理严格限时的事情,缩短中断屏蔽的时间,提高系统的响应能力
下半部:时间宽松,可延后执行的任务,待中断被激活以后再执行
下半部执行不需要一个明确的时间,通常下半部在中断处理程序一返回就会马上运行。下半部执行的关键在于,当它们运行的时候,允许响应所有的中断
BH:bottom half,任务队列,软中断和tasklet
软中断:一种机制,不是中断,软中断和系统调用的软中断不是一回事,是一组静态定义的下半部接口,有32个,类型相同也可以在所有处理器上同时执行,性能要求高的下半部可使用软中断,软中断在编译期间静态注册
tasklet:基于软中断,灵活性强,动态创建的下半部机制,可以看作一个简单易用的软中断,类型相同的tasklet不能同时执行,性能和易用性平衡的产物,tasklet可以通过代码动态注册
工作队列:对要退后执行的工作排队,稍后在进程上下文中执行它们
内核定时器:也可用内核定时器实现工作的延后,在中断处理程序中开启一个定时器,随后退出。在定时到达的时候触发定时中断,执行推后的任务,优点是推后执行时间确定
软中断:
struct softirq_action
{
void (*action)(struct softirq_action *);
};
enum
{
HI_SOFTIRQ=0, //高优先级软中断,也是一种tasklet
TIMER_SOFTIRQ, //内核定时器下半部
NET_TX_SOFTIRQ, //发送网络数据包
NET_RX_SOFTIRQ, //接收网络数据包
BLOCK_SOFTIRQ, //block块设备下半部?
IRQ_POLL_SOFTIRQ,
TASKLET_SOFTIRQ, //正常优先级tasklet下半部
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ, //高精度定时器下半部
RCU_SOFTIRQ,
NR_SOFTIRQS
};
static struct softirq_action softirq_vec[NR_SOFTIRQS]; //每个被注册的软中断占据一项,目前使用了9个
软中断处理程序调用:
void softirq_handler(struct softirq_action *) //参数是为兼容性考虑?
struct softirq_action *my_softirq = softirq_vec[x];
my_softirq->action(my_softirq);
软中断不会抢占软中断,其它软中断(甚至相同类型的)可以在其它处理器上同时执行
软中断被检查和执行的时机:
1. 从中断处理程序返回时
2. 在ksoftirqd线程中 //每个处理器都有一个这样的线程。所有线程的名字都叫做ksoftirq/n,区别在于n,它对应的是处理器的编号。
当内核中出现大量软中断的时候,这些内核进程就会辅助处理它们
为了保证只要有空闲的处理器,它们就会处理软中断,所以给每个处理器都分配一个这样的线程。
只要有待处理的软中断(由softirq_pending()函数负责发现),ksoftirq就会调用do_softirq去处理它们。
如果有必要,每次迭代后都会调用schedule()以便让更重要的进程得到处理机会。当所有需要执行的操作都完成以后,该内核线程将自己
设置为TASK_INTERRUPTIBLE状态,唤起调度程序选择其他可执行进程投入运行。
当大量软中断出现的时候,内核会唤醒一组内核线程来处理这些负载。这些线程在最低的优先级上运行(nice值是19),这能避免它们跟其他重要的任务抢夺资源。
3. 在显式的检查和执行软中断的代码中
创建ksoftirqd线程
do_pre_smp_initcalls //由ktherad_init进程执行
-->spawn_ksoftirqd //由early_initcall定义导出
-->cpuhp_setup_state_nocalls
-->takeover_tasklets
-->smpboot_register_percpu_thread //创建softirq_threads,即ksoftirqd线程
-->run_ksoftirqd //实际创建的函数
-->__do_softirq
raise_softirq //显式的触发软中断raise是给软中断做个标记,不是一定立即执行软中断
-->raise_softirq_irqoff
-->__raise_softirq_irqoff
-->or_softirq_pending //置位图,即将注册好的软中断标记为可执行状态,注册好一个新的软中断之后激活一下
-->if (!in_interrupt()) //判断是否在中断中,若不在,且未禁止软中断,则唤醒ksoftirqd线程,执行软中断的回调函数。
否则什么也不做,软中断会在中断的退出阶段被执行
wakeup_softirqd
-->run_ksoftirqd //实际唤醒的函数
-->__do_softirq //
irq_exit
-->if (!in_interrupt()&&local_softirq_pending()) //判断不在中断上下文且本地无软中断执行
invoke_softirq()
-->__do_softirq(若设置强制,可唤醒wakeup_softirqd)
__do_softirq
-->local_softirq_pending //保存待处理软中断的位图
-->h = softirq_vec;
-->h->action(h); //循环移位处理软中断数组中的软中断
注:软中断最多32个(目前的新内核还是32个吗?),保留给对时间要求最严格以及最重要的下半部使用,目前网络子系统和scsi子系统是直接使用软中断的
open_softirq:注册软中断
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}
在上面的软中断枚举中定义自己的软中断号的宏,用open_softirq来注册软中断处理函数,软中断允许响应中断,自己不会休眠,在其它处理器上可以同时触发任何一个软中断
因此软中断中使用全局变量需要加锁,互斥的加锁其实也不理想,违背高效的原则,因此,使用单处理器数据(专属于某一个处理器的数据)来避免加锁会比较好
在没有严格时限,且不需要同一个处理程序在多个处理器上同时运行的情况下,更应考虑tasklet
raise_softirq:
此函数通常在中断处理函数即上半部中使用,触发相应软中断,然后中断处理函数退出,退出的时候就会去执行软中断,避免长时间在上半部中阻塞共享中断及低优先级中断
如果在外部调用,则会唤醒软中断辅助函数去处理软中断
tasklet:
本质也是一个软中断,同一个处理程序不能同时在多个处理器上执行,即:不能重入
分为两类:HI_SOFTIRQ、TASKLET_SOFTIRQ,唯一区别是HI_SOFTIRQ先于TASKLET_SOFTIRQ执行,(其实就是__do_softirq中根据数组下标从小到大执行)
struct tasklet_struct
{
struct tasklet_struct *next; //指向下一项
unsigned long state; //当前处理函数状态
atomic_t count; //引用计数器
void (*func)(unsigned long); //处理函数指针
unsigned long data; //函数参数
};
就是注册了一个软中断,用已有的tasklet处理函数依次处理tasklet_struct链表上的注册函数
count:当count不为0时,此tasklet禁止执行。当它为0,tasklet被激活,且被设置为挂起状态时,该tasklet才允许执行
state:0或者以下值,为0时tasklet未挂起
enum
{
TASKLET_STATE_SCHED, /* 已被调度,准备投入运行,相当于被挂起 */
TASKLET_STATE_RUN /* 这个tasklet正在运行,多处理器上才会使用,单处理器完全知道tasklet是不是正在运行 */
};
static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec);
static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec);
单处理器变量:以上定义了两个单处理器变量,为每个CPU分配一段专有数据区,并将.data.percpu中的数据拷贝到其中,每个CPU各有一份。
以上分别为TASKLET_SOFTIRQ和HI_SOFTIRQ对应的变量
tasklet_schedule
-->test_and_set_bit //将第TASKLET_STATE_SCHED位设置为1,并返回这一位原来的值
这里设置tasklet_struct.state的状态为TASKLET_STATE_SCHED,并判断之前如果设置过,就返回,没设置过,就执行下面的代码
-->__tasklet_schedule
-->__tasklet_schedule_common //将传进来的tasklet加入tasklet_vec对应的链表。
注意,由于已经设置调度状态,在加表的时候需要关闭本地中断,防止加表加到一半被中断,退出中断正好执行tasklet,就会出问题!!
-->raise_softirq_irqoff //激活TASKLET_SOFTIRQ对应的软中断位图
tasklet_hi_schedule //过程与tasklet_schedule一致,区别只是调度的是HI_SOFTIRQ对应的软中断而已
tasklet的调度基本都是在中断处理函数中执行的,退出阶段会调用tasklet对应的软中断处理函数
tasklet_action:
tasklet_hi_action:
在内核的初始化阶段就调用open_softirq注册过了,这才是tasklet的实际软中断处理函数,我们写的只不过是二级处理函数,由这个函数来依次调用
tasklet_action
-->tasklet_action_common
-->tasklet_trylock //首先清空tasklet_vec链表,然后依次尝试加锁TASKLET_STATE_RUN,这里加锁之后其它处理器检测到之后就不会执行这个tasklet
-->t->func(t->data) //循环检测调度状态的tasklet并执行
-->tasklet_unlock //无论是否被调度状态,都会解锁TASKLET_STATE_RUN状态
最后会将遍历过的tasklet加回tasklet_vec链表
-->__raise_softirq_irqoff //再次激活软中断。(感觉似乎没有必要,这里明显是tasklet_action内部做的处理,完全可以在循环完成之后再激活?)
tasklet使用:
静态定义对象的结构体:
#define DECLARE_TASKLET(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }
#define DECLARE_TASKLET_DISABLED(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data }
创建tasklet结构体并赋值,唯一不同的是count的值一个是0一个是1
动态定义对象的结构体:
tasklet_init //注意,动态创建之前要先定义对应的变量!!!
其实内核中很多地方都提供了静态及动态两种创建对象的相关结构体的方法,静态是以DECLARE打头的宏,动态是xxx_init,用到不同的地方的时候作类比就可以了
注意:tasklet的处理函数中不能使用引起睡眠的函数,比如信号量还有被阻塞的函数等,因为睡眠就会被调度,如果在中断上下文睡眠调度就会报错,阻塞也不可接受,太费时间了
注意:不能执行相同的tasklet是指,tasklet本身已经调度,还没有执行之前,再次调度相同tasklet,也只会执行一次tasklet_handler(因为状态检测已经是调度态,就直接返回了),这里
tasklet_handler里面要考虑连续高速触发tasklet的情况
tasklet_enable/tasklet_disable //对tasklet进行激活和禁止,实际就是对count的原子操作
tasklet_kill //将一个taslet从挂起的队列中去掉,并清除其调度状态
工作队列:能在内核进程上下文中实行的下半部机制,没有不能睡眠阻塞的限制!!
把推后的工作交给一个内核线程去处理,下半部分总是工作在进程上下文中,因此工作队列的处理允许睡眠及重新调度(关键原因在于进程上下文而不是中断上下文)
工作队列是专门用于创建内核线程的接口,在驱动程序中,工作队列创建的进程称为工作者线程,负责执行由内核其它部分排队到队列里的任务
工作队列有一个缺省的工作者线程events/n,即工作队列就是把需要推后的工作交给特定的通用线程,如无必要(即负担较大的任务,单独的工作者线程会避免缺省线程的压力),尽量使用缺省线程
struct worker //每个处理器有自己的工作者线程,都有自己的工作队列struct workqueue_struct *rescue_wq成员
struct workqueue_struct //工作队列将工作串成链表即可
struct work_struct {
atomic_long_t data; //工作处理函数func的参数
struct list_head entry; //连接工作的指针
work_func_t func; //工作队列处理的函数
};
typedef void (*work_func_t)(struct work_struct *work);
即cpu-->worker(worker_thread函数)-->workqueue_struct-->work_struct
实际使用的时候一般定义工作即可使用
当worker被唤醒,就会循环执行它的工作链表上的所有工作,执行一个就从链表去除一个,执行完继续休眠,直到下次唤醒
以下定义的参数data为0的work:
DECLARE_WORK(name, func)
INIT_WORK(_work, _func)
每次进入中断,就执行调度程序,把任务入队
schedule_work(struct work_struct *work)
刷新工作队列:
flush_scheduled_work //在工作队列中的工作都执行完毕以后才返回,对于模块的卸载以及防止竞争都有作用
创建自己的工作者线程及队列:struct workqueue_struct *myworkqueue = create_workqueue(name),name是个字符串,类比events
入队自己的工作:queue_work(myworkqueue,&work)
刷新自己的工作:flush_workqueue
同步问题:
对共享资源(或者说临界资源),需要防止并发访问,假如多个执行代码同时访问或操作资源,可能导致相互覆盖共享数据,造成程序的问题
并发访问:
单处理器:中断或调度时
多处理器:多处理器上的任意内核代码
临界区:访问和操作共享数据的代码段
原子操作:操作结束前不可被打断,不可被分割到下次执行
竞争条件:两个执行代码有可能在同一临界区同时执行的可能性叫做竞争条件
同步:防止并发和竞争条件的出现
单个指令:在单处理器中在内核中本身就是原子操作,因为中断发生于指令之间
锁机制:类比门锁,拿锁-->进门-->执行临界区-->解锁-->其它线程拿锁。锁本身采用原子操作实现,不存在竞争条件
线程持有锁,锁对当前线程保护数据
不同锁机制的区别:
1.锁在被争用时简单的执行忙等待
2.使当前进程睡眠直到锁可用为止
造成并发的原因:中断、软中断和tasklet、内核抢占、睡眠(单处理器伪并行交叉访问);smp同时执行代码
真正困难的是,辨认出真正需要共享的数据和相应的临界区,发现潜在并发执行的可能。如果任何其它东西(比如线程或中断)能看到的数据就要加锁,切记,是给数据而不是代码加锁
CONFIG_SMP:是否支持smp
CONFIG_PREEMPT:支持抢占
锁本身的问题:可能产生死锁:
自死锁:一个线程去获取自己已经持有的锁,它需要等待锁被释放,但是它自身在忙着等待锁,自己也无法去释放锁。(吃着碗里的看着锅里的)
n个线程:各自持有一把锁,各自又在等待其它线程释放锁,造成死锁。(多个和尚没水吃)
预防死锁:
1.对多个锁或者嵌套锁按一定顺序加锁。多个临界区访问共享资源时都按固定顺序加锁(多个锁时)(释放锁最好按照相反顺序来进行)
2.防止得不到锁的代码一直等待下去
3.坚决不要重复请求一个锁
锁的争用:其它线程试图获取正在被占用的锁,高度争用:多个线程等待获得锁,尤其是频繁、长期被持有的锁。
问题:造成性能瓶颈
加锁粒度:加锁保护的数据规模。过粗的锁保护大量数据,精细的锁保护一个变量
锁的争用变得严重时:加锁就趋于精细化,能减少锁的争用,提高大型机器上的性能,但是过于精细的锁粒度会造成小型机器上开销过大,无用的锁增加了复杂度,性能下降。锁的争用不严重时,加锁不用过细
同步方法:
原子操作:其它同步方法的基石,保证执行过程不被分割,不可打断 //例如:i++是单条执令,原子的操作,i = x;i = i + 1;就是非原子的
原子操作接口:一组针对整数,一组针对单独的位
typedef struct { //32位原子变量
int counter;
} atomic_t;
typedef struct { //64位原子变量
s64 counter;
} atomic64_t;
单独定义原子变量:能避免传给原子函数非原子变量,也能避免把原子变量传给非原子函数,造成其它问题
列举部分原子函数,都是内联函数,其它函数类比即可:
#define ATOMIC_INIT(i) { (i) }
atomic_set(atomic_t *v, int i)
atomic_read(const atomic_t *v)
atomic_add(int i, atomic_t *v)
atomic_inc(atomic_t *v)
atomic_inc_and_test(atomic_t *v) //自加并判断判断,为0返回真,非0返回假
原子操作只会设置以及判断,和锁还是有差距的,相当于轻便的锁,但是开销更小
原子位操作是针对普通指针的操作,可以看作就是有原子特性的位操作:
#define set_bit(nr,p) ATOMIC_BITOP(set_bit,nr,p) //nr是位,p是指针
#define clear_bit(nr,p) ATOMIC_BITOP(clear_bit,nr,p)
#define change_bit(nr,p) ATOMIC_BITOP(change_bit,nr,p)
#define test_and_set_bit(nr,p) ATOMIC_BITOP(test_and_set_bit,nr,p) //置位并返回原先的值
#define test_and_clear_bit(nr,p) ATOMIC_BITOP(test_and_clear_bit,nr,p)
#define test_and_change_bit(nr,p) ATOMIC_BITOP(test_and_change_bit,nr,p)
下面的接口用于找到第一个置位或清零的位
unsigned long find_first_bit(const unsigned long *addr, unsigned long size)
unsigned long find_first_zero_bit(const unsigned long *addr, unsigned long size)
自旋锁:(spin lock)同一时刻最多只能被一个可执行线程持有。如果其它线程试图争用锁,就会在试图获取锁的地方一直忙循环-旋转-等待锁,很耗时间(在门外等待拿钥匙进房间?)
因此,自旋锁争用的等待耗时最好小于信号量争用时睡眠和唤醒进程的两次上下文切换时长
typedef struct spinlock {
union {
struct raw_spinlock rlock;
#ifdef CONFIG_DEBUG_LOCK_ALLOC
# define LOCK_PADSIZE (offsetof(struct raw_spinlock, dep_map))
struct {
u8 __padding[LOCK_PADSIZE];
struct lockdep_map dep_map;
};
#endif
};
} spinlock_t;
#define DEFINE_SPINLOCK(x) spinlock_t x = __SPIN_LOCK_UNLOCKED(x)
static __always_inline void spin_lock(spinlock_t *lock)
static __always_inline void spin_unlock(spinlock_t *lock)
单处理器不需要自旋锁
注意:自旋锁不可递归获取,试图获取自己正持有的自旋锁会造成死锁(会一直等待自己解锁,自己又忙着等待,无法去解锁)
中断中使用自旋锁:中断中使用自旋锁之前,首先需要禁止本地中断,否则,当前处理器的嵌套中断中一旦尝试获取自旋锁,就会导致一级的中断没机会解锁,二级的中断又在一直等锁
spin_lock_irqsave(spinlock_t *lock, unsigned long flags)
spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags)
动态的定义自旋锁:
spinlock_t lock;
spin_lock_init (&lock);
static __always_inline int spin_trylock(spinlock_t *lock) //尝试获取锁,如果该锁已被占用,则立即返回非0,如果成功获取锁返回0
static __always_inline int spin_is_locked(spinlock_t *lock) //检测锁是否被占用,并不实际占用。锁已被获取则返回非0,否则返回0
进程上下文和下半部共享数据时,在进程中需要获取锁并禁止下半部
static __always_inline void spin_lock_bh(spinlock_t *lock)
static __always_inline void spin_unlock_bh(spinlock_t *lock)
task_let同类不需要锁保护,不同种类需要锁保护,而软中断只要有数据共享,就需要锁的保护
读-写自旋锁:
当写入数据时,不能有其它代码写入及读取数据,完全互斥,但是,在读取数据时,只要没有写入操作,并发的读取本身是安全的
多个线程可并发的持有读锁,而写的锁只能互斥的持有,并且写锁需要读锁全部释放才能加锁
相当于:写-写互斥,写-读互斥,读-读不互斥
DEFINE_RWLOCK(x)
read_lock(&lock) //读锁只读
write_lock(&lock) //写锁可读写
总之,读写锁也有类似上面的函数簇
读锁-->写锁 会导致死锁
信号量:是一种睡眠锁,一个任务试图获得一个不可用的信号量时,信号量会将其推进一个等待队列,让请求进程睡眠,直到信号量可用时处于等待队列的那个任务再次被唤醒,并获得该信号量。
因此,信号量不会对调度造成影响
如果加锁时间长或者会睡眠,优先使用信号量,能提供更好的处理器利用率,但是自旋锁本身的开销更大,且无法在中断及下半部中使用
计数信号量和二值信号量:信号量在声明时可以指定同时持有者数量:
这个计数为1时,称为二值信号量、互斥信号量
这个计数大于1时,称为计数信号量
信号量进行PV操作:P(down)对信号量减1,V(up)对信号量加1
struct semaphore {
raw_spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
struct semaphore name;
static inline void sema_init(struct semaphore *sem, int val)
DEFINE_SEMAPHORE(name)
int down_interruptible(struct semaphore *sem) //试图获取信号量,不可用时进入睡眠,可用时被唤醒,返回-EINTR
void down(struct semaphore *sem) //不响应信号的P操作
int down_trylock(struct semaphore *sem) //以堵塞的方式获取信号量,无法获取则返回非0
void up(struct semaphore *sem) //释放指定的信号量
读-写信号量:和读-写自旋锁类似,所有读-写信号量计数值都为1
#define DECLARE_RWSEM(name)
int init_rwsem(struct rw_semaphore *sem)
int down_read(struct rw_semaphore *sem);
int up_read(struct rw_semaphore *sem);
int down_write(struct rw_semaphore *sem);
int up_write(struct rw_semaphore *sem);
int down_read_trylock(struct rw_semaphore *sem) //成功返回非0,和down_trylock是相反的
int down_write_trylock(struct rw_semaphore *sem)
互斥体:更简单的,实现互斥的,可睡眠的锁,和二值信号量类似,不能手动初始化
#define DEFINE_MUTEX(mutex)
#define mutex_init(&mutex)
void mutex_lock(struct mutex *lock)
void mutex_unlock(struct mutex *lock)
int mutex_trylock(struct mutex *lock) //成功返回1,失败返回0
bool mutex_is_locked(struct mutex *lock) //已锁返回1,否则返回0
完成变量:一个等待,另一个完成就唤醒等待的任务
DECLARE_COMPLETION(work)
init_completion(struct completion *)
void wait_for_completion(struct completion *)
void complete(struct completion *)
顺序锁:
区别在于它给写者赋予了更高的优先级:在使用顺序锁时即便读者正在进行读操作,写者也可以进行写动作,。
读写锁的优点在于写者永远不会由于有读者正在进行读而等待,其缺点在于读者可能需要尝试读好多次才能读到合法的数据。
#define DEFINE_SEQLOCK(x) \
seqlock_t x = __SEQLOCK_UNLOCKED(x)
static inline void write_seqlock(seqlock_t *sl) //读的时候可写,计数加1
static inline void write_sequnlock(seqlock_t *sl) //写完计数器再加1,多个写操作本身还需要再使用互斥锁
static inline unsigned read_seqbegin(const seqlock_t *sl) //读取数据之前读锁计数
static inline unsigned read_seqretry(const seqlock_t *sl, unsigned start) //读取数据完成之后再读取计数器,并和读取之前做比较
unsigned int seq;
do {
seq = read_seqbegin(&seqlock);
/* ... CRITICAL REGION ... */
} while (read_seqretry(&seqlock, seq));
读与写互不阻塞,读前与读后的计数不一致时,表示中途有写入操作,需重新读取
u64 get_jiffies_64(void)
{
unsigned int seq;
u64 ret;
do {
seq = read_seqbegin(&jiffies_lock);
ret = jiffies_64;
} while (read_seqretry(&jiffies_lock, seq));
return ret;
}
更新jiffies_64时也需要使用顺序锁加锁
内核抢占:
自旋锁本身不会睡眠,同时持有锁也不会被其它线程抢占
#define preempt_disable() //thread_info.preempt_count加1 disable和enable是对称的,禁止多少次就要相应使能多少次
#define preempt_enable() //thread_info.preempt_count减1 当计数降为0时检查执行被挂起的需调度的任务
preempt_enable_no_resched() //只使能,不立即显式调度
preempt_count() //返回抢占计数
也可实现禁止内核抢占:
#define get_cpu() ({ preempt_disable(); __smp_processor_id(); }) //禁止内核抢占,返回cpu编号
#define put_cpu() preempt_enable() //放开禁止
顺序与屏障:
需求:有时需要确保读写顺序按一定的代码顺序来执行,或者,在多处理器上,可能需要按写数据的顺序读数据
问题:编译器优化时可能对代码的读和写顺序进行重排序优化
屏障:(barriers)确保顺序不会进行重新排序的指令
rmb():读内存屏障,确保此接口前面发生的读操作不会和此接口后面的读操作发生重排序
wmb():写内存屏障,确保跨越屏障的存储不会发生重排序
smp处理器下:
smp_rmb():
smp_wmb():
barrier():组织编译器读写顺序的跨屏障优化
定时器和时间管理:
事件驱动:由事件的发生进行驱动
时间驱动:由时间的改变进行驱动
间隔或推后一定时间进行执行的操作,以及系统运行的时间、当前日期时间等
相对时间和绝对时间
系统定时器-->动态定时器
系统定时器:周期性事件由系统定时器驱动,定时器中断负责更新系统时间,执行周期性任务
动态定时器:推后一定时间执行的任务由动态定时器驱动,依托于系统定时器
系统定时器(硬件):以某种频率自行触发时钟中断(也称击中(hitting)或射中(popping)),该频率称为节拍率,节拍率可预编,两次中断的时间间隔称为节拍(tick),为节拍分之一秒
墙上时间:实际时间
系统运行时间:从系统启动运行的时间,用于计算时间流逝
节拍率:HZ,周期:1/HZ,系统定时器中断频率多为100
提高节拍率
优点:能提高中断解析度及时间驱动事件的准确性,粒度更细,提高性能,降低周期内延误的事件
缺点:增加了处理时钟中断的时间,增加了系统负担,减少了处理其它工作的时间,打乱cache的工作并增加耗电
jiffies:定义于<linux/jiffies.h>系统启动以来的节拍总数,系统启动时初始化为0,在时钟中断处理程序中增加这个值。jiffies一秒内增加HZ,系统运行时间就是jiffies/HZ秒。
extern unsigned long volatile __cacheline_aligned_in_smp __jiffy_arch_data jiffies;
秒-->jiffies (seconds*HZ)
unsigned long now = jiffies;
unsigned long next_tick = jiffies + 1;
unsigned long next_twoSeconds = jiffies + 2*HZ;
unsigned long next_halfSeconds = jiffies + HZ/2;
jiffies-->秒 (jiffies/HZ)
vmlinux.lds.S
extern u64 __cacheline_aligned_in_smp jiffies_64; //时间管理使用64位,避免溢出
jiffies = jiffies_64; //jiffies在100HZ下,497天会溢出。取jiffies_64低32位,用于计算时间流逝,同时兼容使用jiffies的代码
32位机:jiffies = jiffies_64; //截取低32位
64位机:jiffies = jiffies_64; //64位机上,它们是同一个变量
读取jiffies_64:
u64 get_jiffies_64(void)
{
unsigned int seq;
u64 ret;
do {
seq = read_seqbegin(&jiffies_lock);
ret = jiffies_64;
} while (read_seqretry(&jiffies_lock, seq));
return ret;
}
假设正好在用jiffies做超时判断的时候,jiffies溢出归零(绕回),会造成超时判断出错,使用这些宏可以避免出错:known表示设定的已知时间,unknown表示jiffies,即未知。
下列宏以known为基点,判断jiffies是否超时,原理是根据无符号补码转有符号加减得出:
#define time_after(unknown,known) ((long)((known) - (unknown)) < 0))
#define time_before(unknown,known) time_after(known,unknown)
#define time_after_eq(unknown,known) ((long)((unknown) - (known)) >= 0))
#define time_before_eq(unknown,known) time_after_eq(known,unknown)
HZ与USER_HZ:我们常希望HZ=USER_HZ,实际情况可能并不相等,那么就需要做一定转化
USER_HZ是用户空间的,常定义为100,比较固定,依赖于固定的HZ值来换算时间。
而HZ是内核空间的,一旦更改,就会导致HZ-->USER_HZ时间换算出错
clock_t jiffies_to_clock_t(unsigned long x)
{
#if (TICK_NSEC % (NSEC_PER_SEC / USER_HZ)) == 0
# if HZ < USER_HZ
return x * (USER_HZ / HZ);
# else
return x / (HZ / USER_HZ);
# endif
#else
return div_u64((u64)x * TICK_NSEC, NSEC_PER_SEC / USER_HZ);
#endif
}
此函数将同样时间内的HZ表示的计数转换为USER_HZ表示的计数,确保用户空间能获得以USER_HZ为基准的实际时间对应的jiffies
jiffies_64_to_clock_t与上面函数类似,只是转换的是64位,jiffies_64_to_clock_t/USER_HZ就是用户空间的秒
硬时钟和定时器:
实时时钟:RTC,掉电持久存放系统时间
系统定时器:PIT,开机实时计时
全局变量xtime,是timeval结构的变量。用来表示当前时间距UNIX时间基准1970-01-01 00:00:00的相对秒数值。其时间精度是纳秒(先前的版本为微秒)。
因为xtime主要供查询使用,所以xtime的更新被放到 timer_interrupt()的后半段执行,和jiffies不同,不是每次时钟中断时都执行。因此看起来会比jiffies慢,两者不同步
RTC-->开机时从RTC同步
xtime -->实际时间 //关机时有写回操作
-->
PIT
-->系统定时器-->动态定时器
时钟相关初始化程序:
tick_init()
init_timers() //PIT系统定时器初始化
-->init_timer_cpus
-->open_softirq //设置TIMER_SOFTIRQ的软中处理函数为run_timer_softirq,在run_timer_softirq将完成对到期的定时器实际的处理工作
hrtimers_init() //初始化高精度定时器
-->hrtimers_prepare_cpu
-->open_softirq //设置HRTIMER_SOFTIRQ的软中处理函数为hrtimer_run_softirq
timekeeping_init() //每次启动时都要通过timekeeping_init从RTC中同步正确的时间信息,墙上时间
一旦初始化完成后,timekeeper就开始独立于RTC,利用自身关联的clocksource进行时间的更新操作,根据内核的
配置项的不同,更新时间的操作发生的频度也不尽相同
系统定时器中断处理:每秒执行HZ次
tick_handle_periodic
-->tick_periodic
-->do_timer
-->jiffies_64 += ticks //更新jiffies,这里获取顺序锁jiffies_lock以安全更新jiffies_64
-->calc_global_load //更新系统的负载统计,好按此调整CFS虚拟时间片
-->update_wall_time //更新墙上时间
-->timekeeping_advance //更新timekeeper时间值,此时独立于RTC,由系统时钟源进行更新
-->update_process_times //更新进程时间片,通过参数user_mode(get_irq_regs())获取cpsr判断系统模式是否为用户模式
-->account_process_tick //对进程节拍进行实质性更新
-->run_local_timers
-->raise_softirq(TIMER_SOFTIRQ) //周期性触发定时器软中断,运行所有的超时定时器函数,会将所有定时器按超时时间分组,接近超时的定时器随组下移
-->scheduler_tick //减少时间片
-->trigger_load_balance //触发负载均衡软中断
-->raise_softirq(SCHED_SOFTIRQ)
-->clockevents_program_event
实际时间:
struct timespec {
__kernel_time_t tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};
struct timeval {
__kernel_time_t tv_sec; /* seconds */
__kernel_suseconds_t tv_usec; /* microseconds */
};
struct timekeeper
xtime.tv_sec记录1970至今的秒数
int gettimeofday(struct timeval *tv, struct timezone *tz) //给用户拷贝墙上时钟
动态定时器:
用于内核推后固定时间执行代码,超时就会自动撤销
struct timer_list {
struct hlist_node entry;
unsigned long expires;
void (*function)(unsigned long);
unsigned long data;
u32 flags;
int slack;
};
#define DEFINE_TIMER(_name, _function)
do_init_timer
my_timer.expires = jiffies + delay;
my_timer.data = 0;
my_timer.function = my_timer_function;
void add_timer(struct timer_list *timer) //激活定时器
{
BUG_ON(timer_pending(timer));
mod_timer(timer, timer->expires);
}
int mod_timer(struct timer_list *timer, unsigned long expires) //更新expires并入队定时器队列,更改超时时间并激活(无论之前有没有激活),会检测执行状态,无疑比较安全
-->enqueue_timer
int del_timer(struct timer_list *timer) //超时前停止定时器,无论定时器有没有激活都删除定时器,如果定时器超时,会自动删除。
可能在接下来访问的资源被没有执行完的定时器代码访问,造成并发
int del_timer_sync(struct timer_list *timer) //定时器执行完才会删除,不能在中断上下文中使用,能使数据更安全
run_timer_softirq //每个节拍检测并处理,然后出队,所以精度并不那么高
如果延时比较短的时间,延时也有其它机制:
使用jiffies和time_before来进行while循环等待,以节拍数整数倍延时,精度不高,各种不靠谱!!!
精确定时:
udelay(n) //依靠执行循环达到效果,PS:bogoMIPS为处理器一秒钟能执行的循环次数,定时按一秒中的循环比例就可实现较为精确的定时
mdelay(n) //bogoMIPS即为loops_per_jiffy,由set_arc-->calibrate_delay算出
ndelay(x)
不推荐:
set_current_state/schedule_timeout //设置为睡眠状态,然后使指定任务睡眠指定节拍数,到时间会自动唤醒,进程上下文,实际是定时器的应用
schedule_timeout
-->schedule
-->__mod_timer
-->process_timeout
-->wake_up_process
内存管理:
要明白的是,内核的内存管理比用户空间要复杂,也要更节约
页:内存管理的基本单位,MMU(管理内存,虚拟地址<-->物理地址)以页为单位进行管理,页表的大小也为页
页大小:
32位:4KB
64位:8KB
以下简化的结构体用于短暂的表示每个物理页,内核仅仅用它来描述当前时刻(假设页在逻辑上还存在,但页的交换,也可能会使页不再和同一个page相关联,PS:页一旦交换,其实就是物理页发生改变
了,下次换回内存不一定还在这个物理页上)在相关物理页存放的东西,即物理页本身,而不是页里面的数据
struct page {
unsigned long flags; //存放页的状态,每一位表示页的一种状态,是否达成指定的状态,对应位置位,所有状态对应enum pageflags
atomic_t _refcount; //页的引用计数,页未被引用时,置-1,调用page_count原子的读取,会返回0表示可被分配,返回正整数表示页在使用中
struct address_space *mapping; //当页作为页高速缓存使用时,指向页高速缓存,配合index进行索引
unsigned long private; //页作为私有数据时,由private指向
pgoff_t index;
atomic_t _mapcount;
struct list_head lru;
void *virtual; //作为页表映射时,表示页在虚拟内存中的地址
} _struct_page_alignment;
注意:所有的页都需要用这一结构来描述(假设4G对20M,代价不大),因为内核需要知道一个页是否空闲,在页已被分配的情况下,是谁在使用(用户空间进程、动态分配的内核数据、内核全局静态
数据、页高速缓存等)
区:由于有些特定页在特定地址上,被用于一些特定的任务或不能用于一些特定的任务。由于这些限制,内核把页划分未不同的区(zone),使用区对具有相似特性的页进行分组。区的划分仅仅是逻辑上的
物理地址空间的顶部以下一段空间,被PCI设备的I/O内存映射占据,它们的大小和布局由PCI规范所决定。640K~1M这段地址空间被BIOS和VGA适配器所占据。
Linux系统在初始化时,会根据实际的物理内存的大小,为每个物理页面创建一个page对象,所有的page对象构成一个mem_map数组。
所有的struct page结构体都共同保存在同一个mem_map数组里面,数组下标实际就是物理内存位置。
struct pglist_data.node_mem_map-->struct page
进一步,针对不同的用途,Linux内核将所有的物理页面划分到3类内存管理区中,如图,分别为ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM。
enum zone_type {
ZONE_DMA,
ZONE_DMA32,
ZONE_NORMAL,
ZONE_HIGHMEM,
ZONE_MOVABLE,
ZONE_DEVICE,
__MAX_NR_ZONES
};
ZONE_DMA:该区域的物理页面专门供I/O设备的DMA使用,因为有些从设备只能访问前面16M空间(x86 32),假设就会把这些空间分配给DMA。某些体系结构在整个ZONE_NORMAL上都能执行DMA,因此ZONE_DMA为空
之所以需要单独管理DMA的物理页面,是因为DMA使用物理地址访问内存,不经过MMU,并且需要连续的缓冲区,所以为了能够提供物理上连续的缓冲区,必须从物理地址空间专门划分一段区域用于DMA。
ZONE_NORMAL的范围是16M~896M,该区域的物理页面是内核能够直接使用的。
内核线性空间存在直接映射关系,所以内核会将频繁使用的数据如kernel代码、GDT、IDT、PGD、mem_map数组等放在ZONE_NORMAL里。
ZONE_HIGHMEM的范围是896M~结束,该区域即为高端内存(x86 32),内核不能直接使用。
目前现实中,64位Linux内核不存在高端内存,因为64位内核可以支持超过512GB内存。若机器安装的物理内存超过内核地址空间范围,就会存在高端内存。
而将用户数据、页表(PT)等不常用数据放在ZONE_ HIGHMEM里,只在要访问这些数据时才建立映射关系(kmap())。
比如,当内核要访问I/O设备存储空间时,就使用ioremap()将位于物理地址高端的mmio区内存映射到内核空间的vmalloc area中,在使用完之后便断开映射关系。
除去高端内存的区域就叫低端内存
区的表示:
struct zone {
unsigned long _watermark[NR_WMARK]; //区的水位,NR_WMARK是3,分别存放最小值、最低和最高位。用来设置每个内存区的内存消耗基准,随空闲内存的多少而变化
水位越高越好,水位越低表示越需要回收内存,和内存回收有关
long lowmem_reserve[MAX_NR_ZONES];
struct pglist_data *zone_pgdat;
struct per_cpu_pageset __percpu *pageset;
/* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT */
unsigned long zone_start_pfn;
atomic_long_t managed_pages;
unsigned long spanned_pages;
unsigned long present_pages;
const char *name; //区的名字,是个字符串
#ifdef CONFIG_MEMORY_HOTPLUG
seqlock_t span_seqlock;
#endif
int initialized;
ZONE_PADDING(_pad1_)
struct free_area free_area[MAX_ORDER];
unsigned long flags;
spinlock_t lock; //保护这个结构,防止并发访问
ZONE_PADDING(_pad2_)
unsigned long percpu_drift_mark;
unsigned long compact_cached_free_pfn;
unsigned long compact_cached_migrate_pfn[2];
unsigned long compact_init_migrate_pfn;
unsigned long compact_init_free_pfn;
unsigned int compact_considered;
unsigned int compact_defer_shift;
int compact_order_failed;
bool compact_blockskip_flush;
bool contiguous;
ZONE_PADDING(_pad3_)
atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS];
atomic_long_t vm_numa_stat[NR_VM_NUMA_STAT_ITEMS];
} ____cacheline_internodealigned_in_smp;
由于开启了分页机制,内核想要访问物理地址空间的话,必须先建立映射关系,然后通过虚拟地址来访问。
为了能够访问所有的物理地址空间,就要将全部物理地址空间映射到1G的内核线性空间中,这显然不可能。
于是,内核将0~896M的物理地址空间一对一映射到自己的线性地址空间中,这样它便可以随时访问ZONE_DMA和ZONE_NORMAL里的物理页面;
此时内核剩下的128M线性地址空间不足以完全映射所有的ZONE_HIGHMEM,Linux采取了动态映射的方法,即按需的将ZONE_HIGHMEM里的物理页面映射到kernel space的最后128M线性地址空间里,使用完
之后释放映射关系,以供其它物理页面映射。虽然这样存在效率的问题,但是内核毕竟可以正常的访问所有的物理地址空间了。
用户进程没有高端内存概念。只有在内核空间才存在高端内存。用户进程最多只可以访问3G物理内存,而内核进程可以访问所有物理内存。
Linux将4G的线性地址空间分为2部分,0~3G为user space,3G~4G为kernel space。
32位系统用户进程最大可以访问3GB,内核代码可以访问所有物理内存。
64位系统用户进程最大可以访问超过512GB,内核代码可以访问所有物理内存。
高端内存只和逻辑地址有关系,和逻辑地址、物理地址没有直接关系。
页表分为用户空间页表和内核空间页表,不同的进程,它用户空间是不同的,所以它的用户空间页表是不同的,但是不同的进程它的内核空间是共享的,它的内核空间页表也是相同的。
page_alloc.c或者gfp.h:
分配页:
static inline struct page *alloc_pages(gfp_t gfp_mask, unsigned int order) //分配2的order个次方的连续物理页,并返回第一个物理页的page结构体指针
static inline void *page_address(const struct page *page) //实际返回virtual指针,即返回当前页的逻辑地址
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order) //实际等同于把上两个函数合并,不会分配高端内存,因为返回的逻辑地址无法代表高端内存
unsigned long get_zeroed_page(gfp_t gfp_mask)-->__get_free_pages(gfp_mask | __GFP_ZERO, 0) //分配一页并设置整个页为0
释放页:
void __free_pages(struct page *page, unsigned int order)
void free_pages(unsigned long addr, unsigned int order)
#define free_page(addr) free_pages((addr), 0)
释放页的时候需要谨慎,不能过多释放
kmalloc:获得以字节为单位的一块连续的物理内存,虚拟地址也是连续的,返回一个逻辑地址。注意:kmalloc基于slab使用了一组通用缓存!!!
static __always_inline void *kmalloc(size_t size, gfp_t flags) //<linux/slab.h>
kmalloc
-->__kmalloc
-->__do_kmalloc
-->slab_alloc
-->____cache_alloc
gfp_t gfp_mask:分配器标志,表示内存分配器将要采取的行动
分为三类:行为修饰符、区修饰符、类型 //gfp.h
行为修饰符:某些特定情况下,需要使用特定的方法分配内存。(PS:在中断中要求分配内存时不能睡眠)
区修饰符:指明从哪个区分配内存
类型:类型标志组合了前两者,简化了修饰符的使用
必须确保不会因为遗漏某个标志导致调用到自身而失败,假设未指明GFP_NOFS,而分配的时候调用到文件系统接口,导致更多的分配文件系统操作,容易出现bug!
#define GFP_ATOMIC (__GFP_HIGH|__GFP_ATOMIC|__GFP_KSWAPD_RECLAIM) //限制比较严格,不允许睡眠,因此内核没有机会用其它操作以增大成功率
#define GFP_KERNEL (__GFP_RECLAIM | __GFP_IO | __GFP_FS) //可能会睡眠,内核可以执行其它操作以增大成功率
#define GFP_KERNEL_ACCOUNT (GFP_KERNEL | __GFP_ACCOUNT)
#define GFP_NOWAIT (__GFP_KSWAPD_RECLAIM)
#define GFP_NOIO (__GFP_RECLAIM)
#define GFP_NOFS (__GFP_RECLAIM | __GFP_IO)
#define GFP_USER (__GFP_RECLAIM | __GFP_IO | __GFP_FS | __GFP_HARDWALL)
#define GFP_DMA __GFP_DMA
#define GFP_DMA32 __GFP_DMA32
#define GFP_HIGHUSER (GFP_USER | __GFP_HIGHMEM)
#define GFP_HIGHUSER_MOVABLE (GFP_HIGHUSER | __GFP_MOVABLE)
#define GFP_TRANSHUGE_LIGHT ((GFP_HIGHUSER_MOVABLE | __GFP_COMP | \
__GFP_NOMEMALLOC | __GFP_NOWARN) & ~__GFP_RECLAIM)
#define GFP_TRANSHUGE (GFP_TRANSHUGE_LIGHT | __GFP_DIRECT_RECLAIM)
进程上下文,可以睡眠 GFP_KERNEL
进程上下文,不可以睡眠 GFP_ATOMIC
中断处理程序 GFP_ATOMIC
软中断 GFP_ATOMIC
Tasklet GFP_ATOMIC
用于DMA的内存,可以睡眠 GFP_DMA | GFP_KERNEL
用于DMA的内存,不可以睡眠 GFP_DMA | GFP_ATOMIC
内核中内存的分配优先从normal分配
kfree:
void kfree(const void *block) //注意和kmalloc配对使用,kfree(NULL)也是可以的
vmalloc:分配的内存较大,在虚拟地址上连续,物理地址不连续,会对页表的逻辑地址作修改,使其逻辑地址连续
void *vmalloc(unsigned long size) 例:vmalloc(16*PAGE_SIZE);
硬件设备不理解虚拟地址,必须给它一个连续的物理地址块;仅供软件使用的内存块可以使用逻辑地址连续的内存块
一般情况下在驱动程序中都是调用kmalloc()来给数据结构分配内存
而vmalloc()用在为活动的交换区分配数据结构,为某些I/O驱动程序分配缓冲区,或为模块分配空间
vmalloc 中调用了 kmalloc (GFP—KERNEL),因此可能睡眠,不能应用于原子上下文。
vmalloc会对内存一个一个的映射,导致更大的tlb(硬缓冲区,用于缓存虚拟地址到物理地址的映射关系,提高性能)抖动,模块装载就是插入到vmalloc分配的内存
void vfree(const void *addr) //也可能睡眠
slab层:
用到空闲链表,此链表包含已经分配好的数据结构块。代码需要的时候直接从链表中抓取,不需要分配内存的时候放回链表而不释放。(相当于高速缓存,缺点是可用内存紧缺时无法及时通知链表释放一部分)
由于以上缺点,提供了slab层(即slab分配器):扮演了通用数据结构缓存层的角色
优点:
1.频繁使用的数据结构需要频繁的分配和释放,应当缓存它们
2.空闲链表的缓存会连续的存放,释放的会放回链表,不会导致内存碎片(难以找到大块连续的可用内存)
3.回收之后可立即投入使用,提高了性能
4.部分缓存专属于单个处理器,那么,分配和释放就可以在不加锁的情况下进行
5.对存放的对象进行着色,避免多个对象映射到相同的高速缓存行(cache line)
高速缓存其实就是一组称之为缓存行(cache line)的固定大小的数据块,其大小是以突发读或者突发写周期的大小为基础的。
每个高速缓存行完全是在一个突发读操作周期中进行填充或者下载的。即使处理器只存取一个字节的存储器,高速缓存控制器也启动整个存取器访问周期并请求整个数据块。
slab把不同的对象划分为高速缓存组,每个高速缓存组存放不同类型的对象(PS:kmalloc基于slab使用了一组通用缓存)
-->缓存组1-->n个slab(页)-->struct task_struct空闲链表
slab(总的概念)-->缓存组2-->n个slab(页)-->struct inode空闲链表
-->缓存组3-->n个slab(页)-->struct timeval空闲链表
这些缓存被划分为slab,slab由一个或多个物理上连续的页组成,slab一般仅一页,每个高速缓存可以由多个slab组成
slab状态:满(无空闲对象)、部分满(分配了一部分)、空(一个都没有分配)
slab的优先分配顺序:满-->部分满-->空-->创建新的slab
实现:精简如下:
struct kmem_cache { //表示一个高速缓存组
struct kmem_cache_node *node[MAX_NUMNODES];
};
struct kmem_cache_node {
spinlock_t list_lock;
#ifdef CONFIG_SLAB
struct list_head slabs_partial; //n个部分满slab队列,指向一串页表,页表通过slab_list构成slab链表
struct list_head slabs_full; //n个满的slab队列
struct list_head slabs_free; //n个空闲的slab队列
unsigned long total_slabs; /* length of all slab lists */
unsigned long free_slabs; /* length of free slab list only */
unsigned int colour_next; /* Per-node cache coloring */
struct array_cache *shared; /* shared per node */
struct alien_cache **alien; /* on other nodes */
unsigned long next_reap; /* updated without locking */
#endif
};
struct page {
union {
struct list_head slab_list; //页表通过slab_list构成slab链表
struct { /* Partial pages */
struct page *next; //一个slab可能不止一页
#ifdef CONFIG_64BIT
int pages; /* Nr of pages left */
int pobjects; /* Approximate count */
#else
short int pages;
short int pobjects;
#endif
};
};
struct kmem_cache *slab_cache; /* not slob */ //指向所在的缓存行
void *freelist; /* first free object */
union {
void *s_mem; /* slab: first object */ //指向slab中的第一个对象
unsigned long counters; /* SLUB */
struct { /* SLUB */
unsigned inuse:16; //slab中已分配的对象数
unsigned objects:15;
unsigned frozen:1;
};
};
}
创建新的高速缓存:参数:缓存名字 单个元素大小 slab内第一个对象的偏移 缓存的行为 ctor已被废弃(追加新的页时调用)
struct kmem_cache *kmem_cache_create(const char *name, unsigned int size, unsigned int align, slab_flags_t flags, void (*ctor)(void *)) //可能会睡眠
kmem_cache_create
-->__kmem_cache_alias //检查是否有现成的slab
-->kmem_cache_create_usercopy
-->create_cache //创建slab描述符
-->kmem_cache_zalloc //分配一个kmem_cache数据结构
-->kmem_cache_alloc
-->slab_alloc
-->__do_cache_alloc
-->____cache_alloc
-->__kmem_cache_create
SLAB_RED_ZONE //在分配的内存周围插入"红色警戒区",探测缓冲越界
SLAB_POISON //用已知值(a5a5)填充slab
SLAB_HWCACHE_ALIGN //把slab内所有对象按高速缓存行对齐,防止其它对象错误的映射到同一缓存行,提高了性能,浪费的内存增多
SLAB_CACHE_DMA //分配位于DMA区用于DMA的slab
SLAB_PANIC //失败时通知slab,用于分配必不可少的slab,失败时报panic,如果没有用这个宏,需要进行返回值检查
void kmem_cache_destroy(struct kmem_cache *s) //注销高速缓存,注意:必须确保任何一个slab中,没有一个对象被分配出去,且撤销过程中,这个缓存不再被访问!!!
void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags) //从指定的高速缓存cachep中获取一个指针(对象结构体指针),假如没有空闲对象,slab会调用kmem_getpages获取新的页
void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags)
-->slab_alloc
-->__do_cache_alloc
-->____cache_alloc
void kmem_cache_free(struct kmem_cache *cachep, void *objp) //释放对象,返还给原来的slab
注意:创建和使用slab高速缓存例子可参考task_struct_cachep的例子,见于fork.c
创建新的slab:slab在未空未满的时候进行页分配,在可用内存紧缺或显式调用时才释放页
static struct page *kmem_getpages(struct kmem_cache *cachep, gfp_t flags, int nodeid) //cachep->gfporder即为要分配的大小,在缓存行信息的基础上进行页分配
kmem_getpages
-->__alloc_pages_node
-->__alloc_pages
-->__alloc_pages_nodemask
slab对应的释放内存页:
static void kmem_freepages(struct kmem_cache *cachep, struct page *page)
-->__free_pages
页-->slab-->kmalloc
内核栈静态分配:
每个进程的内核栈大小依赖于体系结构,两页左右,32位和64位上栈分别是8KB和16KB
内核栈溢出首先会危害thread_info以及task_strcuct
高端内存映射:
高端内存不可能永久映射到内核地址空间
32位上,物理内存896M到结束为高端内存
0~896M物理空间映射3G~3G+896M内核空间,剩下128M内核空间为高端内存映射区,把896M以后的物理地址空间短暂的映射到这128M逻辑地址上,用完即释放映射
永久映射:
void *kmap(struct page *page) //先alloc_pages,使用high_mem分配一个高端的页,然后进行映射。此函数可以睡眠
如果传入的是低端页,就会返回虚拟地址,类似page_address,如果传入的是高端页,就会建立永久映射并返回地址
void kunmap(struct page *page)
临时映射:预先的一组保留的映射,用于存放新创建的临时映射,不会睡眠
void *kmap_atomic(struct page *page)
#define kunmap_atomic(addr)
下一个临时映射是可以直接覆盖上一个临时映射的
每个cpu的数据:
unsigned int my_percpu[NR_CPUS]; //以cpu号为下标,可确保每个cpu变量都只被对应的处理器访问到,不会有其它cpu并发访问
int cpu;
cpu = get_cpu(); //获取cpu号,同时禁止抢占,防止其它代码抢占发生竞争
my_percpu[cpu]++;
do_something();
put_cpu(); //激活抢占,释放处理器
新的单个cpu数据接口,简化操作:
#define DEFINE_PER_CPU(type, name) \ //和DECLARE_PER_CPU用法一样,等同,为每个cpu创建一个
DEFINE_PER_CPU_SECTION(type, name, "")
get_cpu_var(var) //返回当前处理器变量拷贝,禁止抢占
put_cpu_var(var) //完成,即写回
#define per_cpu(var, cpu) (*per_cpu_ptr(&(var), cpu)) //获取其它处理器的变量,这里是没有锁的
动态创建CPU变量:都是返回指针
alloc_percpu(type) //返回指针
void free_percpu(void __percpu *ptr) //释放动态分配的cpu变量
get_cpu_ptr(var)
put_cpu_ptr(var)
ps:缓存抖动:如果处理器操作一个数据,而这个数据在其它处理器中,那么存放该数据的处理器需要清理或刷新自己的缓存。持续不断的缓存失效称之为缓存抖动,对系统的性能影响颇大
虚拟文件系统:VFS,屏蔽下层各具体文件系统的差异,进行抽象,能由unix系统调用进行操作,泛型存取;而各存储介质由块I/O层进行统一
VFS和块I/O层一起屏蔽软硬件差异,使得跨文件系统和存储介质的抽象统一成为现实
VFS:在底层文件系统接口上提供了一个抽象层,提供了一个通用文件系统模型,该模型囊括了各种文件系统的常用功能集和行为,内核通过抽象层对接各种类型的文件系统(想想和驱动是不是
很像,同样的write,经抽象层的sys_write,再到不同的驱动做特定的动作)
UNIX文件系统:使用了四种和文件系统相关的传统抽象概念,即文件、目录项、索引节点和挂载点。文件系统包含文件、目录和相关控制信息,通用操作包含创建、删除和挂载等
文件对象(file):做成一个有序字节串,其中第一个字节是文件头,最后一个字节是文件尾,然后分配一个便于理解的名字
目录项(dentry):用来组织文件,目录也可以包含子目录,层层嵌套,形成文件路径。对VFS来说,目录也属于普通文件,列出其中所有文件,对目录也可以执行和文件相同的操作
索引节点(inode):如控制权限、大小、拥有者、创建时间等信息,被称作文件的元数据,被存储再称为索引节点(inode-->index node)的结构体中
超级块(super block):包含一个实际文件系统(是文件系统而不是文件系统类型,一个文件系统类型下可以包括很多文件系统即很多super block)相关信息的数据结构,一个超级块代表一个具体的已安装
的文件系统。称为文件系统数据元,包含文件系统的类型、大小和状态等,存放于磁盘特定扇区或对于sysfs之类内存文件系统,现场创建并存放于内存
每种对象对应一个操作对象,其中许多方法,可继承使用VFS通用方法,如果某些通函数无法满足,那么使用实际文件系统独有指针去填充这些指针
struct super_block-->struct super_operations //对应于一个文件系统实例,存放于磁盘读取到内存
-->struct file_system_type //描述特定文件系统类型,如ext3、ext4,全局唯一
struct inode-->struct inode_operations //操作文件时,必须在内存中创建索引节点(VFS的inode),从磁盘索引节点读入(文件系统的inode)。有些文件系统在磁盘上没有
索引节点,控制信息作为文件的一部分整体存放,这种情况下也需要提取信息在内存中创建对应的索引节点。
每个inode节点的大小,一般是128字节或256字节。inode节点的总数,在格式化时就给定(现代OS可以动态变化),一般每2KB就
设置一个inode。一般文件系统中很少有文件小于2KB的,所以预定按照2KB分,一般inode是用不完的。
所以inode在文件系统安装的时候会有一个默认数量,后期会根据实际的需要发生变化。
inode号是唯一的,表示不同的文件。其实在Linux内部的时候,访问文件都是通过inode号来进行的。
所谓文件名仅仅是给用户容易使用的。当我们打开一个文件的时候,首先,系统找到这个文件名对应的inode号;然后,通过inode
号,得到inode信息,最后,由inode找到文件数据所在的block,现在可以处理文件数据了。
当创建一个文件的时候,就给文件分配了一个inode。一个inode只对应一个实际文件,一个文件也会只有一个inode。
inodes最大数量就是文件的最大数量。
struct dentry-->struct dentry_operations //为便于路径查找,每个dentry代表路径中的一个部分,目录项没有对应的磁盘数据结构,VFS根据字符串形式的路径名现场创建它们。
根据字符串路径解析的dentry会被链表缓存,增大效率,因为解析比较费力
struct file-->struct file_operations //表示进程已经打开的文件(进程直接处理的是文件而不是超级块,inode或者目录项),包含访问模式,偏移等,没有实际磁盘数据
对应的文件操作read、write等,系统调用open创建创建文件对象,而close撤销文件对象
同一个文件也可能存在多个对应的文件对象,在多个进程的视角上代表已经打开的文件
struct vfsmount //位置和安装标志,当文件系统被实际安装时,将有一个vfsmount结构体在安装点被创建,用来指明目录,黏合超级块
super_block-->vfsmount-->super_block-->inode(flash_partion)<--dentry<--file<--files
和进程相关
struct fs_struct
需要注意的是:单进程命名空间的概念,使得每一个进程在系统中看到唯一的安装文件系统--唯一的根目录,唯一的文件系统层次结构
static struct super_block *alloc_super(struct file_system_type *type, int flags, struct user_namespace *user_ns) //用于在内存中创建超级块并初始化,从磁盘读取文件系统超级块填充
到内存中的超级块对象
struct super_block {
struct list_head s_list; //指向超级块链表的指针
dev_t s_dev; //包含该具体文件系统的块设备标识符
unsigned char s_blocksize_bits; //上面的size大小占用位数,例如512字节就是9 bits
unsigned long s_blocksize; //文件系统中数据块大小,以字节单位
loff_t s_maxbytes; //允许的最大的文件大小(字节数)
struct file_system_type *s_type; //文件系统类型(也就是当前这个文件系统属于哪个类型?ext2还是fat32)一个文件系统类型下可以包括很多文件系统即很
多的super_block
const struct super_operations *s_op; //某个特定的具体文件系统的用于超级块操作的函数集合
const struct dquot_operations *dq_op; //某个特定的具体文件系统用于限额操作的函数集合
const struct quotactl_ops *s_qcop; //配置磁盘限额的的方法
const struct export_operations *s_export_op; //导出方法
unsigned long s_flags; //挂载或安装方法
unsigned long s_iflags; /* internal SB_I_* flags */
unsigned long s_magic; //区别于其他文件系统的标识
struct dentry *s_root; //指向该具体文件系统安装目录的目录项,即挂载点
struct rw_semaphore s_umount; //卸载信号量,即卸载时不能被互斥的访问
int s_count; //超级块引用计数
atomic_t s_active; //活动引用计数
void *s_security; //安全模块相关
const struct xattr_handler **s_xattr; //扩展属性操作
struct block_device *s_bdev; //指向文件系统被安装的块设备
struct hlist_node s_instances;
struct sb_writers s_writers;
void *s_fs_info; /* Filesystem private info */
char s_id[32]; /* Informational name */
uuid_t s_uuid; /* UUID */
unsigned int s_max_links;
fmode_t s_mode;
struct mutex s_vfs_rename_mutex; /* Kludge */
const char *s_subtype;
int s_readonly_remount;
struct workqueue_struct *s_dio_done_wq;
struct user_namespace *s_user_ns;
struct list_head s_inodes; /* all inodes */
spinlock_t s_inode_wblist_lock;
struct list_head s_inodes_wb; /* writeback inodes */
}
以下由VFS在进程上下文中调用
struct super_operations {
struct inode *(*alloc_inode)(struct super_block *sb); //无法直接得到操作函数的父对象,把父对象作为参数传递进去。在给定超级块下创建并初始化新的索引节点
void (*destroy_inode)(struct inode *); //释放给定索引节点
void (*free_inode)(struct inode *);
void (*dirty_inode) (struct inode *, int flags); //VFS在索引节点脏(被修改)时会调用此函数进行日志更新
int (*write_inode) (struct inode *, struct writeback_control *wbc); //将给定索引节点写入磁盘
int (*drop_inode) (struct inode *);
void (*evict_inode) (struct inode *);
void (*put_super) (struct super_block *); //卸载文件系统时由VFS调用,释放超级块,调用者需持有s_lock锁
int (*sync_fs)(struct super_block *sb, int wait);
int (*freeze_super) (struct super_block *);
int (*freeze_fs) (struct super_block *);
int (*thaw_super) (struct super_block *);
int (*unfreeze_fs) (struct super_block *);
int (*statfs) (struct dentry *, struct kstatfs *); //获取文件系统状态
int (*remount_fs) (struct super_block *, int *, char *); //重新安装文件系统
void (*umount_begin) (struct super_block *);
int (*show_options)(struct seq_file *, struct dentry *);
int (*show_devname)(struct seq_file *, struct dentry *);
int (*show_path)(struct seq_file *, struct dentry *);
int (*show_stats)(struct seq_file *, struct dentry *);
#ifdef CONFIG_QUOTA
ssize_t (*quota_read)(struct super_block *, int, char *, size_t, loff_t);
ssize_t (*quota_write)(struct super_block *, int, const char *, size_t, loff_t);
struct dquot **(*get_dquots)(struct inode *);
#endif
int (*bdev_try_to_free_page)(struct super_block*, struct page*, gfp_t);
long (*nr_cached_objects)(struct super_block *,
struct shrink_control *);
long (*free_cached_objects)(struct super_block *,
struct shrink_control *);
};
struct inode {
umode_t i_mode; //访问权限
unsigned short i_opflags;
kuid_t i_uid; //使用者的id
kgid_t i_gid; //使用者的组id
unsigned int i_flags;
const struct inode_operations *i_op; //索引节点操作表
struct super_block *i_sb; //inode所属文件系统的超级块指针
struct address_space *i_mapping; //相关的地址映射
unsigned long i_ino; //索引节点号,每个inode都是唯一的
union {
const unsigned int i_nlink;
unsigned int __i_nlink; //与该节点建立链接的文件数(硬链接数)
};
dev_t i_rdev; //实际磁盘设备号
loff_t i_size; //字节为单位,文件大小
struct timespec64 i_atime; //文件最后访问时间
struct timespec64 i_mtime; //文件最后修改时间
struct timespec64 i_ctime; //inode最后一次修改时间
spinlock_t i_lock; /* i_blocks, i_bytes, maybe i_size */
unsigned short i_bytes; //最后一个块的字节数
u8 i_blkbits;
u8 i_write_hint;
blkcnt_t i_blocks; //文件所占块数
#ifdef __NEED_I_SIZE_ORDERED
seqcount_t i_size_seqcount; //i_size串行计数
#endif
/* Misc */
unsigned long i_state; //状态标志
struct rw_semaphore i_rwsem;
unsigned long dirtied_when; /* jiffies of first dirtying */
unsigned long dirtied_time_when;
struct hlist_node i_hash; //指向hash链表指针,用于inode的hash表
struct list_head i_io_list; /* backing dev IO list */
struct list_head i_lru; /* inode LRU list */
struct list_head i_sb_list;
struct list_head i_wb_list; /* backing dev writeback list */
union {
struct hlist_head i_dentry; //指向目录项链表指针,注意一个inodes可以对应多个dentry,因为一个实际的文件可能被链接到其他的文件,那么就会有
另一个dentry,这个链表就是将所有的与本inode有关的dentry都连在一起
struct rcu_head i_rcu;
};
atomic64_t i_version;
atomic64_t i_sequence; /* see futex */
atomic_t i_count; //引用计数
atomic_t i_dio_count;
atomic_t i_writecount;
union {
const struct file_operations *i_fop; /* former ->i_op->default_file_ops */
void (*free_inode)(struct inode *);
};
struct file_lock_context *i_flctx;
struct address_space i_data;
struct list_head i_devices;
union {
struct pipe_inode_info *i_pipe; //管道、块设备或字符设备,三者选其一
struct block_device *i_bdev;
struct cdev *i_cdev;
char *i_link;
unsigned i_dir_seq;
};
__u32 i_generation
void *i_private; /* fs or device private pointer */
}
struct inode_operations {
struct dentry * (*lookup) (struct inode *,struct dentry *, unsigned int); //在dentry中查找dentry中对应的文件名的索引节点
const char * (*get_link) (struct dentry *, struct inode *, struct delayed_call *);
int (*permission) (struct inode *, int);
struct posix_acl * (*get_acl)(struct inode *, int);
int (*readlink) (struct dentry *, char __user *,int);
int (*create) (struct inode *,struct dentry *, umode_t, bool); //为dentry对象创建一个索引节点
int (*link) (struct dentry *,struct inode *,struct dentry *); //由系统调用link调用,创建第一个dentry中inode的硬链接第二个dentry
int (*unlink) (struct inode *,struct dentry *);
int (*symlink) (struct inode *,struct dentry *,const char *);
int (*mkdir) (struct inode *,struct dentry *,umode_t);
int (*rmdir) (struct inode *,struct dentry *);
int (*mknod) (struct inode *,struct dentry *,umode_t,dev_t);
int (*rename) (struct inode *, struct dentry *,
struct inode *, struct dentry *, unsigned int);
int (*setattr) (struct dentry *, struct iattr *);
int (*getattr) (const struct path *, struct kstat *, u32, unsigned int);
ssize_t (*listxattr) (struct dentry *, char *, size_t);
int (*fiemap)(struct inode *, struct fiemap_extent_info *, u64 start,
u64 len);
int (*update_time)(struct inode *, struct timespec64 *, int);
int (*atomic_open)(struct inode *, struct dentry *,
struct file *, unsigned open_flag,
umode_t create_mode);
int (*tmpfile) (struct inode *, struct dentry *, umode_t);
int (*set_acl)(struct inode *, struct posix_acl *, int);
} ____cacheline_aligned;
struct dentry {
/* RCU lookup touched fields */
unsigned int d_flags; /* protected by d_lock */
seqcount_t d_seq; /* per dentry seqlock */
struct hlist_bl_node d_hash; /* lookup hash list */
struct dentry *d_parent; //父目录的目录项对象
struct qstr d_name; //目录项名字
struct inode *d_inode; //相关联的索引节点
unsigned char d_iname[DNAME_INLINE_LEN]; /* small names */
struct lockref d_lockref; /* per-dentry lock and refcount */
const struct dentry_operations *d_op; //目录项相关操作
struct super_block *d_sb; //文件的超级块
unsigned long d_time; /* used by d_revalidate */
void *d_fsdata; /* fs-specific data */
struct list_head d_child; //目录项内部链表
struct list_head d_subdirs; //子目录链表
};
struct dentry_operations {
int (*d_revalidate)(struct dentry *, unsigned int);
int (*d_weak_revalidate)(struct dentry *, unsigned int);
int (*d_hash)(const struct dentry *, struct qstr *);
int (*d_compare)(const struct dentry *,
unsigned int, const char *, const struct qstr *);
int (*d_delete)(const struct dentry *);
int (*d_init)(struct dentry *);
void (*d_release)(struct dentry *);
void (*d_prune)(struct dentry *);
void (*d_iput)(struct dentry *, struct inode *);
char *(*d_dname)(struct dentry *, char *, int);
struct vfsmount *(*d_automount)(struct path *);
int (*d_manage)(const struct path *, bool);
struct dentry *(*d_real)(struct dentry *, const struct inode *);
} ____cacheline_aligned;
struct file {
union {
struct llist_node fu_llist; //文件对象链表
struct rcu_head fu_rcuhead;
} f_u;
struct path f_path; //包含文件对应的dentry和vfsmount结构体指针
struct inode *f_inode; /* cached value */
const struct file_operations *f_op; //文件操作表
/*
* Protects f_ep_links, f_flags.
* Must not be taken from IRQ context.
*/
spinlock_t f_lock;
enum rw_hint f_write_hint;
atomic_long_t f_count; //文件对象使用计数
unsigned int f_flags; //打开文件时所指定的标志
fmode_t f_mode; //文件的访问模式
struct mutex f_pos_lock;
loff_t f_pos; //文件当前位移量,即文件指针
struct fown_struct f_owner;
const struct cred *f_cred;
struct file_ra_state f_ra; //预读状态
u64 f_version; //版本号
void *private_data;
struct address_space *f_mapping; //页缓存映射
errseq_t f_wb_err;
} __randomize_layout
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int); //更新偏移量指针
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); //从偏移处读取指定字节数到缓冲区,并更新指针
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iopoll)(struct kiocb *kiocb, bool spin);
int (*iterate) (struct file *, struct dir_context *);
int (*iterate_shared) (struct file *, struct dir_context *);
__poll_t (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); //
long (*compat_ioctl) (struct file *, unsigned int, unsigned long); //在64位上兼容32位
int (*mmap) (struct file *, struct vm_area_struct *); //将给定文件映射到指定地址空间
unsigned long mmap_supported_flags;
int (*open) (struct inode *, struct file *); //该函数创建一个新的文件对象,并和相应索引节点关联起来
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *); //当文件最后一个引用被注销时,会被VFS调用
int (*fsync) (struct file *, loff_t, loff_t, int datasync); //将给定文件的所有缓存数据写回磁盘
int (*fasync) (int, struct file *, int); //打开或关闭异步I/O通告信号
int (*lock) (struct file *, int, struct file_lock *); //给指定文件上锁
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
loff_t, size_t, unsigned int);
loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,
struct file *file_out, loff_t pos_out,
loff_t len, unsigned int remap_flags);
int (*fadvise)(struct file *, loff_t, loff_t, int);
} __randomize_layout;
struct file_system_type {
const char *name; //文件系统的名字
int fs_flags; //文件系统类型标志
int (*init_fs_context)(struct fs_context *);
const struct fs_parameter_description *parameters;
struct dentry *(*mount) (struct file_system_type *, int,
const char *, void *);
void (*kill_sb) (struct super_block *); //用来终止访问超级块
struct module *owner; //文件系统模块
struct file_system_type * next; //链表中下一个文件系统类型
struct hlist_head fs_supers; //超级块对象链表
struct lock_class_key s_lock_key;
struct lock_class_key s_umount_key;
struct lock_class_key s_vfs_rename_key;
struct lock_class_key s_writers_key[SB_FREEZE_LEVELS];
struct lock_class_key i_lock_key;
struct lock_class_key i_mutex_key;
struct lock_class_key i_mutex_dir_key;
};
struct vfsmount {
struct dentry *mnt_root; /* root of the mounted tree */
struct super_block *mnt_sb; /* pointer to superblock */
int mnt_flags;
} __randomize_layout;
struct files_struct {
atomic_t count; //结构的使用计数
bool resize_in_progress;
wait_queue_head_t resize_wait;
struct fdtable __rcu *fdt; //指向其它fd表,当进程打开的文件超过NR_OPEN_DEFAULT个时,内核将分配一个新数组,并将fdt
指向它
struct fdtable fdtab; //基fd表
spinlock_t file_lock ____cacheline_aligned_in_smp; //单个文件的锁
unsigned int next_fd; //缓存下一个可用的fd
unsigned long close_on_exec_init[1];
unsigned long open_fds_init[1]; //打开的文件描述符
unsigned long full_fds_bits_init[1];
struct file __rcu * fd_array[NR_OPEN_DEFAULT]; //进程的以打开的文件对象的数组
};
由进程描述符的fs域
struct fs_struct {
int users; //用户数目
spinlock_t lock; //锁
seqcount_t seq; //顺序锁
int umask;
int in_exec; //当前正在执行的文件
struct path root, pwd; //根目录路径以及当前工作目录路径
} __randomize_layout;
块I/O层:
块设备:能够随机访问固定大小数据片的硬件设备,块设备最小的可寻址单元是扇区,扇区大小一般是2的整数倍,最常见的是512字节,扇区大小是设备的物理属性
块设备能够访问的固定大小的数据片称作块,它们是最小逻辑可寻址单元,一般以安装文件系统的方式使用。块是文件系统的一种抽象,各种软件基于块来访问文件系统,执行磁盘操作。
扇区:是最小物理可寻址单元,也称硬扇区或设备块
块:是最小逻辑可寻址单元,也称文件块或I/O块
块必须数倍于扇区,是2的整数倍,且不能超过一个页的长度,所以块大小通常是512字节、1KB或4KB(32位页大小为4KB,64位页大小为8KB) PS:簇、柱面、磁头是针对于硬盘等特定块设备的
块被调入内存时,要存储在一个缓冲区中,每个缓冲区与一个块对应,是磁盘块在内存中的表示。一个页可存储一个或多个块。
每个缓冲区都有一个对应的缓冲区描述符,用buffer_head表示,称作缓冲区头,记录了内核处理数据时的相关控制信息(块属于哪个块设备,块对应的缓冲区等)
说明:缓冲区头的目的仅在于作为一个描述符,描述磁盘块和物理内存缓冲区之间的映射关系:
struct buffer_head {
unsigned long b_state; //缓冲区状态标志位
struct buffer_head *b_this_page; //一般页面可以由多个块组成,一个页面中的所有块是以循环链表组成在一起的,本字段
用于指向下一个缓冲区首部的地址
struct page *b_page; //当前缓冲区所在的页
sector_t b_blocknr; //起始逻辑块号,是b_bdev指明的块设备中与缓冲区对应的磁盘物理块的逻辑块号
size_t b_size; //块的大小
char *b_data; //指向页面内数据的指针,直接指向block所在位置(在b_page中的某个位置),该block起
始于b_data,终止于b_data + b_size
struct block_device *b_bdev; //所属的块设备
bh_end_io_t *b_end_io; //初始化时设置的回调函数,在底层完成数据读写时调用的函数
void *b_private; //b_end_io回调函数的参数
struct list_head b_assoc_buffers; //关联其它映射的链表
struct address_space *b_assoc_map; //这个映射的内核逻辑地址空间
atomic_t b_count; //缓冲区引用计数
};
enum bh_state_bits {
BH_Uptodate, //如果缓冲区包含有效数据则置1
BH_Dirty, //如果buffer脏(缓存中的内容比磁盘新,必须被写回磁盘)
BH_Lock, //锁定缓冲区,以防并发访问
BH_Req, //缓冲区有请求I/O操作
BH_Uptodate_Lock,/* Used by the first bh in a page, to serialise
* IO completion of other buffers in the page
*/
BH_Mapped, //该缓冲区是有对应映射磁盘块的可用缓冲区
BH_New, //缓冲区是新通过get_block映射的,尚且不能访问
BH_Async_Read, //缓冲区正通过end_buffer_async_write被异步I/O读操作使用
BH_Async_Write, //缓冲区正通过end_buffer_async_read被异步I/O写操作使用
BH_Delay, //缓冲区尚未和磁盘块关联
BH_Boundary, //该缓冲区处于连续块区的边界
BH_Write_EIO, //该缓冲区在写的时候遇到I/O错误
BH_Unwritten, //该缓冲区在硬盘上的空间已被申请,但是没有实际数据写出
BH_Quiet, /* Buffer Error Prinks to be quiet */
BH_Meta, /* Buffer contains metadata */
BH_Prio, /* Buffer should be submitted with REQ_PRIO */
BH_Defer_Completion, /* Defer AIO completion to workqueue */
BH_PrivateStart, //从此位往上的位不是通用可用标志位,而是给驱动程序存储自己的自定义状态信息的
};
操作缓冲区头之前,要先使用get_bh增加缓冲头引用计数,确保缓冲头不会再被分配出去;完成对缓冲头的操作后,要使用put_bh减少计数
static inline void get_bh(struct buffer_head *bh) //原子操作,实际就是对count引用计数的增减
static inline void put_bh(struct buffer_head *bh)
bio结构体:
块I/O操作的容器,轻量灵活,优点是可以包括内存中的多个页,而buffer_head只能包含一个缓冲区
struct bio {
struct bio *bi_next; //请求链表
struct gendisk *bi_disk; //该bio所请求的块设备,gendisk结构体表示一个磁盘设备或分区
unsigned int bi_opf;
unsigned short bi_flags; //状态和命令标志
unsigned short bi_ioprio;
unsigned short bi_write_hint;
blk_status_t bi_status;
u8 bi_partno;
struct bvec_iter bi_iter;
atomic_t __bi_remaining;
bio_end_io_t *bi_end_io; //I/O完成方法
void *bi_private; //拥有者的私有域,只有创建者可读写
#ifdef CONFIG_BLK_CGROUP
struct blkcg_gq *bi_blkg;
struct bio_issue bi_issue;
#ifdef CONFIG_BLK_CGROUP_IOCOST
u64 bi_iocost_cost;
#endif
#endif
unsigned short bi_vcnt; //请求向量的数量
unsigned short bi_max_vecs; /* max bvl_vecs we can hold */
atomic_t __bi_cnt; //使用计数,递减,减为0时就应该撤销该bio结构体
struct bio_vec *bi_io_vec; //bio_vec链表,元素个数为bi_vcnt,每个bio_vec对应操作一个页面中的偏移,页不一定连续
即一个bio操作在块设备上对应的块是连续的,但是在物理内存上对应的页不一定是连续的
struct bio_set *bi_pool;
struct bio_vec bi_inline_vecs[0];
};
struct bio_vec {
struct page *bv_page; //操作的页
unsigned int bv_len; //长度
unsigned int bv_offset; //页内块相对于页的偏移量
};
每个请求由多个bio结构体表示,每个请求包含一个或多个块,这些块存储在bio_vec结构体数组中,该结构体代表以向量链表形式组织的活动的块I/O操作,一个向量是一小块连续的内存缓冲区,即bio_vec
一个BIO所请求的数据在块设备中是连续的,放到多个不一定连续的页中,通过bi_io_vec逐一指向这些页中的缓冲偏移。不连续的数据块需要放到多个BIO中,
一个BIO所携带的数据大小是有上限的,该上限值由bi_max_vecs间接指定,超过上限的数据块必须放到多个BIO中
使用bio_for_each_segment来遍历 bio_vec
操作bio的引用计数,操作bio之前要先加计数,操作完毕后要减少计数
static inline void bio_get(struct bio *bio)
void bio_put(struct bio *bio)
块设备将它们挂起的请求保存在请求队列中,文件系统也会将对应的请求加入到队列,队列对应的块设备驱动会从队列获取请求,将其送入对应的块设备
请求队列由request_queue结构体表示,requset表示请求结构体组成链表,一个请求可能要操作多个连续的磁盘块,所以每个请求可以由多个bio结构体组成,可能包含多个bio结构体。
注意:所以一个请求的多个bio,在磁盘上的块必须是连续的,但是在内存中这些块不一定连续
I/O调度程序:
为了提高块设备寻址性能,内核会在提交请求前,先对其执行合并与排序的预操作,内核中负责提交I/O请求的子系统称为I/O调度程序
合并:对多个相邻区域的读写合并成一个请求,减少寻址次数
排序:对扇区比较接近的请求则按扇区增长方向有序排序,保持磁头从一个方向移动到头后反向移动,类似于电梯的运行,所以这种I/O调度也称作电梯调度
I/O调度程序:
1.linus电梯:当有请求入队时,检查是否有请求可以合并,新请求连在现存请求前面,就是向前合并,否则就是向后合并(居多);合并失败则按扇区号增长寻找插入点。如果有驻留过长的请求,新请
求就会放在队尾,防止旧请求被饥饿(只能改善,还是会饥饿,缺陷)
2.最终期限I/O调度程序:(deadline)为了解决linus电梯饥饿问题而产生。处于减少磁盘操作考虑,对磁盘某个区域的频繁读写会使得其它位置的IO请求很难得到运行机会。
写操作和提交它的应用程序异步执行,当提交写请求之后,内核在有空时采取执行写操作(当然,过多的写数据积压在内存缓冲区肯定是不明智的)
读操作和提交它的应用程序同步执行,当提交读请求之后,应用程序会阻塞直到读请求被满足,且读请求往往相互依靠,前一个读完才能执行后一个(比如读取大文件),耗费大量时间
减少饥饿必须以降低全局吞吐量为代价进行折中
即最终期限I/O调度会有三个队列,入队排序队列的同时也会同时入队读或写队列:
读FIFO队列:以时间为基准,默认500ms超时时间
写FIFO队列:以时间为基准,默认5s超时时间
排序队列: 以磁盘扇区排序
以排序队列派发请求,发现读或写队列请求超过指定时间时,从对应FIFO队列提取请求
读优先,通常可防止饥饿,但不严格保证响应时间,良好的读响应时间 deadline.c
3.预测I/O调度程序:最终期限算法在写操作繁重时会损害吞吐量,写操作会被读操作频繁的打断。预测I/O调度程序在最终期限调度的基础上增加了预测启发能力
预测启发:跟踪并统计每个应用程序的I/O操作行为,从而进行预测是否需要在响应提交读请求的时候等待6ms,这6ms用来等待应用程序提交其它的读请求。
从而减少了新入队读请求的次数,减少读请求寻址等开销,几毫秒的损失是值得的。
关键在于预测是否准确
对大部分工作及服务器执行良好,除了少数不常见而工作负荷严格的服务器,是默认的IO调度算法
4.完全公正的排队I/O调度程序:(CFQ)为专有工作负荷设计的,与前面的有根本不同
每个进程都有自己的IO请求队列,在每个队列里又进行合并与排序。
从每个队列中选取请求数为4的请求,进行下一轮基于时间片的调度,在进程级提供了公平,在很多场合都能很好的执行
5.空操作的I/O调度程序:(noop)专为闪存卡、ssd等不需要寻道的随机块设备而实现的,由于没有寻道的负担,只对新入队请求进行合并,队列近乎FIFO的顺序排列,不进行专门的排序操作
1.linus电梯,已被2和3方法淘汰
2.最终期限I/O调度程序,防止读饥饿 deadline
3.预测I/O调度程序,防止读导致写饥饿 as
4.完全公正的排队I/O调度程序,很多场合都不错,缺省的调度方法 cfq
5.空操作的I/O调度程序,用于不需寻道的闪存卡、ssd等 noop
四种IO调度程序都内置在内核中,可以通过以下命令查看和设置:如:
cat /sys/block/{DEVICE-NAME}/queue/scheduler
echo noop > /sys/block/hda/queue/scheduler
进程地址空间:
所有进程之间以虚拟内存技术共享内存,对一个进程而言,它好像可以访问整个所有的物理内存,且单独一个进程所拥有的地址空间可以远大于系统物理内存
每个进程有独立的全部4G虚拟地址空间,互不干涉。但是并不是全部地址空间都实际分配并可以访问,可被访问的被称作内存区域,每个进程可以给自己的地址空间动态的添加或减少内存区域
相关进程可读、可写、可访问有效内存区域,如果进程访问不在有效范围内的内存区域,或以不正确方式访问有效地址,内核会终止该进程,并返回段错误
内存区域:
1.可执行文件代码的内存映射,称为代码段(text section)
2.可执行文件的已初始化全局变量的内存映射,称为数据段(data section)
3.包含未初始化全局变量,也就是bss段的零页(页面中全为0,c规定未初始化的全局变量要赋予特殊的默认值)的内存映射(未占用代码文件空间,载入内存时由零页进行映射)
4.用于进程用户空间栈的零页内存映射(进程的内核栈独立存在由内核维护)
5.每一个如C库或动态链接库等共享库的代码段、数据段和bss也会被载入进程的地址空间
6.内存映射文件
7.任何共享内存段
8.任何匿名的内存映射,如由malloc分配的内存
每一个不同的内存片段都对应有一个独立的内存区域
内核使用进程描述符表示进程的地址空间:
struct mm_struct {
struct {
struct vm_area_struct *mmap; //内存区域链表
struct rb_root mm_rb; //VMA形成的红黑树,与mmap描述的对象是相同的,只是一个是以链表一个是以红黑树的形式存放,一个便于
遍历,一个便于查找
u64 vmacache_seqnum; /* per-thread vmacache */
#ifdef CONFIG_MMU
unsigned long (*get_unmapped_area) (struct file *filp, //用来在进程地址空间中搜索有效的进程地址空间的函数
unsigned long addr, unsigned long len,
unsigned long pgoff, unsigned long flags);
#endif
unsigned long mmap_base; //标识第一个分配文件内存映射的线性地址
unsigned long mmap_legacy_base; /* base of mmap area in bottom-up allocations */
#ifdef CONFIG_HAVE_ARCH_COMPAT_MMAP_BASES
/* Base adresses for compatible mmap() */
unsigned long mmap_compat_base;
unsigned long mmap_compat_legacy_base;
#endif
unsigned long task_size; /* size of task vm space */
unsigned long highest_vm_end; /* highest vma end address */
pgd_t * pgd; //页全局目录
#ifdef CONFIG_MEMBARRIER
atomic_t membarrier_state;
#endif
atomic_t mm_users; //使用地址空间的进程数目,如两个线程共享该地址空间,值便为2
atomic_t mm_count; //主使用区域计数器,无论mm_users为1或9,mm_count都为1,是mm_struct的引用计数,当mm_users为0
时,mm_count才变为0,撤销该结构体
#ifdef CONFIG_MMU
atomic_long_t pgtables_bytes; /* PTE page table pages */
#endif
int map_count; //内存区域的个数
spinlock_t page_table_lock; //页表锁
struct rw_semaphore mmap_sem; //内存区域的信号量
struct list_head mmlist; //所有进程的mm_struct形成的链表,该链表首元素是init_mm,代表init进程的地址空间,操作该链表时需要用
mmlist_lock锁来防止并发访问,锁定义在fock.c中
unsigned long hiwater_rss; //进程拥有的最大页表数目
unsigned long hiwater_vm; /* High-water virtual memory usage */
unsigned long total_vm; /* Total pages mapped */
unsigned long locked_vm; /* Pages that have PG_mlocked set */
atomic64_t pinned_vm; /* Refcount permanently increased */
unsigned long data_vm; /* VM_WRITE & ~VM_SHARED & ~VM_STACK */
unsigned long exec_vm; /* VM_EXEC & ~VM_WRITE & ~VM_STACK */
unsigned long stack_vm; /* VM_STACK */
unsigned long def_flags;
spinlock_t arg_lock; /* protect the below fields */
unsigned long start_code, end_code, start_data, end_data; //代码段的起止地址以及数据段的起止地址
unsigned long start_brk, brk, start_stack; //堆的起止地址以及进程栈的首地址
unsigned long arg_start, arg_end, env_start, env_end; //命令行参数以及环境变量的起止地址
unsigned long saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv */
struct mm_rss_stat rss_stat;
struct linux_binfmt *binfmt;
mm_context_t context;
unsigned long flags; /* Must use atomic bitops to access */
struct core_state *core_state; /* coredumping support */
#ifdef CONFIG_MEMCG
struct task_struct __rcu *owner;
#endif
struct user_namespace *user_ns;
/* store ref to file /proc/<pid>/exe symlink points to */
struct file __rcu *exe_file;
#ifdef CONFIG_MMU_NOTIFIER
struct mmu_notifier_mm *mmu_notifier_mm;
#endif
#if defined(CONFIG_TRANSPARENT_HUGEPAGE) && !USE_SPLIT_PMD_PTLOCKS
pgtable_t pmd_huge_pte; /* protected by page_table_lock */
#endif
atomic_t tlb_flush_pending;
struct uprobes_state uprobes_state;
#ifdef CONFIG_HUGETLB_PAGE
atomic_long_t hugetlb_usage;
#endif
struct work_struct async_put_work;
} __randomize_layout;
unsigned long cpu_bitmap[];
};
分配进程描述符:
task_struct.mm存放着该进程的内存描述符
-->copy_process
-->copy_mm //复制父进程的内存描述符给子进程,若指定CLONE_VM,则父子进程共享地址空间,不分配mm_struct
-->dup_mm
-->allocate_mm
-->kmem_cache_alloc //从mm_cachep这个缓存的slab中分配内存描述符内存空间
#define allocate_mm() (kmem_cache_alloc(mm_cachep, GFP_KERNEL))
exit_mm //一旦有进程退出就调用这个函数
-->mmput //减少mm_users计数
-->__mmput
-->mmdrop //一旦mm_users减少为0,就减少mm_count计数
-->__mmdrop
-->free_mm
-->kmem_cache_free //一旦mm_count减少为0,就将mm_struct释放回mm_cachep这个缓存的slab中
内核线程地址空间:
内核线程没有进程地址空间,也没有对应的内存描述符,内核线程对应的task_struct.mm为空,此时内核线程就会保留前一个内核线程的地址空间,并更新task_struct.active_mm指向前一个进程的内存
描述符,而在不访问用户空间的情况下,内存描述符中和内核内存相关的部分完全相同
虚拟内存区域:
内存区域由vm_area_struct表示,也被称为虚拟内存区域
struct vm_area_struct:虚存管理的最基本的管理单元,它描述的是一段连续的、具有相同访问属性的虚存空间,该虚存空间的大小为物理内存页面的整数倍。每一个VMA就可以代表不同类型的内存
区域(比如bss或者进程用户空间栈)
struct vm_area_struct {
/* The first cache line has the info for VMA tree walking. */
unsigned long vm_start; //区间的首地址
unsigned long vm_end; //区间的尾地址之后第一个字节
struct vm_area_struct *vm_next, *vm_prev; //VMA双向链表
struct rb_node vm_rb; //树上该VMA的节点
unsigned long rb_subtree_gap;
struct mm_struct *vm_mm; //所属的内存地址空间,即相关的mm_struct
pgprot_t vm_page_prot; //访问控制权限
unsigned long vm_flags; //标志
struct {
struct rb_node rb;
unsigned long rb_subtree_last;
} shared;
struct list_head anon_vma_chain; /* Serialized by mmap_sem &
* page_table_lock */
struct anon_vma *anon_vma; //匿名VMA对象
const struct vm_operations_struct *vm_ops; //相关操作表
/* Information about our backing store: */
unsigned long vm_pgoff; //文件在页中的偏移量
struct file * vm_file; //被映射的文件(没有时可为NULL)
void * vm_private_data; //私有数据
}
vm_flags:通过组合构成进程不同区域的访问权限或行为
#define VM_NONE 0x00000000
#define VM_READ 0x00000001 //页面可读取
#define VM_WRITE 0x00000002 //页面可写
#define VM_EXEC 0x00000004 //页面可执行
#define VM_SHARED 0x00000008 //页面可共享,指明此映射是否可以在多进程间共享;多进程共享的为共享映射,只有一个进程使用的为私有映射
/* mprotect() hardcodes VM_MAYREAD >> 4 == VM_READ, and so for r/w/x bits. */
#define VM_MAYREAD 0x00000010 //VM_READ标志可被设置
#define VM_MAYWRITE 0x00000020 //VM_WRITE标志可被设置
#define VM_MAYEXEC 0x00000040 //VM_EXEC标志可被设置
#define VM_MAYSHARE 0x00000080 //VM_SHARED标志可被设置
#define VM_GROWSDOWN 0x00000100 //区域可向下增长
#define VM_UFFD_MISSING 0x00000200 /* missing pages tracking */
#define VM_PFNMAP 0x00000400 /* Page-ranges managed without "struct page", just pure PFN */
#define VM_DENYWRITE 0x00000800 //区域映射一个不可写文件
#define VM_UFFD_WP 0x00001000 /* wrprotect pages tracking */
#define VM_LOCKED 0x00002000 //区域中的页面被锁定
#define VM_IO 0x00004000 //区域包含对设备I/O空间的映射,通常在驱动程序中通过mmap函数进行I/O空间映射时才被设置,同时也表示该区
域不能被包含在任何进程的存放转存中(core dump)
以下两个标志可通过madvise设置。文件预读是指在读数据时有意的按顺序多读取一些本次请求以外的数据,希望多读的数据能很快被用到。预读对顺序读取数据的应用程序有好处,但对随机访问数据是多余的
#define VM_SEQ_READ 0x00008000 //页面可能会被连续访问,暗示内核应用程序对映射内容执行有序的(线性和连续的)读操作
#define VM_RAND_READ 0x00010000 //页面可能会被随机访问,暗示对映射内容执行随机读操作,减少文件预读
#define VM_DONTCOPY 0x00020000 //在fork时不拷贝该区域
#define VM_DONTEXPAND 0x00040000 //区域不能通过mremap增加
#define VM_LOCKONFAULT 0x00080000 /* Lock the pages covered when they are faulted in */
#define VM_ACCOUNT 0x00100000 //该区域是一个记账VM对象
#define VM_NORESERVE 0x00200000 /* should the VM suppress accounting */
#define VM_HUGETLB 0x00400000 //区域使用hugetlb页面
#define VM_SYNC 0x00800000 /* Synchronous page faults */
#define VM_ARCH_1 0x01000000 /* Architecture-specific flag */
#define VM_WIPEONFORK 0x02000000 /* Wipe VMA contents in child. */
#define VM_DONTDUMP 0x04000000 /* Do not include in the core dump */
vm_ops:vm_operations_struct类型的数据,用于操作VMA
struct vm_operations_struct {
void (*open)(struct vm_area_struct * area); //当指定的内存区域被加入到一个地址空间时,该函数被调用
void (*close)(struct vm_area_struct * area); //当指定的内存区域被从一个地址空间删除时,该函数被调用
int (*split)(struct vm_area_struct * area, unsigned long addr);
int (*mremap)(struct vm_area_struct * area);
vm_fault_t (*fault)(struct vm_fault *vmf); //当没有出现在物理内存中的页面被访问时,该函数被页面故障处理调用
vm_fault_t (*huge_fault)(struct vm_fault *vmf,
enum page_entry_size pe_size);
void (*map_pages)(struct vm_fault *vmf,
pgoff_t start_pgoff, pgoff_t end_pgoff);
unsigned long (*pagesize)(struct vm_area_struct * area);
vm_fault_t (*page_mkwrite)(struct vm_fault *vmf); //当某个页面为只读页面时,该函数被页面故障处理调用
/* same as page_mkwrite when using VM_PFNMAP|VM_MIXEDMAP */
vm_fault_t (*pfn_mkwrite)(struct vm_fault *vmf);
int (*access)(struct vm_area_struct *vma, unsigned long addr, //当get_user_pages调用失败时,access_process_vm会调用此函数
void *buf, int len, int write);
const char *(*name)(struct vm_area_struct *vma);
#ifdef CONFIG_NUMA
int (*set_policy)(struct vm_area_struct *vma, struct mempolicy *new);
struct mempolicy *(*get_policy)(struct vm_area_struct *vma,
unsigned long addr);
#endif
struct page *(*find_special_page)(struct vm_area_struct *vma,
unsigned long addr);
}
实际使用的内存区域:
pmap -xx 进程号或者cat /proc/jinchenghao/maps查看给定进程的内存空间和其中所含的内存区域
对于共享和不可写内存区域,内核只需在内存中为其保留一份映射即可,而不必为每个使用到它们的进程分配一份映射
所以一个进程访问了比较大的数据和代码空间时,其自身消耗的内存空间却比较小
如果将零页映射到可写的内存区域,该区域将全部初始化为0。
操作内存区域:
struct vm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr) //为了找到给定的内存地址属于哪一个内存区域,在vma_cache,检查到的话为上一次操作缓存的vma,会节约时
间,如果无法找到,就去查找红黑树
struct vm_area_struct * //返回第一个小于addr的VMA
find_vma_prev(struct mm_struct *mm, unsigned long addr,
struct vm_area_struct **pprev)
static inline struct vm_area_struct * //返回第一个和指定地址区间相交的VMA
find_vma_intersection(struct mm_struct * mm, unsigned long start_addr,
unsigned long end_addr)
find_vma_intersection
-->find_vma
创建地址区间:
do_mmap:系统调用
创建一个新的线性地主之区间,即创建一个新的VMA,但是当创建的地址区间和一个已经存在的地址区间相邻,并且具有相同的访问权限时,两个区间将会合并为一个
unsigned long do_mmap(struct file *file, unsigned long addr,
unsigned long len, unsigned long prot,
unsigned long flags, vm_flags_t vm_flags,
unsigned long pgoff, unsigned long *populate,
struct list_head *uf)
如果file为空,称为匿名映射;如果指定了file,则将文件映射到对应的内存区间 prot指定内存区间中页面的访问权限:读写执行等 flag指定VMA标志 pgoff是页面偏移量
内核do_mmap分配虚拟内存区间的时候,从vm_area_cachep缓存中分配一个vm_area_struct,使用vma_link将新分配的内存区域添加到地址空间的链表和红黑树中,随后更新mm_struct中的total_vm
mmap:
mmap是用户空间的接口,在C库中,内核没有对应的接口了,mmap的字节偏移会转换为页面偏移,转而调用mmap2
munmap和do_munmap:
do_mummap:从特定的进程地址空间删除指定地址区间
int do_munmap(struct mm_struct *mm, unsigned long start, size_t len,
struct list_head *uf)
页表:
当应用程序访问一个虚拟地址时,必须将虚拟地址转化成物理地址,处理器才能解析地址访问请求,而地址的转换工作是通过查询页表完成的
地址转换需要将虚拟地址分段,每段虚拟地址作为一个索引指向页表,页表项则指向下一级别的页表或者指向最终的物理页面,页表搜索一般由硬件完成
三级页表:
页全局目录:PGD,包含一个pgd_t数组
中间页目录:PMD,是pmd_t数组
页表:包含pte_t数组,指向物理页面
mm_struct-->pgd_t-->pmd_t-->pte_t-->物理页 tlb:翻译后缓冲器,将虚拟地址映射到物理地址的硬件缓存,翻译地址时首先检查这个缓冲器
页高速缓存和页回写:
页高速缓存:linux内核实现的磁盘缓存,用来减少对磁盘的I/O操作。就是通过把磁盘中的数据缓存到物理内存,把对磁盘的访问变成对物理内存的访问
页回写:将高速缓存中的变更数据刷新回磁盘的操作
磁盘高速缓存的使用原因:
1.访问磁盘的速度要远低于访问内存的速度(提速)
2.数据一旦被访问,很可能短期内再次被访问。这种短期内集中访问同一片数据的原理称作临时局部原理。(减少访问次数)
缓存手段:
页高速缓存大小能动态调整,既可以占用空闲内存扩张大小,也可以自我收缩缓解内存使用压力。正被缓存的存储设备称为后备存储
读取数据的时候会优先检查页高速缓存,如果查到称为缓存命中,当缓存未命中时,需要去访问磁盘获取数据。读取之后数据会被放入缓存,下次可直接从缓存读取数据
写缓存:三种策略
1.不缓存,写数据时跳过缓存,直接写回磁盘,同时使缓存中的数据失效,很少使用
2.写透缓存,自动更新内存缓存,同时也更新磁盘文件。对保持缓存一致性,即缓存数据时刻和后备缓存同步有好处,同时不需要失效缓存,实现简单
3.回写,写到缓存中,后备存储不会立刻更新。页高速缓存中被写入过的页面被标记成脏,并被加入到脏链表中。然后由一个进程(回写进程)周期性的写回磁盘,最后清理脏页标识。
注意:这里脏是指磁盘中的数据(过时了)而非页高速缓存中的数据
缓存回收:清除数据或收缩大小
缓存回收策略:通过选择缓存中干净的页进行简单替换,如果没有足够的干净页面,内核将强制进行回写操作。
该回收哪些缓存页是一个问题,理想的回收策略是回收那些以后最不可能使用的页面
1.最近最少使用。通过所访问的数据特性,尽量追求预测效率。
(特别是对于通用目的的页高速缓存)最成功的是最近最少算法,简称LRU。通过跟踪每个页面的访问踪迹(或者按访问时间为序的链表),回收最老时间戳的页面,对于只访问一次的数据不太理想
2.双链策略。 linux使用的是修改过的LRU策略,维护两个链表:活跃链表和非活跃链表
活跃链表:其上的页面被认为是"热"的且不会被换出
非活跃链表:其上的页面可以被换出
两个链表会维持平衡,更常见的是n个链表,称为LRU/n
页高速缓存:页高速缓存包含了最近被访问过的文件的数据块
页高速缓存中存在多个不连续的物理磁盘块,查找缓存数据不太容易
address_space:管理缓存项和页I/O操作,是vm_area_struct的物理对等体。当一个文件有10个vm_area_struct内存映射,也就是10个虚拟地址,但是只会有一个address_space,即一个物理内存
address_space的名字其实不太对,叫page_cache_entity或许更好
struct address_space {
struct inode *host; //拥有的索引节点
struct xarray i_pages; //包含全部页面的?
gfp_t gfp_mask;
atomic_t i_mmap_writable; //VM_SHARED计数
#ifdef CONFIG_READ_ONLY_THP_FOR_FS
/* number of thp, only for non-shmem files */
atomic_t nr_thps;
#endif
struct rb_root_cached i_mmap; //优先搜索树,帮助内核高效的找到被缓存文件
struct rw_semaphore i_mmap_rwsem;
unsigned long nrpages; //页总数
unsigned long nrexceptional;
pgoff_t writeback_index; //回写的起始偏移
const struct address_space_operations *a_ops; //页高速缓存操作表,指向哪些为指定缓存对象实现的页IO操作。每个后备存储(或者说每个文件系统类型?)通过自己
的address_space_operations描述自己如何与页高速缓存交互
unsigned long flags; //gfp_mask掩码与错误标识
errseq_t wb_err;
spinlock_t private_lock; //私有address_space锁
struct list_head private_list; //私有address_space链表
void *private_data;
}
struct address_space_operations {
int (*writepage)(struct page *page, struct writeback_control *wbc);
int (*readpage)(struct file *, struct page *);
int (*writepages)(struct address_space *, struct writeback_control *);
int (*set_page_dirty)(struct page *page);
int (*readpages)(struct file *filp, struct address_space *mapping,
struct list_head *pages, unsigned nr_pages);
int (*write_begin)(struct file *, struct address_space *mapping,
loff_t pos, unsigned len, unsigned flags,
struct page **pagep, void **fsdata);
int (*write_end)(struct file *, struct address_space *mapping,
loff_t pos, unsigned len, unsigned copied,
struct page *page, void *fsdata);
sector_t (*bmap)(struct address_space *, sector_t);
void (*invalidatepage) (struct page *, unsigned int, unsigned int);
int (*releasepage) (struct page *, gfp_t);
void (*freepage)(struct page *);
ssize_t (*direct_IO)(struct kiocb *, struct iov_iter *iter);
int (*migratepage) (struct address_space *,
struct page *, struct page *, enum migrate_mode);
bool (*isolate_page)(struct page *, isolate_mode_t);
void (*putback_page)(struct page *);
int (*launder_page) (struct page *);
int (*is_partially_uptodate) (struct page *, unsigned long,
unsigned long);
void (*is_dirty_writeback) (struct page *, bool *, bool *);
int (*error_remove_page)(struct address_space *, struct page *);
int (*swap_activate)(struct swap_info_struct *sis, struct file *file,
sector_t *span);
void (*swap_deactivate)(struct file *file);
}
最重要的是read_page和writepage
基树:缓存的搜索是组织成树进行的,提高效率
flusher:此进程用于将页高速缓存的数据写回磁盘,多个flusher相互独立的将不同设备的回写队列分别写回,避免了单个flusher处理由于某个磁盘存储设备的回写任务重导致
其它存储设备的回写被阻塞
回写的时机:
1.当空闲内存低于一个特定阈值,这时需要回收内存,但是只有干净的内存才能回收
2.当脏页在内存中驻留超过一个特定时间,需要回写以避免无限期驻留在内存中
3.当用户进程调用sync和fsync时
膝上计算机模式:特殊的页回写策略,将硬盘转动的行为最小化,允许硬盘尽可能长时间的节电,以延长电池的供电时间
通过/proc/sys/vm/laptop_mode设置,为0关闭膝上计算机模式,使用常规页回写策略,为1则开启膝上计算机模式,关闭磁盘驱动器是重要的节电手段
优点:提升了计算机的续航
缺点:系统崩溃或出问题时使得未写回的数据更易丢失
设备与模块:
块设备:块大小随设备不同而不同,可以块为单位进行寻址,支持重定位(seeking),即对数据的随机访问,通常被挂载为文件系统
字符设备:不可寻址,字符的流式访问
网络设备:最常见为以太网设备,物理适配器加相应的协议,打破了所有一切都是文件的原则,通过套接字API接口来访问的
杂项设备:miscdev,实际是个简化的字符设备,能很容易的表示一个简单设备,实际是对通用基本框架的一种折中
伪设备:不表示物理设备,是虚拟的,仅提供访问内核功能,称为伪设备(pseudo device),如:
/dev/random、/dev/urandom(内核随机数发生器)
/dev/zero(零设备)
/dev/null(空设备)
/dev/full(满设备)
/dev/mem(内存设备)
模块:将功能做成单个模块,动态插入,能使内核模块尽可能小,按需添加模块,随时热插拔方便调试
驱动:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>
static int hello_open(struct inode *inode, struct file *file)
{
printk("hello word!\n");
return 0;
}
static struct file_operations hello_drv_fops = {
.owner = THIS_MODULE, /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
.open = hello_open,
};
/*把上述的结构体告诉内核,所以要注册*/
static int __init hello_init(void)
{
register_chrdev(99,"hello_dev",&hello_drv_fops);/*参数依次为主设备号、设备名称、结构体*/
return 0;
}
static void __exit hello_exit(void) //如果模块被编译到内核,退出函数就不会被包含
{
unregister_chrdev(99,"hello_dev");/*参数依次为主设备号、设备名称*/
return 0;
}
/*修饰*/
module_init(hello_init);
module_exit(hello_exit);
/*许可证*/
MODULE_LICENSE("GPL"); //用于指定版权,载入非GPL模块,会在内核中设置被污染标识
1.使得开发者对oops中设置了被污染标识的bug报告缺乏信任,认为无法调试它
2.非GPL模块不能调用GPL_only符号
makefile:
KERN_DIR = /work/system/linux-2.6.22.6
all:
make -C $(KERN_DIR) M=`pwd` modules
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
obj-m += arm_hello.o
测试程序:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
int main(int argc, char **argv)
{
int fd;
int val = 1;
fd = open("/dev/xyz", O_RDWR);//根据自己的设备名称
if (fd < 0)
{
printf("can't open!\n");
}
return 0;
}
构建模块:
1.放在内核源码树中
在对应的驱动程序文件夹上级目录makefile中添加路径 obj-m += xxx/
想控制编译时:obj-$(CONFIG_XXX_XX) += xxx/
在xxx的目录下添加makefile,其中加入 obj-m += xxxxx.o
或者obj-$(CONFIG_XXX_XX) += xxxxx.o
在多个文件时:xxxxx-objs := ccc.o yyy.o
2.内核代码外
见上makefile
安装模块:
make modules_install
模块依赖性:
模块之间存在依赖性,加载的时候有先后顺序
depmod
depmod -A 只生成新模块的依赖
载入模块:
insmod module.ko
rmmod module
modprobe:插入模块依赖自动分析,自动加载任何它需要依赖的模块
modprobe module [ module parameters ]
modprobe -r modules
kbuild系统:
在本级kconfig中加入配置:
config (CONFIG_)XXX_XX //注意CONFIG_前缀不需要写上
tristate "shfkjshf sdf dfs" //tristate代表三态,Y:编进内核 M:模块编译 N:不编译 如果是系统功能而非模块:bool 使用Y或者N,引号是在菜单显示的字符串名字
default n //默认不编译进内核
deponds on FISH_EAT //假设有此项,即CONFIG_FISH_EAT被选择前,此选项不能使用
select BAIT //表示当CONFIG_XXX_XX被激活,即CONFIG_FISH_EAT也会被激活
help //帮助信息
djaijoiajfjafpafpap efafafafaf
在上级char的kconfig中引入本级kconfig:
source "drivers/char/flshing/kconfig"
deponds on FISH_EAT && !NO_SMOKING //多条件依赖
使用if配合tristate或者bool可以配合使用显示或不显示某些选项
CONFIG_EXPERIMENTAL:表示是测试功能,默认不开启
模块参数:
module_param(name, type, perm)
mm_struct数据结构中的pgd_t * pgd
mm_init ()->mm_alloc_pgd(struct mm_struct *mm) ->pgd_alloc(structmm_struct *mm) ->#define __pgd_alloc() (pgd_t*)__get_free_pages(GFP_KERNEL, 2),分配的空间为16k, 即4096*4bytes,从此处分配的页表空间包括了用户空间页表和内核空间页表