# linux大作业 **Repository Path**: guyue-personspace/linux-homework ## Basic Information - **Project Name**: linux大作业 - **Description**: Linux原理与应用课程大作业 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2023-12-07 - **Last Updated**: 2025-09-28 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 武汉大学 Linux 基本原理与应用 2023 ## 小组成员及分工: - 2021302111366 刘忠烈 - work1,work3,work4,work6 - 2021302111230 胡茏涛 - work2,work5,work7 ## 目录 - work1 - work2 - work3 - work4 - work5 - work6 - work7 ## WORK1 ### 系统开发环境说明 - 操作系统:`openEuler22.03`版本 - gcc 10.3.1 - cmake 2.8.12.2 - git 2.41.0.windows.3 ### git托管库地址 https://gitee.com/guyue-personspace/linux-homework ## WORK2 ### 任务一:抓取网络包,提取 IP 地址到指定文件。 #### 实验目的 - 实现使用Shell脚本在openEuler操作系统上捕获网络数据包,并从中提取IP地址。 #### 实验步骤 ##### 1. 初始化数据包文件、输出文件和网络接口 - 根据`ifconfig`命令查找云服务器上的网络接口,如下图:![ifconfig执行结果](work2/img/ifconfig.png) - 指定`eth0`为网络接口,`capture.pcap`为抓取的网络包文件,`ip_addresses.txt`为最终的IP输出文件。 ``` interface="eth0" pcap_file="capture.pcap" output_file="ip_addresses.txt" ``` ##### 2. 网络包的捕获 - 使用 `tcpdump` 命令捕获`10s`网络流量,并将输出重定向到文件`capture.pcap`。 - 命令:`timeout 10 sudo tcpdump -i "$interface" -n -w "$pcap_file" ` ##### 3. IP地址的提取 - 使用 `awk` 从前一步捕获的网络包文件中提取IP地址,并将其保存到 `ip_addresses.txt`。其中利用了管道命令`|`。 - 命令:`sudo tcpdump -r "$pcap_file" -n | awk '{print $3}' > "$output_file"` ##### 4. getIP.sh运行 - 授权:使用`chmod`命令,为`getIP.sh`脚本开放执行权限 - 执行结果:![getIP.sh执行结果](work2/img/getIP%E6%89%A7%E8%A1%8C%E7%BB%93%E6%9E%9C.png) ##### 5. 查看输出文件 - ![部分输出文件](work2/img/ip_addresses%E5%86%85%E5%AE%B9.png) #### 实验反思 在实验过程中,我会遇到一些问题,例如权限不足、捕获不到数据包等。通过查阅文档、搜索解决方案等方法,解决了这些问题,确保实验进展顺利。同时,我也学到了关于网络监控的相关知识: - 学会了`ifconfig`命令的用法。 - 学会了使用`tcpdump`命令的基本语法和参数。 - 通过`awk`命令,提取数据包中的信息。 ### 任务二:编写 C 语言程序,打印 “Hello,openEuler!” #### 实验目的 - 在openEuler操作系统上编写、编译并运行实现打印 "Hello, openEuler!"的 C 语言程序。 #### 实验步骤 ##### 1. 程序编写 - 创建`hello.c`文件 - 写入以下代码: ``` #include int main() { printf("Hello, OpenEuler!\n"); return 0; } ``` ##### 2. Makefile文件编写 - 指定编译器、源文件、输出可执行文件,代码如下: ``` CC = gcc SRC = hello.c EXE = getIP.sh ``` - 编译、运行源文件: ``` # 默认运行 all: $(OUT) # 编译 $(OUT): $(SRC) $(CC) -o $(OUT) $(SRC) # 运行 run: $(OUT) ./$(OUT) ``` - clean——清除编译结果: ``` clean: rm -f $(OUT) ``` ##### 3. 程序运行 - 利用Makefile文件,使用`make`命令编译运行清除执行文件: - `make` - `make run` - `make clean` ![make相关](work2/img/make%E7%9B%B8%E5%85%B3.png) #### 实验反思 实验过程中,我熟悉了 Makefile 的基本语法和 C 语言程序的编写,为我后续学习和实践提供了基础。 - 学会了编写 Makefile 文件,定义目标和依赖关系。 - 了解了 make 工具的基本使用,如何通过 make 命令编译程序。 - 掌握了简单的 C 语言程序在openEuler系统上的编写运行。 #### 总结 - 在这次实验中,我成功地通过编写 Makefile 文件和 C 语言程序,在 openEuler 操作系统上实现了打印 "Hello, openEuler!" 的功能。通过这个实验,我不仅学会了如何组织一个简单的 Makefile 文件来管理程序的编译过程,还掌握了基本的 C 语言编程技能。 - 通过网络数据包捕获和处理的部分,我深入理解了使用 tcpdump 工具的方法。这为我提供了对网络通信结构和协议的实际经验,并增强了我的网络技能。 在整个实验过程中,我遇到了一些挑战,例如解决编译错误、理解网络数据包的结构等。通过查阅文档、在线资源以及尝试不同的解决方案,我成功地克服了这些问题,这使我对问题解决的方法和思考过程有了更深入的了解。 总体而言,这次实验不仅帮助我掌握了特定的技术要求,还培养了我的问题解决能力和实践经验。通过在 openEuler 操作系统上进行实验,我对Linux环境和命令行工具有了更深入的了解。 ## WORK3 ### 实验任务一:打印系统当前所有进程:进程号、名字 #### 实验目的 - 基于进程文件系统/proc ,打印当前进程中的所有进程,包括进程号以及进程名称 #### 实验思路及步骤 ##### 1. 遍历/proc目录下的子目录,每个子目录代表一个进程 相应伪代码如下: ``` for pid in /proc/*/; do if [ -d "$pid" ]; then # 从/proc/[pid]/status文件中提取进程名字和进程号 # 打印进程号和名字 fi done ``` ##### 2. 找到并输出相应的进程号以及进程名称,相应代码如下: ``` if [ -d "$pid" ]; then # 从/proc/[pid]/status文件中提取进程名字和进程号 process_name=$(awk -F: '/Name/ {print $2}' "$pid/status") process_pid=$(basename "$pid") # 打印进程号和名字 echo "进程号: $process_pid, 名字: $process_name" fi ``` #### 实验结果 根据/proc目录下的子目录,输出相应的进程信息如下: ![Alt text](work3/img/%E6%98%BE%E7%A4%BA%E6%89%80%E6%9C%89%E8%BF%9B%E7%A8%8B%E5%9B%BE.png) ### 实验任务二: 根据程序参数,杀死指定进程 #### 实验目的 - 基于进程文件系统/proc,实现根据用户输入的进程名称,找到对应的进程并杀死该进程 #### 实验思路及步骤 ##### 1. 验证用户输入是否合法有效 - 检查用户是否输入进程名称作为参数,代码如下: ``` # 检查是否提供了进程名称作为参数 if [ $# -ne 1 ]; then echo "用法: $0 <进程名称>" exit 1 fi ``` ##### 2. 从用户输入中提取所需要的进程名称 - 代码如下: ``` # 要查找和终止的进程名称 target_name="$1" ``` ##### 3. 遍历`/proc`目录下的子目录,每个子目录代表一个进程,从所有进程中找到用户输入的进程名称 - 对`/proc`目录进行遍历操作,采用for循环实现,伪代码如下: ``` # 遍历/proc目录下的子目录,每个子目录代表一个进程 for pid in /proc/*/; do # 循环内相应操作 done ``` - 检查是否存在`status`文件,在该文件中提取进程名称,代码如下: ``` if [ -d "$pid" ]; then # 检查是否存在status文件 status_file="$pid/status" if [ -e "$status_file" ]; then # 从/proc/[pid]/status文件中提取进程名字 process_name=$(awk -F: '/Name/{print $2}' "$status_file") process_pid=$(basename "$pid") # 检查进程名字是否匹配目标名称 trimmed_process_name=$(echo "$process_name" | awk '{$1=$1};1') if [ "$trimmed_process_name" = "$target_name" ]; then echo "找到进程号 $process_pid,名字为 $process_name,将终止它。" # 终止进程 kill -9 "$process_pid" fi else echo "未找到 $status_file 文件" fi fi ``` #### 实验结果 - 在本次实验中,本人以`55334号进程java`为例,展示根据用户输入参数杀死指定进程操作 - 通过调用`kill_process.sh`程序,用户输入进程名,终止指定进程,同时对用户输入的参数进行判断: ![Alt text](work3/img/%E6%8F%90%E7%A4%BA%E8%BE%93%E5%85%A5%E8%BF%9B%E7%A8%8B%E5%90%8D%E7%A7%B0.png) - 在杀死指定进程之前,执行`list_process.sh`文件,输出当前`/proc`目录下的所有进程,并找到`55334号进程java`,如下: ![Alt text](work3/img/55334%E5%8F%B7java%E8%BF%9B%E7%A8%8B.png) - 调用指令: ``` ./kill_process.sh java ``` - 杀死指定`55334号进程java`,效果如下: ![Alt text](work3/img/%E7%BB%88%E6%AD%A255334%E5%8F%B7java%E8%BF%9B%E7%A8%8B.png) - 终止`55334号进程java`后,再显示此时`/proc`目录下的所有进程,发现没有找到`55334号进程java`,实验成功: ![Alt text](work3/img/55334%E5%8F%B7%E8%BF%9B%E7%A8%8B%E6%B6%88%E5%A4%B1.png) #### 实验总结 - 在本次实验之中,基于对文件系统`/proc`的相应操作,使本人对于进程的理解进一步加深。在实验过程中,本人采用for循环遍历`/proc`文件的子文件目录,输出当前的所有进程。在这个过程中,增进了本人对于进程在内存中的存在形式的理解。 - 在第二个实验任务中,本人实现通过用户输入的参数终止指定进程。在实际情况下,用户可输入进程号或者进程名称,然而用户输入进程号终止进程时可直接调用指令: ``` kill -9 process_num ``` 实现对应操作。因此本人选择让用户输入进程名称,通过进程名找到对应进程号,再通过进程号删除指定进程。 - 在整个实验过程当中,本人对于操作系统进程的理解进一步加深,了解到进程在运行时是如何在内存中记录,并且如何终止指定进程等操作。 ## WORK4 ### 实验任务:模仿 malloc、free 函数分配内存 #### 实验目的 - 本实验旨在设计并实现一个简易的内存管理系统,模仿 `malloc` 和 `free `函数的行为,用于内存的动态分配和释放。 - 通过本次实验,理解操作系统是如何进行内存地址的分配,为今后编写更高质量的程序奠定基础 #### 主要数据结构 ##### 内存块结构体`Block` ``` // 内存块结构体 typedef struct Block { size_t size; struct Block* next; } Block; ``` #### 实验思路及步骤 ##### 1. 模拟操作系统初始化内存池 - 定义内存块结构体`Block`,用于模拟系统内存,定义如下: ``` // 内存块结构体 typedef struct Block { size_t size; struct Block* next; } Block; ``` - 编写`initializeMemory()`方法模拟操作系统初始化内存池,代码如下: ``` // 初始化内存池 void initializeMemory() { freeList = (Block*)mem; freeList->size = MEMORY_SIZE - sizeof(Block); freeList->next = NULL; } ``` ##### 2. 编写测试函数`umallocTest()`模拟malloc、free 函数分配内存 - 在`umallocTest()`函数中为对应示例分配相应内存空间,并释放部分内存,代码如下: ``` // 分配内存 void* ptr1 = umalloc(64); void* ptr2 = umalloc(128); void* ptr3 = umalloc(256); // 释放内存 ufree(ptr1); ufree(ptr3); // 再次分配内存 void* ptr4 = umalloc(64); void* ptr5 = umalloc(512); ``` - 在内存分配以及回收完成之后,输出对应内存结果 ``` // 输出内存分配结果 printf("Allocated memory blocks:\n"); printf("ptr1: %p\n", ptr1); printf("ptr2: %p\n", ptr2); printf("ptr3: %p\n", ptr3); printf("ptr4: %p\n", ptr4); printf("ptr5: %p\n", ptr5); ``` ##### 3. 模拟内存分配与回收 - 模拟内存分配`umalloc(size_t size)`函数: 1. 遍历链表查找合适大小的内存块,相应伪代码如下: ``` // 遍历链表查找合适大小的内存块 while (currentBlock) { if (currentBlock->size >= size) { // 找到合适大小的内存块 // ...执行相应操作 } prevBlock = currentBlock; currentBlock = currentBlock->next; } return NULL; // 没有足够的内存块 ``` 2. 找到合适大小内存块后,如果剩余空间足够大,将其拆分为两块,相应代码如下: ``` // 找到合适大小的内存块 if (currentBlock->size > size + sizeof(Block)) { // 如果剩余空间足够大,将其拆分为两块 Block* newBlock = (Block*)((char*)currentBlock + sizeof(Block) + size); newBlock->size = currentBlock->size - size - sizeof(Block); newBlock->next = currentBlock->next; currentBlock->size = size; currentBlock->next = newBlock; } if (prevBlock) { prevBlock->next = currentBlock->next; } else { freeList = currentBlock->next; } return (void*)((char*)currentBlock + sizeof(Block)); ``` - 模拟内存释放`ufree(void* ptr)`函数:对相应的内存进行释放和回收,相关代码如下: ``` // 自定义内存释放函数 void ufree(void* ptr) { if (ptr == NULL) { return; } Block* block = (Block*)((char*)ptr - sizeof(Block)); block->next = freeList; freeList = block; } ``` - 自定义内存碎片整理函数`umerge()`: 在内存分配和回收结束过后,需要对已分配的内存空间进行整理,若内存中存在两空间分布连续的内存碎片,则将两内存碎片相连接,相关代码如下: ``` // 自定义碎片整理函数 void umerge() { Block* currentBlock = freeList; while (currentBlock) { Block* nextBlock = currentBlock->next; if (nextBlock && (char*)currentBlock + sizeof(Block) + currentBlock->size == (char*)nextBlock) { // 合并相邻的内存块 currentBlock->size += sizeof(Block) + nextBlock->size; currentBlock->next = nextBlock->next; } else { currentBlock = currentBlock->next; } } } ``` #### 实验结果 - 在进行相应的内存分配以及回收后,最终各个内存分配情况如下: ![Alt text](work4/img/%E6%9C%80%E7%BB%88%E7%BB%93%E6%9E%9C.png) #### 实验总结 - 在进行本次实验的过程当中,本人对于操作系统中的内存分配和回收的理解进一步加深。在之前的学习过程中,本人在大多数情况下都是直接使用malloc、free 函数分配以及回收相应的内存,对于其具体实现了解较少。而在本次实验当中,本人通过编写内存申请函数 `umalloc()`,内存释放函数 `ufree()`,碎片整理函数 `umerge()`,实现了自己对于内存的分配和回收操作,极大增进了本人对于操作系统的理解。 - 同时,在本次实验中也存在一些局限。在进行内存分配时,本人采用的方式是遍历整个空闲内存链表,系统开支较大,且并没有用到相应的操作系统内存分配策略。在今后的学习生活中,还能够改进该内存分配方式,在进行内存分配时可采取优先匹配低内存空间或高内存空间进行分配。 ## WORK5 ### 任务:读取某存储设备中 EXT 文件系统元数据,并格式化打印。 #### 实验目的 - 实现读取设备中的指定块数据并根据读取的数据分析文件系统超级块、组描述符表、位图解析、inode 表项解析再格式化打印结果的 C 语言程序。 #### 实验步骤 ##### 1. 编写相应的C语言程序代码 - 整体思路: - 使用C语言的文件操作函数`fopen`打开设备文件,使用`fseek`函数将文件指针移动到要读取的块的位置,使用`fread`函数读取指定块的数据。将读取到的数据存储在一个缓冲区。 - 定义相关结构体表示`超级块`、`块组描述符`、`位图`、`inode`,根据文件系统的结构解析提取到结构体中的元素。 - 将解析得到的信息按照易读的格式打印出来。 - 另外,为了结果能有效展示,只处理第一个块组的数据,和第一个inode。 - 主要数据结构: - 超级块:从上之下依次是`inode节点数`、`总块数`、`保留块数`、`空闲块数`、`空闲inode结点数`、`数据块大小的对数值`、`inode大小`、`块组号`、`兼容特性位图`、`不兼容特性位图`、`只读兼容特性位图`。 ``` struct ext_super_block { uint32_t s_inodes_count; uint32_t s_blocks_count; uint32_t s_r_blocks_count; uint32_t s_free_blocks_count; uint32_t s_free_inodes_count; uint32_t s_first_data_block; uint32_t s_log_block_size; uint32_t s_inode_size; uint32_t s_block_group_nr; uint32_t s_feature_compat; uint32_t s_feature_incompat; uint32_t s_feature_ro_compat; }; ``` - 块组描述符:从上之下依次是`块位图所在块号`、`inode位图所在块号`、`inode表起始块号`、`空闲块数`、`空闲inode结点数`。 ``` struct ext_group_desc { uint32_t bg_block_bitmap; uint32_t bg_inode_bitmap; uint32_t bg_inode_table; uint16_t bg_free_blocks_count; uint16_t bg_free_inodes_count; }; ``` - inode:从上之下依次是`文件模式`、`用户ID`、`文件大小`、`最后访问时间`、`创建时间`、`最后修改时间`。 ``` struct ext_inode { uint16_t i_mode; uint16_t i_uid; uint32_t i_size; uint32_t i_atime; uint32_t i_ctime; uint32_t i_mtime; }; ``` - 主要算法:针对问题,基于给出的思路,我们认为主要的算法代码块如下: - `open`:通过给定的块设备名称,打开准备读取的设备文件。 ``` fd = open(argv[1], O_RDONLY); if (fd < 0) { perror("无法打开块设备"); exit(1); } ``` - `lseek`:通过设置偏移量,让文件指针移动到指定的位置进行读取操作。 ``` off_t offset = 1024; if (lseek(fd, offset, SEEK_SET) < 0) { perror("lseek 错误"); close(fd); exit(1); } ``` - `read`:在文件指针移动到指定位置后,根据要读取的结构体的大小,对相应大小的数据进行读取,并将数据保存至相应结构体变量中。 ``` ssize_t bytes_read = read(fd, &superblock, sizeof(superblock)); if (bytes_read < 0) { perror("读取超级块错误"); close(fd); exit(1); } ``` - `print_xxx`:对结构体相应处理后,进行`printf`输出。(以下以打印位图为例): ``` void print_bitmap(int fd, uint32_t block_number, int block_size) { off_t offset = (off_t)block_number * block_size; unsigned char *bitmap = (unsigned char *)malloc(block_size); if (lseek(fd, offset, SEEK_SET) < 0 || read(fd, bitmap, block_size) < 0) { perror("Error reading bitmap"); free(bitmap); return; } for (int i = 0; i < block_size; i++) { for (int j = 0; j < 8; j++) { if (bitmap[i] & (1 << j)) printf("1"); else printf("0"); } printf(" "); } printf("\n"); free(bitmap); } ``` 通过给定的文件描述符(fd)、块号(block_number)和块大小(block_size),计算偏移量、动态分配内存、使用lseek和read系统调用,在块设备文件中定位相应的块位图读取。最终,通过嵌套的循环遍历每个位,并根据位的值输出"1"或"0",以展示块的分配状态。 ##### 2. 获取设备块名称 - 使用 `fdisk` 获取操作系统所有的设备块名称。 - 命令:`fdisk -l ` - 结果展示: ![设备块名称](work5/img/%E8%AE%BE%E5%A4%87%E5%9D%97%E5%90%8D%E7%A7%B0.png) ##### 3. 运行程序,解析文件系统指定设备块元数据 - 我们取`/dev/vda2`进行解析。 - 编译并运行C语言程序,结果如下: - 超级块、组描述符信息: ![超级块和组描述符信息](work5/img/%E8%B6%85%E7%BA%A7%E5%9D%97%E5%92%8C%E7%BB%84%E6%8F%8F%E8%BF%B0%E7%AC%A6%E4%BF%A1%E6%81%AF.png) - 块位图信息(部分): ![块位图信息](work5/img/%E5%9D%97%E4%BD%8D%E5%9B%BE%E4%BF%A1%E6%81%AF.png) - inode位图信息(部分): ![inode位图信息](work5/img/inode%E4%BD%8D%E5%9B%BE%E4%BF%A1%E6%81%AF.png) - inode数据项信息: ![inode数据项信息](work5/img/inode%E6%95%B0%E6%8D%AE%E9%A1%B9%E4%BF%A1%E6%81%AF.png) #### 总结 这次实验中,我们成功实现了读取EXT文件系统的元数据,并进行格式化打印。 挑战主要在于理解EXT文件系统的内部结构和元数据的组织方式。另一个挑战是如何在程序中解析这些信息,并以可读的格式进行打印。 为了解决这些挑战,我们研究了EXT文件系统的规范和数据结构,它可以帮助解析EXT文件系统的元数据。 这个实验让我们更深入地了解了文件系统的内部结构和元数据的存储方式,并且学会了如何使用C语言代码进行编程来解析并打印这些信息。 ## WORK6 ### 实验任务:挂载支持 webdav 协议的网盘 #### 实验思路介绍 - 实验介绍 本实验要求在本地挂载支持webdav协议的网盘。当用户按照以下格式输入信息时,程序将会执行相应操作: ``` [挂载|卸载] 本地挂载点 [用户名:密码@网盘地址:端口] ``` 其中`webdav`(Web-based Distributed Authoring and Versioning),是一组基于超文本传输协议的技术集合,有利于用户间协同编辑和管理存储在万维网服务器文档。国外的很多网盘,包括 `owncloud`、`nextcloud`、`坚果云`等都支持 `webdav`。该脚本将简化挂载过程,用户只需指定必要参数,如挂载点、凭据、URL和端口。 - 实验思路 在本次实验过程中,需要首先对输入参数进行识别设置,确保理解脚本的参数和用法,包括`挂载/卸载动作`、`本地挂载点`和 `WebDAV URL`。 由于需要将支持`webdav`协议的网盘的网盘挂载到本地路径中,所以需要相应的挂载工具以及正确选择网盘工具。本次实验使用第三方协议支持模块 davfs2进行网盘的挂载操作,同时使用坚果云作为远程网盘,将其挂载到本地。 #### 第三方模块介绍: - davfs2 `davfs2` 是一个用于在 Linux 系统中挂载 WebDAV 资源的模块。它实现了将远程 WebDAV 服务器映射为本地文件系统的功能,使得用户可以通过常见的文件操作命令(如`ls`, `cp`, `rm`等)直接访问和管理 WebDAV 服务器上的文件。这个模块特别适用于需要远程文档共享和协作的场景。`davfs2` 提供了以下关键特性: **透明访问**:用户可以像访问本地文件一样访问 WebDAV 服务器上的文件。 **安全性**:支持通过 HTTPS 进行安全的数据传输。 **灵活的配置**:提供配置文件,允许自定义挂载选项,如读写权限、缓存策略等。 **自动化登录**:通过秘钥文件实现自动化的登录过程,无需每次手动输入凭据。 - 坚果云 `坚果云`是一款支持`webdav`协议的、便捷、安全的专业网盘产品,通过文件自动同步、共享、备份功能,为用户实现智能文件管理,提供高效办公解决方案。坚果云还具有强大的共享功能,用户可以邀请他人与自己同步指定文件夹,即共享该文件夹。 本次实验将采用`坚果云`进行本地网盘的挂载操作。 #### 主要数据结构 - `action`用户指定操作 对应用户输入的相应操作,有`挂载`和`卸载`两种不同的操作 - `local_mount_point`本地挂载点 对应用户输入的本地挂载点 - `webdav_url`对应webdav协议网盘的远程路径信息 对应用户输入的其他关于网盘的相应信息,包括`用户名`、`密码`、`网盘地址`、`端口号`: - `username` 用户输入的用户名 - `password` 用户输入的密码 - `remote_address` 用户输入的对应坚果云网盘地址 - `port` 网盘地址对应的端口号 #### 主要算法与程序流程 1. **用户输入参数验证**:验证命令行参数的数量,确保输入正确。 - **检查传入参数的数量是否正确**: - 代码中使用 `"$#"` 来计数命令行参数的数量。正确的参数数量应为 3,包括`action`、`local_mount_point`以及`webdav_url` - 如果参数数量不正确,程序将输出用法信息并返回错误代码 1。 对应代码信息如下: ``` # 检查脚本参数是否正确 if [ "$#" -ne 3 ]; then echo "使用方式:$0 [挂载|卸载] 本地挂载点 [用户名:密码@网盘地址:端口]" exit 1 fi ``` 2. **根据用户输入信息提取对应参数**:解析提供的用户名、密码和URL。 - **解析命令行参数中的操作、挂载点和webdav URL**: 根据用户输入的参数顺序进行提取和识别,从输入参数中分离出操作、挂载点和webdav URL: ``` # 提取参数 action="$1" local_mount_point="$2" webdav_url="$3" ``` - **对webdav_url中的数据进行二次操作,使其符合davfs2格式**: 对应代码如下: ``` # 根据WebDAV URL提取用户名、密码、网盘地址和端口 username=$(echo $webdav_url | awk -F ':' '{print $1}') password=$(echo $webdav_url | awk -F '@' '{print $2}' | awk -F ':' '{print $2}') remote_address=$(echo $webdav_url | awk -F '@' '{print $3}' | awk -F ':8' '{print $1}' | cut -d'/' -f3-) port=$(echo $webdav_url | awk -F ':' '{print $4}') ``` 3. **挂载和卸载操作**: - **挂载操作**: - **检查挂载点目录是否存在**:如果对应挂载点不存在,则先创建对应的本地挂载点: ``` # 检查 $local_mount_point 路径是否存在 if [ ! -d "$local_mount_point" ]; then # 如果路径不存在,则创建路径 mkdir -p "$local_mount_point" fi ``` - **构建挂载命令并执行**:调用对应mount.davfs指令,将`坚果云`网盘挂载到本地当中: 调用的mount.davfs指令如下: ``` # 挂载 WebDAV sudo mount.davfs -o Username=$username,Password=$password,port=$port $remote_address $local_mount_point ``` - **挂载后输出相应信息**:输出信息提示用户挂载状况,成功则运行技术,失败则返回1并提示用户挂载失败 对应代码如下: ``` if [ $? -eq 0 ]; then echo "WebDAV 成功挂载到 $local_mount_point" else echo "挂载失败" exit 1 fi ``` - **卸载操作**: - **构建卸载命令并执行**:调用umount指令将webdav网盘从本地卸载 ``` # 卸载 WebDAV sudo umount "$local_mount_point" ``` - **删除本地对应的挂载文件夹**:调用rm命令删除刚刚挂载的本地文件夹 ``` rm -rf "$local_mount_point" ``` - **输出对应反馈信息**:根据执行结果输出对应的反馈信息 ``` if [ $? -eq 0 ]; then echo "WebDAV 成功从 $local_mount_point 卸载" else echo "卸载失败" exit 1 fi ``` #### 实验结果 1. **参数验证和处理**: - 若用户输入错误信息或者没有信息,需要对用户输入进行相应的提示 用户输入格式提示示意图: ![Alt text](work6/img/%E7%94%A8%E6%88%B7%E8%BE%93%E5%85%A5%E6%8F%90%E7%A4%BA%E4%BF%A1%E6%81%AF.png) - 用户输入密码错误次数超过三次取消当前挂载 用户密码输出错误次数过多示意图: ![Alt text](work6/img/%E5%AF%86%E7%A0%81%E8%BE%93%E5%85%A5%E5%A4%B1%E8%B4%A5.png) 2. **坚果云网盘信息配置**: - 在坚果云客户端配置相应的webdav网盘信息,设置用户名、端口号、密码以及对应的服务器地址 坚果云网盘信息配置示意图: ![Alt text](work6/img/%E5%9D%9A%E6%9E%9C%E4%BA%91%E8%B4%A6%E6%88%B7%E8%AE%BE%E7%BD%AE.png) 3. **挂载和卸载webdav网盘**: - 挂载操作 根据用户输入挂载坚果云网盘到本地挂载点当中,若本地挂载点不存在则创建对应文件目录 执行挂载操作命令示意图: ![Alt text](work6/img/%E8%B0%83%E7%94%A8%E6%8C%82%E8%BD%BD%E5%91%BD%E4%BB%A4.png) 执行成功后本地文件显示: ![Alt text](work6/img/%E6%8C%82%E8%BD%BD%E6%88%90%E5%8A%9F%E7%A4%BA%E6%84%8F%E5%9B%BE.png) - 卸载操作 根据用户的输入信息,将对应的挂载点取消挂载并删除对应挂载点 调用卸载命令示意图: ![Alt text](work6/img/%E8%B0%83%E7%94%A8%E5%8D%B8%E8%BD%BD%E7%BD%91%E7%9B%98.png) 卸载结果示意图: ![Alt text](work6/img/%E7%BD%91%E7%9B%98%E5%8D%B8%E8%BD%BD%E6%88%90%E5%8A%9F.png) #### 缺失和待改进之处 1. **在openEuler系统中对于davfs2的应用不够熟练** 在本次实验当中,采用davfs2作为第三方工具完成网盘的挂载,因此需要在openEuler系统中下载对应的davfs2工具。 本次实验中使用的操作系统版本为openEuler22.03版本,此版本中不可直接通过一下指令下载davfs2工具: ``` yum -y install davfs2 ``` 在具体使用时,需要自行从网络上下载对应版本的davfs2工具包并进行编译和环境配置: ``` # 从网络上下载davfs2包 wget -P YourInstallDirectory https://download.savannah.nongnu.org/releases/davfs2/davfs2-1.7.0.tar.gz ./configure make sudo make install sudo groupadd davfs2 sudo usermod -a -G davfs2 $(id -un) groupadd davfs2 sudo useradd davfs2 -g davfs2 # 环境变量配置 export PATH=$PATH:/usr/local/sbin ``` 2. **对于挂载网盘流程不够熟练** 由于这是本人第一次将远程网盘挂载到本地,本人还存在对于网盘挂载和卸载操作不够熟练的问题。在具体实验过程中,本人对于坚果云网盘的使用不够熟练。 #### 总结 - 在这次实验中,本人主要编写了mount_webdav.sh文件,并下载对应版本的davfs2实验工具并配置相应的环境变量。在这个过程中,我体会到如何将远程的网盘挂载到本地文件,在熟悉对网盘进行系列操作的同时增进了自己对于davfs2工具的理解以及对于webdav协议网盘的理解 在整个实验过程中,本人遇到了一系列挑战和困难。刚开始在配置实验环境准备实验工具时就遇到无法正常下载davfs2工具的问题,通过查阅相应的文献以及参考资料最后得到解决。而在这个遇到问题,不断解决问题的过程当中,本人对于openEuler操作系统的认识也不断加深,且对于相应网盘操作的使用也更加熟练,相信在今后的学习过程中本人还会有更深的体会。 ## WORK7 ### 任务:传入指定参数,实现点对点聊天。 #### 实验目的 - 实现根据已知IP地址、输入指定参数认证、进行点对点通信的 C 语言程序。 #### 实验步骤 ##### 1. 编写相应的C语言程序代码 - 整体思路: - 对于`点对点通信`的要求,我们无法通过服务端和客户端`一对多`的模式进行通信,因为这违背了点对点通信的要求,即`两个节点之间直接建立连接进行通信的方式,中间不借助任何设备`。因此,我们大胆设计为一个节点`既包括服务端功能,又包括客户端功能`。通过运行两个同样的程序不同的用户、IP和端口,来实现题目所要求的点对点通信。 - 对于`指定参数:用户名:密码@IP地址:端口`的要求,因为它要求只能有一个参数,则让我们无法通过仅通过一个代码程序来泛化性地生成不同的用户程序,因此我们只能将同一个代码程序同样的逻辑和代码创建了两个代码文件,里面包括了该用户的账号密码和拥有的服务端功能的特有IP和端口号。 - 此外,为了方便我们在上面提到的账号密码和IP端口号直接使用全局变量的模式,规定在相应的代码文件中。 - 主要数据结构: - 每个代码文件中都拥有各自的账号密码和用来给对方连接的服务器IP和端口。(以communication3.c文件为例) ``` // 用户名和密码 #define USERNAME "user3" #define PASSWORD "pass3" // 服务端IP和端口号 #define SERVEIP "127.0.0.3" #define SERVER_PORT 12345 ``` - 主要算法:基于给出的思路,我们认为主要的算法代码块如下: - `server_function`: - 通过`socket`函数创建一个服务器套接字 (server_socket)。 - 设置服务器地址结构 (server_addr),包括 IP 地址和端口号,并通过`bind`函数绑定套接字。 - 通过`listen`函数开始监听客户端的连接请求。 - 进入循环,不断接受客户端连接,创建新的线程处理每个连接。 - 在每个连接中,通过`recv`函数接收客户端发送的消息,并解析其中的用户名和内容部分。 - 使用`computeTime`函数显示接收到消息的时间,并打印用户名和内容。 - 如果接收到的消息是 "exit",表示客户端请求断开连接,退出循环并关闭当前连接的套接字。 - 最终关闭服务器套接字。 - 代码如下:(部分省略) ``` void *server_function(void *arg) { // …… // 创建套接字 server_socket = socket(AF_INET, SOCK_STREAM, 0); if (server_socket < 0) error("无法创建套接字"); // 设置服务器地址结构 memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = inet_addr(SERVEIP); server_addr.sin_port = htons(SERVER_PORT); // 绑定套接字 if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) error("绑定失败"); // 监听 if (listen(server_socket, 5) < 0) error("监听失败"); // …… // 循环接受客户端连接 while (1) { // 接受客户端连接 client_socket = accept(server_socket, (struct sockaddr *)&client_addr, &client_len); if (client_socket < 0) { printf("接受连接失败\n"); continue; // 继续等待下一个连接 } // 处理客户端的消息 while (1) { ssize_t byteRead = recv(client_socket, buffer, BUFFSIZE, 0); if(byteRead == -1){ printf("服务器:接收套接字内容错误\n"); return NULL; } // 处理传来的用户名和内容 colonPosition = strchr(buffer, ':'); colonIndex = colonPosition - buffer; // 拷贝用户名部分 strncpy(userName, buffer, colonIndex); userName[colonIndex] = '\0'; // 添加字符串结束符 // 将message置空 memset(message, 0, sizeof(message)); // 拷贝内容部分 strcpy(message, colonPosition + 1); // 调用显示时间函数 computeTime(); printf("%s:%s\n",userName,message); // 将buffer置空 memset(buffer, 0, sizeof(buffer)); // 满足要求则断开连接 if (strcmp(message, "exit") == 0) { printf("对方断开连接\n"); break; // 跳出内层循环,关闭当前连接 } } // 关闭当前连接的套接字 close(client_socket); break; } // 关闭服务器套接字 close(server_socket); return NULL; } ``` - `client_function`: - `解析`传入的连接信息,包括用户名、密码、服务器 IP 地址和端口号。 - 使用`socket`创建客户端套接字 (client_socket),并尝试连接服务器。连接失败时,尝试最多三次,每次之间有5秒的延迟。 - 如果连接成功,模拟`登录验证`用户名和密码,并显示成功连接信息。 - 进入循环,等待用户在终端输入消息,将用户名和消息结合后通过`send`函数发送给服务器。 - 如果用户输入 "exit",表示断开连接,退出循环并关闭客户端套接字。 - 最终关闭客户端套接字。 ``` void *client_function(void *arg) { char *connection_info = (char *)arg; char username[50], password[50], ip[50]; int port; char input[BUFFSIZE]; char myInput[BUFFSIZE]; sscanf(connection_info, "%[^:]:%[^@]@%[^:]:%d", username, password, ip, &port); // 模拟登录 if (strcmp(username, USERNAME) != 0 || strcmp(password, PASSWORD) != 0) { printf("登录失败:用户名或密码错误\n"); return NULL; } int client_socket; struct sockaddr_in server_addr; // 尝试连接服务器,最多尝试三次 int max_attempts = 3; for (int attempt = 1; attempt <= max_attempts; ++attempt) { // 创建套接字 client_socket = socket(AF_INET, SOCK_STREAM, 0); if (client_socket < 0) error("无法创建套接字"); // 设置服务器地址结构 memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = inet_addr(ip); server_addr.sin_port = htons(port); // 连接到服务器 if (connect(client_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) { printf("连接失败,尝试重连(%d/%d)...\n", attempt, max_attempts); close(client_socket); if (attempt < max_attempts) { // 延迟5秒再次尝试 sleep(5); } else { error("无法连接到服务器"); } } else { // 连接成功 printf("登录成功,连接到对方服务器!\n"); // 在这里可以添加客户端发送消息的逻辑 printf("请输入要发送的消息(输入 'exit' 断开连接)\n"); while (1) { /* 用户键盘输入 */ fgets(myInput, sizeof(input), stdin); myInput[strcspn(myInput, "\n")] = '\0'; // 移除输入中的换行符 /* 在消息前加上用户名 */ sprintf(input, "%s:%s", USERNAME, myInput); /* 发送数据 */ ssize_t byteSend = send(client_socket, input, strlen(input), 0); /* 检查是否需要断开连接 */ if (strcmp(myInput, "exit") == 0) { printf("断开连接\n"); break; } /* 显示发送数据的信息 */ computeTime(); printf("%s\n",input); } close(client_socket); return NULL; } } return NULL; } ``` - 线程函数:在主函数中,使用线程函数对上述服务端和客户端进行线程启动,以满足一个用户程序既有服务段功能又有客户端功能。 - 创建`两个线程`,一个用于运行服务器端 (server_function),另一个用于运行客户端端 (client_function)。 - 将服务器线程设置为`后台线程`,以便在主线程结束时自动回收资源,保证服务段功能一直存在。 - 使用`pthread_join`等待两个线程结束。 ``` // 创建服务器线程 if (pthread_create(&server_thread, NULL, server_function, NULL) != 0) { error("无法创建服务器线程"); } // 将服务器线程设置为后台线程 if (pthread_detach(server_thread) != 0) { error("无法设置服务器线程为后台线程"); } // 创建客户端线程 if (pthread_create(&client_thread, NULL, client_function, argv[1]) != 0) { error("无法创建客户端线程"); } // 等待线程结束 pthread_join(server_thread, NULL); pthread_join(client_thread, NULL); ``` ##### 2. 编译运行两个代码程序 - 编译并运行C语言程序,注:运行方式为`./communicationX userX:passX@127.0.0.X:1234X`,其中`@`前部分为登录该程序客户端的账号和密码,后部分为该程序用户要去连接的另外一个程序用户的服务端IP和端口号。 - 结果展示: - 连接失败图: ![连接失败](work7/img/%E8%BF%9E%E6%8E%A5%E5%A4%B1%E8%B4%A5%E5%9B%BE.png) - 重连图: ![重连](work7/img/%E9%87%8D%E8%BF%9E%E5%9B%BE.png) - 登录验证失败图: ![登录验证失败](work7/img/%E7%99%BB%E5%BD%95%E5%A4%B1%E8%B4%A5%E5%9B%BE.png) - 点对点通信图: ![点对点通信](work7/img/%E7%82%B9%E5%AF%B9%E7%82%B9%E9%80%9A%E4%BF%A1%E5%9B%BE.png) - 断开连接图: ![断开连接](work7/img/%E6%96%AD%E5%BC%80%E8%BF%9E%E6%8E%A5%E5%9B%BE.png) #### 总结 在这次实验中,我们成功实现了一个基于线程的简单客户端-服务器点对点通信模型,通过网络实现了消息的传递和显示。 通过这个实验,我们学习了如何使用`pthread`库创建和管理多个线程,实现了服务器和客户端的并发运行。了解了基本的套接字编程,包括创建套接字、绑定地址、监听连接、接收和发送数据等基本操作。实现了简单的网络通信模型,通过套接字在服务器和客户端之间传递消息。 通过这次实验,我们对网络编程和多线程编程有了实际的应用经验,同时也学到了一些解决问题的方法。这为进一步深入学习和应用网络编程打下了基础。