#Linux Kernel

本次的编程环境:

  • CentOS 6.8
  • Linux centos 2.6.32-573.8.1.el6.x86_64

在内核的源代码中定义了很多进程和进程调度相关的内容。其实Linux内核中所有关于进程的表示全都放在进程描述符这个庞大的结构体当中,关于这个结构体的内容和定义,可以在内核的linux/sched.h文件中找到。
现在就来通过编程实现对进程描述符的操作,主要是读取。至于修改等操作,将在后面的内容中提到。
通过对进程描述符的读取,可以获取进程的一切内容,包括进程的ID,进程的地址空间等等。

不多说,上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//20160912
//currentptr.c

#include <linux/tty.h>
#include <linux/init.h>
#include <linux/sched.h>
#include <linux/kernel.h>
#include <linux/module.h>

/*Function To Write Msg To TTY*/
void tty_write_message(struct tty_struct *tty,char *msg){
if(tty && tty->ops->write){
tty->ops->write(tty,msg,strlen(msg));
}
return;
}

static int my_init(void){
char *msg="Hello tty!\n";
tty_write_message(current->signal->tty,msg);
printk("Hello -- from the kernel...\n");
printk("Parent pid: %d(%s)\n",current->parent->pid,current->parent->comm);
printk("Current pid: %d(%s)\n",current->pid,current->comm);
printk("Current fs: %d\n",current->fs);
printk("Current mm: %d\n",current->mm);
return 0;
}

static void my_cleanup(void){
printk("Goodbye -- from the kernel...\n");
}

module_init(my_init);
module_exit(my_cleanup);

以上的代码,主要就是通过引入内核头文件,进而引用进程描述符中的指针,并通过这种方式获取当前进程和相关进程的描述信息。
Makefile文件如下:

#Makefile
obj-m += currentptr.o

编译的指令:

make -C /usr/src/linux SUBDIRS=$PWD modules

然后通过insmod把模块装载进内核,首先tty输出了Hello tty!
同时在/var/log/message中,模块打印出了这些内容:

Sep 12 10:35:36 centos kernel: Hello -- from the kernel...
Sep 12 10:35:36 centos kernel: Parent  pid: 2235(bash)
Sep 12 10:35:36 centos kernel: Current pid: 13197(insmod)
Sep 12 10:35:36 centos kernel: Current fs: 927961856
Sep 12 10:35:36 centos kernel: Current mm: 932613056

分别是这个进程的相关信息。
对于进程描述符的定义,在本次实验用来编译的内核源码包(kernel-devel-2.6.32-642.4.2.el6.x86_64)中,
进程描述符具体定义在include/linux/sched.h的1326行往后。

需要参考的进程具体信息都在其中,可随时参考,以备不时之需。

Linux内核编程一直是我很想掌握的一个技能。如果问我为什么,我也说不上来。
也许是希望有一天自己的名字也出现在内核开发组的邮件列表里?或是内核发行文件的CREDITS上?
也许是吧。其实更多的,可能是对于底层的崇拜,以及对于内核的求索精神。
想到操作系统的繁杂,想到软件系统之间的衔接,内心觉得精妙的同时,更是深深的迷恋。
所以从这篇文章开始,我要真正的走进Linux内核里了,让代码指引我,去奇妙的世界一探究竟。

在这篇文章中,一起来对内核说Hello World。

本次的编程环境:

  • CentOS 6.8
  • Linux centos 2.6.32-573.8.1.el6.x86_64

没有安装内核的,可能需要安装一下内核源码包

  • kernel-devel-2.6.32-642.4.2.el6.x86_64

    yum install kernel-devel-2.6.32-642.4.2.el6.x86_64

安装好之后,这个版本内核可以在/usr/src/linux找到。

然后话不多说,首先看代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//20160904
//kernel_hello_world.c

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

static int __init lkp_init(void){
printk("Hello,World! --from the kernel space...\n");
return 0;
}

static void __exit lkp_cleanup(void){
printk("Goodbye,World! --leaving kernel space...");
}

module_init(lkp_init);
module_exit(lkp_cleanup);

以上代码是kernel_hello_world.c内容。
作为内核模块,在编译的时候,Makefile文件这样写:

1
2
#File:Makefile
obj-m += kernel_hello_world.o

然后可以通过这条命令来编译:

1
make -C /usr/src/linux SUBDIRS=$PWD modules

编译好以后,目录下面的文件可能是这样子:

kernel_hello_world.ko.unsigned  kernel_hello_world.o  Module.symvers
kernel_hello_world.c   kernel_hello_world.mod.c        Makefile
kernel_hello_world.ko  kernel_hello_world.mod.o        modules.order

有这么多文件被生成,其中kernel_hello_world.ko就是本次编译出来的内核模块文件,在Linux内核中有很多这样的模块,它们可能充当着不同的角色,可能是驱动,也可能是各种设备。

这个模块会在/var/log/message文件中打印一行字,即Hello,World! –from the kernel space…

可以使用insmod kernel_hello_world.ko来将这个模块载入到内核,使用lsmod来查看是否已经加载,使用rmmod kernel_hello_world.ko来卸载这个模块。

可以tail /var/log/message来看一下是否成功执行了呢?

Hello,Kernel.

内核源码树

arch        特定体系结构的源码
block        Crypto API
crypto        内核源码文档
drivers        设备驱动程序
firmware    
fs        VFS和各种文件系统
include        内核头文件
init        内核引导和初始化
ipc        进程间通信代码
kernel        像调度程序这样的核心子系统
lib        通用内核函数
Makefile    
Makefile.common
mm        内存管理子系统和VM
Module.symvers
net        网络子系统
samples
scripts        编译内核所用的脚本
security    Linux安全模块
sound        语音子系统
System.map
tools
usr        早期用户空间代码
virt

编译内核

配置内核(不同的选项)

make config
make menuconfig
make xconfig
make gconfig

创建默认配置

make defconfig
make oldconfig

编译

make

记录编译信息

make >../log.txt

忽略编译信息

make >/dev/null

衍生多个编译作业

make -j[任务数量]

如双核处理器上,每个处理器衍生两个作业

make -j4

安装内核

把arch/i386/boot/bzImage拷贝到/boot
依照vmlinuz-version来命名
编辑/boot/grub/grub.conf文件,为新内核建立新的启动项
使用LILO的系统则编辑/etc/lilo.conf,然后运行lilo

安装模块

make modules_install

内核开发的特点

  1. 没有libc库

大部分常用的C库函数在内核中都已经得到了实现

  1. GNU C

内联函数

把对时间要求比较高而本身长度又比较短的函数定义成内联函数

1
static inline void test(unsigned long tail_size)

内联函数必须在使用前就定义好,一般在头文件或者文件头中定义内联函数。

内联汇编

分支声明(为了优化)

1
2
3
4
5
6
7
8
9
likely()
unlikely()
if(unlikely(foo){
/*****/
}

if (likely(foo)){
/****/
}
  1. 没有内存保护机制
  2. 不要轻易在内核中使用浮点数
  3. 内核空间具有容积小而固定的栈
  4. 同步和并发
  5. 可移植性

进程管理

进程是处于执行期的程序以及它所包含的资源的总称

  1. 进程描述符及任务结构
  • 内核把进程存放在任务队列中。任务队列是各双向循环链表,链表中的每一项都是类型为task_struct,称为进程描述符的结构,定义在<linux/sched.h>文件中。

  • 进程描述符中包含一个具体进程的所有信息

  • task_struct在32位机器上有1.7k字节,其中包含的数据能完整的描述一个正在执行的程序。(打开的文件,进程的地址空间,挂起的信号,进程的状态还有其他更多信息)

  1. 分配进程描述符

通过slab分配器分配task_struct结构

  1. 进程描述符的存放

内核通过一个唯一的进程标识值或PID类标识每个进程,PID最大默认值为32768(short int的最大值),可以修改/proc/sys/kernle/pid_max来提高上限。

  1. 进程状态

进程描述符中的state域描述了进程的当前状态,系统中每个进程都必须处于五种状态中的一种。

  • TASK_RUNNING(运行)
  • TASK_INTERRUPTIBLE(可中断)
  • TASK_UNINTERRUPTIBLE(不可中断)
  • TASK_ZOMBIE(僵死)
  • TASK_STOPPED(停止)
  1. 设置当前进程状态
1
2
3
set_task_state(task,state);
/*将任务task的状态设置为state*/
set_current_state(state) <=> set_stask_state(current,state)
  1. 进程上下文

进程家族树

  • 所有的进程都是PID为1的init进程的后台

  • 内核在系统启动的最后阶段启动init进程,该进程读取系统的初始化脚本并执行其他的相关程序

  • 进程间的关系存放在进程描述符中,每个tack_struct都包含一个指向其父进程task_struct,叫做parent的指针

获取其父进程的进程描述符:

1
struct task_struct *my_parent = ourrent->parent;

访问子进程:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct task_struct *task;
struct list_head *list;

list_for_each(list,$current->children){
task = list_entry(list,sruct task_struct,sibling);
/* task 现在指向当前的某个子进程*/
}

struct task_struct *task;
for (task =current;task != $init_task; task= task->parent)
/* task 现在指向init */

}

系统调用

  • API.POSIX.C

  • 系统调用(系统调用号)

  • 系统调用处理程序

    int $0x80(十进制128) system_call()
    第128号异常处理程序
    系统调用号通过eax寄存器传递给内核
    call *sys_call_table(,%eax,4)
    系统调用表的表项是以32位类型存放的,所以内核需要将给定的系统调用号乘以4,然后查询
    参数传递:ebx,ecx,edx,esi和edi按照顺序存放前五个参数

  • 系统调用的实现

  • 系统调用上下文

    • entry.s(系统调用表)
  • 中断和中断处理程序

    • IRQ:中断请求
    • ISR:中断服务例程
  • 注册中断处理程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//request_irq()成功执行会返回0
int request_irq(unsigned int irq,
irqreturn_t(*handler)(int,void *,struct pt_regs *),
unsighed long irqflags,
const char* devname,
void *dev_id)

/*
参数说明:
irq:要分配的中断号
handler:指针,指向处理这个中断的世纪中断处理程序。
handler函数的原型是特定的,接受三个参数,并有一个类型为irqresutn_t的返回值。
irqflags:可以为0,也可以为下列标志的位掩码
SA_INTERRUPT:表示给定的中断处理程序是一个快速中断处理程序
SA_SAMPLE_RANDOM:
SA_SHIRQ:
devname:与中断相关的ASCII文本表示法.
dev_id:
*/
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×