LerningNotes

OSLAB Notes4

这个实验的要求是利用linux的字符设备(char devices)创建一个类似管道(pipe)的媒介以供进程间进行通信。

我主要参考了Linux Devices Drivers, Third Edition(LDD3)这本书,有关字符设备的内容在第三章以及第六章,另外该书的源码在github上有,here。愿意深入研究的同学可以去看一下。(注意:LDD3针对的是2.6,如果使用的是3.x版本需要修改一些地方,我的Ubuntu是3.13

完成这个实验,主要需要两方面的知识,一是Linux的字符设备的相关函数,二是如何利用信号量来进行同步。省事起见,我的代码很多细节都没有考虑,完全是为了达到实验效果而写:)

字符设备

Linux将所有的外设都包装为文件来进行处理,这样能极大方便用户态的程序,使用现成的文件操作就可以与外设进行交互。为了包装成文件,需要提供相应的一些操作,如文件的打开,关闭,读写等。在内核中定义了这样的一个结构file_operations,通过其成员可以为一个文件提供各种操作,如其read成员负责着文件的读取,具体的可以参考LDD3 ch03。若为了完成本次实验的效果,只需要使用readwrite就好。

读操作函数形式为,ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); 写操作函数形式为,ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); 这里第一个参数为在内核中文件的指针;第二个参数为用户态程序提供的用来交互的buffer,我们向其中读写;第三个参数为用户态希望读写的长度;第四个则是偏移量。

在定义了我们的操作后,需要将其与设备关联起来,并且在内核中注册。设备有major number和minor number两个标号,major区分着设备的类型,而由于同一类型的设备可能有多种,需要使用minor来进行区分。这里我们不管minor,只实现一个就好。注册字符设备可以使用两种方法,LDD3上推荐使用的新方法比较麻烦,需要申请、注册、各种初始化,不表。我们使用老方法。

注册int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);

注销int unregister_chrdev(unsigned int major, const char *name);

注册时可以直接硬编码一个major,但是这样可能会出现冲突等问题。我们可以令major为0,register_chrdev会为我们返回注册到的号,使用printk将其输出即可。注意,注册后并不会在文件系统中生成文件,需要另外编码,或者在用户态中使用mknod。简单起见,我们使用后者。

同步

因为多个进程要同时操作一个文件,这会带来竞争问题。我们可以使用信号量以及睡眠/唤醒机制来控制文件的同步。这里具体可以参考LDD3 ch06。

信号量

信号量semaphore,其定义在<linux/semaphore.h>内。我们只需要以下的几种操作:

初始化

1
2
struct semaphore sem; 
sema_init(&sem,1); //将sem初始化为1,即一个mutex

P操作 down_interruptible(&sem),V操作up(&sem)。(down_interruptible,故名思议,允许在函数执行时发生中断,不解释细节,下同)

睡眠/唤醒

当某资源不可用时,我们可以通过令进程进入睡眠态来阻塞进程,而后将其唤醒,这样能使得效率高一些。

唤醒的时候存在这样一个问题,我们需要知道去哪找那些睡着了的进程,也就是说需要存储下来睡眠态的进程。内核提供了wait_queue_head_t这样的一种数据结构用以存储睡眠的进程。其初始化方法为init_waitqueue_head(&que)

当我们希望一个进程睡眠时,可以使用wait_event_interruptible(que, condition)来将其放入que中以备将来唤醒。这里的condition可以是任意的表达式,其作用相当于循环中的入口条件,开始时当condition不满足时进程会进入睡眠,当其被唤醒后会再次检查condition若仍不满足会继续睡眠。这里就很迷惑了,函数是按值传递的,condition怎么还能这样用,还可以检测它变动的值?其实看源码的话会发现,wait_event_interruptible是一个宏函数,它会被展开成相应的条件循环逻辑。

换行时使用wake_up_interruptible(&que),其会将que中的所有使用wait_event_interruptible放入的进程唤醒。

制作管道

有了以上的预备知识后,也就能开始搞我们的程序了(buggy)。

为了尽量简单,我们将存储的buffer,以及等待队列等数据结构都只做一份全局的,因为我们只需要一个设备。注册模块的时候完成各种初始化以及字符设备的注册,并将注册到的major号输出出来以备使用。

具体数据结构如下,

1
struct plypy_pipe {
    wait_queue_head_t inq, outq;       /* read and write queues */
    char buffer[MAXN], *end;           /* static buffer */
    char *wp;                          /* where the data ends */
    struct semaphore sem;              /* mutual exclusion semaphore */
};

inq,outq分别用来存储读/写的进程。buffer数组用来存储数据,end是一个辅助的变量用来标记buffer的末尾。wp用来标记buffer数据的末尾,可以用来判断buffer是否为空。sem则为一个信号量。

简单起见,我们的读写逻辑是这样的。buffer中只存储一次写的数据,不支持连续写,不支持连续读。即只有在buffer为空的时候,才可以再写入下一个数据;只有在buffer中有数据的时候,才能读取数据,并且每次读取完毕后将其设为空。可以看出我们的管道只支持‘写读写读写读……’这样的操作序列,并且每次数据的传输都是从某一个写进程传向某一个读进程,并非广播。

在读写数据时,涉及到一次数据从内核到用户的传输,需要使用copy_to_usercopy_from_user两个函数来完成。

读写的流程也比较简单,不再赘述,直接看源码吧,如下:

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
/**
* Create a virtual char devices
**/


#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/semaphore.h>
#include <linux/types.h>
#include <linux/wait.h>
#include <linux/cdev.h>
#include <linux/sched.h>
#include <asm/uaccess.h>

#define MAXN 1024
#define PLYPY_DEV_NAME "plypy_chrdev"


/* static int plypy_dev_open(struct inode *, struct file *filp); */
static ssize_t plypy_dev_read(struct file *, char *, size_t, loff_t *);
static ssize_t plypy_dev_write(struct file *, const char *, size_t, loff_t *);
/* static int plypy_dev_release(struct inode *, struct file *filp); */

struct file_operations fops =
{
/* .open = plypy_dev_open, */
/* .release = plypy_dev_release, */
.read = plypy_dev_read,
.write = plypy_dev_write
};

int Major;
struct plypy_pipe {
wait_queue_head_t inq, outq; /* read and write queues */
char buffer[MAXN], *end; /* static buffer */
char *wp; /* where the data ends */
struct semaphore sem; /* mutual exclusion semaphore */
};

static struct plypy_pipe plypy_pipe;
static struct plypy_pipe *dev = &plypy_pipe;

static ssize_t plypy_dev_read(struct file *filp, char __user *buf, size_t count,
loff_t *offset)

{

if (down_interruptible(&dev->sem))
return -ERESTARTSYS;

/* There may be multiple readers, so the use of loop is necessary */
while (dev->buffer == dev->wp) { /* nothing to read, wait for inputs */
up(&dev->sem);

if (wait_event_interruptible(dev->inq, (dev->buffer != dev->wp)))
return -ERESTARTSYS;
/* Loop and reacquire the lock */
if (down_interruptible(&dev->sem))
return -ERESTARTSYS;
}

/* read data */
count = min(count, (size_t)(dev->wp - dev->buffer));
if (copy_to_user(buf, dev->buffer, count)) {
/* error happened */
up(&dev->sem);
return -EFAULT;
}
dev->wp = dev->buffer;
up(&dev->sem);

wake_up_interruptible(&dev->outq);
return count;
}


static ssize_t plypy_dev_write(struct file *filp, const char __user *buf,
size_t count, loff_t *offset)

{

if (down_interruptible(&dev->sem))
return -ERESTARTSYS;

while (dev->buffer != dev->wp) { /* the old data haven't been retrieved */
up(&dev->sem);
if (wait_event_interruptible(dev->outq, (dev->buffer == dev->wp)))
return -ERESTARTSYS;
/* P and loop again */
if (down_interruptible(&dev->sem))
return -ERESTARTSYS;
}

count = min(count, (size_t)( dev->end - dev->buffer ));
if (copy_from_user(dev->buffer, buf, count)) {
/* error happened */
up(&dev->sem);
return -EFAULT;
}
dev->wp += count;
up(&dev->sem);
wake_up_interruptible(&dev->inq);

return count;
}

static int plypy_init(void)
{

plypy_pipe.end = dev->buffer+MAXN;
plypy_pipe.wp = dev->buffer;
init_waitqueue_head(&dev->inq);
init_waitqueue_head(&dev->outq);
sema_init(&dev->sem, 1);

Major = register_chrdev(0, PLYPY_DEV_NAME, &fops);
if (Major < 0) {
return Major;
}
printk(KERN_INFO "The %s is assigned major number %d",
PLYPY_DEV_NAME, Major);
printk(KERN_INFO "Use 'mknod /dev/%s c %d 0' to create a file",
PLYPY_DEV_NAME, Major);
return 0;
}

static void plypy_exit(void)
{

unregister_chrdev(Major, PLYPY_DEV_NAME);
printk(KERN_INFO "The %s is destroyed", PLYPY_DEV_NAME);
}

module_init(plypy_init);
module_exit(plypy_exit);

MODULE_LICENSE("GPL");

编译&测试

我使用的是如下的Makefile进行的测试

1
source := plypy
cdevname := plypy_chrdev
major := $(shell awk -v mod='$(cdevname)' '$$2==mod{print $$1}' /proc/devices)

ifneq ($(KERNELRELEASE),)
	obj-m:=$(source).o
else
	KERNELDIR:=/lib/modules/$(shell uname -r)/build
	PWD:=$(shell pwd)
endif
build:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules

install:
	insmod $(source).ko
	mknod /dev/$(cdevname) c $(major) 0

remove:
	rmmod $(source)
	rm /dev/$(cdevname)

clean:
	rm modules.order Module.symvers *.ko *.o

source这里是你的源文件的名字(无后缀),cdevname是注册字符设备时使用的名字,需要通过它在/proc/devices里找刚刚我们的设备注册到的major。

在root下依次执行如下命令,编译安装模块并创建字符设备文件。

1
#make build
#make install

接下来可以用catecho来测试,开启一个终端执行#cat /dev/plypy_chrdev,在另一个终端下不断用echo写入数据,如下:

1
#echo 20 > /dev/plypy_chrdev
#echo 30 > /dev/plypy_chrdev

可以看到每次写入后,均会在cat中出现。

若要编程测试的话也比较简单,无非就是一端read,一端write

读程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h> /* O_RDWR */
#include <unistd.h> /* read/write */
#include <fcntl.h> /* open */
#define MAXN 128

char buffer[MAXN];
int main(void)
{

int fd = open("/dev/plypy_chrdev", O_RDWR);
while (1) {
printf("Read something?");
memset(buffer, 0, sizeof(buffer));
while (getchar() != '\n') /* eat it all */
continue;
read(fd, buffer, MAXN-1);
puts(buffer);
}
return 0;
}

写程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h> /* O_RDWR */
#include <unistd.h> /* read/write */
#include <fcntl.h> /* open */
#define MAXN 128

char buffer[MAXN];
int main(void)
{

int fd = open("/dev/plypy_chrdev", O_RDWR);
while (1) {
printf("Write something:\n");
memset(buffer, 0, sizeof(buffer));
gets(buffer);
write(fd, buffer, strlen(buffer)+1);
}
return 0;
}

编译后在root下执行即可。

End

就这样

OSLAB Notes3

这学期的操作系统实验基本上算是上学期操作系统课的一个延续,实验的内容上面也是。。。多有重复。。。

第一个任务就是添加系统调用,编译内核。。。上一次的笔记OSLAB Adding a system call to Linux kernel. 以下是老师的要求

为Linux内核设计添加一个系统调用,将系统的相关信息(CPU型号、操作系统的版本号、系统中的进程等类似于Windows的任务管理器的信息)以文字形式列表显示于屏幕,并编写用户程序予以验证 对于proc文件系统的相关说明,读取proc文件系统的相关信息,可考虑相应的proc编程实验

关于proc,可以参考wikipediaman proc。简要来说就是内核通过一个虚拟的文件系统,向用户空间的程序提供的一个信息交换的渠道。比如说你可以用 cat /proc/version读出你的操作系统的相关信息,实际上各种工具如uname, ps所做的事情就是读取proc文件并进行解析。

按我揣测来看,老师的意思是让我们在内核态下使用proc来读出各种各样的信息。依我愚见,这是不能完成的,因为内核是proc的提供者,而非使用者,内核态下连文件系统的概念都还没有(尚为源码,还未实现),怎么去读取。而且就算有方法读取,但是你作为提供者,为什么还要费工夫再以使用者的身份调用自己的API,多此一举。所以我把基于系统调用的和proc的分开成两个做了。

UPDATE


其实肯定是有方法可以做的,毕竟一切皆可实现,只不过是漂亮不漂亮,符不符合正常逻辑的问题。我跟指导老师谈了一下这个,老师告诉我编译的时候是没有文件系统,可运行的时候就有了,然后读文件的方法用vfs就可以。详见vfs_read(),当然还存在一些其他的函数,更底层并且更不安全。总的来说吧,合理的逻辑就是,内核提供文件以及proc等,然后各种用户态的程序再去访问它们。虽然我们可以皆由hack调用其他的函数去读取文件,但这本质上是脏的,是不符合设计哲学的,不过毕竟是实验,听老师的……有兴趣的同学可以去hack一下,我还是保留我这个方案。

另外老师要求要直接输出到屏幕上,以下的代码调用的printk生成的输出都需要通过dmesg去访问,老师说不符合要求…… printk其实是有记录级别的,就是常见的那种log level,这些level的宏定义在linux/kernel.h中。warning, error, info啊之类的,可以看一下百度百科,另外关于printk的教程在

这些记录级别其实就是一个数字,越小的越严重,在Linux运行的时候他的console有一个console log level可以通过cat /proc/sys/kernel/printk来查看,第一个数字既是。一般来说默认的应该是4(warning),那么只有小于4的可以被输出到console中。

可以通过printk(KERN_DEBUG "str")这样来明确具体输出的级别,当不声明级别的时候一般默认为KERN_WARNING(4)。为了保证输出到console,可以采用最高级别KERN_EMERG。

但是如果已经编译了内核了,再修改再编译就太蛋疼了,可以通过dmesg -n x来将其修改,我们使用5就可以显示结果了。

另外如果你在图形界面下的终端去执行的话,仍然会看不到dmesg的结果,需要切换到text console(tty1~tty6),可以通过Ctrl+Alt+Fx切换到ttyx,切换到tty7即可回到图形界面。tty会要求你登录,依次输入用户名,密码即可,接下来就跟操作终端一样了。

我编译的内核的tty给挂掉了,显示的是一个空黑屏,AskUbuntu上的这个帖子提供了解决方案,遇到同样问题的可以参考一下。

注意老师要求的是使用SYSCALL, 可以忽略proc那部分


UPDATE END

我只实现了显示内核版本,数个进程名与PID的功能。关于内核版本的查询方式,可参照/proc/version使用utsname()->release源码。 遍历所有进程可以采用如下代码(代码我是手敲的没编译,可能存在错误,下同)

1
2
3
4
5
6
#include <linux/sched.h>
struct task_struct *task;
for_each_process(task)
{
printk("%s [%d]\n", task->comm, task->pid);
}

动态模块加载

再具体实现的时候,可以先用Linux的动态模块来测试,这样就不需要说整整编译一次源码了,可以参考实验书。下面是一个最简单的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>

static int plypy_init(void)
{

printk("Loading Ply_py's module\n");
return 0;
}

static void plypy_exit(void)
{

printk("Dropping Ply_py's module\n");
}

module_init(plypy_init);
module_exit(plypy_exit);
MODULE_LICENSE("GPL");

基本上就是为自己的模块提供上init和exit函数,然后再用module_init,module_exit去注册即可,另外这里MODULE_LICENSE是一个声明许可证的宏,用GPL就行了。再添加一个Makefile,这是实验书上的。(注意Makefile是使用TAB字符进行缩进的)

1
ifneq ($(KERNELRELEASE),)
	obj-m:=plypy_mod.o
else
	KERNELDIR:=/lib/modules/$(shell uname -r)/build
	PWD:=$(shell pwd)
modules:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif

基本上这个Makefile就是切换了一下目录,然后使用了当前正在运行的内核编译模块的Makefile。然后# make,(一般#前缀表示root用户,$表示普通用户)。接下来正常的话会生成一堆文件,其中有一个plypy_mod.ko,是我们用来加载的模块。 使用# insmod plypy_mod.ko来加载,# rmmod plypy_mod.ko来卸载。 同时借助dmesg可以观察到相应的信息。

然后可以先将,之前读取内核版本以及进程的逻辑置于我们模块的init函数中做一个测试。

PROC_FS

由于proc提供的是一个虚拟的文件系统,所以我们需要将我们的信息包装成一个文件的形式,为其提供open,read等操作相对应的服务,参照fs/proc/version.c。 基本上就是为我们的虚拟文件提供了open服务。观察代码,我们可以发现,虚拟文件系统这个名字描述地非常准确,实际上这个文件在物理上并不存在(即不存在于磁盘中),每当用户请求打开文件的时候,内核才会动态生成其内容。 代码如下:

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
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/sched.h>
#include <linux/utsname.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/fs.h>

struct proc_dir_entry *entry;

static int plypy_proc_show(struct seq_file *m, void *v)
{

struct task_struct *task;
int i = 0;
seq_printf(m, "Kernel version: %s\n", utsname()->release);
seq_printf(m, "Processes, to name a few:\n");
for_each_process(task)
{
if (i++ > 9) break; // show only 10 processes at most
seq_printf(m, "%s [%d]\n", task->comm, task->pid);
}
return 0;
}

static int plypy_proc_open(struct inode *inode, struct file *file)
{

return single_open(file, plypy_proc_show, NULL);
}

static const struct file_operations plypy_proc_fops = {
.open = plypy_proc_open,
.read = seq_read,
.llseek = seq_lseek,
.release = single_release,
};

static int plypy_init(void)
{

printk("Loading Ply_py's module\n");
entry = proc_create("plypy", 0, NULL, &plypy_proc_fops);
return 0;
}

static void plypy_exit(void)
{

proc_remove(entry);
printk("Dropping Ply_py's module\n");
}
module_init(plypy_init);
module_exit(plypy_exit);
MODULE_LICENSE("GPL");

通过insmod加载了后,可以通过cat /proc/plypy观察结果。

SYSCALL

这个可以参照之前的那篇OSLAB Adding a system call to Linux kernel,只用把具体的函数逻辑改一改就行,如下:

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

asmlinkage long sys_plypy_hello(void)
{
    struct task_struct *task;
        int i = 0;
        printk("Kernel version: %s\n", utsname()->release);
        printk("Processes, to name a few:\n");
        for_each_process(task)
        {
                if (i++ > 9) break; // show only 10 processes at most
                printk("%s [%d]\n", task->comm, task->pid);
        }
    return 0;
}

就这样。

OSLAB notes 2

This time, we are asked to play with the fork(), signal(), kill() and some other POSIX APIs related with process control and communication.

Fork and pass signal

The first task is to use fork to create 2 child-processes and then use soft interrupt (signal) to make interprocess communication.

Fork

Well, first of all you need to create 2 child-processes. And how to achieve that, this?

1
2
pid1 = fork();
pid2 = fork();

Well, young man, you’re simply naiveeee.

Before making further explanation, you’d better run this code yourself.

1
2
3
4
pid_t pid1, pid2;
pid1 = fork();
pid2 = fork();
printf("The father %d created, pid1: %d \t pid2: %d\n", getpid(), pid1, pid2);

The output goes like this,

The father 6281 created, pid1: 6282     pid2: 6283
The father 6282 created, pid1: 0        pid2: 6284
The father 6284 created, pid1: 0        pid2: 0
The father 6283 created, pid1: 6282     pid2: 0

Strange, huh? There is 4 processes created, rather than 3 that we expected; and the child process 6282 has also created one process 6284.

Well, let’s explain this. The fork will create a duplicate of the running process, and the process created will be the child of the original process. Since the child is a replica, it will have the same local variables ,same PC(program counter) and a bunch of other stuff you may see its man page for details.

See the child and the parent processes are almost identical, the child will have the same PC as the parent’s, which means, it will continue to execute the code after where fork is called. And that is the reason of the “wrong” behavior we have above. The execution flow goes like this.


  1. Parent 6281 created child 6282, then 6281 & 6282 will continue to execute pid2 = fork()
  2. 6281 created another child 6283, while child 6282 also created one child of its own, 6284.

Then how shall we distinguish the child and the parent? Notice that some pids are 0. After fork returned the child and the parent will receive 2 different value. The parent will get the pid of its child, meanwhile the child will have a 0. Also -1 for failed duplicate. And we can work on that.

Signal and Kill

In this task, we used software interrupt to achieve interprocess communication. That is, one process listen for a specific signal (interrupt) and then the other one send that signal. These are done by signal and kill respectively.

The signal(signum, handler) will bind the specified signal with id signum with the handler function handler. And kill(pid, sig) will send signal sig to process pid.

Design

Then everything is simple, as the task asked us to let the parent listen for keyboard interrupt and then send signal to kill the children. We shall simply let the parent listen to the SIGINT, and then bind a handler function that will send signals to the 2 children, the children then wait for the signals to commit suicide.

However, there are few things to note.

Notes

The guide provided is… well, it made few mistakes. Only on some specific OS the break and delete will generate the SIGINT, mostly it’s for Ctrl+c. And since we are using SIGINT, it’s vital to overwrite the orignal handler for it. Or it will cause the processes to terminate, though the outcome is same, it’s not what we want.

All in all, it’s my code below.

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
/**
* Author: Ply_py
* OSLAB: fork and signal passing
*/

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>

void killer(int);
void suicide(int);

pid_t pid1, pid2;

int
main(int argc, int *argv[])
{

// make all process of this group ignore the normal soft interrupt
signal(SIGINT, SIG_IGN);
// create child process 1
while (-1 == (pid1 = fork())) continue;

if (pid1 > 0) { // parent
// create child process 2
while (-1 == (pid2 = fork())) continue;

if (pid2 > 0) { // parent
printf("Parent: %ld\t Child1: %ld\t Child2: %ld\n",
(long) getpid(), (long) pid1, (long) pid2);

signal(SIGINT, killer);
// wait for death of 2 children
wait(0);
wait(0);
puts("The parent has killed his children");
exit(EXIT_SUCCESS);
} else { // child 2
printf("child 2: %ld created\n", (long) getpid());
signal(16, suicide);
while (1) sleep(5);
}

} else { // child 1
printf("child 1: %ld created\n", (long) getpid());
signal(17, suicide);
while (1) sleep(5);
}
printf("I, %ld can get here\n", (long) getpid());
}

void
killer(int signum)
{

static counter = 0;
printf("process %ld catching signal %d\n", (long) getpid(), signum);
if (0 == counter) {
kill(pid1, 17); // kill child 1
} else {
kill(pid2, 16); // kill child 2
}
++counter;
}

void
suicide(int signum)
{

printf("Process %ld is committing suicide, by signal %d\n",
(long) getpid(), signum);
exit(EXIT_SUCCESS);
}

Pipe

The task 2 demand us to implement another interprocess communication via pipe.

Well it’s rather easy, as we already got here. Use pipe to create a communication tunnel, and use that to pass messages. But notice there are 2 process writing to that pipe concurently, so proper lock is needed. Check lockf.

Code here,

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
/**
* Author: Ply_py
* OSLAB: pipe
*/

#include <unistd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>

#define BUF_SIZE 105
pid_t pid1,pid2;

int
main(int argc, char const *argv[])
{

int fd[2];
// fd[0] dest, fd[1] source
char in_buf[BUF_SIZE];
char out_buf[BUF_SIZE];

if (-1 == pipe(fd)) {
perror("pipe");
exit(EXIT_FAILURE);
}

while (-1 == (pid1 = fork())) continue;

if (0 == pid1) {//child1
lockf(fd[1], F_LOCK, 0);
sprintf(out_buf, "\nChild process 1 is sending a message\n");
write(fd[1], out_buf, BUF_SIZE);
lockf(fd[1], F_ULOCK, 0);
exit(EXIT_SUCCESS);
} else {
while (-1 == (pid2 = fork())) continue;

if (0 == pid2) {// child2
lockf(fd[1], F_LOCK, 0);
sprintf(out_buf, "\nChild process 2 is sending a message\n");
write(fd[1], out_buf, BUF_SIZE);
lockf(fd[1], F_ULOCK, 0);
exit(EXIT_SUCCESS);
} else { // parent
read(fd[0], in_buf, BUF_SIZE);
puts(in_buf);
wait(pid1);
read(fd[0], in_buf, BUF_SIZE);
puts(in_buf);
wait(pid2);
exit(EXIT_SUCCESS);
}
}
return 0;
}

VHDL notes type subtype and generate

Last week’s Lab of Computer Organization Theory, we are asked to implement something that is constituted of several components, so called a hierarchical design, or rather, top down design.

Anyway, what we have to do is to code serveral basic blocks, and then connect them to form a high-level design.

type and subtype

While coding, I found that, I was constantly repeating to write something like,

a0: in std_logic_vector(15 downto 0);
a1: in std_logic_vector(15 downto 0);
a2: in std_logic_vector(15 downto 0);
a3: in std_logic_vector(15 downto 0);

which is quiet ugly indeed. I was just repeating myself.

I want to spare my life, so I wonder if there is a 2-dimensional array of std_logic, then I can return to the happay days that I have in C, std_logic a[4][15].

There is quiet a lot resources online, so without many pain, I found myself the User defined types. Since what I want is an array of std_logic_vector, I wrote these,

-- alias for std_logic_vector(15 downto 0)
subtype std_logic_16 is std_logic_vector(15 downto 0);
-- Array type for std_logic_vector
type std_logic_ar16 is array(natural range <>) of std_logic_16;

Now, I can write

a: in std_logic_ar16(3 downto 0);

The life is wonderful again! However, the VHDL is different from high-level language, the array type is different from ordinary type. If you want to declare an array, you must use array type, the same goes for nonarray type. In detail, if you want to have a single instance of std_logic_vector, you must write a: std_logic_16 , instead of a: std_logic_ar16.

That seems to be evident, however, I once thought the std_vector_array(15 downto 0) as the int [15], so I made a stupid mistake regarding std_vector_array as the int. anyway…

for generate

While I was mapping the components, the same thing happened again. Say, I have to connect 16 and gates with 16 out ports of an unit. I need to write the port map thing for 16 times…

The people created VHDL aren’t fools, they have for generate.

Life is once again wonderful.

OSLAB Adding a system call to Linux kernel

操作系统实验记录—-编译Linux内核添加系统调用

学过好多东西。。。但往往一定时间后自己都会忘掉。。。想去学就还得再重复一次之前的过程。还是把自己学习的过程记录下来吧,方便以后查阅。

我渣交计算机大三的课程是相当的充实啊,海量的专业课与实验,再加上其他的一些事情,最近几天变身“真学狗”。。。

这次操作系统实验是给Linux内核添加一个系统调用,然后重编内核。我的环境如下:Windows下VirtualBox 4.6+Ubuntu12.04(64bit)+Linux3.16

虚拟机&Ubuntu

编译Linux内核当然首先需要一个Linux的发行版了,我用的是小白福音Ubuntu。实验指导是让在虚拟机下编译内核的,但听夏赢家说可以直接在实际的系统下搞这件事情,这样最后不过只是给系统添加了一个启动时的选项而已,不会影响原来的内核。

于是我欢心雀跃的跑到我Ubuntu下面开始编译。。。为了追求速度,用了多线程编译。没曾想,电脑太渣,只听风扇飞转,过一会机器就黑了。。。大概是CPU过热保护断电了吧,呃,Linux的桌面版对于个人用户来说还是有些渣啊。思考再三后我决定还是在虚拟机下搞这件事情,因为看到SO上有不少人说搞内核这个东西可能会”Messing up your production machine”

正题开始,首先我们需要VirtualBox,推荐使用较新的4.x版本,有不少方便的功能,以及一个Ubuntu的映像文件,自己去下载吧。(UPDATE 蟹老板告诉我vmware可以轻松ctrl+c/v。。。,想试的同学可以去搞一下)

好了后新建一个虚拟机,选择对应版本的Ubuntu,这个一定要跟自己准备安装的Ubuntu的版本对上。然后再几个选项,注意硬盘容量这里要选大一点。。。否则会不够用,我被坑了两次,大概30G就够用了。还有这里需要注意一点,Windows下VirtualBox会默认将磁盘文件存储在C盘下,这个路径想改的话可以在Settings-General里改掉。并且把内存设置得大一些,1G就够了,否则会悲剧,下面会讲到。

然后我们再来配置一下这个虚拟机,我改了这几个

  • General—>Shared Clipboard:共享虚拟机和主机器的黏贴板
  • System—>Acceleration:硬件加速,能快一点,但似乎有些机器需要在BIOS上先启用硬件加速功能
  • System—>Processor: 这里选择和你机器一样的CPU数目,效率会高一些。
  • Network:我使用了最为简单的NAT,不需要配置什么的

下面这些部分都是关于配置VirtualBox的虚拟机与主系统进行文件拷贝的

  • Shared Folders:虚拟机和主机间共享的文件夹,可以方便的用来传输文件。在Machine Folders里添加一个你想要向虚拟机里共享的文件夹吧,然后把auto mount,permanent勾上。

接下来启动虚拟机,按照提示选择之前准备好的Ubuntu镜像,安装就是了。安装完毕后进入Ubuntu,还不能直接就开始干活。在上方菜单栏里找Devices—>Insert Guest Additions CD image。这个是VirtualBox的一个增强插件,不安装的话无法使用共享文件夹等功能。点击后,虚拟机会加载这个镜像,然后弹出窗口,选Run就是了。关于Guest Additions具体参考官方文档 https://www.virtualbox.org/manual/ch04.html

这是在 /media下面会加载我们之前共享的文档,Ctrl+Alt+T呼叫出终端,执行

cd /media
ls -l

可以看到这个目录下有一个sf_开头的文件夹这个就是我们共享的文件夹。我的是sf_OSLAB。但是此时如果我们访问的话是会显示Permission denied,因为应当注意到这个文件夹是属于vboxsf这个组的,我的用户名为plypy,执行

sudo adduser plypy vboxsf

然后注销再进入系统就可以搞定了

向Linux内核添加Hello world syscall

呼出终端,建立一个文件夹用于此次实验

mkdir OSLAB

Update:本来用的是助教ftp上的3.13但是悲剧了,编译安装后没办法启动于是我就去下载了 3.16,最近网速蛮快的,我就直接在虚拟机下下载了压缩包

wget https://www.kernel.org/pub/linux/kernel/v3.x/linux-3.16.tar.xz

More Update操**的,3.16也挂了。。。不过还好3.16显示了Kernel Panic的信息,”not syncing out of memory and no killable processes”关掉虚拟机,把内存增加设置到1G就解决问题了。想必之前3.13没成功也是这个问题,但是3.13当时没有显示Kernel Panic信息。我也不清楚,还是推荐大家用3.16或者其他的稳定版本吧。


把东西都放进那个共享的文件夹后,在Linux下执行命令将东西都拷贝到之前建立的目录下吧

cp /media/sf_OSLAB/* OSLAB/ -r

我将Linux的压缩包放在了里面,所以进去解压

cd OSLAB
tar -xvf linux-3.16.tar.xz

接下来进入解压后的目录

cd linux-3.16

arch/x86/syscalls/syscall_64.tbl这个文件存储的是64位所有syscall的表,进去添加一个。如果你安装的是32位的Ubuntu的话修改syscall_32.tbl就好。

cd arch/x86/syscalls

这张表的结构是

<number> <abi> <name> <entry point>
  • number, 是对应的syscall的编号。
  • abi,文档说是The ABI, or application binary interface, to use. Either 64, x32, or common for both。
  • name,是名字
  • entry point是你定义的函数的名字。按照惯例应该为 sys_function_name

我添加了一条

317 common  plypy_hello     sys_plypy_hello

关于如何编辑这个文件,可以使用

vi syscall_64.tbl

这使用的是系统自带的vim,这是一个蛮不错的编辑器。想学习的话运行一下vimtutor。或者简单一点用gedit也可以,

gedit syscall_64.tbl

接下来找include/linux/syscalls.h,将我们的syscall声明添加进该头文件,仿照其他的声明,写

asmlinkage long sys_plypy_hello(void);

因为我们将添加的是一个无参数的syscall所以声明成这样,注意在C语言下声明不带参量的函数需要使用void关键字,如

return_value foo(void);

另外这里的asmlinkage是用来告诉GCC不要将这个函数的参量存入寄存器而是栈中,详见Google。还有syscall需要返回一个long。

接下来去实现自己的syscall,建立kernel/plypy_hello.c,如下

1
2
3
4
5
6
#include <linux/kernel.h>
asmlinkage long sys_plypy_hello(void)
{

printk("Ply_py says, Hello World!\n");
return 0;
}

这里调用了printk,其是printf的兄弟函数,作用是向kernel的日志文件写信息。

由于我们添加了新的源文件,为了将其链接进来。修改kernel/Makefile,将plypy_hello.o添加至obj-y的那个表中,完了是这样的。

obj-y     = fork.o exec_domain.o panic.o \
            cpu.o exit.o itimer.o time.o softirq.o resource.o \
            sysctl.o sysctl_binary.o capability.o ptrace.o timer.o user.o \
            signal.o sys.o kmod.o workqueue.o pid.o task_work.o \
            extable.o params.o posix-timers.o \
            kthread.o sys_ni.o posix-cpu-timers.o \
            hrtimer.o nsproxy.o \
            notifier.o ksysfs.o cred.o reboot.o \
            async.o range.o groups.o smpboot.o plypy_hello.o

至此我们就可以编译内核啦。

内核编译

接下来返回所有源文件的根目录,开始编译

首先把编译需要的东西下载了

sudo apt-get install build-essential libncurses5-dev

配置

make menuconfig

基本采取默认配置即可,可以在General—>Local version处修改一下,方便区分自己搞的内核。(Update注意Local Version中不要使用奇怪的字符,不要有空格,因为它将来是要作为目录名的一部分的。) Save后,开始编译链接,之前开启了多个处理器的选项,所以可以使用多线程编译,-j(n)选项是使用n线程编译,差不多几个核就几线程吧。

MORE UPDATE 一些发行版(比如Ubuntu)会将当前使用的内核的配置文件放在/boot/config-$(uname-r),可以考虑直接把那个拷贝过来用就好,即cp /boot/config-$(uname-r) /YOURPATH/.config

make -j2

编译是一个非常漫长的过程。。。至少对于我的渣电脑来说

然后编译各个模块

make modules -j2

Update: 经过夏赢家提点,我看了一下Makefile,在make的时候已经编译过了内核所以编译模块这一步是不需要的。

安装模块及内核

sudo make modules_install
sudo make install

测试

到这里就把内核安装好了,接下来重启一下,在启动的时候应该能看到Grub的启动菜单,选择之前编译好的linux3.16plypyhello就好,使用如下代码

1
2
3
4
5
6
7
8
9
#include <sys/syscall.h>

#define SYS_PLYPY_HELLO 317

int main(void)
{

syscall(SYS_PLYPY_HELLO);
return 0;
}

然后编译运行

gcc testsyscall.c -o tester
./tester

此时kernel的日志文件里应该多了一句”Ply_py says, Hello World!”,查看一下

dmesg

就这样吧。

错误处理

编译出错,这个错误只可能发生在之前修改的几个文件当中。我数次把kernel写成了kernal。。。

修复完错误后需要重新编译,但得先清除上一次编译遗留的东西,由于我们的配置非常少所以不妨直接全部清除 make distclean,然后再重复编译的过程就可以了。具体可以参考帮助文档make help

删除内核文件,有时会悲剧地编译通过后无法启动。。。把多余的内核放在有限的虚拟机空间里也不太好,可以去/lib/modules/,/boot/下删除掉之前生成的东西

sudo rm -rf *plypyhello/ 

然后更新一下Grub

sudo update-grub

总结

真tm吃力。。。