Dorapocket
moveonmoveonmoveonmoveon
Dorapocket

操作系统实验系列

操作系统实验系列

实验一 Linux内核编译及添加系统调用

设计目的和要求

  • 添加一个系统调用,实现对指定进程的 nice 值的修改或读取功能,并返回进程最新的 nice 值及优先级 prio。
  • 写一个简单的应用程序测试添加的系统调用。
  • 若程序中调用了 Linux 的内核函数,要求深入阅读相关函数源码。

实验步骤

修改内核文件

  1. 安装Linux虚拟机(略过)
  2. 获取Linux内核源码 搜索:The Linux Kernel Archives
  3. 运行指令查看文件是否存在

$ cd ~/Downloads
$ ls

  1. 解压内核源码

tar xvJf linux-4.19.25.tar.xz -C /usr/src

  1. 查看系统调用表 分配新的系统调用号,运行以下指令

nano /usr/src/linux-4.19.25/arch/x86/entry/syscalls/syscall_64.tbl

  1. 添加自己的系统调用

335 64 mysetnice __x64_sys_mysetnice

  1. 声明系统调用服务程序原型

nano /usr/src/linux-4.19.25/include/linux/syscalls.h

  1. 在最后加上自己的服务程序原型

asmlinkage long sys_mysetnice(pid_t pid, int flag, int nicevaluse, void __user* prio, void __user* nice);

  1. 实现系统调用服务程序

nano /usr/src/linux-4.19.25/linux/kernel/sys.c

SYSCALL_DEFINE5(mysetnice, pid_t, pid, int, flag, int, nicevalue, void __user *,
                prio, void __user *, nice) {
    int cur_prio, cur_nice;
    struct pid *ppid;
    struct task_struct *pcb;
    // 通过进程PID号找到进程的PID结构体
    ppid = find_get_pid(pid);
    // 通过进程的PID结构体,找到与之对应的进程控制块
    pcb = pid_task(ppid, PIDTYPE_PID);
    // 如果flag=1则修改进程的nice值为nicevalue
    if (flag == 1) {
        set_user_nice(pcb, nicevalue);
    }  // flag既不为1也不为0的时候,即flag出错,此时返回EFAULT
    else if (flag != 0) {
        return EFAULT;
    }
    // 获取进程当前的最新nice值和prio值
    cur_prio = task_prio(pcb);
    cur_nice = task_nice(pcb);
    // 利用copy_to_user()函数将内核空间的数据复制到用户空间
    copy_to_user(prio, &cur_prio, sizeof(cur_prio));
    copy_to_user(nice, &cur_nice, sizeof(cur_nice));
    printk("Hello! This is LiGuoyu Syscall No.18061615");
    return 0;
}

编译内核

  1. 安装包

sudo apt-get install libncurses5-dev make openssl libssl-dev bison flex libelf-dev

  1. 定位到源代码所在文件夹:

cd /xx/xx/linux-x.xx.xx
make menuconfig

  1. 出现界面配置菜单,左右键移动下方光标选中 Save,按 Enter 结束。点击 Ok 和之后出现的 Exit。
  2. 编译内核

sudo make clean
sudo make -j4

  1. 安装内核

sudo make modules_install
sudo make install

  1. 重启计算机 编写用户态程序
#include <unistd.h>
#include <sys/syscall.h>
#include <stdio.h>
#define _SYSCALL_MYSETNICE_ 335
#define EFALUT 14
int main()
{
    int pid, flag, nicevalue;
    int prev_prio, prev_nice, cur_prio, cur_nice;
    int result;

    printf("Please input variable(pid, flag, nicevalue): ");
    scanf("%d%d%d", &pid, &flag, &nicevalue);

    result = syscall(_SYSCALL_MYSETNICE_, pid, 0, nicevalue, &prev_prio,
                     &prev_nice);
    if (result == EFALUT)
    {
        printf("ERROR!");
        return 1;
    }

    if (flag == 1)
    {
        syscall(_SYSCALL_MYSETNICE_, pid, 1, nicevalue, &cur_prio, &cur_nice);
        printf("Original priority is: [%d], original nice is [%d]\n", prev_prio,
               prev_nice);
        printf("Current priority is : [%d], current nice is [%d]\n", cur_prio,
               cur_nice);
    }
    else if (flag == 0)
    {
        printf("Current priority is : [%d], current nice is [%d]\n", prev_prio,
               prev_nice);
    }

    return 0;
}

并进行测试

实验心得

本次实验使我明白了系统用户态和核心态之间的差异,了解了Linux内核架构系统调用的组织形式和方法。而且尝试自己编译了自己的内核,对Linux系统有了更深的理解,受益匪浅。

实验二 Linux内核模块编程

设计目的和要求

Linux 内核采用了整体结构,上一个实验体会了编译内核时间的冗长与繁杂,一步错就要重新编译,这虽然提高了效率,但同时也让后续的维护变得困难,在这个基础上,Linux 内核引入了动态模块机制加以改进。
– 设计一个模块,要求列出系统中所有内核线程的程序名、PID、进程状态、进程优先级、父进程的 PID。
– 设计一个带参数的模块,其参数为某个进程的 PID 号,模块的功能是列出该进程的家族信息,包括父进程、兄弟进程和子进程的程序名、PID 号及进程状态。
– 请根据自身情况,进一步阅读分析程序中用到的相关内核函数的源码实现。
补充要求:展示出进程树图,并可打印单个进程进程树

实验步骤

  1. 编写内核模块程序
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/sched/signal.h>
#include <linux/sched.h>
MODULE_LICENSE("GPL");
static int __init show_all_kernel_thread_init(void)
{
    struct task_struct *p; //进程结构体
    printk("%-20s%-6s%-6s%-6s%-6s", "Name", "PID", "State", "Prio", "PPID");
    printk("--------------------------------------------");
    for_each_process(p)//遍历内核进程
    {
        if (p->mm == NULL)
        {
            printk("%-20s%-6d%-6d%-6d%-6d", p->comm, p->pid, p->state, p->prio,
                   p->parent->pid);//格式输出
        }
    }
    return 0;
}
static void __exit show_all_kernel_thread_exit(void)
{
}
module_init(show_all_kernel_thread_init);//模块入口
module_exit(show_all_kernel_thread_exit);//模块销毁

其中关键函数是

for_each_process(p)

此函数用于循环遍历系统中的PCB控制块。其中Linux PCB内容可自行查询
2. 在此基础上由PCB中进程父子进程PCB指针生成进程树并显示

//进程树生成关键函数
char* print_process(struct task_struct *p,int depth,char* buffer_pointer, int *indenti,int *isnewline);
char* visit(struct task_struct *p,int depth,char* buffer_pointer,int* isnewline);

void sys_mypstree(char *buffer2copy){
    int depth = 0;
    int isnewline = 1;
    struct task_struct *p = current;
    char* buffer = (char*)vmalloc(64*1024);
    while(p -> pid != 1)
        p = p -> parent;
    for_each_process(p){
        if(p->pid==spid){
            //p=p->parent;
            visit(p->real_parent,depth,buffer,&isnewline);
        }
    }
    printk(buffer);
    //copy_to_user(buffer2copy,buffer,64*1024);         
}
char* print_process(struct task_struct *p,int depth,char* buffer_pointer,int *indent,int* isnewline){
    //printk("print_process 0 : depth : %d  indent : %d isnewline: %d \n",depth,*indent,*isnewline);
    char* ptr = buffer_pointer;
    int i = 0;
    if(p == NULL){
        *ptr = '\n';
        ptr++;
        return ptr;
    }
    if(*isnewline == 1){
        *indent = 0;
        while(i < depth){
            *ptr = ' ';
            ptr++;
            i ++;
            (*indent) ++;
        }
    }
    *isnewline = 0;
    if(depth != 0){
        *ptr = '-';
        ptr ++;
        *ptr = '-';
        ptr ++;
        *ptr = '|';
        ptr++;
        *ptr = '-';
        ptr++;
        *ptr = '-';
        ptr ++;
        (*indent) += 5;
    }
    memcpy(ptr,p->comm,strlen(p->comm));
    ptr += strlen(p->comm);
    (*indent) += strlen(p->comm);
    return ptr;
}
char* visit(struct task_struct *p,int depth,char* buffer_pointer,int* isnewline){
    struct list_head *pos;
    struct task_struct *childTask;
    char* ptr = NULL;
    int indent = depth;
    ptr =  print_process(p,depth,buffer_pointer,&indent,isnewline);

    if(p -> pid == 1)
        depth = 4;
    else
        depth = indent;
    if((&(p -> children))->next == &(p->children)){
        *ptr = '\n';
        ptr ++;
        *isnewline = 1;
        return ptr;
    }
    list_for_each(pos,&(p -> children)){
        childTask = list_entry(pos,struct task_struct,sibling);
        ptr = visit(childTask,depth,ptr,isnewline);
            //printk("%s",ptr);
    }
    return ptr;
}
static int __init init_pstree(void){
    printk("HI!");
    char* a = (char*)vmalloc(64*1024);
    sys_mypstree(a);
    printk(a);
return 0;
}

  1. 编写Makefile生成.KO文件
  2. 装载内核进行测试

安装

sudo insmod show_all_kernel_thread.ko
sudo insmod show_task_family pid=xxxx
#卸载
sudo rmmod show_all_kernel_thread
sudo rmmod show_task_family

实验心得

本次实验着重于对进程链表和进程控制块PCB的理解。通过实际操作查看了进程控制块的内容,了解了进程控制块的实际用途,并再内核代码层面模拟了pstree的生成。

实验三 Linux进程管理

设计目的和内容要求

  1. 通过对Linux进程控制的相关系统调用的编程应用,加深对进程概念的理解,明确进程和程序的联系和区别,理解进程并发执行的具体含义。
  2. 通过对Linux管道通信机制、消息队列通信机制、共享内存通信机制的应用,加深对不同类型进程通信方式的理解。
  3. 通过对Linux的Posix信号量及IPC信号量的应用,加深对信号量同步机制的理解。
  4. 请根据自身情况,进一步阅读分析程序中用到的相关内核函数的源码实现。
    实验任务:

– 实现一个模拟的shell 补充:在书本基础上额外添加grep、find指令
– 实现一个管道通信程序 补充:在书本基础上增加进程有名管道通信
– 利用Linux的消息队列通信机制实现两个线程之间的通信
– 利用Linux的共享内存通信机制实现两个进程间的通信 补充:增加双向通信内容

实验步骤:

实现一个模拟的shell

主要思路:利用内存替换 使用exec函数执行后将结果返回控制台
关键代码

void do_execvp(int argc,char* argv[])
{
    int flag=0;
    if(strcmp(argv[0],"cmd1")==0){
        argv[0]="./cmd1";
        flag=1;
    }else if(strcmp(argv[0],"cmd2")==0){
        argv[0]="./cmd1";
        flag=1;
    }else if(strcmp(argv[0],"cmd3")==0){
        argv[0]="./cmd1";
        flag=1;
    }else if(strcmp(argv[0],"grep")==0){
        flag=1;
    }else if(strcmp(argv[0],"find")==0){
        flag=1;
    }
if(flag==1){
    if(fork() == 0)//子进程会进入if
    {
        execvp(argv[0],argv);//内存替换
        perror("execvp");
        exit(0);//perror报错并退出程序
    }
    wait(NULL);//防止父进程和子进程并发运行
}else{
    printf("exp3-1 shell>> Command not found\n");
}
}

实现一个管道通信程序

pipe方法

关键:信号量的使用。

sem_t *w,*r;
int main(){
    int filedis[2];
    pipe(filedis);
    char buf[256];
    char *name1="writer";
    char *name2="reader";
    int x,y;
    w=sem_open(name1,O_CREAT,0666,1);//写信号量
    r=sem_open(name2,O_CREAT,0666,0);//读信号量
    //sem_getvalue(w,&x);
    //if(x==0) sem_post(w);
    pid_t pid1,pid2,pid3;
    pid1=1;pid2=1;pid3=1;//初始化pid
    pid1=fork();
    if(pid1>0) pid2=fork();
    if(pid1>0&&pid2>0) pid3=fork();//连续创建三个子进程
    if(pid1==0){
        close(filedis[0]);//0为读管道
        sem_wait(w);//写管道阻塞
        printf("This is child 1\n");
        write(filedis[1],"Hey I am Child 1 ",strlen("Hey I am Child 1 "));
        sem_post(w);
        sem_post(r);//写好了
        exit(0);
    }
    if(pid2==0){
        close(filedis[0]);
        sem_wait(w);
        printf("This is child 2\n");
        write(filedis[1],"Hey I am Child 2 ",strlen("Hey I am Child 2 "));
        sem_post(w);
        sem_post(r);
        exit(0);
    }       
    if(pid3==0){
        close(filedis[0]);
        sem_wait(w);
        printf("This is child 3\n");
        write(filedis[1],"Hey I am Child 3 ",strlen("Hey I am Child 3 "));
        sem_post(w);
        sem_post(r);
        exit(0);
    }
    if(pid1>0&&pid2>0&&pid3>0){
        sem_wait(r);//三个都好了
        sem_wait(r);
        sem_wait(r);
        sem_wait(w);
        printf("This is father ,I got child messages:");
        close(filedis[1]);//1为写
        read(filedis[0],buf,sizeof(buf));
        printf("%s\n",buf);
        sem_post(w);
    }   
}

令:测试pipe大小的程序 思路:将管道写满,直到不能写入,记录写入的字节数。

int main()
{
    int _pipe[2];
    if(pipe(_pipe)==-1)
    {
        printf("create pipr error\n");
        return 1;
    }
    int ret;
    int count=0;
    int flag=fcntl(_pipe[1],F_GETFL);
    fcntl(_pipe[1],F_SETFL,flag|O_NONBLOCK);
    while(1)
    {
        ret=write(_pipe[1],"0",1);
        if(ret==-1)
        {
            printf("pipe full!\n");
            break;
        }
        count++;
    }
    printf("size=%d\n",count);
    return 0;
}

fifo方法(有名管道通信)

主要思路:创建FIFO文件,通过文件进行读写传输
server.c

int main()
{
printf("******exp3-2 FIFO MSG Server******\n");
int ret;
       ret=mkfifo("./fifo",0777);
       if(ret<0)
       {
              printf("creat fifo failure\n");
       }
       printf("Creat fifo sucess\n");
       int fd;
       char rdbuf[128]={0};
       fd=open("./fifo",O_RDONLY);
       if(fd<0)
       {
              printf("Do not have fifo files! Generate first!\n");
       }
    printf("Server Ready!");
       while(1)
       {
              read(fd,rdbuf,128);
              printf("recive:%s",rdbuf);
              if(!strcmp(rdbuf,"quit\n"))
              break;
              memset(rdbuf,0,128);
       }
       sleep(1);
       close(fd);
       return 0;
}

client.c

int main()
{
printf("******EXP3-2 FIFO MSG Client******");
int fd;
char wrbuf[128];
fd=open("./fifo",O_WRONLY);
if(fd<0)
{
       printf("open fifo failure\n");
       return -1;
}
while(1)
{
       memset(wrbuf,0,sizeof(wrbuf));
       fgets(wrbuf,128,stdin);
       write(fd,wrbuf,strlen(wrbuf));
       if(!strcmp(wrbuf,"quit\n"))
       {
              break;
       }
}
sleep(1);
close(fd);
return 0;
}

利用Linux的消息队列通信机制实现两个线程之间的通信

思路:创建Linux消息队列实现线程通信
源代码较长,仅给出关键代码
具体步骤为 创建信号量 创建消息队列 创建线程 线程根据信号量进行传输数据

int main(){
           int msqid = msgget((key_t)8088, 0666 | IPC_CREAT);//创建消息队列
           msgctl(msqid, IPC_RMID, 0);
           //清除信号量
           sem_unlink("mutex");
           sem_unlink("sender1_over");
           sem_unlink("sender2_over");
           sem_unlink("receive1_over");
           sem_unlink("receive2_over");
//创建信号量
           sem_t *mutex = sem_open("mutex", O_CREAT | O_RDWR, 0666, 0);
           sem_t *sender1_over = sem_open("sender1_over", O_CREAT | O_RDWR, 0666, 0);
           sem_t *receive1_over = sem_open("receive1_over", O_CREAT | O_RDWR, 0666, 0);
           sem_t *sender2_over = sem_open("sender2_over", O_CREAT | O_RDWR, 0666, 0);
           sem_t *receive2_over = sem_open("receive2_over", O_CREAT | O_RDWR, 0666, 0);

           pthread_t pt1,pt2,pt3; //创建线程
           pthread_create(&pt1, NULL, sender1, NULL);
           pthread_create(&pt2, NULL, sender2, NULL);
           pthread_create(&pt3, NULL, receive, NULL);
           sem_post(mutex);
           pthread_join(pt1, NULL);
           pthread_join(pt2, NULL);
           pthread_join(pt3, NULL);
           return 0;
}

利用Linux的共享内存通信机制实现两个进程间的通信

思路:在书本的基础上,再开一个信号量用来标识消息传输方向,实现“乒乓”memory,从而进行双向通信
具体代码过长,仅给出关键代码。

int main(){
        char buf[50];

        int shmid1 = shmget((key_t)8890, 50, IPC_CREAT | 0666); //创建共享内存8888
    int shmid2 = shmget((key_t)8891, 50, IPC_CREAT | 0666); //创建共享内存8889
        shmctl((key_t)8890, IPC_RMID, NULL);//控制 写null
    shmctl((key_t)8891, IPC_RMID, NULL);//控制 写null
        sem_unlink("mutex1");//删除两个信号量
        sem_unlink("mutex2");
    sem_unlink("mutex3");
    sem_unlink("mutex4");
        sem_t *mutex1 = sem_open("mutex1", O_CREAT | O_RDWR, 0666, 0);//创建信号量
        sem_t *mutex2 = sem_open("mutex2", O_CREAT | O_RDWR, 0666, 0);
    sem_t *mutex3 = sem_open("mutex3", O_CREAT | O_RDWR, 0666, 0);
    sem_t *mutex4 = sem_open("mutex4", O_CREAT | O_RDWR, 0666, 0);
        shmid1 = shmget((key_t)8890, 50, IPC_CREAT | 0666);//创建
    shmid2 = shmget((key_t)8891, 50, IPC_CREAT | 0666);//创建

        if( shmid1 == -1 || shmid2==-1){
            printf("create failed");
            exit(-1);
        }
    pid_t pid1=fork();
    if(pid1==0){
while(1){
        sem_wait(mutex3);
        char *n = (char *)shmat(shmid2, NULL, 0);//启动对共享内存访问 拉到当前地址空间
        printf("\nreceive:%s\n",n);
        strcpy(buf, "over");
            memcpy(n,buf,sizeof(buf));
        shmdt(shmid2);//分离共享内存
        sem_post(mutex4);
}
    }
    if(pid1>0){
while(1){
        char *m = (char *)shmat(shmid1, NULL, 0);//启动对共享内存访问 拉到当前地址空间
            printf("\nwriting:");
            scanf("%s", buf);
            memcpy(m,buf,sizeof(buf));
        shmdt(shmid1);//分离共享内存
            sem_post(mutex1); //m1标志1已经发送

        sem_wait(mutex2);
        m = (char *)shmat(shmid1, NULL, 0);//启动对共享内存访问 拉到当前地址空间
        printf("\nreturn:%s\n", m);//返回的信息
        shmdt(shmid1);
}
    }
        shmctl((key_t)8890, IPC_RMID, NULL);//删除共享内存
    shmctl((key_t)8891, IPC_RMID, NULL);//删除共享内存
        sem_unlink("mutex1");//删除信号量
        sem_unlink("mutex2");
    sem_unlink("mutex3");
    sem_unlink("mutex4");
        return 0;
}

实验心得

本次实验通过对Linux进程控制的相关系统调用的编程应用,加深对进程概念的理解,明确进程和程序的联系和区别,理解进程并发执行的具体含义。通过对Linux管道通信机制、消息队列通信机制、共享内存通信机制的应用,加深对不同类型进程通信方式的理解。通过对Linux的Posix信号量及IPC信号量的应用,加深对信号量同步机制的理解。

实验五 简单文件系统的实现

设计目的和内容要求

通过具体的文件存储空间的管理、文件物理结构、目录结构和文件操作的实现,加深对文件系统内部的数据结构、功能以及实现过程的理解。
– 在内存中开辟一个虚拟磁盘空间作为文件存储分区,在其上实现一个简单的基于多级目录的单用户单任务系统中的文件系统。在推出该文件系统的使用时,应将虚拟磁盘上的内容以一个文件的方式保存到磁盘上,一遍下次可以将它恢复到内存的虚拟磁盘中
– 文件物理结构可采用显式链接或其他结构
– 空闲磁盘空间的管理可选择FAT表、位示图或其他办法
– 文件目录结构采用多级目录结构。为简单起见,可以不使用索引结点,每个目录项应包含文件名、物理地址、长度等信息,还可以通过目录项实现对文件的读和写的保护
– 需要提供以下操作命令:my_format、my_mkdir、my_rmdir、my_ls、my_cd、my_create、my_open、my_close、my_write、my_read、my_rm、my_exitsys
补充要求1:打印FAT数据
补充要求2:实现文件顺序、随机读写
补充要求3:写入多于一个扇区数据或目录信息
补充要求4:实现扇区大小可变

实验步骤

本代码较长,开发步骤总体可分为三个模块:具体功能实现模块、文件操作模块、shell模块。具体功能试验模块实现了各个操作的函数,文件操作模块通过对linux系统中文件操作模拟磁盘读写操作、shell模块解析用户输入命令行,并调用相关函数。
具体代码可见:

实验结果

实验心得

本实验通过自己动手写文件系统,让我加深了对文件系统内部的数据结构、功能以及实现过程的理解。并进一步熟悉了linux上的文件操作。了解了文件系统的运作过程和FAT表的具体含义,乐在其中。

发表回复

textsms
account_circle
email

Dorapocket

操作系统实验系列
实验一 Linux内核编译及添加系统调用 设计目的和要求 添加一个系统调用,实现对指定进程的 nice 值的修改或读取功能,并返回进程最新的 nice 值及优先级 prio。 写一个简单的应用程…
扫描二维码继续阅读
2019-12-11