Linux内核编译之系统调用

基于华为鲲鹏920的ARM64架构,编译OpenEuler操作系统内核。

1 系统调用

1.1 系统调用的流程

/os-kernel-arm-system-call-01/images/syscall.webp
系统调用图

首先应用程序进程在用户态执行访管中断指令,使CPU从用户态陷入内核态,然后根据访管指令的向量号查找中断描述符表,从中断处理程序system_call()的入口地址处开始执行指令;system_call()函数先把系统调用号和异常处理程序要用到的所有CPU寄存器都保存到相应的栈中,然后对用户态进程传递来的系统调用号进行有效性检查,如果该调用号大于系统调用分派表的表项数,系统调用处理程序就终止,否则就调用eax寄存器(x86架构)中的系统调用号对应的特定服务例程。系统调用服务例程执行完毕后,system_call()函数从eax寄存器中获得返回值,并将其保存在曾保存用户态eax寄存器值的那个栈单元中;最后,system_call()函数返回到用户态的应用程序进程中,用户态进程将在eax中找到系统调用的返回码。

1.2 添加一个系统调用

添加一个系统调用
获取指定进程标识符PID所对应的资源使用情况,包括用户态和内核态的执行时间(以秒和微秒为单位)、无需和需要物理输入输出操作的页面错误次数、进程置换出内存的次数。

首先查看Linux的内核文档可知,已存在一个需2个参数的系统调用getrusage,其原型如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
SYSCALL_DEFINE2(getrusage, int, who, struct rusage __user *, ru)
{
	struct rusage r;

	if (who != RUSAGE_SELF && who != RUSAGE_CHILDREN &&
	    who != RUSAGE_THREAD)
		return -EINVAL;

	getrusage(current, who, &r);
	return copy_to_user(ru, &r, sizeof(r)) ? -EFAULT : 0;
}

在该系统调用中又调用了getrusage()函数,而该函数的原型如下:

 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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
void getrusage(struct task_struct *p, int who, struct rusage *r)
{
	struct task_struct *t;
	unsigned long flags;
	u64 tgutime, tgstime, utime, stime;
	unsigned long maxrss = 0;

	memset((char *)r, 0, sizeof (*r));
	utime = stime = 0;

	if (who == RUSAGE_THREAD) {
		task_cputime_adjusted(current, &utime, &stime);
		accumulate_thread_rusage(p, r);
		maxrss = p->signal->maxrss;
		goto out;
	}

	if (!lock_task_sighand(p, &flags))
		return;

	switch (who) {
	case RUSAGE_BOTH:
	case RUSAGE_CHILDREN:
		utime = p->signal->cutime;
		stime = p->signal->cstime;
		r->ru_nvcsw = p->signal->cnvcsw;
		r->ru_nivcsw = p->signal->cnivcsw;
		r->ru_minflt = p->signal->cmin_flt;
		r->ru_majflt = p->signal->cmaj_flt;
		r->ru_inblock = p->signal->cinblock;
		r->ru_oublock = p->signal->coublock;
		maxrss = p->signal->cmaxrss;

		if (who == RUSAGE_CHILDREN)
			break;

	case RUSAGE_SELF:
		thread_group_cputime_adjusted(p, &tgutime, &tgstime);
		utime += tgutime;
		stime += tgstime;
		r->ru_nvcsw += p->signal->nvcsw;
		r->ru_nivcsw += p->signal->nivcsw;
		r->ru_minflt += p->signal->min_flt;
		r->ru_majflt += p->signal->maj_flt;
		r->ru_inblock += p->signal->inblock;
		r->ru_oublock += p->signal->oublock;
		if (maxrss < p->signal->maxrss)
			maxrss = p->signal->maxrss;
		t = p;
		do {
			accumulate_thread_rusage(t, r);
		} while_each_thread(p, t);
		break;

	default:
		BUG();
	}
	unlock_task_sighand(p, &flags);

out:
	r->ru_utime = ns_to_timeval(utime);
	r->ru_stime = ns_to_timeval(stime);

	if (who != RUSAGE_CHILDREN) {
		struct mm_struct *mm = get_task_mm(p);

		if (mm) {
			setmax_mm_hiwater_rss(&maxrss, mm);
			mmput(mm);
		}
	}
	r->ru_maxrss = maxrss * (PAGE_SIZE / 1024); /* convert pages to KBs */
}

显然,在getrusage系统调用中,getrusage()函数的task_struct参数被赋值current,即传入了当前进程(调用该系统调用的进程)的task_struct结构体指针。因此Linux内核中的getrusage()函数的功能是获取当前进程的资源使用情况,而int who参数则规定了获取的资源使用情况的范围,即获取当前进程的资源使用情况、获取当前线程的资源使用情况、还是获取当前进程的子进程的资源使用情况。因此我们需重新添加系统调用,添加进程PID参数及指定使用情况的范围,以满足新系统调用的需求。

1.2.1 添加系统调用服务例程

在Linux内核中,系统调用的服务例程都存放在/kernel/sys.c文件中,因此我们需要在该文件中添加新的系统调用服务例程my_getrsuage

 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
// Customed getrusage for the kernel
SYSCALL_DEFINE2(my_getrusage,pid_t,p_pid,struct rusage __user *, u_rusage)
{
	struct task_struct *p;
	struct rusage *k_rusage;
	p=pid_task(find_get_pid(p_pid),PIDTYPE_PID);
	if(p==NULL)
	{
		printk(KERN_ALERT"Could not find a process by pid %d",p_pid);
		return -EFAULT;
	}else{
		printk(KERN_ALERT"PID[%d]@[%s]",p->pid,p->comm);
	}

	k_rusage=kmalloc(sizeof(struct rusage),GFP_KERNEL);
	if(k_rusage==NULL)
	{
		printk(KERN_ALERT"Could not allocate memory for k_rusage");
		return -EFAULT;
	}

	getrusage(p, RUSAGE_SELF, k_rusage);

	if(copy_to_user(u_rusage, k_rusage, sizeof(struct rusage))!=0){
		printk(KERN_ALERT"Could not copy usage to user");
		return -EFAULT;
	}
	return 0;
}

在我们自定义的系统调用中,设置了两个系统调用需传递的参数:pid_t p_pidstruct rusage __user *u_rusage。其中,p_pid是进程PID,u_rusage是指向用户空间的rusage结构体指针(用户空间的变量需在类型中添加__user宏定义)。在系统调用服务例程中,首先通过pid_task函数根据进程PID获取进程的task_struct结构体指针,然后调用getrusage函数获取进程的资源使用情况,最后将资源使用情况拷贝到用户空间

内核函数copy_to_user()用于将内核空间的数据拷贝到用户空间,需要传递3个参数:

  • void __user *to 指向用户空间的指针
  • const void *from 指向内核空间的指针
  • unsigned long n 拷贝的字节数 如果拷贝成功,返回0,否则返回拷贝失败的字节数。

kmallc()则用于在内核空间动态分配内存,需要传递2个参数:

  • size_t size 分配的内存大小
  • gfp_t flags 分配内存的标志位,GFP_KERNEL表示分配的内存可以被内核访问

1.2.2 添加系统调用号

在Linux内核中,系统调用号存放在/include/uapi/asm-generic/unistd.h文件中,因此我们需要在该文件中追加新的系统调用号__NR_my_getrusage

1
2
#define __NR_my_getrusage 295
__SYSCALL(__NR_my_getrusage, sys_my_getrusage)

这里需要注意的是,系统调用号不能与已有的系统调用号重复,否则会导致系统调用号冲突。此外,在添加的系统调用号后面的系统调用号,应该依次加1。

1.2.3 添加系统调用服务例程的声明

/include/linux/syscalls.h文件中,需要添加新的系统调用服务例程的声明:

1
asmlinkage long sys_my_getrusage(pid_t p_pid, struct rusage __user *u_rusage);

2 编译内核

使用默认的makefile配置文件编译内核:

1
make openeuler_defconfig

编译内核:

1
make -j24

安装模块与内核:

1
make modules_install && make install

重启系统进入GRUB选择内核:

1
reboot

3 测试

3.1 编写用户程序

通过syscall()函数调用系统调用,需要传递3个参数:

  • int number 系统调用号
  • pid_t pid 进程PID
  • struct rusage *usage 用户空间已分配内存的rusage结构体指针
 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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <stdio.h>
#include <stdlib.h>
#include <linux/kernel.h>
#include <sys/syscall.h>
#include <sys/resource.h>
#include <unistd.h>

#define MY_GETRUSAGE 295

void printUsage(struct rusage *usage);

int main(){
    pid_t pid;
    struct rusage *usage = NULL;

    printf("Enter the pid of the process: \n");
    scanf("%d", &pid);

    printf("The pid received is %d\n", pid);

    usage=malloc(sizeof(struct rusage));
    if(usage==NULL){
        printf("Error in allocating memory to 'usage'\n");
        return -1;
    }

    syscall(MY_GETRUSAGE, pid, usage);

    if(usage == NULL){
        printf("Error in the system call\n");
    }
    else{
        printf("The system call was successful\n");
        printUsage(usage);
    }
    return 0;
}

void printUsage(struct rusage *usage){
    printf("\nfunction printUsage() called\n");
    printf("%-15s %-15s %-20s %-20s %-15s %-15s\n", "[User Time]", "[System Time]", "[Page Faults|MIN]", "[Page Faults|MAX]", "[Block IN]","[Block OUT]");

    printf("%-15.3f %-15.3f %-20ld %-20ld %-15ld %-15ld\n",
           usage->ru_utime.tv_sec + usage->ru_utime.tv_usec*1.0,
           usage->ru_stime.tv_sec + usage->ru_stime.tv_usec*1.0,
           usage->ru_minflt,
           usage->ru_majflt,
           usage->ru_inblock,
           usage->ru_oublock);
}

3.2 编译用户程序

1
2
3
4
5
# 编译
gcc getrusage.c -o getrusage.o

# 运行
./getrusage.o
/os-kernel-arm-system-call-01/images/test.webp
测试效果

4 问题解决

4.1 缺少依赖库

在编译内核时,会出现缺少openssl、bison、flex等依赖库的情况,如下图所示:

/os-kernel-arm-system-call-01/images/openssl.webp
缺少openssl-dev库
/os-kernel-arm-system-call-01/images/libefl.png
缺少openssl-dev库
/os-kernel-arm-system-call-01/images/flex-not-found.png
缺少openssl-dev库

编译之前安装所需依赖库即可:

1
yum install -y openssl-devel bison flex libelf-devel

4.2 ISO C90 警告

/os-kernel-arm-system-call-01/images/warning.webp
ISO C90 警告

笔者在编译内核时,一些变量会出现ISO C90警告的情况,如上图所示,这是由于变量未定义在函数开头的警告,可通过调整变量定义至函数开头解决问题。

4.3 内核崩溃

/os-kernel-arm-system-call-01/images/kernelcrash.png
内核崩溃

笔者在实现一个系统调用服务例程时,从用户态传入了一个int __user * u_len型指针,以传递自定义结构体数组struct buffer*的长度。经过调试发现,在调用copy_from_user()函数后,程序正常进行,而在向buffer中添加数据时,内核崩溃,此时系统自动重启。

通过在服务例程中添加一行打印u_len指针后,发现该变量的值为0。笔者在服务例程中调用copy_to_user()函数后,验证函数返回值(copy_to_user()函数复制成功时,会返回0)为0,说明调用该函数成功返回,但是为什么调用copy_from_user()函数将u_len指针的值复制给int k_len(定义在内核态服务例程中)后,其值始终为0呢?

这里我们再了解一下copy_from_user()函数的原型:

1
2
3
4
5
6
copy_from_user(void *to, const void __user *from, unsigned long n)
{
	if (check_copy_size(to, n, false))
		n = _copy_from_user(to, from, n);
	return n;
}

其中,tofrom参数均为指针变量,而笔者在用户态函数中调系统调用函数时,传入的int u_len忘记加&取地址了,这说明笔者传入的参数类型和系统调用的参数类型并不一致,而这并未引起异常。(这也说明内核崩溃是因为利用k_lenbuffer分配的内存大小为0

4.4 内核引导阶段内核加载失败

笔者编译的某个内核版本出现了一些未知错误,导致内核无法正常引导,而且笔者设置了GRUB引导时默认加载第一个内核,因此无法选择其他内核进入系统。

笔者实验环境为华为云鲲鹏服务器,在该服务器内保存了较多代码(未创建快照)。由于该服务器的存储磁盘是挂载到服务器内的OpenEuler系统上的,因此笔者通过卸载该磁盘,并创建一个新的服务器,将该磁盘重新作为数据盘挂载到新服务器中,得以重新访问该磁盘,从而恢复了笔者的代码。

1
2
3
fdisk -l #查看磁盘信息

umount /dev/vdb1  /mnt # 将vdb1设备挂载到/mnt目录下

4.5 加载内核模块时提示未知参数被忽略

/os-kernel-arm-system-call-01/images/parameter_ignored.webp
未知参数被忽略

在成功编译内核模块后,通过insmod命令加载模块,而模块中有一个p_pid参数需要指定。在加载模块时,需指明参数名称:

1
insmod ./test.ko p_pid=1234
给作者倒杯卡布奇诺 ~
Albresky 支付宝支付宝
Albresky 微信微信