一、linux内核定时器学习总结
1.度量时间差
时钟中断是由系统的定时硬件以周期性的时间间隔产生,这个间隔(即频率)由内核根据HZ来确定,HZ是一个与体系结构无关的常量(定义在),可配置(50-1200),在X86平台,默认值为1000.HZ的含义是系统每秒钟产生的时钟中断的次数.每当时钟中断发生时,全局变量jiffies(一个32位的unsigned long 变量,定义在)就加1,因此jiffies记录了字linux系统启动后时钟中断发生的次数.驱动程序常利用jiffies来计算不同事件间的时间间隔.
内核提供了一组宏用来比较时间量:
#include
int time_after(unsigned long a, unsigned long b);
int time_before(unsigned long a, unsigned long b);
int time_after_eq(unsigned long a, unsigned long b);
int time_after_eq(unsigned long a, unsigned long b);
获取当前时间:
#include
struct timeval {
time_t tv_sec;
suseconds_t tv_usec;
};
void do_gettimeofday(struct timeval *tv
2.内核定时器
内核定时器用于控制某个函数(定时器处理函数)在未来的某个特定时间执行.内核定时器注册的处理函数只执行一次.处理过后即失效.当内核定时器被调度运行时,几乎可以肯定其不会在注册这些函数的进程正在执行时.相反,它会异步的执行.这种异步类似于硬件中断发生时的情景.实际上,内核定时器是被"软件中断"调度运行的.因此,其运行于原子上下文中.这点和tasklet很类似.处于原子上下文的进程有一些运行时的限制:
(1)不能访问用户空间.因为没有进程上下文.无法与特定的进程与用户关联
(2)不能执行调度或休眠.
(3)Current指针在原子模式下无意义.
内核定时器被组织成双向链表,使用struct timer_list结构描述.
struct time_list{
unsigned long expires; //超时的jiffies值
void(*function)(unsigned long) ; //注册的定时器处理函数
unsigned long data; //定时器处理函数的参数
}
这3个字段表示,当jiffies等于expires时,内核会调度function函数运行.data是传递给function的参数的指针,如果function函数需要不止一个参数,那么可以将这几个参数组成一个结构体,并将结构体的指针赋值给data.
3.管理定时器的接口
void init_timer(struct time_list *timer);
初始化定时器队列结构.timer_list结构在使用前必须初始化,这是要保证结构体中其他的成员能正确赋值.
void add_timer(struct time_list *timer);
启动定时器.
int del_timer(struct time_list *timer);
在定时器超时前将定时器删除.当定时器超时后,系统会自动将其删除.
二、linux内核内存管理学习总结
内存管理子系统提供两个功能界面: 一个是给用户进程使用的系统调用界面, 另外一个是给其他内核子系统使用来完成他们的任务的调用界面。
(1)系统调用界面
malloc() 或 free() - 分配或释放内存的进程的地区使用
mmap() 、 munmap() 、 msync() 或 mremap() - 将文件映射到虚拟内存区域
mprotect - 更改虚拟内存保护的地区
mlock() 、 mlockall() 、 munlock() 或 munlockall() - 超级用户程序,以防止内存交换
swapon() 或 swapoff() - 超级用户程序添加和删除系统交换文件
(2)内核内部调用界面
kmalloc() 或 kfree() - 分配和释放内存使用的内核的数据结构
verify_area() - 验证用户内存区域映射使用所需的权限
get_free_page() 或 free_page() - 分配和释放物理内存页
另外, 内存管理子系统使他的所有数据结构和大部分子例程在内核内部可见。许多具备内存管理的内核子系统模块通过存取这些数据结构来实现系统的细节.
三、分析Linux内核创建一个新进程的过程
实验修改与补充##
分析内核处理过程sys_clone
fork、vfork和clone三个系统调用实际上都是通过do_fork来实现进程的创建,见如下语句:
return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);
do_fork 函数:do_fork函数真正实现复制是copy_process
{
...
p = copy_process(clone_flags, stack_start, stack_size,
child_tidptr, NULL, trace);
...
}
copy_process函数:主要完成进程数据结构,各种资源的初始化。
p = dup_task_struct(current); #调用dup_task_struct()为新进程创建一个内核栈
dup_task_struct()
static struct task_struct *dup_task_struct(struct task_struct *orig)
{
tsk = alloc_task_struct_node(node); #为task_struct开辟内存
if (!tsk)
return NULL;
ti = alloc_thread_info_node(tsk, node); #ti指向thread_info的首地址,同时也是系统为新进程分配的两个连续页面的首地址。
if (!ti)
goto free_tsk;
err = arch_dup_task_struct(tsk, orig); #复制父进程的task_struct信息到新的task_struct里, (dst = src;)
if (err)
goto free_ti;
tsk->stack = ti; #task的对应栈
# ifdef CONFIG_SECCOMP
tsk->seccomp.filter = NULL;
# endif
}
gdb跟踪sys_clone
用GDB来跟踪sys_clone,设置以下断点:
运行后首先停在sys_clone处:
然后是do_fork,之后是copy_process:
进入copy_thread:
新进程是从哪里开始执行的?
在之前的分析中,谈到copy_process中的copy_thread()函数,正是这个函数决定了子进程从系统调用中返回后的执行.
执行起点与内核堆栈如何保证一致?
(1)在ret_from_fork之前,也就是在copy_thread()函数中*childregs = *current_pt_regs();该句将父进程的regs参数赋值到子进程的内核堆栈,
(2)*childregs的类型为pt_regs,里面存放了SAVE ALL中压入栈的参数
故在之后的RESTORE ALL中能顺利执行下去.
总结
·Linux通过复制父进程来创建一个新进程,通过调用do_fork来实现
·Linux为每个新创建的进程动态地分配一个task_struct结构.
·为了把内核中的所有进程组织起来,Linux提供了几种组织方式,其中哈希表和双向循环链表方式是针对系统中的所有进程(包括内核线程),而运行队列和等待队列是把处于同一状态的进程组织起来
·fork()函数被调用一次,但返回两次