当前位置:文档之家› 操作系统实验报告 西安交通大学

操作系统实验报告 西安交通大学

操作系统专题实验报告

系 别 计算机科学与技术 专业班级 计算机15 学生姓名 高君宇 学 号 2110505112 联系电话 187******** 提交日期 2014年5月4日

实验一:系统调用 (3)

1.实验目的 (3)

2.实验内容 (3)

3.实验原理 (3)

4.实验步骤 (4)

4.1.设计系统调用源代码并将其加入Linux 内核源代码中 (4)

4.2.修改与系统调用表中对应的系统调用名 (6)

4.3.修改头文件syscalls.h,定义新的系统调用所对应的系统调用号 (6)

4.4.编译内核 (7)

4.5.重启系统,进入新编译的内核 (7)

4.6.利用用户程序测试新设计的系统调用 (7)

实验二:动态模块与设备驱动 (9)

1.实验目的 (9)

2.实验内容 (9)

3.实验原理:Linux下设备驱动程序基础知识 (9)

4.实验步骤 (12)

4.1.编写模块程序globalvar.c (12)

4.2.同目录下建立Makefile文件,编译,加载 (14)

4.3.编写读写测试程序read.c 和write.c (15)

实验三:linux下基于定时器及事件驱动编程的贪吃蛇游戏 (15)

1.实验目的 (15)

2.实验内容 (15)

3.实验原理 (16)

4.实验步骤 (17)

4.1.编写程序源代码mysnake1.0.c (17)

4.2.使用gcc mysnake1.0.c –lcurses –o mysnake1.0编译 (24)

4.3.运行程序,体验游戏效果 (24)

实验心得 (25)

实验一:系统调用

1. 实验目的

掌握系统调用的设计过程,为以后设计更复杂的系统调用奠定基础。

2. 实验内容

安装Linux,熟悉Linux的操作,并编译内核,构建、使用自己的Linux内核设计系统调用:设计、添加自己的系统调用,并重新编译内核予以验证

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

3. 实验原理

系统调用是应用程序和操作系统内核之间的功能接口,其主要目的使用户可以使用操作系统提供的有关设备管理、输入/输出系统、文件系统和进程控制、通信以及存储管理等方面的功能,而不必了解系统程序的内部结构和有关硬件细节,从而起到减轻用户负担和保护系统以及提高资源利用率的作用。

在Linux 系统中,系统调用是作为一种异常类型实现的,它将执行相应的机器代码指令

来产生异常信号,产生中断或异常的重要效果是系统自动将用户态切换为核心态来对它进行处理。这就是说,执行系统调用异常指令时,自动地将系统由用户态切换为核心态,并安排

异常处理程序的执行。Linux 用来实现系统调用异常的实际指令是:

int $0x80

这一指令使用中断/异常向量号128(即16 进制的80)将控制权转移给内核。为达到在使用系统调用时不必用机器指令编程,在标准的C 语言库中为每一系统调用提供了一段短的子程序,完成机器代码的编程工作。事实上,机器代码段非常简短。它所要做的工作只是

将送给系统调用的参数加载到CPU 寄存器中,接着执行int $0x80 指令,然后运行系统调用。系统调用的返回值将送入CPU 的一个寄存器中,标准库的子程序取得这一返回值,并将它送回用户程序。

4. 实验步骤

4.1.设计系统调用源代码并将其加入Linux 内核源代码中

在/usr/src/Linux-3.8/kernel 目录下,打开文件sys.c , 在头文件中添加:

#include ,在文件的最后部位增加系统调用函数:

asmlinkage int sys_my_syscall(int n)

{

struct file *fp;

mm_segment_t fs;

loff_t pos;

int i;

char buf1[120];

char buf2[40];

char buf3[800];

char buf4[800];

char str1=' ';

char str2='(';

fp=filp_open("/proc/cpuinfo",O_RDONL Y,0);

fs=get_fs();

set_fs(KERNEL_DS);

pos=0;

vfs_read(fp,buf1,sizeof(buf1),&pos);

buf1[119]='\0';

printk("%s\n\n",buf1);

filp_close(fp,NULL);

set_fs(fs);

fp=filp_open("/proc/version",O_RDONL Y,0);

fs=get_fs();/*get_fs()一般也可能是宏定义,它的作用是取得当前的设置*/

set_fs(KERNEL_DS);

pos=0;

vfs_read(fp,buf2,sizeof(buf2),&pos);

for(i=0;i<39;i++)

if(buf2[i]==str1&&buf2[i+1]==str2)

{buf2[i]='\0';break;}

printk("%s\n\n",buf2);

filp_close(fp,NULL);

set_fs(fs);

fp=filp_open("/proc/1/status",O_RDONL Y,0); fs=get_fs();

set_fs(KERNEL_DS);

pos=0;

vfs_read(fp,buf3,sizeof(buf3),&pos);

printk("%s\n\n",buf3);

filp_close(fp,NULL);

set_fs(fs);

fp=filp_open("/proc/2/status",O_RDONL Y,0); fs=get_fs();

set_fs(KERNEL_DS);

pos=0;

vfs_read(fp,buf4,sizeof(buf4),&pos);

printk("%s\n\n",buf4);

filp_close(fp,NULL);

set_fs(fs);

return n;

}

4.2.修改与系统调用表中对应的系统调用名

编辑系统调用入口文件。打开/usr/src/linux-3.8/arch/x86/systemcalls 目录下的syscall_32_tbl 文件,在最后添加系统调用符号名。

4.3.修改头文件syscalls.h,定义新的系统调用所对应的系统调用号

对于新设计的系统调用,还要求定义一个新的系统调用号,此时需要修改syscalls.h,

其路径为:

/usr/src/linux-3.8/arch/x86/include/asm/

在末尾适当位置添加系统调用函数声明:

asmlinkage int sys_my_syscall(int n)

4.4.编译内核

编译内核详细过程略,给内核版本号添加标志,内核编号Linux-3.8.0-2

4.5.重启系统,进入新编译的内核

在终端输入uname –r, 检查内核版本

4.6.利用用户程序测试新设计的系统调用

编写用户程序syscall_test.c 。

#include

#include

int main()

{

int ret;

/*351 号系统调用是新设计的系统调用*/

ret=syscall(351,2);

printf("%d\n",ret);

if(2==ret)

printf("the first syscall is success!\n");

return 0;

}

编译运行测试程序

gcc –o syscall_test syscall_test.c

./ syscall_test

运行成功在终端显示:

通过内核日志查看命令dmesg 进行观察。

输入命令:#dmesg ,显示内核进程输出结果。

实验二:动态模块与设备驱动

1. 实验目的

●熟悉动态模块的加载运作机制

●理解Linux 字符设备驱动程序的基本原理。

●掌握字符设备驱动程序的编写原则和过程。

●学会编写字符设备驱动程序。

2. 实验内容

编写一个简单的字符设备驱动程序。

该字符设备并不驱动特定的硬件, 用内存文件模拟实现,要求该字符设备包括以下几个基本操作,打开、读、写和释放,并编写测试程序用于测试所编写的字符设备驱动程序。在此基础上,编写程序实现对该字符设备的同步操作。

3. 实验原理:Linux下设备驱动程序基础知识

Linux 函数(系统调用)是应用程序和操作系统内核之间的接口,而设备驱动程序是内核和硬件设备之间的接口,设备驱动程序屏蔽硬件细节,且设备被映射成特殊的文件进行处理。每

个设备都对应一个文件名,在内核中也对应一个索引节点,应用程序可以通过设备的文件名来访问硬件设备。Linux 为文件和设备提供了一致性的接口,用户操作设备文件和操作普通文件类似。例如通过open() 函数可打开设备文件,建立起应用程序与目标程序的连接;之后,可以通过read() 、write()等常规文件函数对目标设备进行数据传输操作。

设备驱动程序封装了如何控制设备的细节,它们可以使用和操作文件相同的、标准的系统调用接口来完成打开、关闭、读写和I/O 控制操作,而驱动程序的主要任务也就是要实现这些系统调用函数。设备驱动程序是一些函数和数据结构的集合,这些函数和数据结构是为实现设备管理的一个简单接口。操作系统内核使用这个接口来请求驱动程序对设备进行I/O操作。

2)字符设备驱动重要的数据结构

字符设备是以字节为单位逐个进行I/O 操作的设备,不经过系统的I/O 缓冲区,所以需要管理自己的缓冲区结构。在字符设备驱动程序中,主要涉及3 个重要的内核数据结构,分别是file_operations、file和inode,当用户访问设备文件时,每个文件的file 结构都有一个索引节点inode 与之对应。在内核的inode 结构中,有一个名为i_fop 成员,其类型为file_operations。

file_operations定义文件的各种操作,用户对文件进行诸如open 、close、read 、write 等操作时,Linux 内核将通过file_operations结构访问驱动程序提供的函数。内核通过这3个数据结构的关联,将用户对设备文件的操作转换为对驱动程序相关函数的调用。

include/linux/fs.h中的数据结构file_operations如下:

struct file_operations {

...

loff_t (*llseek) (struct file *, loff_t, int);

ssize_t (*read) (struct file *, char *, size_t, loff_t *);

ssize_t (*write) (struct file *, const char *, size_t, loff_t *);

int (*readdir) (struct file *, void *, filldir_t);

unsigned int (*poll) (struct file *, struct poll_table_struct *);

int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);

int (*open) (struct inode *, struct file *);

int (*flush) (struct file *);

int (*release) (struct inode *, struct file *) ;

ssize_t (*readv) (struct file *, const struct iovec *, unsigned long,

loff_t *);

...

};

file 代表一个打开了的文件。它由内核在使用open ()函数时建立,并传递给该文

件上进行操作的所有函数,直到最后的close() 函数。当文件的所有操作结束后,内核会释

放该数据结构。在file 结构中,一些比较重要的成员含义如下:

struct file{

...

mode_t f_mode;

loff_t f_pos;

/*文件模式,用于标记文件是否可读或可写*/

/*当前的读写位置*/

file_operations *f_op;/* 与文件相关操作。内核执行open() 操作时对该指针赋值

*/

void *private_data;/* 驱动程序可将这个字段用于任何目的,但是要在release()

方法中,释放该字段占用的主存*/

}

内核用inode 结构在内部标识文件,它和file 结构不同,后者标识打开的文件描述符。对于单个文件,可能会有许多个表示打开的文件描述符的file 结构,但它们都指向同一个inode 结构,inode 结构中有两个重要的字段:

struct cdev *i_cdev;

dev_t i_rdev;

类型dev_t描述设备号,i_cdev 结构表示字符设备的内核数据结构,内核不推荐开发者

直接通过结构的i_rdev 结构获得主次设备号,而提供下面两个函数取得主、次设备号。Unsigned int iminor(struct inode *inode);

Unsigned int imajor(struct inode *inode);

3)并发控制

在驱动程序中,当多个线程同时访问相同的资源时(驱动程序中的全局变量是一种典型

的共享资源),可能会引发“竞争” ,因此必须对共享资源进行并发控制。在此驱动程序

中,可用信号量机制来实现并发控制。首先要包含相应的头文件,添加信号量struct semaphore sem ,接着就是初始化,使用void init_MUTEX(struct semaphore *sem),内核对信号量的P、V 操作通过void down_interruptible(struct semaphore *sem)和void up(struct semaphore*sem) 完成。在Linux 驱动程序中,可以使用等待队列(wait queue)来实现阻塞操作。等待队列可以用来同步对系统资源的访问,这里所讲述Linux 信号量在内核中也是由等待队列来实现的。参考程序中定义设备的设备为“globalvar ”,它可以被多个进程打开,但是每次只有当一个进程写入了一个数据之后本进程或其它进程才可以读取该数据,否则一直阻塞。

1)相关数据结构与函数

设备驱动程序结构字符设备的结构描述如下:

struct Scull_Dev{

struct cdev devm; //字符设备

struct semaphore sem; //信号量,实现读写时的PV 操作

wait_queue_head_t outq;

//等待队列,实现阻塞操作

int flag; //阻塞唤醒条件

char buffer[MAXNUM+1]; //字符缓冲区

char *rd,*wr,*end; //读,写,尾指针

};

字符设备的数据接口字符设备的数据接口将文件的读写,打开释放等操作映射为相应的函数。

struct file_operations globalvar_fops =

{

read:globalvar_read,

write:globalvar_write,

open:globalvar_open,

release:globalvar_release,

};

字符设备的注册与注销

字符设备的注册采用静态申请和动态分配相结合的方式,使用6.3节中的

register_chrdev_region函数和alloc_chrdev_region函数来完成。

字符设备的注销采用6.3节中的unregister_chrdev_region函数来完成。

字符设备的打开与释放

打开设备是通过调用file_operations 结构中的函数open()来完成的,设备的打开提供给驱动程序初始化的能力,从而为以后的操作准备,此外还会递增设备的使用记数,防止在文

件被关闭前被卸载出内核。

释放设备是通过调用file_operations 结构中的函数release()来完成的。设备的释放作用

刚好与打开相反。但基本的释放操作只包括设备的使用计数递减。

字符设备的读写操作

字符设备的读写操作相对比较简单,直接使用函数read( ) 和write( )就可以了。文件读操作的原型如下:Ssize_t device_read(struct file* filp,char __user* buff,size_t le n,loff_t* offset); 其中,filp 是文件对象指针;buff 是用户态缓冲区,用来接受读到的数据;len 是希望读取的数据量;offset 是用户访问文件的当前偏移。文件写操作的原型和读操作没有区别,只是操作方向改变而已。由于内核空间与用户空间的内存不能直接互访,因此借助函数copy_to_user()完成用户空间到内核空间的复制,函数copy_from_user()完成内核空间到用户空间的复制。

4. 实验步骤

4.1.编写模块程序globalvar.c

#include

#include

#include

#include

MODULE_LICENSE("GPL");

#define MAJOR_NUM 254 /*主设备号*/

static ssize_t globalvar_read(struct file *, char *, size_t, loff_t*);

static ssize_t globalvar_write(struct file *, const char *, size_t, loff_t*);

/*初始化字符设备驱动的file_operations结构体*/

struct file_operations globalvar_fops =

{

read: globalvar_read,

write: globalvar_write,

};

static int global_var = 0; /*"globalvar"设备的全局变量*/

static int __init globalvar_init(void)

{

int ret;

/*注册设备驱动*/

ret = register_chrdev(MAJOR_NUM, "globalvar", &globalvar_fops);

if (ret)

{

printk("globalvar register failure");

}

else

{

printk("globalvar register success");

}

return ret;

}

static void __exit globalvar_exit(void)

{

int ret;

/*注销设备驱动*/

ret = unregister_chrdev(MAJOR_NUM, "globalvar");

if (ret)

{

printk("globalvar unregister failure");

}

else

{

printk("globalvar unregister success");

}

}

static ssize_t globalvar_read(struct file *filp, char *buf, size_t len, loff_t *off)

{

/*将global_var从内核空间复制到用户空间*/

if (copy_to_user(buf, &global_var, sizeof(int)))

{

return - EFAULT;

}

return sizeof(int);

}

static ssize_t globalvar_write(struct file *filp, const char *buf, size_t len, loff_t*off) {

/*将用户空间的数据复制到内核空间的global_var*/

if (copy_from_user(&global_var, buf, sizeof(int)))

{

return - EFAULT;

}

return sizeof(int);

}

module_init(globalvar_init);

module_exit(globalvar_exit);

4.2.同目录下建立Makefile文件,编译,加载

4.3.编写读写测试程序read.c 和write.c

实验三:linux下基于定时器及事件驱动编程的贪吃蛇游戏

1. 实验目的

本次实验我主要想了解linux下基于定时器及事件驱动编程相关技巧,通过实现一个贪吃蛇的游戏来体现。

2. 实验内容

本次开放实验我选择在linux下完成一个经典的贪吃蛇游戏。主要用到了linux中的时钟和定时器机制(#include)以及事件驱动编程

(#include)。

我通过使用settitimer(...)函数来设置linux系统下的定时器的值来控制对屏幕的刷新。当定时时间到达时,会发送一个SIGALRM信号,这个信号会被signal函数所捕捉,从而可以触发自己定义的屏幕刷新函数(show())。屏幕刷新函数是通过使用Curses库函数实现的,Curses库函数专门用来进行linux/unix终端环境下的屏幕界面处理及I/O处理,在本次游戏编程中我是这样使用的:利用初始化和重置函数initscr()对物理界面进行初始化,利用move ()移动光标位置,利用printw()函数在屏幕上进行显示,利用cbreak(),getch()函数读取按键送来的键值,还有关闭中断、退出curses库等操作……将这两种技术有机的结合起来,便可以控制游戏的基本运行和界面显示了。

接下来我主要介绍一下本游戏的算法。蛇的表示是用一个带头尾结点的双向链表来表示的,

蛇的每一次前进,都是在链表的头部增加一个节点,在尾部删除一个节点

,如果蛇吃了一个食物,那就不用删除节点了。另外,我加入初始化蛇长度的变量(length)还有控制游戏难度的变量(level,随length增大而增大)。

利用随机种子产生每一次食物出现的随机位置,加入了蛇头碰到边界死亡和触碰到自己身体死亡的判断条件,当蛇达到一定长度后,成功通关,游戏结束。

3. 实验原理

Curses实际上是一个函数开发包,专门用来进行UNIX下终端环境下的屏幕界面处理以及I/O处理。通过这些函数库,C和C++程序就可以控制终端的视频显示以及输入输出。使用curses包中的函数,用户可以非常方便的创建和操作窗口,使用菜单以及表单,而且最为重要的一点是使用curses包编写的程序将独立于各种具体的终端,这样的一个直接的好处就是程序具有良好的移植性。这一点在网络上显得尤其重要,因为你面对的可能是上百种终端,如果为每一个终端都专门重新编写一套新的程序,那么复杂程度出乎想象,而且几乎不可能。为了能够达到这样的目的,curses包使用了终端描述数据库(Terminal Description Databases)terminfo(TERMinal INFOrmation database)或者termcap(TERMinal CAPabilitie database),这两个数据库里存放了不同终端的操作控制码和转义序列以及其余相关信息,这样当使用每一个终端的时候,curses将首先在终端描述数据库中查找是否存在该类型的终端描述信息,如果找到则进行适当的处理。如果数据库中没有这种终端信息,则程序无法在该终端上运行,除非用户自己增加新的终端描述。

内核定时器是内核用来控制在未来某个时间点(基于jiffies)调度执行某个函数的一种机制,其实现位于 和 kernel/timer.c 文件中。

被调度的函数肯定是异步执行的,它类似于一种“软件中断”,而且是处于非进程的上下文中,所以调度函数必须遵守以下规则:

1) 没有 current 指针、不允许访问用户空间。因为没有进程上下文,相关代码和被中断的进程没有任何联系。

2) 不能执行休眠(或可能引起休眠的函数)和调度。

3) 任何被访问的数据结构都应该针对并发访问进行保护,以防止竞争条件。

内核定时器的调度函数运行过一次后就不会再被运行了(相当于自动注销),但可以通过在被调度的函数中重新调度自己来周期运行。

4. 实验步骤

4.1.编写程序源代码mysnake1.0.c

//mysnake1.0.c

//编译命令:cc mysnake1.0.c -lcurses -o mysnake1.0

//用方向键控制蛇的方向

#include

#include

#include

#include

#include

#define NUM 60

struct direct //用来表示方向的

{

int cx;

int cy;

};

typedef struct node //链表的结点

{

int cx;

int cy;

struct node *back;

struct node *next;

}node;

void initGame(); //初始化游戏

int setTicker(int); //设置计时器

void show(); //显示整个画面

void showInformation(); //显示游戏信息(前两行)

void showSnake(); //显示蛇的身体

void getOrder(); //从键盘中获取命令

void over(int i); //完成游戏结束后的提示信息

void creatLink(); //(带头尾结点)双向链表以及它的操作void insertNode(int x, int y);

void deleteNode();

void deleteLink();

int ch; //输入的命令

int hour, minute, second; //时分秒

int length, tTime, level; //(蛇的)长度,计时器,(游戏)等级

struct direct dir, food; //蛇的前进方向,食物的位置

node *head, *tail; //链表的头尾结点

int main()

{

initscr();

initGame();

signal(SIGALRM, show);

getOrder();

endwin();

return 0;

}

void initGame()

{

cbreak(); //把终端的CBREAK模式打开

noecho(); //关闭回显

curs_set(0); //把光标置为不可见

keypad(stdscr, true); //使用用户终端的键盘上的小键盘

srand(time(0)); //设置随机数种子

//初始化各项数据

hour = minute = second = tTime = 0;

length = 1;

dir.cx = 1;

dir.cy = 0;

ch = 'A';

food.cx = rand() % COLS;

food.cy = rand() % (LINES-2) + 2;

creatLink();

setTicker(20);

}

//设置计时器(这个函数是书本上的例子,有改动)

int setTicker(int n_msecs)

{

struct itimerval new_timeset;

long n_sec, n_usecs;

n_sec = n_msecs / 1000 ;

n_usecs = ( n_msecs % 1000 ) * 1000L ;

new_timeset.it_https://www.doczj.com/doc/2211134759.html,_sec = n_sec; /* set reload */

new_timeset.it_https://www.doczj.com/doc/2211134759.html,_usec = n_usecs; /* new ticker value */ n_msecs = 1;

n_sec = n_msecs / 1000 ;

n_usecs = ( n_msecs % 1000 ) * 1000L ;

new_timeset.it_https://www.doczj.com/doc/2211134759.html,_sec = n_sec ; /* store this */

new_timeset.it_https://www.doczj.com/doc/2211134759.html,_usec = n_usecs ; /* and this */

return setitimer(ITIMER_REAL, &new_timeset, NULL);

}

void showInformation()

{

tTime++;

if(tTime >= 1000000) //

tTime = 0;

if(1 != tTime % 50)

return;

move(0, 3);

//显示时间

printw("time: %d:%d:%d %c", hour, minute, second);

second++;

if(second > NUM)

{

second = 0;

minute++;

}

if(minute > NUM)

{

minute = 0;

hour++;

}

//显示长度,等级

move(1, 0);

int i;

for(i=0;i

addstr("-");

move(0, COLS/2-5);

printw("length: %d", length);

move(0, COLS-10);

level = length / 3 + 1;

printw("level: %d", level);

}

//蛇的表示是用一个带头尾结点的双向链表来表示的,

//蛇的每一次前进,都是在链表的头部增加一个节点,在尾部删除一个节点//如果蛇吃了一个食物,那就不用删除节点了

void showSnake()

{

if(1 != tTime % (30-level))

//判断蛇的长度有没有改变

bool lenChange = false;

//显示食物

move(food.cy, food.cx);

printw("@");

//如果蛇碰到墙,则游戏结束

if((COLS-1==head->next->cx && 1==dir.cx)

|| (0==head->next->cx && -1==dir.cx)

|| (LINES-1==head->next->cy && 1==dir.cy)

|| (2==head->next->cy && -1==dir.cy))

{

over(1);

return;

}

//如果蛇头砬到自己的身体,则游戏结束

if('*' == mvinch(head->next->cy+dir.cy, head->next->cx+dir.cx) ) {

over(2);

return;

}

insertNode(head->next->cx+dir.cx, head->next->cy+dir.cy);

//蛇吃了一个“食物”

if(head->next->cx==food.cx && head->next->cy==food.cy) {

lenChange = true;

length++;

//恭喜你,通关了

if(length >= 50)

{

over(3);

return;

}

//重新设置食物的位置

food.cx = rand() % COLS;

food.cy = rand() % (LINES-2) + 2;

}

if(!lenChange)

{

move(tail->back->cy, tail->back->cx);

printw(" ");

deleteNode();

}

move(head->next->cy, head->next->cx);

相关主题
文本预览
相关文档 最新文档