其他
工程师深度:学通Linux内核(含详细代码)
内核探索工具类
关于内核方面内容也可参见:
《深度:关于Linux内核最硬核的文章》
《解读:一文看懂Linux内核》
内核中常见的数据类型有链表、查找、树等等。链表可以说贯穿整个Linux内核,在Linux中,链表才常常以循环双向链表的形式出现,所以给定链表中的任一结点,均可以找到下一结点和前一结点,链表是个很重要的知识点,需要学学数据结构才能掌握,这里不多阐释,有关链表定义的所有代码可以在头文件include/linux/list.h中查看。至于查找算法,主要表现在大O表示法,它代表对于一个定值n在最坏情况下所花费的查找时间,另外就是数,数被用在Linux的内存管理中,能够有效访问并操作数据,前几天恶补了一下树的相关知识以及代码,收获颇丰,树在Linux中通常以链表和数组的形式实现,其中二叉树和红黑树在Linux中使用最多,这里提出来有助大家理解Linux的内核,这样可以更好的理解复杂的操作。
asmlinkage
asmlinkage要使用局部堆栈来传递参数,这就涉及到了宏FASTCALL,它通知汇编程序使用通用寄存器来传递参数,下面举一个asmlinkage的例子:
asmlinkage long sys_gettimeofday(struct timeval *tv, struct timezone __user *tz)
UL
UL其实就是unsigned long啦,它告诉编译程序将这个数值当作long型数值处理,使用UL有助于编写出和体系结构无关的代码,内核中有些这样的例子,例如include/linux.h中
#define ULONG_MAX (~0UL)
inline 表明要优化 函数的可执行代码,Linux内核中时用的 inline函数大多被定义为static类型(static inline),,这样的申明意味着直接将它加入调用程序中,优点是可以免除函数调用的任何开销,不足之处在于使用inline会增加二进制映象的大小,因此会降低访问CPU高速缓存的速度,所以不能始终使用inline。
const和volatile
这两个真的很容易混淆并且不是很好懂,const不一定只代表常数有时会是“只读”的意思,这里提问一下“const int *x”和“int const * x”有什么区别??一下是一个const的例子
static inline void prefetch(const void *x)
{
__asm__ __volatile__ ("dcbt 0,%0" : : "r" (x));
}
关键字volatile表明变量无需警告就可以被修改,它通知编译程序每次使用该变量时候都要重新加载其值,一下是include/linux/spinlock.h关于如何使用volatile的一个例子
typedef struct {
...
volatile unsigned int lock;
...
} spinlock_t
那么内核在运行当中有什么内部的奥秘呢,接下来介绍内核中常用的探究各种内核文件的工具。
objdump/readelf
objdump/readelf可以分别用于显示目标文件和ELF文件的任何信息,可以在终端上借助命令行参数使用命令来查看给定的目标文件的头文件、文件大小和结构,
hexdump
hexdump可以显示给定的十六进制/ASCII/八进制文件的内容。这些需要一些命令来举例,大家可以在实践的 时候自己去掌握如何使用hexdump这个命令
nm
nm可以列出指定目标文件的符号,能够显示符号的值,类型和名字。
objcopy
objcopy,看半个单词就知道是复制的意思,当你想复制一个目标文件而忽略或改变其某方面的内容时候,就可以使用该命令。
ar
ar命令最常用在Make文件中,它将一些常用的函数连接到单个库文件中,也可以将单个程序库中的目标文件分离出来,有助于维护链接程序时用的索引库函数库。
printk()
大家都知道printf()函数,那么为什么Linux为什么不使用呢,这是因为内核中没有链接标准C函数库,但是两者的接口是一样的 ,printk是在内核中运行的向控制台输出显示的函数,Linux内核首先在内核空间分配一个静态缓冲区,作为显示用的空间,然后调用sprintf,格式化显示字符串,最后调用tty_write向终端进行信息的显示。
dmesg
dmesg内核有多种方式可以用在存储日志和信息上,dmesg是一种程序,用于检测和控制内核环缓冲,可以用于显示存储在/proc/kmsg中的缓冲内容,并能够根据消息级别来选择是否过滤这个缓冲区,并且这个程序用来帮助用户了解系统的启动信息。
/var/log/messages
Linux系统的 /var/log/messages下存储的大都是已经登录系统的消息。messages 日志是核心系统日志文件。它包含了系统启动时的引导消息,以及系统运行时的其他状态消息。IO 错误、网络错误和其他系统错误都会记录到这个文件中。其他信息,比如某个人的身份切换为 root,也在这里列出。如果服务正在运行,比如 DHCP 服务器,您可以在 messages 文件中观察它的活动。通常,/var/log/messages 是您在做故障诊断时首先要查看的文件。
在探索Linux内核之前应该具备的基础知识和背景,对上述讲到的数据结构的基本了解有助于理解以后会讲到的进程和分页机制,关于内核、的工具集也是粗了点讲解了一下,但是需要在实际操作中多运用才能体会才能更加深刻的了解,有哪些问题希望大家指正。
进程
首先我们要明确一个概念,我们说的程序是指由一组函数组成的可执行文件,而进程则是特定程序的个体化实例,进程是对硬件所提供资源进行操作的基本单位。在我们继续讨论进程之前,得明白一个几个命名习惯,通常说的“任务“和”进程“就是一回事。
事实上,进程都有一个生命周期,进程从创建过后会经历各种状态后死亡,下面的例子帮助大家理解一下程序是如何实例化进程的。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcnt1.h>
int main(int argc, char *argv[])
{
int fd;
int pid;
pid = fork();
if(pid == 0)
{
execle("/bin/ls", NULL);
exit(2);
}
if(waitpid(pid) <0 )
printf("wait error\n");
pid = fork();
if(pid == 0)
{
fd = open("Chapter_2.txt",O_RDONLY);
close(fd);
}
if(waitpid(pid)<0)
printf("wait error\n");
exit(0);
}
creat_process
一个进程包括了很多属性,使进程彼此互不相同,在内核中,进程描述符是一个task_struct的结构体,用来保存进程的属性和相关信息,内核使用循环双向链表task_list存放所有进程描述符,同时借助全局变量current保存当前运行进程的task_struct。至于task_struct的定义大家可以参见include/Linux/sched.h这里我讲不了辣么多,不过我得说明一下进程和线程的区别,进程由一个或者多个线程组成,每个线程对应一个task_struct,其中包含一个唯一的线程ID。线程作为调度和分配的基本单位,而进程作为拥有资源的基本单位;不仅进程之间可以并发执行,同一个进程的多个线程之间也可以并发执行;进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源。
进程描述符(task_struct)某些字段含义,这里有太多的与进程相关的域,我罗列一些如下,,假设进程为P。
state:P进程状态,用set_task_state和set_current_state宏更改之,或直接赋值。 thread_info:指向thread_info结构的指针。 run_list:假设P状态为TASK_RUNNING,优先级为k,run_list将P连接到优先级为k的可运行进程链表中。 tasks:将P连接到进程链表中。 ptrace_children:链表头,链表中的所有元素是被调试器程序跟踪的P的子进程。 ptrace_list:P被调试时,链表中的所有元素是被调试器程序跟踪的P的子进程。 pid:P进程标识(PID)。 tgid:P所在的线程组的领头进程的PID。 real_parent:P的真实的父进程的进程描述符指针。 parent:P的父进程的进程描述符指针,当被调试时就是调试器进程的描述符指针。 children:P的子进程链表。 sibling:将P连接到P的兄弟进程链表。 group_leader:P所在的线程组的领头进程的描述符指针。
这三个系统最终都调用了do_fork()函数,do_fork()是内核函数,它完成与进程创建有关的大部分工作,下面 我来粗略介绍一下fork()、vfork()、clone()函数。
fork()函数
fork()函数返回两次,一次是子进程,返回值为0;一次是父进程,将返回子进程的PID,
vfork()函数
和fork()函数类似,但是前者的父进程一直阻塞,直到子进程调用exit()或exec()后。
clone()函数
clone()函数接受一个指向函数的指针和该函数的参数,由do_fork()创建的子进程一诞生就调用这个库函数。
三者的唯一区别,在最终调用do_fork()函数设置的那些标志不一样,如下表。
fork.c
int do_fork(unsigned long clone_flags,unsigned long stack_start, struct pt_regs *regs,
unsigned long stack_size)
{
int retval;
struct task_struct *p;
struct completion vfork;
retval = -EPERM ;
if ( clone_flags & CLONE_PID )
{
if ( current->pid )
goto fork_out;
}
reval = -ENOMEM ;
p = alloc_task_struct(); // 分配内存建立新进程的 task_struct 结构
if ( !p )
goto fork_out;
*p = *current ; //将当前进程的 task_struct 结构的内容复制给新进程的 PCB结构
retval = -EAGAIN;
//下面代码对父、子进程 task_struct 结构中不同值的数据成员进行赋值
if ( atomic_read ( &p->user->processes ) >= p->rlim[RLIMIT_NPROC].rlim_cur
&& !capable( CAP_SYS_ADMIN ) && !capable( CAP_SYS_RESOURCE ))
goto bad_fork_free;
atomic_inc ( &p->user->__count); //count 计数器加 1
atomic_inc ( &p->user->processes); //进程数加 1
if ( nr_threads >= max_threads )
goto bad_fork_cleanup_count ;
get_exec_domain( p->exec_domain );
if ( p->binfmt && p->binfmt->module )
__MOD_INC_USE_COUNT( p->binfmt->module ); //可执行文件 binfmt 结构共享计数 + 1
p->did_exec = 0 ; //进程未执行
p->swappable = 0 ; //进程不可换出
p->state = TASK_UNINTERRUPTIBLE ; //置进程状态
copy_flags( clone_flags,p ); //拷贝进程标志位
p->pid = get_pid( clone_flags ); //为新进程分配进程标志号
p->run_list.next = NULL ;
p->run_list.prev = NULL ;
p->run_list.cptr = NULL ;
init_waitqueue_head( &p->wait_childexit ); //初始化 wait_childexit 队列
p->vfork_done = NULL ;
if ( clone_flags & CLONE_VFORK ) {
p->vfork_done = &vfork ;
init_completion(&vfork) ;
}
spin_lock_init( &p->alloc_lock );
p->sigpending = 0 ;
init_sigpending( &p->pending );
p->it_real_value = p->it_virt_value = p->it_prof_value = 0 ; //初始化时间数据成员
p->it_real_incr = p->it_virt_incr = p->it_prof_incr = 0 ; //初始化定时器结构
init_timer( &p->real_timer );
p->real_timer.data = (unsigned long)p;
p->leader = 0 ;
p->tty_old_pgrp = 0 ;
p->times.tms_utime = p->times.tms_stime = 0 ; //初始化进程的各种运行时间
p->times.tms_cutime = p->times.tms_cstime = 0 ;
#ifdef CONFIG_SMP //初始化对称处理器成员
{
int i;
p->cpus_runnable = ~0UL;
p->processor = current->processor ;
for( i = 0 ; i < smp_num_cpus ; i++ )
p->per_cpu_utime[ i ] = p->per_cpu_stime[ i ] = 0;
spin_lock_init ( &p->sigmask_lock );
}
#endif
p->lock_depth = -1 ; // 注意:这里 -1 代表 no ,表示在上下文切换时,内核不上锁
p->start_time = jiffies ; // 设置进程的起始时间
INIT_LIST_HEAD ( &p->local_pages );
retval = -ENOMEM ;
if ( copy_files ( clone_flags , p )) //拷贝父进程的 files 指针,共享父进程已打开的文件
goto bad_fork_cleanup ;
if ( copy_fs ( clone_flags , p )) //拷贝父进程的 fs 指针,共享父进程文件系统
goto bad_fork_cleanup_files ;
if ( copy_sighand ( clone_flags , p )) //子进程共享父进程的信号处理函数指针
goto bad_fork_cleanup_fs ;
if ( copy_mm ( clone_flags , p ))
goto bad_fork_cleanup_mm ; //拷贝父进程的 mm 信息,共享存储管理信息
retval = copy_thread( 0 , clone_flags , stack_start, stack_size , p regs );
//初始化 TSS、LDT以及GDT项
if ( retval )
goto bad_fork_cleanup_mm ;
p->semundo = NULL ; //初始化信号量成员
p->prent_exec_id = p-self_exec_id ;
p->swappable = 1 ; //进程占用的内存页面可换出
p->exit_signal = clone_flag & CSIGNAL ;
p->pdeatch_signal = 0 ; //注意:这里是父进程消亡后发送的信号
p->counter = (current->counter + 1) >> 1 ;//进程动态优先级,这里设置成父进程的一半,应注意的是,这里是采用位操作来实现的。
current->counter >> =1;
if ( !current->counter )
current->need_resched = 1 ; //置位重新调度标记,实际上从这个地方开始,分裂成了父子两个进程。
retval = p->pid ;
p->tpid = retval ;
INIT_LIST_HEAD( &p->thread_group );
write_lock_irq( &tasklist_lock );
p->p_opptr = current->p_opptr ;
p->p_pptr = current->p_pptr ;
if ( !( clone_flags & (CLONE_PARENT | CLONE_THREAD ))) {
p->opptr = current ;
if ( !(p->ptrace & PT_PTRACED) )
p->p_pptr = current ;
}
if ( clone_flags & CLONE_THREAD ){
p->tpid = current->tpid ;
list_add ( &p->thread_group,¤t->thread_group );
}
SET_LINKS(p);
hash_pid(p);
nr_threads++;
write_unlock_irq( &tasklist_lock );
if ( p->ptrace & PT_PTRACED )
send_sig( SIGSTOP , p ,1 );
wake_up_process(p); //把新进程加入运行队列,并启动调度程序重新调度,使新进程获得运行机会
++total_forks ;
if ( clone_flags & CLONE_VFRK )
wait_for_completion(&vfork);
//以下是出错处理部分
fork_out:
return retval;
bad_fork_cleanup_mm:
exit_mm(p);
bad_fork_cleanup_sighand:
exit_sighand(p);
bad_fork_cleanup_fs:
exit_fs(p);
bad_fork_cleanup_files:
exit_files(p);
bad_fork_cleanup:
put_exec_domain( p->exec_domain );
if ( p->binfmt && p->binfmt->module )
__MOD_DEC_USE_COUNT( p->binfmt->module );
bad_fork_cleanup_count:
atomic_dec( &p->user->processes );
free_uid ( p->user );
bad_fork_free:
free_task_struct(p);
goto fork_out;
}
fork
Linux中的进程有7种状态,进程的task_struct结构的state字段指明了该进程的状态。下图形象的形容了各个状态之间的转换,这里不多加阐释,大家看图体会。
可运行状态(TASK_RUNNING) 可中断的等待(TASK_INTERRUPTIBLE) 不可中断的等待(TASK_UNINTERRUPTIBLE) 暂停状态(TASK_STOPPED) 跟踪状态(TASK_TRACED):进程被调试器暂停或监视。 僵死状态(EXIT_ZOMBIE):进程被终止,但父进程未调用wait类系统调用。 僵死撤销状态(TASK_DEAD):父进程发起wait类系统调用,进程由系统删除。
至于进程的终止,上文已经提到过了exit()函数,进程终止有三种方式:明确而自愿的终止,隐含但也是自愿终止,自然而然的运行终止,这些可以通过sys_exit()函数、do_exit()函数来实现,这里不多说了,都很好懂的,到此,我们应该对进程在生命周期中所经历的各种状态,完成状态转换的大部分函数等等等有了了解了,有需要补充的或者不懂再借阅i些资料就应该能够对进程的相关知识有了很好的掌握了,希望大家能够理解,那么我的任务也算完成了一半了。
struct prio_array {
int nr_active; //计数器,记录优先权数组中的进程数
unsigned long bitmap[BITMAP_SIZE]; //bitmap是记录数组中的优先权,实际长度取决于系统无符号长整型的大小
struct list_head queue[MAX_PRIO]; //queue存储进程链表的数组,且每个链表含有特定优先权的进程
};
异常:
处理器产生的(Fault,Trap,Abort)异常 programmed exceptions(软中断):由程序员通过INT或INT3指令触发,通常当做trap处理,用处:实现系统调用。
异常也叫做同步中断,是发生在整个处理器硬件内部的事件。异常通常发生在指令执行之后。大多数现代 处理器允许程序员通过执行某些指令来产生一个异常。其中一个例子就是系统调用。
用户态的程序调用的许多C库例程,就是把代码和一个或者多个系统调用捆绑在一起形成一个单独的函数。当用户进程调用其中一个函数的时候,某个值被放入适当的处理器寄存器中,并产生一个软中断irp(异常)。然后这个软中断调用内核入口点。系统调用能够在用户空间和内核空间之间传递数据,由两个内核函数来完成这个任务:copy_to_user()和copy_from_user()。系统调用号和所有的参数都先被存入处理器的寄存器中,当x86的异常处理程序处理软中断0x80时,它对系统调用表进行索引。
可屏蔽中断:所有有I/O设备请求的中断都是,被屏蔽的中断会一直被CPU 忽略,直到屏蔽位被重置。 不可屏蔽中断:非常危险的事件引起(如硬件失败)
中断对处理器的执行是异步的,就是说中断能够早指令之间发生。一般要发生中断,中断控制器是必须的(x86用的是8259中断处理器)。当中断处理器有有一个待处理的中断时,它就触发连接到处理器的相应INT线,然后处理器通过触发线来确认这个信号,确认线连接到INTA线上。这时候,中断处理器就可以把IRQ数据传到处理器上了,这就是一个中断确认周期。具体的例子就不好列举了,需要太大篇幅,也需要更多的知识才能去深刻了解。
硬件设备控制器通过IRQ线向CPU发出中断,可以通过禁用某条IRQ线来屏蔽中断。 被禁止的中断不会丢失,激活IRQ后,中断还会被发到CPU 激活/禁止IRQ线 != 可屏蔽中断的 全局屏蔽/非屏蔽
本结主要是解释了为何引入进程,简单讨论了用户空间与内核空间的控制流,并且讨论了进程在内核中是如何实现的,里面涉及到队列的知识,本问没有讲到,就需要读者自己去学习数据结构,总之Linux内核需要很好的数据结构知识,最后还粗略涵盖了终端异常,总之,感觉进程是个大骨头,讲的很笼统,还需要大量时间去学习,并且分析Linux内核源代码,总之,继续加油~
内存管理方式
32位系统:页大小4KB
struct page
{
unsigned long flags; //flags用来存放页的状态,每一位代表一种状态
atomic_t count; //count记录了该页被引用了多少次
unsigned int mapcount;
unsigned long private;
struct address_space *mapping; //mapping指向与该页相关的address_space对象
pgoff_t index;
struct list_head lru; //存放的next和prev指针,指向最近使用(LRU)链表中的相应结点
union
{
struct pte_chain;
pte_addr_t;
}
void *virtual; //virtual是页的虚拟地址,它就是页在虚拟内存中的地址
}
要理解的一点是page结构与物理页相关,而并非与虚拟页相关。因此,该结构对页的描述是短暂的。内核仅仅用这个结构来描述当前时刻在相关的物理页中存放的东西。这种数据结构的目的在于描述物理内存本身,而不是描述包含在其中的数据。
一些硬件只能用某些特定的内存地址来执行DMA 一些体系结构其内存的物理寻址范围比虚拟寻址范围大的多。这样,就有一些内存不能永久地映射在内核空间上。
ZONE_DMA:这个区包含的页用来执行DMA操作。 ZONE_NOMAL:这个区包含的都是能正常映射的页(用于映射非DMA) ZONE_HIGHEM:这个区包"高端内存",其中的页能不永久地映射到内核地址空间。
struct zone {
spinlock_t lock; //lock域是一个自旋锁,这个域只保护结构,而不是保护驻留在这个区中的所有页
unsigned long free_pages; //持有该内存区中所剩余的空闲页链表
unsigned long pages_min, pages_low, pages_high; //持有内存区的水位值
unsigned long protection[MAX_NR_ZONES];
spinlock_t lru_lock; //持有保护空闲页链表的自旋锁
struct list_head active_list; 在页面回收处理时,处于活动状态的页链表
struct list_head inactive_list; //在页面回收处理时,是可以被回收的页链表
unsigned long nr_scan_active;
unsigned long nr_scan_inactive;
unsigned long nr_active;
unsigned long nr_inactive;
int all_unreclaimable; //内存的所有页锁住时,此值置1
unsigned long pages_scanned; //用于页面回收处理中
struct free_area free_area[MAX_ORDER];
wait_queue_head_t * wait_table;
unsigned long wait_table_size;
unsigned long wait_table_bits; //用于处理该内存区页上的进程等待
struct per_cpu_pageset pageset[NR_CPUS];
struct pglist_data *zone_pgdat;
struct page *zone_mem_map;
unsigned long zone_start_pfn;
char *name;
unsigned long spanned_pages;
unsigned long present_pages;
};
内核提供了一种请求内层的底层机制,并提供了对它进行访问的几个接口。所有这些接口都是以页为单位进行操作的页面是物理内存存储页的基本单元,只要有进程申请内存,内核便会请求一个页面给它,同理,如果页面不再使用,那么内核将其释放,以便其他进程可以使用,下面介绍一下这些函数。
#define alloc_pages(gfp_mask,order)
alloc_pages_node(numa_node_id(),gfp_mask,order)
#define alloc_page(gfp_mask)
alloc_pages_node(numa_node_id(),gfp_mask,0)
__get_free_page() 请求单页面操作的简化版本
include/linux/gfp.h
#define __get_dma_pages(gfp_mask,order) \
__get_free_pages((gfp_mask)|GFP_DMA,(order))
__get_dma_pages() 用于从ZONE_DMA区请求页面
include/linux/gfp.h
#define __get_dma_pages(gfp_mask,order) \
__get_free_pages((gfp_mask)|GFP_DMA,(order))
当你不再需要页时可以用下列函数释放它们,只是提醒:仅能释放属于你的页,否则可能导致系统崩溃。内核是完全信任自己的,如果有非法操作,内核会开心的把自己挂起来,停止运行。
extern void __free_pages(struct page *page, unsigned int order);
extern void free_pages(unsigned long addr, unsigned int order);
上面提到都是以页为单位的分配方式,那么对于常用的以字节为单位的分配来说,内核通供的函数是kmalloc(),和mallloc很像吧,其实还真是这样,只不过多了一个flags参数。用它可以获得以字节为单位的一块内核内存。
kmalloc()函数与用户空间malloc一组函数类似,获得以字节为单位的一块内核内存。
void *kmalloc(size_t size, gfp_t flags)
void kfree(const void *objp)
void *vmalloc(unsigned long size)
void vfree(const void *addr)
vmalloc()与kmalloc方式类似,vmalloc分配的内存虚拟地址是连续的,而物理地址则无需连续,与用户空间分配函数一致。
vmalloc通过分配非连续的物理内存块,在修正页表,把内存映射到逻辑地址空间的连续区域中,虚拟地址是连续的。 是否必须要连续的物理地址和具体使用场景有关。在不理解虚拟地址的硬件设备中,内存区都必须是连续的。通过建立页表转换成虚拟地址空间上连续,肯定存在一些消耗,带来性能上影响。所以通常内核使用kmalloc来申请内存,在需要大块内存时使用vmalloc来分配。
struct kmem_cache_s{
struct kmen_list3 lists; //lists域中包含三个链表头,每个链表头均对应了slab所处的三种状态(满,未满,空闲)之一,
unsigned int objsize; //objsize域中持有缓存中对象的大小
unsigned int flags; //flags持有标志掩码,其描述了缓存固有特性
unsigned int num; //num域中持有缓存中每个slab所包含的对象数目
unsigned int gfporder; //缓存中每个slab所占连续页面数的幂,该值默认0
size_t color;
unsigned int color_off;
unsigned int color_next;
kmem_cache_t *slabp_cache; //可存储在自身缓存中也可以存在外部其他缓存中
unsigned int dflags;
void (*ctor) (void *,kmem_cache_t*,unsigened long);
void (*dtor)(void*,kmem_cache_t *,unsigend long);
const char *name; //name持有易于理解的名称
struct list_head next; //next域指向下个单向缓存描述符链表上的缓存描述符
};
如我们所讲,作为通用目的的缓存大小都是被定义好的,且成对出现,一个为从DMA内存分配对象,另一个从普通内存中分配,结构cache_sizes包含了有关通用缓存大小的所有信息。代码解释如下:
struct cache_sizes{
size_t cs_size; //持有该缓存中容纳的内存对象大小
kmem_cache_t *cs_cachep; //持有指向普通内存缓存描述符飞指针
kmem_cache_t *cs_dmacachep; //持有指向DMA内存缓存描述符的指针,分配自ZONE_DMA
};
最后介绍一下Slab状态和描述符域的值,如下表(N=slab中的对象数目,X=某一变量的正数)
现在我们再内核运行的整个生命周期范围内观察缓存和slab分配器第如何交互的,内核需要某些特殊结构以支持进程的内存请求和动态可加载模块来创建特定缓存,内核函数 kmem_cache_create 用来创建一个新缓存。这通常是在内核初始化时执行的,或者在首次加载内核模块时执行.
struct kmem_cache *kmem_cache_create (
const char *name, //定义了缓存名称
size_t size, //指定了为这个缓存创建的对象的大小
size_t align, //定义了每个对象必需的对齐。
unsigned long flags, //指定了为缓存启用的选项
void (*ctor)(void *)) //定义了一个可选的对象构造器和析构器。构造器和析构器是用户提供的回调函数。当从缓存中分配新对象时,可以通过构造器进行初始化。
void kmem_cache_alloc( struct kmem_cache *cachep, gfp_t flags );
//cachep是需要扩充的缓存描述符
//flags这些标志将用于创建slab
缓存和slab都可被销毁,其步骤与创建相逆,但是对齐问题在销毁缓存时候不需要关心,只需要删除缓存描述符和释放内存即可,其步骤有三如下:
从缓存链表中删除缓存 删除slab描述符 删除缓存描述符
int kmem_cache_destroy(kmem_cache_t *cachep)
{
int i;
if(!cache || in_interrupt())
BUG(); //完成健全性检查
down(&cache_chain_sem);
list_del(&cachep->next);
up(&cache_chain_sem); //获得cache_chain信号量从缓存中删除指定缓存,释放cache_chain信号量
if(_cache_shrink(cachep)){
slab_error(cachep,"Can't free all objects");
down(&cache_chain_sem);
list_add(&cache->next,&cache_chain);
up(&cache_chain_sem);
return 1; //该段负责释放为使用slab
}
...
kmem_cache_free(&cache_cache,cachep); //释放缓存描述符
return 0;
}
目前为止,我们讨论完了slab分配器,那么实际的内存请求是怎么样的呢,slab分配器是如何被调用的呢?这里我粗略讲解一下。
当内核必须获得字节大小的内存块时,就需要使用函数kmalloc(),它实际上会调用函数kmem_getpages完成实际分配,调用路径如下:
#define __pa(x) ((unsigned long)(x)-PAGE_OFFSET)
extern inline unsigned long virt_to_phys(volatile void * address)
{
return __pa(address);
}
那么内核是如何管理它们使用内存的呢,用户进程一旦创建便要分配一个虚拟地址空间,其地址范围可以通过增加或者删除线性地址间隔得以扩大或者缩减,在内核中进程地址空间的所有信息都被保存在mm_struct结构中,mm_struct和vm_area_struct结构之间的关系如下图:
struct mm_struct {
struct vm_area_struct * mmap; /* 指向虚拟区间(VMA)链表 */
rb_root_t mm_rb; /*指向red_black树*/
struct vm_area_struct * mmap_cache; /* 指向最近找到的虚拟区间*/
pgd_t * pgd; /*指向进程的页目录*/
atomic_t mm_users; /* 用户空间中的有多少用户*/
atomic_t mm_count; /* 对"struct mm_struct"有多少引用*/
int map_count; /* 虚拟区间的个数*/
struct rw_semaphore mmap_sem;
spinlock_t page_table_lock; /* 保护任务页表和 mm->rss */
struct list_head mmlist; /*所有活动(active)mm的链表 */
unsigned long start_code, end_code, start_data, end_data; /*start_code 代码段起始地址,end_code 代码段结束地址,start_data 数据段起始地址, start_end 数据段结束地址*/
unsigned long start_brk, brk, start_stack; /*start_brk 和brk记录有关堆的信息, start_brk是用户虚拟地址空间初始化时,堆的结束地址, brk 是当前堆的结束地址, start_stack 是栈的起始地址*/
unsigned long arg_start, arg_end, env_start, env_end; /*arg_start 参数段的起始地址, arg_end 参数段的结束地址, env_start 环境段的起始地址, env_end 环境段的结束地址*/
unsigned long rss, total_vm, locked_vm;
unsigned long def_flags;
unsigned long cpu_vm_mask;
unsigned long swap_address;
....
};
最后简单讲一下进程映象分布于线性地址空间的相关重点,当用户程序被载入内存之后,便被赋予 了自己的线性空间,并且被映射到进程地址空间,下面需要注意:
void *kmap(struct page *page)
void kunmap(struct page *page)
void *kmap_atomic(struct page *page)
static int memory_open(struct inode * inode * inode,struct file * filp)
{
switch (iminor(inode)) { //switch语句根据从设备号来初始化驱动程序的数据结构
case 1:
...
case 8:
filp->f_op = &random_fops;
break;
case 9:
filp->f_op = &urandom_fops;
break;
struct file {
struct list_head f_list;
struct dentry *f_dentry;
struct vfsmount *f_vfsmnt;
struct file_operations *f_op;
atomic_t f_count;
unsigned int f_flags;
...
struct address_space *f_mapping;
};
struct file_operations random_fops = {
.read = random_read,
.write = random_write,
.poll = random_poll, //poll操作允许某种操作之前查看该操作是否阻塞
.ioctl = random_ioctl,
}; //随机设备提供的操作有以上
struct file_operations urandom_fops = {
.read = random_read,
.write = random_write,
.ioctl = random_ioctl,
}; //urandom设备提供的操作有以上
static ssize_t extract_entropy(struct entract_syore *r,void *buf,size_t nbytes,int flags)
{
...
{
static ssize_t extract_entropy(struct entropy_store *r,void *buf,size_t nbytes,int flags)
{
下面我们来介绍怎么去编写源代码,当我们去编写一个复杂的设备驱动程序时,也许要输出驱动程序中定义的某些符合,以便让内核其它模块使用,这些通常被用在低级的驱动程序中,以便根据这些基本的函数来构建更高级的驱动程序,在Linux2.6内核中,code monkey可以用如下两个宏输出符号,代码在include/linux/module.h中查看:
#define EXPORT_SYMBOL(sym)
__EXPORT_SYMBOL(sym, "")
#define EXPORT_SYMBOL_GPL(sym)
__EXPORT_SYMBOL(sym, "_gpl")
int ioctl(int fd, ind cmd, …);
其中fd就是用户程序打开设备时使用open函数返回的文件标示符,cmd就是用户程序对设备的控制命令,至于后面的省略号,那是一些补充参数,一般最多一个,有或没有是和cmd的意义相关的。ioctl函数是文件结构中的一个属性分量,就是说如果你的驱动程序提供了对ioctl的支持,用户就可以在用户程序中使用ioctl函数控制设备的I/O通道。
代表数据传输的方向,占2位,可以是_IOC_NONE(无数据传输,0U),_IOC_WRITE(向设备写数据,1U)或_IOC_READ(从设备读数据,2U)或他们的逻辑或组合,当然只有_IOC_WRITE和_IOC_READ的逻辑或才有意义。
ioctl命令序号,一般8位。对于一个指定的设备驱动,可以对它的ioctl命令做一个顺序编码,一般从零开始,这个编码就是ioctl命令的序号。
ioctl命令的参数大小,一般14位。ioctl命令号的这个数据成员不是强制使用的,你可以不使用它,但是我们建议你指定这个数据成员,通过它我们可以检查用户空间数据的大小以避免错误的数据操作,也可以实现兼容旧版本的ioctl命令。
ioctl函数的返回值是一个整数类型的值,如果命令执行成功,ioctl返回零,如果出现错误,ioctl函数应该返回一个负值。这个负值会作为errno值反馈给调用此ioctl的用户空间程序。关于返回值的具体含义,请参考<linux/errno.h>和<asm/errno.h>头文件。
unsigned long __must_check copy_to_user(void __user *to,
const void *from, unsigned long n);
unsigned long __must_check copy_from_user(void *to,
const void __user *from, unsigned long n);
copy_from_user和copy_to_user一般用于复杂的或大数据交换,对于简单的数据类型,如int或char,内核提供了简单的宏来实现这个功能:
#define get_user(x,ptr)
#define put_user(x,ptr)//x是内核空间的简单数据类型地址,ptr是用户空间地址指针。
一个cmd参数被分为4段,每段都有其特殊的含义,cmd参数在用户程序端由一些宏根据设备类型、序列号、传送方向、数据尺寸等生成,这个整数通过系统调用传递到内核中的驱动程序,再由驱动程序使用解码宏从这个整数中得到设备的类型、序列号、传送方向、数据尺寸等信息,然后通过switch{case}结构进行相应的操作。解释一下四部分,全部都在<asm-generic/ioctl.h>和ioctl-number.txt这两个文档有说明的 。
_IOC_NONE:值为0,无数据传输。 _IOC_READ:值为1,从设备驱动读取数据。 _IOC_WRITE:值为2,往设备驱动写入数据。 _IOC_READ|_IOC_WRITE:双向数据传输。
在驱动程序中实现的ioctl函数体内,实际上是有一个switch{case}结构,每一个case对应一个命令码,做出一些相应的操作。怎么实现这些操作,这是每一个程序员自己的事情,因为设备都是特定的,这里也没法说,关键在于怎么样组织命令码,因为在ioctl中命令码是唯一联系用户程序命令和驱动程序支持的途径。
所以在Linux核心中是这样定义一个命令码的:
static inline struct proc_dir_entry *create_proc_read_entry(const char *name,
mode_t mode,struct proc_dir_entry *base,
read_proc_t *read_proc,void * data)
*name是结点在/proc文件系统的入口,*base指向设置proc文件的目标路径,如果它的值为NULL,表示该文件就在/proc目录下,读取该文件可以调用*read_proc指向的函数。这里也不多加阐释了,整个也是很简单的过程。
今天的重点是iotcl函数了,其中还有很多向内核中添加代码的细节没有讲到,主要是这些都涉及到过多的操作,需要大家多看源代码并且多动手在Linux上操作才能完全掌握,,今天写的一些也借鉴了一些大牛的文章,总之 收获很多,最后几天了,真的是很开心啦,和大家一起分享真的很快乐的~~
http://www.cnblogs.com/lihuidashen/p/4236635.html
http://www.cnblogs.com/lihuidashen/p/4239672.html
http://www.cnblogs.com/lihuidashen/p/4242645.html
来源:技术让梦想更伟大