实验1-1-1内核编译与安装
实验目的
采用Ubuntu、openEuler等Linux操作系统,参考实验指导书,编译Linux内核源码,运行操作系统
资料2-华为实验指导/01-3《openEuler操作系统》内核实验手册-树莓派版v1.0/第1章 安装及内核编译(内核模块)/第一章 实验课指导书-openEuler操作系统安装与内核编译安装
资料1-自定义实验指导/附件3-1《操作系统》课程设计指导-嵌入式Linux关键技术:5.1节 Linux启动过程优化
实验步骤
1.查看当前内核版本
uname -a
2.下载内核源码
我下载的内核版本是4.4
3.使用如下命令解压
tar -xavf ...
4.内核配置
sudo make menuconfig
5.编译内核
sudo make -j4
6.安装内核
sudo make modules_install
sudo make install
7.重启
sudo reboot
8.查看内核版本,可以看到内核版本发生了改变
实验总结
由于我用的系统是ubuntu,因此参考了网上的资料。实验步骤参考(14条消息) ubuntu上编译Linux内核步骤_ubuntu编译内核_errorproducer的博客-CSDN博客
踩坑1:
原本下载的内核版本是linux-4.14.312,但是在编译过程中报错You are building kernel with non-retpoline compiler, please update your compiler
。参考网上资料(12条消息) ubuntu编译错误:You are building kernel with non-retpoline compiler, please update your compiler_ajuse的博客-CSDN博客,解决方法有两个,升级gcc版本或者降低内核版本。由于升级gcc的编译过程长达1-2小时,我选择降低内核版本。最简单的方法就是选择和当前内核相同的内核版本。参考如下连接(12条消息) ubuntu 系统获取和阅读 linux 源码_ubuntu源码在哪里_chip1234的博客-CSDN博客,我下载了4.4.0版本的源码,编译成功。
踩坑2:
重启之后出现问题The system is running in low-graphics modeYour screen,graphics card,and input device setting could not be detected correctly.
解决方式:
实验1-1-2-1 内核模块实验-1/模块编程
实验目的
了解内核模块实现机制,参照:
资料2-华为实验指导/01-3《openEuler操作系统》内核实验手册-树莓派版v1.0/第1章 安装及内核编译(内核模块)/第一章 实验课指导书-openEuler操作系统安装与内核编译安装
资料1-自定义实验指导/附件1-2-1 Linux内核编译及添加系统调用
,完成“Hello World”实验
建议:采用gettime等函数,测量内核某段代码执行时间
helloworld.c
#include<linux/module.h>
MODULE_LICENSE("GPL");
int __init hello_init(void)
{
printk("hello init\n");
printk("hello,world!\n");
return 0;
}
void __exit hello_exit(void)
{
printk("hello exit\n");
}
module_init(hello_init);
module_exit(hello_exit);
Makefile
ifneq ($(KERNELRELEASE),)
obj-m := helloworld.o
else
KERNELDIR ?= /usr/src/linux-source-4.4.0/linux-source-4.4.0
PWD := $(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif
.PHONY:clean
clean:
-rm *.mod.c *.o *.order *.symvers *.ko
实验步骤
1.编译前在一个新建目录下放好helloworld.c
和Makefile
文件。
记住千万不要动源码目录下的Makefile
,也不要手贱执行make clean
。好不容易编译好的源码可能就被破坏掉了,会出现各种奇怪的bug。
2.执行make
,编译源码。
3.编译完成之后,目录下出现如下所示的.ko文件。
4.模块的加载
insmod helloworld.kodmesg | tail -n 2lsmod | grep helloworld
5.模块的卸载
rmmod helloworlddmesg | tail -n 2lsmod | grep helloworld
实验总结
这个实验是我消耗时间最长的实验,本来应该是没什么难度的实验,偏偏就难在了实验指导书上没有告诉我们要单独新建一个文件夹来做,我在源码目录下做之后会出现各种问题。以下是其中的一些问题:
问题1
我有以上的Makefile文件,与helloworld.c一同放在内核源码目录/usr/src/linux-source-4.4.0/linux-source-4.4.0下。但是当我执行make命令时,显示如下错误:
root@ubuntu:/usr/src/linux-source-4.4.0/linux-source-4.4.0# makemake -C /usr/src/linux-source-4.4.0/linux-source-4.4.0 M=/usr/src/linux-source-4.4.0/linux-source-4.4.0 modulesmake[1]: Entering directory '/usr/src/linux-source-4.4.0/linux-source-4.4.0'make[1]: *** No rule to make target 'modules'. Stop.make[1]: Leaving directory '/usr/src/linux-source-4.4.0/linux-source-4.4.0'Makefile:7: recipe for target 'default' failedmake: *** [default] Error 2
并且如果新建一个目录,在目录下存放helloword.c与Makefile文件,执行make报错内容相同。
解释
这是因为我们删除了源码目录下原本的Makefile文件,导致所有的make命令都失效了(make clean除外)。此时无论使用命令make menuconfig
还是make oldconfig
还是make -j4
等等都一样无法执行,显示:
make: *** No rule to make target xxx
问题2
我恢复了源码目录下的Makefile,并且新建了一个文件夹用来编译模块,为什么make会报警告:
WARNING: Symbol version dump ./Module.symvers is missing; modules will have no dependencies and modversions.
解释
首先如果上网搜,大家的解释是源码目录下缺少Module.symvers文件,把当前目录下的Module.symvers文件移动到源码目录下就好了。没错这可以解决一点问题,就是不会报warning了,但是在下一步你一样会卡主,报错:
insmod: ERROR: could not insert module helloworld.ko: Invalid module format
总之我后来发现这是因为我删掉了源码目录下的Module.symvers文件,大概是用rm误删了。这个文件还有其他一系列文件都是源码编译的时候生成的,如果拿你自己生成的替换他肯定是不行的。
解决方法
如果Makefile丢失了的话还可以补救,可以试试下载和自己之前编译过的内核相同版本的内核源码,然后把Makefile文件复制到你编译好的内核源码目录下。(我就是这么做的)如果还手贱删掉了其他的文件,或者执行了make clean,估计就得重新编译了。(蓝过
在配环境这件事情上如果真的配不好还是放弃比较划算……不要学我一样配了一个通宵,哭着问chatgpt也不管用。
实验1-1-4 信号
实验目的
参照以下资料,编写信号捕获程序,实现中断和异常管理
资料2-华为实验指导/01-2《openEuler操作系统》内核实验手册-Taishan物理服务器版v1.0(或: 01-3《openEuler操作系统》内核实验手册-树莓派版v1.0)/ 01-2《openEuler操作系统》内核实验手册-Taishan物理服务器版v1.0/第四章 中断和异常管理
资料2-华为实验指导\01-4 tasks_k-实验源码\4-中断和异常管理\task3
实验步骤
实现一个捕获信号程序,捕获终端按键信号
#include <signal.h>#include <unistd.h>#include <stdio.h>#include <stdlib.h>void signal_handler(int sig){ switch(sig){ case SIGINT: printf("\nGet a signal:SIGINT. You pressed ctrl+c.\n"); break; case SIGQUIT: printf("\nGet a signal:SIGQUIT. You pressed ctrl+\\.\n"); break; case SIGTSTP: printf("\nGet a signal:SIGHUP. You pressed ctrl+z.\n"); break; } exit(0);}int main(){ printf("Current process ID is %d\n", getpid()); signal(SIGINT, signal_handler); signal(SIGQUIT, signal_handler); signal(SIGTSTP, signal_handler); for(;;);}
编译
gcc catch_signal.c -o catch_signal
运行
./catch_signal
终端按下ctrl+c、ctrl+\、ctrl+z会被捕获。
实验结果
实验7-1 I/O编程
了解掌握Linux系统提供的select、epoll等I/O机制,编写客户端、服务器端程序,编程验证对比这几种网络I/O机制
参考资料
- 资料1-6-1-自定义实验指导/附件1-6-1 Linux 五种 IO 模式及 select、poll、epoll 详解(附样例代码)
- 附件1-6-2 深入理解Linux内核的Epoll机制原理与实现(代码演示)
- 附件1-6-3 网络编程学习(七)_epoll编程实例
- CSDN、知乎上的epoll编程示例
实验设计
首先我们设计一个客户端程序。该程序可以不断向服务端发送和接收tcp消息。
client.c
1、向服务端(127.0.0.1:9000)建立tcp连接
int sockfd = socket(AF_INET, SOCK_STREAM, 0);struct sockaddr_in serv_addr;memset(&serv_addr, 0, sizeof(serv_addr));serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(9000);inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addrconnect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr))
2、以下代码均卸载while循环内
while(1)
3、发送消息并接收消息
memset(recvline, 0, sizeof(recvline));memset(writeline, 0, sizeof(writeline));//发送消息printf("send to server:");fgets(writeline,sizeof(writeline),stdin);write(sockfd,writeline,strlen(writeline));len = read(sockfd,recvline,1024);if(len == 0){ printf("server is close");}printf("receive from server:%s\n",recvline);
在以下的三种服务端实现方式中,我们都将先建立一个tcp服务端socket,绑定在9000端口上。
static int start_up(const char* _ip,int _port){int sock = socket(AF_INET,SOCK_STREAM,0); if(sock < 0) { perror("socket"); exit(1); } struct sockaddr_in local; local.sin_family = AF_INET; local.sin_port = htons(_port); local.sin_addr.s_addr = inet_addr(_ip); if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0) { perror("bind"); exit(2); } if(listen(sock,10) < 0) { perror("listen"); exit(3); } return sock;}
select实验
思路:
1、应用进程和内核都可以设置rfds和wfds,应用进程设置rfds和wfds是为了通知内核去等待rfds和wfds中的文件描述符的读事件。而内核设置rfds和wfds是为了告诉应用进程哪些读事件生效。
2、如果我们要不断轮询等待文件描述符,则应用进程需要不断的重新设置readfds和writefds,因为每一次调用select,内核会修改rfds和wfds,所以我们需要在应用程序中设置一个数组array来保存程序需要等待的文件描述符。
3、如果是一个select服务器进程,则服务器进程会不断的接收新链接,每个链接对应一个文件描述符,如果想要我们的服务器能够同时等待多个链接的数据的到来,我们监听套接字listensock读取新链接的时候,我们需要将新链接的文件描述符保存到array数组中,下次轮询检测的就会将新链接的文件描述符设置进rfds和wfds中,如果有链接关闭,则将相对应的文件描述符从array数组中拿走。
缺点:
1、由于fd_set的上限是1024(x86)或2048(x64),所以select能等待的读事件的文件描述符和写事件的文件描述是有上限的,如果作为一个大型服务器,能够同时链接的客户端是远远不够的。
2、每次调用都要进行线性遍历,时间复杂度为O(n)。
3、每次调用select,都需要把fd集合从用户态拷贝到内核态。
my_select.c
关键代码及注释,完整可运行代码见附录:
int main(int argc,char* argv[]){ int listensock = start_up(argv[1],atoi(argv[2])); //先创建一个服务端tcp socket int maxfd = 0; fd_set rfds; //监听是否可读的文件描述符集合 fd_set wfds; //监听是否可写的问题件描述符集合 int array[4096]; array[0] = listensock; //用于建立连接的socket放在array的第0位 int i = 1; int array_size = sizeof(array)/sizeof(array[0]); for(; i < array_size;i++) { array[i] = -1; //初始化,将array中除了第0位以外其他位置都置为-1 } while(1) { FD_ZERO(&rfds); //在while循环的最开始,清空rfds和wfds FD_ZERO(&wfds); for(i = 0;i < array_size;++i) { /*文件描述符在形式上是一个非负整数。实际上,它是一个索引值, 指向内核为每一个进程所维护的该进程打开文件的记录表。 array[i] > 0代表array[i]中的文件描述符存在*/ if(array[i] > 0) { FD_SET(array[i],&rfds); //将array中所有文件描述符都加入rfds和wfds FD_SET(array[i],&wfds); if(array[i] > maxfd) //置maxfd为最大的文件描述符 { maxfd = array[i]; } } } switch(select(maxfd + 1,&rfds,&wfds,NULL,NULL)) { default: { int j = 0; for(; j < array_size; ++j) //遍历array { /*FD_ISSET函数用于判断指定的文件描述符是否在给定的文件描述符集合中, 若存在则返回非零值。*/ if(j == 0 && FD_ISSET(array[j],&rfds)) //如果是array[0]有读事件生效 { //listensock happened read events struct sockaddr_in client; socklen_t len = sizeof(client); int new_sock = accept(listensock,(struct sockaddr*) \ &client,&len); if(new_sock < 0)//accept failed { perror("accept"); continue; } else//accept success,成功建立tcp连接,得到new_sock { printf("get a new client%s\n", \ inet_ntoa(client.sin_addr)); fflush(stdout); int k = 1; for(; k < array_size;++k) { if(array[k] < 0) //在array中寻找一个空位存放new_sock { array[k] = new_sock; if(new_sock > maxfd) maxfd = new_sock; //置maxfd为最大的文件描述符 break; } } if(k == array_size) { printf("to many clients...\n"); //array满了 close(new_sock); } } }//j == 0 else if(j != 0 && FD_ISSET(array[j], &rfds)) //可读 { //new_sock happend read events char buf[1024]; ssize_t s = read(array[j],buf,sizeof(buf) - 1); if(s > 0)//read success { buf[s] = 0; printf("clientsay#%s\n",buf); if(FD_ISSET(array[j],&wfds)) //可写 { char *msg = "200 OK\r\n"; write(array[j],msg,strlen(msg)); } } else if(0 == s) { printf("client quit!\n"); close(array[j]); array[j] = -1; } }//else j != 0 } break; } } } return 0;}
poll实验
思路:poll 函数与 select 不同,不需要为三种事件分别设置文件描述符集,而是构造了 pollfd 结构的数组,每个数组元素指定一个描述符fd
以及对该描述符感兴趣的条件(events)。 poll 调用返回时,每个描述符fd
上产生的事件均被保存在 revents 成员内。
缺点:内核将用户的fds结构体数组
拷贝到内核中。当有事件发生时,再将所有事件都返回到fds结构体数组
中,poll只返回已就绪事件的个数,所以用户要操作就绪事件就要用轮询
的方法。
my_poll.c部分关键代码,完整代码见附录:
int main(int argc, char*argv[]){ int sock = start_up(argv[1],atoi(argv[2])); struct pollfd peerfd[1024]; peerfd[0].fd = sock; peerfd[0].events = POLLIN; int nfds = 1; int ret; int maxsize = sizeof(peerfd)/sizeof(peerfd[0]); int i = 1; int timeout = -1; for(; i < maxsize; ++i) { peerfd[i].fd = -1; } while(1) { switch(ret = poll(peerfd,nfds,timeout)) { default: { if(peerfd[0].revents & POLLIN) { struct sockaddr_in client; socklen_t len = sizeof(client); int new_sock = accept(sock,\ (struct sockaddr*)&client,&len); printf("accept finish %d\n",new_sock); if(new_sock < 0) { perror("accept"); continue; } printf("get a new client\n"); int j = 1; for(; j < maxsize; ++j) { if(peerfd[j].fd < 0) { peerfd[j].fd = new_sock; peerfd[j].events = POLLIN; break; } } if(j == maxsize) { printf("to many clients...\n"); close(new_sock); } if(j + 1 > nfds) nfds = j + 1; } for(i = 1;i < nfds;++i) { if(peerfd[i].revents & POLLIN) { printf("read ready\n"); char buf[1024]; ssize_t s = read(peerfd[i].fd,buf, \ sizeof(buf) - 1); if(s > 0) { buf[s] = 0; printf("client say#%s",buf); fflush(stdout); peerfd[i].events = POLLOUT; } else if(s <= 0) { printf("client quit!\n"); close(peerfd[i].fd); peerfd[i].fd = -1; } }//i != 0 else if(peerfd[i].revents & POLLOUT) { char *msg = "200 OK\r\n"; write(peerfd[i].fd,msg,strlen(msg)); //close(peerfd[i].fd); //peerfd[i].fd = -1; peerfd[i].events = POLLIN; } }//for }//default break; } } return 0;}
epoll实验
思路:
1、前提先包含一个头文件:#include <sys/epoll.h>
2、然后通过create_epoll来创建一个epoll,其参数为你epoll所支持的最大数量(已忽略)。这个函数会返回一个新的epoll句柄,之后的所有操作将通过这个句柄来进行操作。
3、之后在主循环里面,每次调用epoll_wait来查询所有的网络接口,看哪一个可以读,哪一个可以写了。基本的语法为:
nfds = epoll_wait(kdpfd, events, maxevents, -1);
其中kdpfd为用epoll_create创建之后的句柄。
events是一个epoll_event*的指针,当epoll_wait这个函数操作成功之后,epoll_events里面将储存所有的读写事件。
max_events是当前需要监听的所有socket句柄数。
最后一个timeout是 epoll_wait的超时,为0的时候表示马上返回,为-1的时候表示一直等下去,直到有事件发生,为任意正整数的时候表示等这么长的时间,如果一直没有事件,则等待。
4、epoll_wait
函数之后用一个循环,遍利events里的所有的事件。
server.c
每次接收到消息后直接发送消息
my_epoll.c
每次接收到消息后用epoll_ctl(epollfd,EPOLL_CTL_MOD, evs[i].data.fd,&ev);
将监听的事件改为EPOLLOUT
,在下一轮轮训就会监听到可写事件并回送消息,此时再把监听事件改回EPOLLIN
继续监听。
int main(int argc, char*argv[]){ int sock = start_up(argv[1],atoi(argv[2])); int epollfd = epoll_create(256); /*作用:创建一个epoll句柄,告诉他需要监听的数目(也可以理解成申请一片空间,用于存放监听的套接字)参数一:通知内核监听size个fd,只是个建议值并与硬件有关系。(从 Linux 内核 2.6.8 版本起,size 这个参数就被忽略了,只要求 size 大于 0 即可)返回值:返回epoll句柄(fd)*/ if(epollfd < 0) { perror("epoll_create"); return 5; } struct epoll_event ev; ev.events = EPOLLIN; ev.data.fd = sock; if(epoll_ctl(epollfd,EPOLL_CTL_ADD,sock,&ev) < 0) //注册新的fd到epfd { perror("epoll_ctl"); return 6; } int evnums = 0;//epoll_wait return val struct epoll_event evs[64]; int timeout = -1; while(1) { switch(evnums = epoll_wait(epollfd,evs,64,timeout)) /*参数一:int epfd:epoll_create()函数返回值参数二:struct epoll_events* events用于回传代处理事件的数组(也就是存放产生动静的事件)参数三:int maxevents 同时最多产生多少事件,告诉内核events有多大,该值必须大于0参数四:int timeout表示 -1相当于阻塞,0相当于非阻塞,超时时间(单位:毫秒)*/ { case 0: printf("timeout...\n"); break; case -1: perror("epoll_wait"); break; default: { int i = 0; for(; i < evnums; ++i) { struct sockaddr_in client; socklen_t len = sizeof(client); // 有新的连接 if(evs[i].data.fd == sock && evs[i].events & EPOLLIN) { int new_sock = accept(sock, (struct sockaddr*)&client,&len); if(new_sock < 0) { perror("accept"); continue; }//if accept failed else { printf("Get a new client[%s]\n", \ inet_ntoa(client.sin_addr)); ev.data.fd = new_sock; ev.events = EPOLLIN; epoll_ctl(epollfd,EPOLL_CTL_ADD,\ new_sock,&ev); //注册新的fd到epfd }//accept success }//if fd == sock else if(evs[i].data.fd != sock && \ evs[i].events & EPOLLIN) { char buf[1024]; ssize_t s = read(evs[i].data.fd,buf,sizeof(buf) - 1); if(s > 0) //客户端发送消息 { buf[s] = 0; printf("client say#%s",buf); ev.data.fd = evs[i].data.fd; ev.events = EPOLLOUT; epoll_ctl(epollfd,EPOLL_CTL_MOD, \ evs[i].data.fd,&ev); //修改已经注册的fd监听事件 }//s > 0 else //客户端连接关闭 { printf("client close"); close(evs[i].data.fd); //关闭fd epoll_ctl(epollfd,EPOLL_CTL_DEL, \ evs[i].data.fd,NULL); //从epfd删除一个fd } }//fd != sock else if(evs[i].data.fd != sock \ && evs[i].events & EPOLLOUT) { char *msg = "200 OK\r\n"; write(evs[i].data.fd,msg,strlen(msg)); //close(evs[i].data.fd); //epoll_ctl(epollfd,EPOLL_CTL_DEL, \ //evs[i].data.fd,NULL); ev.data.fd = evs[i].data.fd; ev.events = EPOLLIN; epoll_ctl(epollfd,EPOLL_CTL_MOD, \ evs[i].data.fd,&ev); }//EPOLLOUT }//for }//default break; }//switch }//while return 0;}
实验步骤,结果及分析
select实验
-
首先,先对
my_select.c
文件进行编译,然后通过./
运行 -
然后开启client端,每发一次信息,服务端都会接收到并返回200 OK。
分析:这是因为服务端维护了一个数组,在array[0]
中存放初始建立的tcp socket,之后每次客户端连接后,都会将accept得到的new socket存入用户维护的array数组中。在下一轮循环中,会先将array中所有>0的fd加入可读和可写fd_set,然后使用select
函数将阻塞并检查是否有事件发生,如果有的话将可读可写fd_set中没发生的文件描述符清空并返回。使用FD_ISSET
则可以检查array中的某一个fd在不在fd_set中。如果在的话说明事件发生。
poll实验
- 首先对
my_poll.c
进行编译后运行 - 然后开启client端,每发一次信息,服务端都会接收到并返回200 OK。
分析:poll在每次有事件发生时返回,然后遍历数组peerfd,检查每一个文件描述符是否有相应,相应类型为POLLIN
还是POLLOUT
。如果是POLLIN的话可以读,POLLOUT可以写。
我们在peerfd数组的第0位放入建立的socket,如果有客户端建立请求,将accept得到的new socket放入peerfd数组中,并设置关注的响应类型为POLLIN。如果客户端发送了消息,则POLLIN相应,输出得到的信息,并把关注的响应类型设为POLLOUT。下一轮poll会检测到POLLOUT有相应,服务端就会写入200OK。这是因为在正常情况下(缓冲区没满)输出都是可行的。在服务端向socket写入200OK后再将关注的相应类型改回POLLIN,就可以再次接收客户端的消息。
epoch实验
-
对上述三个文件分别使用gcc -o编译
-
首先,我们测试client.c和server.c的通信
- 先
./server
启动服务端,然后./client
启动客户端 - 然后在客户端发送
hi
,服务端收到 - 在客户端发送
hi client
,客户端收到 - 客户端可以再次发送消息……
-
启动两个客户端,测试多客户端向同一个服务端发送消息
-
如果有客户端发送了请求,该请求对应的fd就绪,该事件会在服务端下一次执行
int nready=epoll_wait(efd,events,OPEN_MAX,-1);
时被放入event数组中。
- 先
-
将
my_epoch.c
作为服务端,与client.c
进行测试- 先启动服务端,再启动客户端,服务端在接收到连接的时候打印
a new client
- 客户端向服务端发送消息,服务端接到消息后,使用
epoll_ctl
将fd的监听事件设为可写,从而在下一次epoll_wait
时进入了可写部分的代码,在发送200 OK
后再次使用epoll_clt
将监听事件设为可读。 - 因此客户端在每次发送消息后都可以收到来自服务端的
200 OK
,并且再次向服务端发送消息仍然会被服务端的socket文件描述符监听到。
- 先启动服务端,再启动客户端,服务端在接收到连接的时候打印
实验总结
经过本次实验,我对linux作为服务端管理请求的方式有了更深入的了解。以前我就很好奇服务端具体应该如何实现监听多个请求,现在我相信我已经非常了解了,尤其是epoll的回调方式非常玄妙。感谢老师布置本次实验。
实验9-2-1 USB设备驱动程序开发
实验目的
参照资料2-华为实验指导/01-3《openEuler操作系统》内核实验手册-树莓派版v1.0/第6章 设备管理
-
参考内核源码中的drivers/usb/usb-skeleton.c文件,编写一个USB探测驱动程序,能够实现以下基本功能:
-
(1)在插入U盘时能够探测到;
(2)在拔出U盘时能够探测到; -
加载、卸载模块并查看模块打印信息。
实验步骤
编译
make
加载驱动
insmod usb_detect.kodmesg | tail -n 2
插入U盘
查看信息可见,模块检测到U盘插入。获取到的U盘信息与lsusb显示的一致
弹出U盘
lsusb也没有之前插入U盘的列表
卸载U盘检测模块
rmmod usb_detect
实验结果
-
正确编写满足功能的源文件,正确编译。
-
正常加载、卸载内核模块;且内核模块功能满足任务所述。
实验9-1-1 内核模块测试硬盘读写速率
编译
安装模块并测试写速度
卸载模块
Comments NOTHING