Archive for January, 2016

用 qemu 来调试 Kernel

January 13th, 2016

引言

  • kgdb 方式

kgdb 的方式需要两台电脑,一台是宿主机,另一台是开发机,在开发机上编译打好补丁的内核代码,然后拷贝到宿主机上运行。注:目前 kgdb 支持的版本比较低了,好像在 2.6.19 左右,如果需要调试高版本的内核比较麻烦,而且需要通过串口方式调试,必须需要两台电脑,安装配置也比较麻烦,不过该方式调试比较准确,不会因为优化问题而无法查看变量。

  • uml 方式

uml ( user mode linux kernel ),是一种在用户态调试内核的方式,该调试方式在 2.6 就进入主线了。在源码包中,进入 arch/um 文件夹,就能看到该方式。该方式存在问题是无法调试硬件相关,如果你只需要调试调度、调试文件系统等,那么你可以使用,该方式比较简单,可自行百度。

  • printk 方式

这个方式也就是说在想调试的地方打印调试信息,需要反复的编译,反复增减调试信息是比较繁琐的一个地方。

qemu 调试内核

建议不要从源中拉版本安装,因为可能源中的版本太低,这个问题困扰了我很久,如果版本太低的话,导致文件系统加载的时候会出现故障,会出现以下提示: cannot load filesystem…
首先从 http://wiki.qemu.org/Download 下载最新版的 qemu 源码,我下的是: qemu-2.4.0.tar.bz2 版本,按照下列方式安装就可以了。

$tar -xvf qemu-2.4.0.tar.bz2
$cd qemu-2.4.0
$./configuration
$make install

我就默认安装的,并没有修改安装地址,如果有需要的话请自行定制。

源码配置

$tar -xvf linux-source**
$cd linux-source**
$vim Makefile

编辑 Makefile 文件,将所有 -O2 优化方式修改为 -O0 ,这样可以部分减少查看变量时的 optimized 提示(也就是说变量被编译器优化了,放到寄存器了,无法打印)。

$make menuconfig

接下来修改内核调试选项

kernel将上述选项都选中。 然后就是 make bzImage。这样就会在 arch/i386/boot/ 下生成 bzImage 文件,内核部分就结束了。

文件系统制作

这块我主要是借鉴了网上的一个帖子: http://blog.csdn.net/wesleyluo/article/details/7943087 该帖子详细讲解了如何制作根文件系统,如果你遇到跟我一样的问题,就是制作的根文件系统无法使用的话,也就是提示找不到 filesystem ,那么请你转到 buildroot 工具,制作根文件系统。

gdb 调试

接下来就是调试你的内核啦,不要太激动啊,因为你还是有可能遇到文件系统无法加载啊, qemu 调试报错啊等等问题。
制作一个脚本来快速启动 qemu 调试

#!/bin/bash
qemu-system-i386 -kernel linux_path/arch/i386/bzImage -hda rootfs.ext2 -append "root=/dev/sda rw" -s -S

关于这个 shell 可能有些疑惑, -kernel 就是使用后边的 bzImage 作为内核镜像。 -hda 我的理解就是作为硬盘引导项, -s 是 gdb 调试的快捷方式相当于 -gdb tcp::1234, 打开一个 gdbserver 在 TCP 端口 1234.-S 选项是启动之后就暂停,等待用户命令。

然后新打开一个终端,输入

$gdb linux_path/vmlinux

等待 gdb 把符号加载完成,加载提示:

$Reading symbols from ..../vmlinux ...done

这样就加载完成了,接下来输入:

$target remote localhost:1234
$b start_kernel
$c

就开始运行然后停在了 start_kernel , OK 大功告成了。

参考:

http://blog.csdn.net/wesleyluo/article/details/7943087

gnuplot 尝鲜

January 12th, 2016

gnuplot是一款画图软件,可以将数据以一定的方式显示在坐标系中,可以生成二维三维的数据分布。我们依赖这种数据可视化做到对于数据的分析。在各种操作系统性能分析中,该工具也占有举足轻重的位置。这篇文章默认你已经懂得了gnuplot基本操作,我们试着将数据集以可视化的方式表现出来。

在linux终端下,输入gnuplot,可以直接进入到该软件操作界面下:

➜  Desktop  gnuplot         

	G N U P L O T
	Version 5.0 patchlevel 0    last modified 2015-01-01 

	Copyright (C) 1986-1993, 1998, 2004, 2007-2015
	Thomas Williams, Colin Kelley and many others

	gnuplot home:     http://www.gnuplot.info
	faq, bugs, etc:   type "help FAQ"
	immediate help:   type "help"  (plot window: hit 'h')

Terminal type set to 'qt'
gnuplot> 

一般情况下,我们可以在命令行中断直接一步一步设置我们需要设置的图像,但是这里我们直接使用bash脚本方式生成图像,其中脚本模板如下:

#!/bin/bash
gnuplot<<FFF
...

FFF
exit 0

中间省略号的部分就是我们在命令行中输入的,比如set terminal png truecolor 就是设置gnuplot因该采用什么样的格式;set autoscale代表让gnuplot自己计算x轴y轴范围。如果x轴是特殊的数值,比如时间那么使用%d %n %y

set xdata time
set timefmt "%H:%M:%S"

来定义x轴数据格式

如果我们在一个二维坐标系中画多条折线图,有两种方式,我们可以在每个文件中定义一条折线的坐标值。然后使用下面这种方式,一个filename对应一条折线,title是关键字,代表折线名字,with也是关键字,代表图的类型,这里是表示由线组成的点,pointtype 4表示用空心方块中重点标示每个点。你可以修改后面的数字,这样会得到不同的线型。

plot 'filename' title 'Sequential Read' with linespoints pointtype 4,'filename2' title '...' with linespoints pointtype 4

第二种也是一种方式,但是我不太常用。第二种方式主要把每个filename中的数据都放在同一个文件中,每一列数据代表一组线,他们的纵坐标都是同第一列。每一列数据中间用空格分离,使用using 关键字。比如using 1:2,表示使用第1列作为x轴,绘制第2列数据。

plot 'test.log' using 1:2 title "line 1",using 1:3 title "line 2"

如果对图像要求比较高,还可以设置网格 set grid;设置x轴y轴范围set xrange [“13:00:00″:”17:00:00”] set yrange[“…”:”…”]设置x轴y轴标签 set xlabel “…” set ylabel “…”

以上都是折线图,下面我们来尝试画一个柱状图:

对于柱状图,其实只需要设置set style data histograms 就可以生成图形,但是我们必须对图形进行微调。gnuplot 按以下次序绘制框的边框:顶、底、左和右,值分别为 1、2、4、8。要想删除一条或多条边框线,只需提供相应值的和。在这个示例中,使用 -1 选项删除底部边框线。指定 fill 选项就会用默认颜色填充框:

set style fill solid 1.00 border -1

对于 x 坐标,这里不使用时间,而是使用组名称。使用 xtic 选项让 gnuplot 沿着 x 轴放置 tic 和数据标签(第 1 列)。在这里就是组名称。但是,有时候标签包含许多字符,或者 xtic 的时间格式在图形上的 tic 之间放不下。这时就会看到标签相互重叠。为了避免这个问题,把标签旋转 90 度(通过试验找到合适的角度),让它们垂直显示。可以使用以下命令来实现这种效果:

set xtic rotate by 90

其中这个90度可以为负。

第 2 列中的数据使用第 1 列(x 数据)作为参照:

2:xtic (1)

最后生成的柱状图是:

static

如果多个柱状图为一个x轴为参考点,那么可以使用

plot "disk.txt"  using 2:xtic(1) title "Oct-09 data growth(gb)", '' using 3 title "Nov-09 data growth(gb)", '' using 4 title "Dec-09 data growth(gb)"

diskimage

参考:

gnuplot 入门教程1   : http://blog.csdn.net/liyuanbhu/article/details/8502383

gnuplot 入门教程2   : http://blog.csdn.net/liyuanbhu/article/details/8502418

 gnuplot 入门教程3   : http://blog.csdn.net/liyuanbhu/article/details/8502450

gnuplot 入门教程4   : http://blog.csdn.net/liyuanbhu/article/details/8502461

gnuplot 让您的数据可视化  : http://www.ibm.com/developerworks/cn/linux/l-gnuplot/index.html

 使用 gnuplot 在网页中显示数据   : http://www.ibm.com/developerworks/cn/aix/library/au-gnuplot/index.html

内核函数copy_process()分析

January 11th, 2016

内核通过调用函数copy_process()创建进程,copy_process()函数主要用来创建子进程的描述符以及与子进程相关数据结构。这个函数内部实现较为复杂,在短时间内,对于内部详细代码原理和实现并不能全部理解。因此,接下来的分析侧重于copy_process()的执行流程。

1. 定义返回值变量和新的进程描述符。

2.  对clone_flags所传递的标志组合进行合法性检查。当出现以下四情况时,返回出错代号:

  • CLONE_NEWNS和CLONE_FS同时被设置。前者标志表示子进程需要自己的命名空间,而后者标志则代表子进程共享父进程的根目录和当前工作目录,两者不可兼容。
  • CLONE_NEWUSER和CLONE_FS同时被设置。
  • CLONE_THREAD被设置,但CLONE_SIGHAND未被设置。如果子进程和父进程属于同一个线程组(CLONE_THREAD被设置),那么子进程必须共享父进程的信号(CLONE_SIGHAND被设置)。
  • CLONE_SIGHAND被设置,但CLONE_VM未被设置。如果子进程共享父进程的信号,那么必须同时共享父进程的内存描述符和所有的页表(CLONE_VM被设置)。

3. 调用security_task_create()和后面的security_task_alloc()执行所有附加的安全性检查。

4. 调用dup_task_struct()为子进程分配一个内核栈、thread_info结构和task_struct结构。

 p = dup_task_struct(current);
        if (!p)
               goto fork_out;

这个dup_task_struct函数首先定义创建了指向task_struct和thread_inof结构体的指针。然后让子进程描述符中的thread_info字段指向ti变量;最后让子进程thread_info结构中的task字段指向tsk变量。然后返回tsk,这个时候子进程和父进程的描述符中的内容是完全相同的。在后面的代码中,我们将会看到子进程逐渐与父进程区分开。

static struct task_struct *dup_task_struct(struct task_struct *orig)
{
        struct task_struct *tsk;
        struct thread_info *ti;
        int node = tsk_fork_get_node(orig);
        int err;
 
        tsk = alloc_task_struct_node(node);
        if (!tsk)
                return NULL;

        ti = alloc_thread_info_node(tsk, node);
        if (!ti)
                goto free_tsk;

        err = arch_dup_task_struct(tsk, orig);
        if (err)
                goto free_ti;
        tsk->stack = ti;

5. 开始设置子进程的task_struct

根据clone_flags的值继续更新子进程的某些属性。将 nr_threads++,表明新进程已经被加入到进程集合中。将total_forks++,以记录被创建进程数量。

这部分工作还包含初始化双链表、互斥锁和描述进程属性的字段等,其中包括大量的与cgroup相关的属性,。它在copy_process函数中占据了相当长的一段的代码,不过考虑到task_struct结构本身的复杂性,也就不奇怪了。

如果上述过程中某一步出现了错误,则通过goto语句跳到相应的错误代码处;如果成功执行完毕,则返回子进程的描述符p。do_fork()执行完毕后,虽然子进程处于可运行状态,但是它并没有立刻运行。至于子进程合适执行这完全取决于调度程序schedule()。

 

http://lxr.free-electrons.com/source/kernel/fork.c#L1242

fork系统调用分析

January 9th, 2016

0 前言

在Linux中,主要是通过fork的方式产生新的进程,我们都知道每个进程都在 内核对应一个PCB块,内核通过对PCB块的操作做到对进程的管理。在Linux内核中,PCB对应着的结构体就是task_struct,也就是所谓的进程描述符(process descriptor)。该数据结构中包含了程相关的所有信息,比如包含众多描述进程属性的字段,以及指向其他与进程相关的结构体的指针。因此,进程描述符内部是比较复杂的。这个结构体的声明位于include/linux/sched.h中。

task_struct中有指向mm_struct结构体的指针mm,也有指向fs_struct结构体的指针fs,这个结构体是对进程当前所在目录的描述;也有指向files_struct结构体的指针files,这个结构体是对该进程已打开的所有文件进行描述。这里我们要注意进程在运行期间中可能处于不同的进程状态,例如:TASK_RUNNING/TASK_STOPPED/TASK_TRACED 等

1 fork调用

在用户态下,使用fork()创建一个进程。除了这个函数,新进程的诞生还可以分别通过vfork()和clone()。fork、vfork和clone三个API函数均由glibc库提供,它们分别在C库中封装了与其同名的系统调用fork()。这几个函数调用对应不同场景,有些时候子进程需要拷贝父进程的整个地址空间,但是子进程创建后又立马去执行exec族函数造成效率低下。

  • 写时拷贝满足了这种需求,同时减少了地址空间复制带来的问题。
  • vfork 则是创建的子进程会完全共享父进程的地址空间,甚至是父进程的页表项,父子进程任意一方对任何数据的修改使得另一方都可以感知到。
  • clone函数创建子进程时灵活度比较大,因为它可以通过传递不同的参数来选择性的复制父进程的资源

系统调用fork、vfork和clone在内核中对应的服务例程分别为sys_fork(),sys_vfork()和sys_clone()。例如sys_fork()声明如下(arch/x86/kernel/process.c):

int sys_fork(struct pt_regs *regs)
{
        return do_fork(SIGCHLD, regs->sp, regs, 0, NULL, NULL);
}
int sys_vfork(struct pt_regs *regs)
{
        return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs->sp, regs, 0,
                       NULL, NULL);
}
sys_clone(unsigned long clone_flags, unsigned long newsp,
          void __user *parent_tid, void __user *child_tid, struct pt_regs *regs)
{
        if (!newsp)
                newsp = regs->sp;
        return do_fork(clone_flags, newsp, regs, 0, parent_tid, child_tid);
}

可以看到do_fork()均被上述三个服务函数调用。do_fork()正是kernel创建进程的核心()。通过分析调用过程如下,其中我分析的是最新版4.X Linux源码,在i386体系结构中,采取0x80中断调用syscall:

fork

 

从图中可以看到do_fork()和copy_process()是本文的主要分析对象。

do_fork函数的主要就是复制原来的进程成为另一个新的进程,在一开始,该函数定义了一个task_struct类型的指针p,用来接收即将为新进程(子进程)所分配的进程描述符。但是这个时候要检查clone_flags是否被跟踪就是ptrace,ptrace是用来标示一个进程是否被另外一个进程所跟踪。所谓跟踪,最常见的例子就是处于调试状态下的进程被debugger进程所跟踪。ptrace字段非0时说明debugger程序正在跟踪父进程,那么接下来通过fork_traceflag函数来检测子进程是否也要被跟踪。如果trace为1,那么就将跟踪标志CLONE_PTRACE加入标志变量clone_flags中。没有的话才可以进程创建,也就是copy_process()。

long _do_fork(unsigned long clone_flags,
              unsigned long stack_start,
              unsigned long stack_size,
              int __user *parent_tidptr,
              int __user *child_tidptr,
              unsigned long tls)
{
        struct task_struct *p;
        int trace = 0;
        long nr;
        if (!(clone_flags & CLONE_UNTRACED)) {
                if (clone_flags & CLONE_VFORK)
                        trace = PTRACE_EVENT_VFORK;
                else if ((clone_flags & CSIGNAL) != SIGCHLD)
                        trace = PTRACE_EVENT_CLONE;
                else
                        trace = PTRACE_EVENT_FORK;
                if (likely(!ptrace_event_enabled(current, trace)))
                        trace = 0;
        }

这条语句要做的是整个创建过程中最核心的工作:通过copy_process()创建子进程的描述符,并创建子进程执行时所需的其他数据结构,最终则会返回这个创建好的进程描述符。因为copy_process()函数过于巨大,所以另外开辟一篇文章讲解该函数实现。

 p = copy_process(clone_flags, stack_start, stack_size,
                        child_tidptr, NULL, trace, tls);

如果copy_process函数执行成功,那么将继续下面的代码。定义了一个完成量vfork,之后再对vfork完成量进行初始化。如果使用vfork系统调用来创建子进程,那么必然是子进程先执行。原因就是此处vfork完成量所起到的作用:当子进程调用exec函数或退出时就向父进程发出信号。此时,父进程才会被唤醒;否则一直等待。

 if (!IS_ERR(p)) {
                struct completion vfork;
                struct pid *pid;

                trace_sched_process_fork(current, p);

                pid = get_task_pid(p, PIDTYPE_PID);
                nr = pid_vnr(pid);
                if (clone_flags & CLONE_PARENT_SETTID)
                     put_user(nr, parent_tidptr);
                if (clone_flags & CLONE_VFORK) {
                     p->vfork_done = &vfork;
                        init_completion(&vfork);
                        get_task_struct(p);
                }

下面通过wake_up_new_task函数使得父子进程之一优先运行;如果设置了ptrace,那么需要告诉跟踪器。如果CLONE_VFORK标志被设置,则通过wait操作将父进程阻塞,直至子进程调用exec函数或者退出。

                wake_up_new_task(p);

                /* forking complete and child started to run, tell ptracer */
                if (unlikely(trace))
                        ptrace_event_pid(trace, pid);
                if (clone_flags & CLONE_VFORK) {
                    if (!wait_for_vfork_done(p, &vfork))
                               ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
                }
                put_pid(pid);

如果copy_process()在执行的时候发生错误,则先释放已分配的pid;再根据PTR_ERR()的返回值得到错误代码,保存于pid中。 返回pid。这也就是为什么使用fork系统调用时父进程会返回子进程pid的原因。

        } else {
                nr = PTR_ERR(p);
        }
        return nr;
}

参考:

http://cs.lmu.edu/~ray/notes/linuxsyscalls/

http://www.x86-64.org/documentation/abi.pdf

http://www.tldp.org/LDP/tlk/ds/ds.html

浅谈APIC timer

January 9th, 2016

最近在写毕业论文,博客比较荒废,下面我们来谈一下APIC timer吧。

因为我的毕业论文涉及benchmark,所以测试性能与时间紧密相关,我必须调整操作系统的时钟频率,操作本身计时器不够精确,所以必须手动调整Local APIC时钟触发时间中断间隔(每秒触发多少时间中断)。硬件实现上每个Local APIC连接一个CPU core。

所以在编写APIC timer驱动的时候,提供了2-3种模式来实现,第一种和第二种分别是周期模式和one-shot模式,被所有的Local APIC支持,第三种叫TSC-Deadline mode,是最近的CPU型号支持,比如在MCA的君主模式中,我看到时间戳通常使用第三种定时器模式。

周期模式

驱动可以通过设置初始count值作为发生时间中断的依据,每当这个count值减为0,就会产生一次timer IRQ,然后重新设置为初始count值,重新开始自减,所以这种模式下Local APIC产生中断的间隔取决于初始count值,而自减频率与CPU的外频和步长(divided by the value)相关,步长值存储在“Divide Configuration Register” 寄存器中。

举个例子2.4GHz CPU拥有外频800MHZ,如果步长为4,初始count值为123456,那么Local APIC将以200MHZ的速率自减couns值,每个timer IRQ中断间隔为617.28us,时间中断频率则是1620.01Hz。

one-shot模式

这个模式和周期模式很类似,不同的是他不会重置初始count值,也就是说驱动必须亲自重置这个count值,如果内核想要更多的IRQ中断。这种模式的优势是驱动可以更加精确地控制timer IRQ的产生。举个例子,内核切换进程时可以依赖新进程的优先级(Priority),这样可以动态改变IRQ时钟频率。一些内核可以使用这种方式实现更加精确地timer服务。

比如当前运行的进程应该抢先1234纳秒,而同时一个睡眠进程要在333纳秒后醒来,时间中断将会在44444纳秒后到来。那么初始count值可以设置为333纳秒,那是内核发生Timer IRQ,内核知道当前进程还有901纳秒被调度,同时下次Timer IRQ将在441111纳秒后到来。

这种模式的缺点在于很难跟踪实时进程,并且需要避免竞争条件,特别是新的count值在旧count值结束前被设置。

TSC-Deadline 模式

这种模式和前两种完全不同,他不是使用外频/总线频率降低count数值,而是通过软件设置deadline,当CPU时间戳计数大于deadline时,Local APIC产生timer IRQ。这种方式相比one-shot模式,Timer IRQ可以有更高的精度,因为时间戳是以CPU主频的方式自增,这个明显高于外频,也避免了竞争条件。

 

参考:

http://wiki.osdev.org/APIC_timer

http://www.cs.columbia.edu/~junfeng/11sp-w4118/lectures/trap.pdf