Archive for the ‘Kernel内核编程’ category

如何给 kernel.org 下的项目贡献代码

March 17th, 2016

kernel的开发模式,不用细说了,直接切入正题!

1.  首先你总要把kernel-stable 和 linux-next git仓库克隆下来

2.  修改文件,blabla…. 然后使用 git 生成补丁

git format-patch -1

git 会将最近一次的提交生成补丁文件,可以在当前目录下看到 0001-*.patch 文件

3.  使用邮件发送你的 patch 给 maintainer 与 mailing list

代码库下会有文件描述 maintainer 的信息,这里以 rt-tests 为例,在 MAINTAINERS 文件中有 maintainer 的邮件地址,可将你的patch 发给上述地址。

4.  使用 Mutt 发送 patch
Mutt是一个命令行界面下的邮件客户端,具体配置方式这里就不再赘述,可以参考http://jingyan.baidu.com/article/0aa22375bbc3be88cc0d6425.html 来配置。

===================

如果遇到大patch的话,社区通常会reject,我们需要把patch分成若个小patch,每个patch叙述一部分功能,这样可以方便review,而且使得源码容易维护。那么如何拆分大patch呢?

比如这时在本地我们已经提交了一个大patch,然后可以执行以下操作:

1.  将当前提交撤销,重置到上一次。

$ git reset HEAD^

2.  通过补丁块拣选方式择要提交的修改。 Git 会逐一显示工作区更改,如果确认此处动要 会逐一显示工作区更改,如果确认此处动要提交,输入 “y“。

$ git add -p

以撤销提交的说明为蓝本,撰写新的commit。

$ git commit -e -C HEAD@{1}

3.  如果提交代码过于密集,耦合太强,那么上面这种方式不太适用,那么这时可以 直接编辑文件,删除要剥离出此次提交的修改然后执行:

$ git commit -- amend

然后执行下面的命令,还原有文件修改再提交:

$ git checkout HEAD@{1}-- .
$ git commit

 

参考:

http://jingyan.baidu.com/article/0aa22375bbc3be88cc0d6425.html

用 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

内核线程中poll的操作

October 8th, 2015

在用户空间我们可以使用poll()函数或者select()函数对一个设备进行轮训操作,但是在内核空间呢?虽然read()/write()在内核空间有vfs统一管理,故我们可以使用vfs_read()/vfs_write()对文件进行读取(参见)。但是我找不到vfs_poll()。要想实现poll的功能,考虑使用等待队列造个poll的轮子

如果我们设计一个字符设备,这个字符设备出现数据的时候,我们需要在适当的wake_up(),在创建内核线程中,我们需要实现一个业务逻辑:

DECLARE_WAIT_QUEUE_HEAD(my_waitqueue);
static int xxx_kernel_thread(void)
{
       DECLARE_WAITQUEUE(wait,current);
       while(1)
       {
              add_wait_queue(&my_waitqueue,&wait);
              set_current_state(TASK_INTERRUPTIBLE);

              schedule();
              set_current_state(TASK_RUNNING);
              remove_wait_queue(&my_waitqueue,&wait);
              //do_something
        }
...
}

这个唤醒操作很有意思,当我们将当前等待项加入到等待队列后,设置当前的内核线程睡眠,主动调用schedule()让出cpu,当其他的某个地方唤醒这个等待队列后,代码从schedule()下一句开始执行。然后将当前内核线程设置为运行,然后移除等待项,通过这种业务逻辑做到了内核线程的轮询。

我在编写这个代码的时候犯了一个低级错误内核线程被唤醒后,没有设置TASK_RUNNING,而直接移除等待队列,这个就会导致BUGON的产生,虽然业务逻辑可以顺利执行,有时间需要看看调度的流程,才可以透彻的理解调度的实际含义。

 

http://dashan8020.blog.163.com/blog/static/4796750420115180227132/

在内核中对文件进行读写

September 28th, 2015

我们知道在用户态下,使用各种文件的系统调用即可对文件进行读写操作,open()、write()、read()等。这些调用最后都会通过内核的VFS模型,调用到设备驱动函数,这里我们可以简单看一下open()、write()、read()驱动函数接口:

static int xxx_open(struct inode *inode, struct file *file);
static int xxx_release(struct inode *inode, struct file *file);
static ssize_t xxx_read(struct file *filp, char __user *ubuf,
                                  size_t usize, loff_t *off);
static unsigned int xxx_poll(struct file *file, poll_table *wait);
static long xxx_ioctl(struct file *f, unsigned int cmd,
                                  unsigned long arg);
ssize_t xxx_write(struct file *filp, const char __user *ubuf,
                          size_t usize, loff_t *off)

实际上在内核驱动调用的函数传入的参数远比我们在用户态下看到的复杂的多,那么问题来了:如果在内核态下对文件进行读取?当然我们不能在内核中使用syscall了,这里我们有两种方式:1.将全部的驱动函数导出 2.使用VFS在内核态中的接口。

将全部的驱动函数导出的方式虽然可行,但是这样做等于将函数暴露在全局,不利于封装,不推荐使用这种方式。第二种是我们推荐的方式,内核为开发者提供了filp_open(),filp_close(),vfs_read(),vfs_write(),vfs_fsync()接口,我们只需要调用这些接口即可在内核态下对文件进行操作。

首先我们要包含头文件:

#include <linux/fs.h>
#include <asm/segment.h>
#include <asm/uaccess.h>
#include <linux/buffer_head.h>

当然了,很多时候我们在内核层下封装了这些接口,使其可以像用户态下open那般简单易用。不过我们要注意open的返回值不再是fd,而是struct file *类型的指针!而path就是路径,flag是读写权限。

struct file* file_open(const char* path, int flags, int rights) {
    struct file* filp = NULL;
    mm_segment_t oldfs;
    int err = 0;

    oldfs = get_fs();
    set_fs(get_ds());
    filp = filp_open(path, flags, rights);
    set_fs(oldfs);
    if(IS_ERR(filp)) {
        err = PTR_ERR(filp);
        return NULL;
    }
    return filp;
}

关闭一个文件:

void file_close(struct file* file) {
    filp_close(file, NULL);
}

读取文件的封装接口参数比较多,第一个参数是文件指针,第二个是偏移量,第三个是buffer,第四个是读取的大小。与用户态下的read()类似!

int file_read(struct file* file, unsigned long long offset, unsigned char* data, unsigned int size) {
    mm_segment_t oldfs;
    int ret;

    oldfs = get_fs();
    set_fs(get_ds());

    ret = vfs_read(file, data, size, &offset);

    set_fs(oldfs);
    return ret;
}   

写数据到文件中:

int file_write(struct file* file, unsigned long long offset, unsigned char* data, unsigned int size) {
    mm_segment_t oldfs;
    int ret;

    oldfs = get_fs();
    set_fs(get_ds());

    ret = vfs_write(file, data, size, &offset);

    set_fs(oldfs);
    return ret;
}

立即回写到磁盘,同步文件:

int file_sync(struct file* file) {
    vfs_fsync(file, 0);
    return 0;
}

 

http://stackoverflow.com/questions/1184274/how-to-read-write-files-within-a-linux-kernel-module
https://en.wikipedia.org/wiki/Virtual_file_system

字符驱动poll函数与select()函数的交互

September 25th, 2015

在字符驱动中,我们经常要实现poll()的功能,具体实现在注册到file_operations 的函数中,举个例子

static unsigned int xxx_poll(struct file *file, poll_table *wait)
{
         poll_wait(file, &mp_chrdev_wait, wait);
         if (rcu_access_index(mplog.next))
                 return POLLIN | POLLRDNORM;
 
         return 0;
}

我们必须在这个函数中返回POLLIN、POLLOUT等状态,从而我们可以在用户态下使用FD_ISSET()判断数据是否到来。而其中void poll_wait(struct file *filp, wait_queue_head_t *queue, poll_table *wait);它的作用就是把当前进程添加到wait参数指定的等待列表(poll_table)中。需要注意的是这个函数是不会引起阻塞的。

这里我们实现创建了一个mp_chrdev_wait的等待队列,它会把这个轮训进程放入一个等待队列中,然后这个进程会睡眠(表现在select()上就是阻塞)。当某个条件满足时,唤醒这个等待队列,也就是唤醒了轮训进程,也就是内核通知应用程序(应用程序的select函数会感知),这个时候mask返回值中有数据。然后就会接着select操作。所以我们要在恰当的位置wake_up_interruptible(&mp_chrdev_wait)

在用户空间中的代码,我们需要使用select()轮训在这个设备上:

          int register_fd, ret;
          fd_set rds;
  
          register_fd = open(CONFIG_PATH, O_RDWR);
          if (register_fd < 0)
                 err("opening of /dev/mplog");
 
          FD_ZERO(&rds);
          FD_SET(register_fd,&rds);
  
  
          while (1) {
                  /*
                   * Proceed with the rest of the daemon.
                  */
                 memset(temp, 0, MP_LOG_LEN * sizeof(struct mp));
  
 
                  ret = select(register_fd+1,&rds,NULL,NULL,NULL);
                  if(ret < 0 )
                  {
                          close(register_fd);
                          err("select error!");
                  }
                  if(FD_ISSET(register_fd,&rds))
                         read(register_fd, temp, MP_LOG_LEN * sizeof(struct mp));
 .... 
          }