20169215《Linux内核原理与分析》第八周作业
时间和内存管理
定时器和时间管理
内核中大量函数都是基于时间驱动的。体系结构提供了两种设备进行计时——系统定时器和实时时钟。实时时钟(RTC)是用来持久存放系统时间的设备,系统关闭后靠主板上的微型电池供电,它在系统启动时初始化xtime变量。
系统定时器以某种频率自行触发时钟中断,该频率可通过编程预定,称作节拍率;连续两次时钟中断的时间间隔称为节拍。提高节拍率有利有弊,优点是:
- 内核定时器能以更高的频度和更高的准确度运行;
- 依赖定时值执行的系统调用能够以更高的精度运行;
- 对资源消耗和系统运行时间等的测量会有更精细的解析度;
- 提高进程抢占的准确度。
缺点是:节拍率越高,时钟中断频率越高,处理器用来执行时钟中断处理程序的时间越多,系统负担就越重。
全局变量jiffies用来记录自系统启动以来产生的节拍总数。jiffies是无符号长整型数据,其他任何类型存放它都不正确。
内核定义了USER_HZ代表用户空间看到的HZ值,内核可以使用函数jiffies_to_clock_t()将一个由HZ表示的节拍数转换成一个由USER_HZ表示的节拍计数(如果是64位jiffies,则使用函数jiffies_64_to_clock_t())。
tick_periodic()是调用体系结构无关的时钟例程。
do_timer()承担着对jiffies_64的实际增加操作;update_process_times()根据流逝的时间更新墙上时钟。do_timer()返回时调用update_process_times()更新所耗费的各种节拍数。
account_process_tick()函数对进程时间进行实质性更新;scheduler_tick()负责减少当前运行进程的时间片计数值并且在需要时设置need_resched标志。
墙上时间就是当前实际时间,定义为xtime,读写xtime变量需要使用xtime_lock锁,该锁是一个seqlock锁。从用户空间取得墙上时间主要接口是gettimeofday(),对应的系统调用为sys_gettimeofday()。设置当前时间的系统调用是settimeofday()。
定时器是管理内核流逝的时间基础,由结构timer_list表示:
struct timer_list {
struct list_head entry;//定时器链表入口
unsigned long expires;//以jiffies为单位的定时值
void (*function) (unsigned long);//定时器处理函数
unsigned long data;//传给处理函数的长整型参数
struct tvec_t_base_s *base;//定时器内部值,用户不要使用
}
定时器其他函数:
add_timer(&my_timer);//激活定时器
mod_timer(&my_timer,jiffies+new_delay);//修改超时时间
del_timer(&my_timer);//停止定时器
内存管理
内核把物理页作为内存管理的基本单位。内何用struct page结构表示系统中每个物理页。
struct page {
unsigned long flags;
atomic_t _count;
atomic_t _mapcount;
unsigned long private;
struct address_space *mapping;
pgoff_t index;
struct list_head lru;
void *virtual;
}
flags域存放页的状态,_count域存放页的引用计数,virtual域是页的虚拟地址。
获得页最核心的函数是:
struct page * alloc_pages(gft_t gfp_mask, unsigned int order);
该函数分配2^order个连续的物理页并返回指向第一个页的page结构体的指针。可以通过下面函数把给定页换成逻辑地址:
void * page_address(struct page *page);
slab是为了防止频繁的数据结构的分配和释放而提出的。当不需要一个数据结构实例时,是把它放入空闲链表等待下次分配,而不是释放它。
网络云课堂学习
本次实验重点是分析进程创建的过程,Linux中进程创建通过fork()函数实现。
int Fork(int argc, char *argv[])
{
int pid;
/* fork another process */
pid = fork();
if (pid<0)
{
/* error occurred */
fprintf(stderr,"Fork Failed!");
exit(-1);
}
else if (pid==0)
{
/* child process */
printf("This is Child Process!\n");
}
else
{
/* parent process */
printf("This is Parent Process!\n");
/* parent will wait for the child to complete*/
wait(NULL);
printf("Child Complete!\n");
}
}
fork()执行一次,返回两次,其中父进程和子进程各返回一次。执行fork()后,父进程返回子进程的PID,子进程返回0。
它调用copy_process函数来向子进程拷贝父进程的进程环境和全部寄存器副本,调用dup_task_struct()为新进程创建一个内核栈、thread_info结构和task_struct,这些值与当前进程的值相同,此时,子进程和父进程的描述符是完全相同的,又调用copy_thread来拷贝父进程的系统堆栈并做相应的调整。
使用gdb跟踪调试Linux下进程创建的过程:
首先在test.c文件下添加fork命令,编译运行后如图:
然后在通过gdb设置断点跟踪分析进程创建过程。一共设置了6个断点,分别在sys_clone、do_fork、dup_task_struct、copy_process、copy_thread、ret_from_fork处。
接下来就开始跟踪进程的创建,继续执行会在第一个断点sys_clone处停下来:
进入内部,我们可以看到它调用了do_fork:
然后就会在我们的第二个断点do_fork停下来。
- clone_flags:与clone()参数flags相同, 用来控制进程复制过的一些属性信息, 描述你需要从父进程继承那些资源。该标志位的4个字节分为两部分。最低的一个字节为子进程结束时发送给父进程的信号代码,通常为SIGCHLD;剩余的三个字节则是各种clone标志的组合(本文所涉及的标志含义详见下表),也就是若干个标志之间的或运算。通过clone标志可以有选择的对父进程的资源进行复制;
- stack_start:与clone()参数stack_start相同, 子进程用户态堆栈的地址;
- regs:是一个指向了寄存器集合的指针, 其中以原始形式, 保存了调用的参数, 该参数使用的数据类型是特定体系结构的struct pt_regs,其中按照系统调用执行时寄存器在内核栈上的存储顺序, 保存了所有的寄存器, 即指向内核态堆栈通用寄存器值的指针,通用寄存器的值是在从用户态切换到内核态时被保存到内核态堆栈中的(指向pt_regs结构体的指针。当系统发生系统调用,即用户进程从用户态切换到内核态时,该结构体保存通用寄存器中的值,并被存放于内核态的堆栈中);
- stack_size:用户状态下栈的大小, 该参数通常是不必要的, 总被设置为0;
- parent_tidptr:与clone的ptid参数相同, 父进程在用户态下pid的地址,该参数在CLONE_PARENT_SETTID标志被设定时有意义;
- child_tidptr:与clone的ctid参数相同, 子进程在用户态下pid的地址,该参数在CLONE_CHILD_SETTID标志被设定时有意义。
多次单步执行之后是copy_process断点:
然后是copy_thread:
然后我们可以找到子进程fork返回值是0的原因:
调度到子进程时的内核栈顶:
调度到子进程时的第一条指令,把ret_from_fork的地址赋值给p->thread.eip,p->thread.eip表示当进程下一次调度时的指令开始地址:
然后是ret_from_fork,到这里说明,新的线程已经产生了:
然后就追踪不到了:
然后完成fork:
参考资料
1.http://www.360doc.com/content/12/0323/18/7982302_197046600.shtml
2.http://www.poluoluo.com/server/201004/82854_2.html
3.http://blog.csdn.net/zhaolianxun1987/article/details/51049448