├── .gitignore ├── README.md ├── lab01 ├── answer.md ├── answer_template.md ├── img │ ├── 1.png │ ├── 10.png │ ├── 11.png │ ├── 12.png │ ├── 13.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ ├── 7.png │ ├── 8.png │ ├── 9.png │ └── vi.jpg ├── lab01.md └── lab01.zip ├── lab02 ├── answer.md ├── answer_template.md ├── img │ ├── 10.png │ ├── 11.png │ ├── 12.png │ ├── 13.png │ ├── 14.png │ ├── 15.png │ ├── 6.png │ ├── 7.png │ ├── 8.png │ ├── 9.png │ ├── image-20210321194020680.png │ ├── image-20210516123802596.png │ ├── image-20210516123816504.png │ ├── image-20210516123839754.png │ └── image-20210516123912837.png ├── lab02.md ├── lab02.zip ├── lab02 │ ├── main.c │ ├── mytool1.c │ ├── mytool1.h │ ├── mytool2.c │ └── mytool2.h └── make_practice.zip ├── lab03a ├── answer.md ├── answer_template.md ├── img │ ├── 1.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ ├── 7.png │ ├── 8.png │ ├── bashrc.jpg │ ├── export_path.jpg │ ├── getenv.jpg │ ├── path.jpg │ ├── sysenv.jpg │ └── which.jpg └── lab03a.md ├── lab03b ├── answer.md ├── answer_template.md └── lab03b.md ├── lab04 ├── answer.md ├── answer_template.md ├── img │ ├── edit.jpg │ ├── fd1.png │ ├── fd2.png │ ├── fhs.jpg │ ├── inode.jpg │ ├── inode_num.jpg │ ├── ls.jpg │ ├── ls_detail.jpg │ ├── pwd.jpg │ └── tree.jpg └── lab04.md ├── lab05 ├── answer.md ├── answer_template.md ├── img │ ├── cpu_switch.jpg │ ├── fork.jpg │ ├── pcb.jpg │ ├── process.jpg │ └── process_status.jpg └── lab05.md ├── lab06 ├── answer.md ├── answer_template.md ├── img │ └── signal.png └── lab06.md ├── lab07 ├── answer.md ├── answer_template.md ├── img │ ├── 1.png │ ├── 2.png │ ├── 3.png │ └── shm.png └── lab07.md ├── lab08 ├── answer.md ├── answer_template.md └── lab08.md ├── week02 ├── answer.md ├── answer_template.md └── img │ ├── errorinfo.png │ └── quitinfo.png ├── week05 ├── answer.md └── answer_template.md ├── week06 ├── answer.md ├── answer_template.md └── img │ └── right.png ├── week09 ├── answer.md ├── answer_template.md └── img │ └── 2021-06-17-20-14-32.png ├── week10 ├── answer.md ├── answer_template.md └── img │ ├── 2021-06-17-20-21-12.png │ ├── 2021-06-17-20-21-44.png │ ├── 2021-06-17-20-23-21.png │ ├── 2021-06-17-20-24-15.png │ └── 2021-06-17-20-28-04.png ├── week13 ├── answer.md ├── answer_template.md └── img │ └── jbw_res.png └── week14 ├── answer.md └── answer_template.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.pdf 2 | *.zip 3 | *.log -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sp-labs 2 | 3 | System Programming Labs 2021 Spring 4 | 5 | ## Repo 文件说明 6 | 7 | - `lab/week`: 实验指导书 & 题目。 8 | - `answer_template`: 提供对应 lab 的参考作答模板。建议将图片放至对应的`img/`文件夹下,使用 Markdown 编写,最后通过 Typora 转成 PDF 后交至云平台。 9 | - `answer`: 实验/作业的参考答案。 10 | 11 | ## 实验安排 12 | 13 | (可至相应助教处答疑) 14 | 15 | | Labs | Status | Leader | Collaborators | 16 | | ------------------------------------ | --------- | ------ | ------------- | 17 | | 01: 基础命令 & vi 操作 | 已发布 | 陈伟东 | 朱英豪 | 18 | | 02: GCC & GDB & Makefile | 已发布 | 王博瑞 | 陈伟东 | 19 | | 03a: 权限 & 重定向 & 管道 & 环境变量 | 已发布 | 朱英豪 | 陈伟东 | 20 | | 03b: Shell 编程 | 已发布 | 朱英豪 | 陈伟东 | 21 | | 04: 文件 I/O 操作 | 已发布 | 王博瑞 | 朱英豪 | 22 | | 05: Linux 进程管理 | 已发布 | 邢湧喆 | 朱英豪 | 23 | | 06: 信号及信号处理 | 已发布 | 李晓洲 | 王博瑞 | 24 | | 07: 进程间通信 | 已发布 | 李晓洲 | 邢湧喆 | 25 | | 08: 多线程编程 | 已发布 | 邢湧喆 | 李晓洲 | 26 | 27 | ## 机房排班 28 | 29 | | 日期 | 实验内容 | 1 号机房 | 2 号机房 | 3 号机房 | 30 | | ---- | ------------------------ | -------- | -------- | -------- | 31 | | 3.21 | 01: 基础命令 & vi 操作 | 陈伟东 | 朱英豪 | 李晓洲 | 32 | | 3.28 | 02: GCC & GDB & Makefile | 陈伟东 | 朱英豪 | 王博瑞 | 33 | | 4.18 | 03a/b: Shell 编程相关 | 陈伟东 | 朱英豪 | 邢湧喆 | 34 | | 4.25 | 04: 文件 I/O 操作 | 陈伟东 | 朱英豪 | 王博瑞 | 35 | | 5.16 | 05: Linux 进程管理 | 邢湧喆 | 朱英豪 | 陈伟东 | 36 | | 5.23 | 06: 信号及信号处理 | 邢湧喆 | 李晓洲 | 王博瑞 | 37 | | 6.13 | 07/08: 进程间通信,多线程编程 | 邢湧喆 | 李晓洲 | 王博瑞 | 38 | | 6.20 | 期末考试 | 祝 | 好 | 运 | 39 | -------------------------------------------------------------------------------- /lab01/answer.md: -------------------------------------------------------------------------------- 1 | # Lab01 Assignment 参考答案 2 | 3 | ## 实验准备 4 | 5 | - 请安装一个合适的 Linux 系统,你安装的 Linux 发行版及版本号是什么?内核版本号是什么? 6 | 7 | 截图:![fig](img/1.png) 8 | 9 | 答案:Centos 7.6;Linux version 3.10.0-957.el7.x86_64; 10 | 11 | - 查看你的根目录下有哪几个子目录,每个子目录主要用来做什么用的? 12 | 13 | 截图:![fig](img/2.png) 14 | 15 | 答案: 16 | 17 | - /srv 主要用来存储本机或本服务器提供的服务或数据。 18 | - /media 是挂在多媒体设备的目录,如默认情况下的光盘、优盘、硬盘等设备都挂在在此目录 19 | - /opt 用来安装附加软件包,是用户级的程序目录 20 | - /run 是一个临时文件系统,存储系统启动以来的信息。当系统重启时,这个目录下的文件应该被删掉或清除。如果你的系统上有 /var/run 目录,应该让它指向 run。 21 | - /boot 这里存放的是启动 Linux 时使用的一些核心文件,包括一些连接文件以及镜像文件。 22 | - /sys sysfs 文件系统集成了下面 3 种文件系统的信息:针对进程信息的 proc 文件系统、针对设备的 devfs 文件系统以及针对伪终端的 devpts 文件系统。该文件系统是内核设备树的一个直观反映。当一个内核对象被创建的时候,对应的文件和目录也在内核对象子系统中被创建。 23 | - /bin 二进制可执行命令 24 | - /dev 设备特殊文件 25 | - /etc 系统管理和配置文件 26 | - /home 用户主目录的基点,比如用户 user 的主目录就是/home/user 27 | - /lib 标准程序设计库,又叫动态链接共享库,作用类似 windows 里的.dll 文件 28 | - /sbin 系统管理命令,这里存放的是系统管理员使用的管理程序 29 | - /tmp 保存在使用完毕后可随时销毁的缓存文件。 30 | - /root 系统管理员的主目录 31 | - /mnt 系统提供这个目录是让用户临时挂载其他的文件系统。 32 | - /proc 虚拟的目录,是系统内存的映射。可直接访问这个目录来获取系统信息。 33 | - /var 系统产生的不可自动销毁的缓存文件、日志记录 34 | - /usr 最庞大的目录,要用到的应用程序和文件几乎都在这个目录 35 | - /usr/bin 众多的应用程序 36 | - /usr/sbin 超级用户的一些管理程序 37 | - /usr/lib 常用的动态链接库和软件包的配置文件 38 | 39 | - 查看自己的 ip 地址,并 ping 一下 baidu.com 看网络是否连通? 40 | 41 | 命令:ip addr;ping baidu.com 42 | 43 | 截图: 44 | ![fig](img/3.png) 45 | ![fig](img/4.png) 46 | 47 | - 用软件安装命令下载 build-essential。 48 | 49 | 命令: sudo apt-get install build-essential 50 | 51 | 截图: 52 | 53 | ## 1. Linux 命令操作 54 | 55 | - Linux 命令行操作,请用你学到的 Linux 命令,实现以下操作,给出每一步你的命令行截图。 56 | - 用 wget 从`https://github.com/BUAA-SE-2021/patpat/releases/download/v0.1.3/patpat-linux-amd64`下载你们的 Linux 版 OOP 课 Java 自助评测机 patpat 57 | - `https://github.com/BUAA-SE-2021/sp-labs/lab01/lab01.zip`处下载实验压缩包 58 | - 解压`lab01.zip`。 59 | - 进入`lab01`目录,进入子目录`etc`,打印当前路径并在当前路径下创建名为`a1`的目录,并在`a1`目录中创建名为`b1`的目录。 60 | - 进入`b1`目录中,创建两个文件`a.txt`,`b.txt`。 61 | - 退回子目录`etc`,删除目录`a1`。 62 | - 将`etc`目录下所有以`tmp`开头的文件移动到`lab01`目录下的`Download`目录下的`tmp`目录中。 63 | - 查看`tmp`目录下`a1005.cpp`的内容。 64 | - 查看`tmp`目录下`a1009.cpp`的前十行和后十行。 65 | - 将`tmp`目录下的所有文件打成一个`tar`包,并命名为`tmp.tar.gz`。 66 | - 返回`lab01`目录,列出当前目录下的文件大小。 67 | - 用命令找出空目录并将空目录删除。 68 | 69 | 命令: 70 | 71 | ```shell 72 | # 不完整的部分自行补充,填写必要注释 73 | wget -O patpat https://github.com/BUAA-SE-2021/patpat/releases/download/v0.1.3/patpat-linux-amd64 # 下载 74 | wget -O lab01.zip https://github.com/BUAA-SE-2021/sp-labs/lab01/lab01.zip # 下载实验资料 75 | unzip -n lab01.zip # 解压 76 | cd lab01/etc # 进入目录 77 | pwd # 打印当前路径 78 | mkdir a1 # 创建目录`a1` 79 | mkdir a1/b1 # 创建子目录`b1` 80 | cd a1/b1 # 进入`b1`目录中 81 | touch a.txt # 创建文件`a.txt` 82 | touch b.txt # 创建文件`b.txt` 83 | cd ../../ # 返回子目录`etc` 84 | rm -rf a1 # 删除`a1` 85 | cd c1 86 | mv tmp* ../../Download/tmp # 移动 87 | cd ../../Download/tmp 88 | cat a1005.cpp # 查看 89 | head -n 10 a1009.cpp # 查看前十行 90 | tail -n 10 a1009.cpp # 后十行 91 | tar cvf tmp.tar.gz * # 打包 92 | cd ../../ # 返回`lab01`目录 93 | du -sh # 列出当前目录下的文件大小 94 | find -type d -empty # 用命令找出空目录 95 | rmdir ./upload # 删除空目录 96 | ``` 97 | 98 | 必要的实验截图(如查看前十行、查看文件大小的展示效果) 99 | 100 | ![fig](img/5.png) 101 | ![fig](img/6.png) 102 | ![fig](img/7.png) 103 | ![fig](img/8.png) 104 | 105 | ## 2. vi 模式 106 | 107 | - vi 编辑器有哪几种模式?简述这几种模式间如何互相切换? 108 | 109 | 模式: 110 | 111 | 1. 命令模式 112 | 2. 底行模式 113 | 3. 插入模式 114 | 115 | 如何切换:用 vi 打开文件后默认进入命令模式。在命令模式输入操作符后进入插入模式,输入`esc`退出插入模式,返回命令模式;在命令模式下输入`:`或`/`进入底行模式,输入`esc`退出底行模式,返回命令模式。 116 | 117 | ## 3. vi 命令 118 | 119 | > 写出以下⼀系列操作使⽤的**命令**(底⾏模式的命令加上 : 或 / ): 120 | 121 | ### 3.1. ⽤ `vi` 在当前⽤户家⽬录下新建⽂件 `testfile` 并打开 122 | 123 | ```shell 124 | vi testfile 125 | ``` 126 | 127 | ### 3.2. 设置显示⾏号 128 | 129 | ```shell 130 | :set number 131 | ``` 132 | 133 | ### 3.3. 进⼊ `insert mode` ,输⼊ `3` ⾏⽂本 134 | 135 | ```plain 136 | This is the first line. 137 | This is the second line. 138 | This is the third line. 139 | ``` 140 | 141 | ![fig](img/9.png) 142 | 143 | ### 3.4. 返回到 `command mode` ,将光标移动到第 `2` ⾏,复制这⼀⾏ 144 | 145 | ```shell 146 | yy 147 | ``` 148 | 149 | ### 3.5. 移动光标到⽂档最后⼀⾏,将复制内容粘贴到这⼀⾏后⾯ 150 | 151 | ```shell 152 | p 153 | ``` 154 | 155 | ![fig](img/10.png) 156 | 157 | ### 3.6. 移动光标到第⼀⾏,删除第⼀⾏ 158 | 159 | ```shell 160 | dd 161 | ``` 162 | 163 | ![fig](img/11.png) 164 | 165 | ### 3.7. 从⽂档开头开始查找 `second` ,然后查找下⼀个 166 | 167 | ```shell 168 | /second 169 | n 170 | ``` 171 | 172 | ### 3.8. 将全部 `second` 替换为 `fourth` ,替换过程不需要询问 173 | 174 | ```shell 175 | :%s/second/fourth/g 176 | ``` 177 | 178 | ![fig](img/12.png) 179 | 180 | ### 3.9. 取消显示⾏号 181 | 182 | ```shell 183 | :set nonumber 184 | ``` 185 | 186 | ![fig](img/13.png) 187 | 188 | ### 3.10. 保存修改并退出 `vi` 189 | 190 | ```shell 191 | :wq! 192 | ``` 193 | 194 | ### 3.11. 再次⽤ `vi` 打开 `testfile` ,另存为 `testfile2` 195 | 196 | ```shell 197 | vi testfile 198 | w testfile2 199 | q! 200 | ``` 201 | 202 | ## 4. POSIX 203 | 204 | - 什么是 POSIX 标准?哪些操作系统支持 POSIX 标准? 205 | POSIX 表示可移植操作系统接口,定义了操作系统应该为应用程序提供的接口标准,是 IEEE 为要在各种 UNIX 操作系统上运行软件而定义的一系列 API 标准的总称。 206 | POSIX 并不局限于 Unix 系统,Microsoft windows NT 等也支持 POSIX 标准。 207 | 208 | ## 5. LSB 209 | 210 | - 什么是 LSB 标准?分析它和 POSIX 标准的异同。 211 | LSB 是 Linux 标准化领域中事实上的标准,使各种软件可以很好地在兼容 LSB 标准的系统上运行。 212 | LSB 以 POSIX 和 SUS 标准为基础,还增加了对二进制可执行文件格式规范的定义,保证 Linux 上应用程序源码和二进制文件的兼容性。 213 | -------------------------------------------------------------------------------- /lab01/answer_template.md: -------------------------------------------------------------------------------- 1 | # Lab01 Assignment 2 | 3 | > 班级: 4 | > 学号: 5 | > 姓名: 6 | 7 | ## 实验准备 8 | 9 | - 请安装一个合适的 Linux 系统,你安装的 Linux 发行版及版本号是什么?内核版本号是什么? 10 | 11 | 截图: 12 | 13 | 答案: 14 | 15 | - 查看你的根目录下有哪几个子目录,每个子目录主要用来做什么用的? 16 | 17 | 截图: 18 | 19 | 答案: 20 | 21 | - 查看自己的 ip 地址,并 ping 一下 baidu.com 看网络是否连通? 22 | 23 | 命令: 24 | 25 | 截图: 26 | 27 | - 用软件安装命令下载 `build-essential`(注:在 Ubuntu 等 Debian 系的系统中下为`build-essentian`;在 CentOS / Arch 系的系统中有类似的 package,请自行安装,为后续实验作准备)。 28 | 29 | 命令: 30 | 31 | 截图: 32 | 33 | ## 1. Linux 命令操作 34 | 35 | - Linux 命令行操作,请用你学到的 Linux 命令,实现以下操作,给出每一步你的命令行截图。 36 | - 用 wget 从`https://github.com/BUAA-SE-2021/patpat/releases/download/v0.1.3/patpat-linux-amd64`下载你们的 Linux 版 OOP 课 Java 自助评测机 patpat 37 | - `https://github.com/BUAA-SE-2021/sp-labs/lab01/lab01.zip`处下载实验压缩包 38 | - 解压`lab01.zip`。 39 | - 进入`lab01`目录,进入子目录`etc`,打印当前路径并在当前路径下创建名为`a1`的目录,并在`a1`目录中创建名为`b1`的目录。 40 | - 进入`b1`目录中,创建两个文件`a.txt`,`b.txt`。 41 | - 退回子目录`etc`,删除目录`a1`。 42 | - 将`etc`目录下所有以`tmp`开头的文件移动到`lab01`目录下的`Download`目录下的`tmp`目录中。 43 | - 查看`tmp`目录下`a1005.cpp`的内容。 44 | - 查看`tmp`目录下`a1009.cpp`的前十行和后十行。 45 | - 将`tmp`目录下的所有文件打成一个`tar`包,并命名为并命名为`tmp.tar.gz`。 46 | - 返回`lab01`目录,列出当前目录下的文件大小。 47 | - 用命令找出空目录并将空目录删除。 48 | 49 | 命令: 50 | 51 | ```shell 52 | # 不完整的部分自行补充,填写必要注释 53 | code # 下载 54 | code # 下载实验资料 55 | code # 解压 56 | code # 进入目录 57 | code # 打印当前路径 58 | code # 创建目录`a1` 59 | code # 创建子目录`b1` 60 | code # 返回子目录`etc` 61 | code # 删除`a1` 62 | code # 移动 63 | code # 查看 64 | code # 查看前十行和后十行 65 | code # 打包 66 | code # 返回`lab01`目录 67 | code # 列出当前目录下的文件大小 68 | code # 用命令找出空目录 69 | code # 删除空目录 70 | ``` 71 | 72 | 必要的实验截图(如查看前十行、查看文件大小的展示效果) 73 | 74 | ![fig](img/xxx.jpg) 75 | 76 | ## 2. vi 模式 77 | 78 | - vi 编辑器有哪几种模式?简述这几种模式间如何互相切换? 79 | 80 | 模式: 81 | 82 | - 1 83 | - 2 84 | - 3 85 | - 4 86 | - 5 87 | 88 | 如何切换: 89 | 90 | ## 3. vi 命令 91 | 92 | > 写出以下⼀系列操作使⽤的**命令**(底⾏模式的命令加上 : 或 / ): 93 | 94 | ### 3.1. ⽤ `vi` 在当前⽤户家⽬录下新建⽂件 `testfile` 并打开 95 | 96 | ```shell 97 | # 粘贴你的代码 98 | ``` 99 | 100 | ### 3.2. 设置显示⾏号 101 | 102 | ```shell 103 | # 粘贴你的代码 104 | ``` 105 | 106 | ### 3.3. 进⼊ `insert mode` ,输⼊ `3` ⾏⽂本 107 | 108 | ```plain 109 | This is the first line. 110 | This is the second line. 111 | This is the third line. 112 | ``` 113 | 114 | ![fig](img/xxx.jpg) 115 | 116 | ### 3.4. 返回到 `command mode` ,将光标移动到第 `2` ⾏,复制这⼀⾏ 117 | 118 | ```shell 119 | # 粘贴你的代码 120 | ``` 121 | 122 | ### 3.5. 移动光标到⽂档最后⼀⾏,将复制内容粘贴到这⼀⾏后⾯ 123 | 124 | ```shell 125 | # 粘贴你的代码 126 | ``` 127 | 128 | ![fig](img/xxx.jpg) 129 | 130 | ### 3.6. 移动光标到第⼀⾏,删除第⼀⾏ 131 | 132 | ```shell 133 | # 粘贴你的代码 134 | ``` 135 | 136 | ![fig](img/xxx.jpg) 137 | 138 | ### 3.7. 从⽂档开头开始查找 `second` ,然后查找下⼀个 139 | 140 | ```shell 141 | # 粘贴你的代码 142 | ``` 143 | 144 | ### 3.8. 将全部 `second` 替换为 `fourth` ,替换过程不需要询问 145 | 146 | ```shell 147 | # 粘贴你的代码 148 | ``` 149 | 150 | ![fig](img/xxx.jpg) 151 | 152 | ### 3.9. 取消显示⾏号 153 | 154 | ```shell 155 | # 粘贴你的代码 156 | ``` 157 | 158 | ![fig](img/xxx.jpg) 159 | 160 | ### 3.10. 保存修改并退出 `vi` 161 | 162 | ```shell 163 | # 粘贴你的代码 164 | ``` 165 | 166 | ### 3.11. 再次⽤ `vi` 打开 `testfile` ,另存为 `testfile2` 167 | 168 | ```shell 169 | # 粘贴你的代码 170 | ``` 171 | 172 | ## 4. POSIX 173 | 174 | - 什么是 POSIX 标准?哪些操作系统支持 POSIX 标准? 175 | 176 | ## 5. LSB 177 | 178 | - 什么是 LSB 标准?分析它和 POSIX 标准的异同。 179 | 180 | ## 6. 实验感想 181 | -------------------------------------------------------------------------------- /lab01/img/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab01/img/1.png -------------------------------------------------------------------------------- /lab01/img/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab01/img/10.png -------------------------------------------------------------------------------- /lab01/img/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab01/img/11.png -------------------------------------------------------------------------------- /lab01/img/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab01/img/12.png -------------------------------------------------------------------------------- /lab01/img/13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab01/img/13.png -------------------------------------------------------------------------------- /lab01/img/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab01/img/2.png -------------------------------------------------------------------------------- /lab01/img/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab01/img/3.png -------------------------------------------------------------------------------- /lab01/img/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab01/img/4.png -------------------------------------------------------------------------------- /lab01/img/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab01/img/5.png -------------------------------------------------------------------------------- /lab01/img/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab01/img/6.png -------------------------------------------------------------------------------- /lab01/img/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab01/img/7.png -------------------------------------------------------------------------------- /lab01/img/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab01/img/8.png -------------------------------------------------------------------------------- /lab01/img/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab01/img/9.png -------------------------------------------------------------------------------- /lab01/img/vi.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab01/img/vi.jpg -------------------------------------------------------------------------------- /lab01/lab01.md: -------------------------------------------------------------------------------- 1 | # Lab01: 基础命令 & vi 操作 2 | 3 | [TOC] 4 | 5 | ## 1. 实验目的 6 | 7 | 通过使用 Linux 的基础命令和了解 vi 的基本操作,初识 Linux 系统及其基础概念,体会`UNIX`**一切皆*文件***的设计思想。通过学习 vi 编辑器并查看启动配置文件,体会`UNIX`**一切皆*文本***的设计理念。 8 | 9 | 提示: 10 | 11 | - 结合书上例子和所给的 Linux 常用命令结合学习。 12 | 13 | ## 2. 实验内容 14 | 15 | - Linux 基本命令 16 | - 绝对路径与相对路径的区别。 17 | - 一些常用的文件操作命令:包括对于文件的增删改查等常用操作。 18 | - 压缩解压命令:Linux 下有多种格式的压缩文件,掌握并知道他们的不同。 19 | - 权限操作命令:通过常用的权限操作命令,了解 Linux 系统下的不同权限与角色。 20 | - 网络管理命令:了解 Linux 下一些常用的网络管理命令,便于了解自己机器的网络状态。 21 | - 软件安装命令:掌握两种常见的软件安装命令。 22 | - 文件系统相关命令(选学)。 23 | - vi 编辑器的使用 24 | - 学习三种使用模式下常用的操作及其相互切换方法。 25 | 26 | ## 3. 实验指南 27 | 28 | ### 3.1. 基本命令行操作 29 | 30 | Linux 命令是对 Linux 系统进行管理的工具。虽然现在的操作系统大多搭载了图形用户界面(GUI),但是在比如说服务器上,一般只有命令行用户界面(CLI),所以掌握一些常用的命令行操作是十分实用且有必要的。 31 | 32 | #### 3.1.1. 绝对路径与相对路径 33 | 34 | Linux 操作系统中存在着两种路径:**绝对路径**和**相对路径**。我们在访问文件或文件夹的时候,其实都是通过路径来操作的。两种路径在实际操作中能起到同等的作用。 35 | 36 | **绝对路径**永远都是相对于根文件夹的。它们的标志就是第一个字符永远都是“/”。 37 | 38 | **相对路径**永远都是相对于我们所处的文件夹位置。它们的第一个字符没有“/”。 39 | 40 | 比如`/home/usr/test/h.c`,这就是绝对路径。如果当前处于`usr`文件夹下,那么相对路径就是`test/h.c`。 41 | 42 | > `.`表示*当前路径*,`./test/h.c`和以上相对路径等价,`..`表示上一级目录,故也可写作 `../usr/test/h.c` 43 | 44 | 下面给出一些用的可能比较多的命令,要求**熟练掌握**,剩下一些命令可在日后学习开发过程中逐渐接触。 45 | 46 | #### 3.1.2. 文件操作命令 47 | 48 | - `ls` 查看目录和文件 如 `ls /home/usr/test`,就会列出该目录下文件。 49 | - `pwd` 查看当前路径。 50 | - `mkdir` 创建目录, `mkdir /home/usr` 在`/home`下创建`usr`目录。 51 | - `cd` 切换目录,`cd /home/usr/test` 。`cd` 可以使用`.`和`..` ,即 `cd .` 和 `cd ..`。 52 | - `touch` 本用于修改文件或者目录的时间属性,包括存取时间和更改时间,格式为`touch [-选项][-d<日期时间>][-r<参考文件或目录>] [-t<日期时间>][--help][--version][文件或目录…]`当文件不存在时,会**建立一个新的文件**,所以此命令一般也用于创建文件。 53 | - `cp` 复制文件,命令格式 `cp [源路径] [目的路径]`。 54 | - `mv` 移动文件,命令格式 `mv[源路径] [目的路径]`。 55 | - `rm` 删除文件或目录,命令格式 `rm [目标路径]`,参数`-r`递归删除,即删除子文件夹,`-f`强制删除。 56 | - `rmdir` 删除目录 `rmdir [路径]`。 57 | - `cat` 查看文件内容, `cat /home/usr/test/h.c` , 输出文件中内容。 58 | - `more` 分页查看。比如当`cat`输出大量文本内容时,很难看清,则可以通过`cat [文件] | more`来分页查看。`f/`空格显示下一页,回车显示下一行,`Q/q`退出。此处涉及到管道,有兴趣的可自行搜索。 59 | - `less`是`more`的加强版,`less`在查看之前不会加载整个文件,并且可以随意浏览文件,不像`more`仅能向前移动,推荐使用! 60 | - `which` 查找某一命令所在的路径,格式为`which 命令`,`whereis`命令与其类似,但是`whereis`还能展示命令帮助文档所在的路径。 61 | - `find`,是最强大的搜索命令,格式为`find 搜索路径 [选项] 搜索关键字`,常用选项为`-name`,`-size`,`-user`,分别可以按文件名查找,根据文件大小查找,根据文件所有者查找。 62 | - `locate` 功能与`find -name`相同,但是实现原理不同,`find`是遍历了搜索路径下的每个文件,而`locate`是搜索在`/var/lib/locatedb`里的一个数据库,相当于散列查找。要注意这个数据库并不是每秒都更新的,所以我们在使用`locate`命令之前,可先使用`updatedb`命令手动更新数据库。 63 | 64 | **使用 pushd 和 popd 命令可以实现目录的快速切换**,具体的操作可参考[简书:Linux 中的 pushd 和 popd](https://www.jianshu.com/p/53cccae3c443)。 65 | 66 | #### 3.1.3. 压缩与解压命令 67 | 68 | Linux 下存在多种格式的压缩文件,其压缩和解压命令如下: 69 | 70 | - `gzip/gunzip` 71 | 72 | 格式为`gzip/gunzip [选项] 文件/压缩包包名`,可分别实现文件的压缩和压缩包的解压,要注意下面两点: 73 | 74 | 1. `gzip`压缩后不保存源文件。 75 | 2. 若同时压缩多个文件,则每个文件会被单独压缩。 76 | 77 | - `zip/unzip` 78 | 79 | 格式为`zip [-r] [压缩后文件名称] 文件或目录`与`unzip [选项] 压缩包包名`。`-r`代表压缩的是一个目录,压缩时会保留源文件。 80 | 81 | #### 3.1.4. 网络管理命令 82 | 83 | - `ifconfig` 查看当前网络配置,如`ip`等。 84 | - `netstat` 查看当前网络状态。 85 | - `ping` 检查网络是否连通。ping www.baidu.com 。 86 | 87 | #### 3.1.5. 软件安装命令 88 | 89 | 在 Linux 下安装软件主要通过以下几种方式: 90 | 91 | - 通过包管理工具,比如`Ubuntu`下的`apt`,`CentOS`的`yum`等,通过包管理工具安装程序非常方便,除了方便安装管理包,还能帮我们解决依赖问题。下面以`Ubuntu`下安装和卸载`mysql`为例。 92 | 93 | ```shell 94 | sudo apt-get install package 安装包 95 | sudo apt-get remove package 删除包 96 | sudo apt-get update 更新源 97 | ``` 98 | 99 | - 先把包通过某些命令下载到本地,再在本地运行安装程序。经常使用的是`wget`命令,格式为`wget [选项] [URL地址]`,可从指定的`URL`下载文件,还可以实现断点续传等功能,详细请看[wget 命令详解](https://www.cnblogs.com/sx66/p/11887022.html)。 100 | 101 | ### 3.2. vi 编辑器的使用 102 | 103 | - 使用 vi 打开文件: 104 | 105 | ```shell 106 | $ vi file 打开已有文件file/创建名为file的新文件并打开 107 | $ vi 创建一个新文件并打开,文件名在保存时指定 108 | ``` 109 | 110 | vi 有三种模式:命令模式(command mode),插入模式(insert mode),底行模式(last line mode)。 111 | 112 | ![vi](img/vi.jpg) 113 | 114 | - 命令模式(command mode):使用 vi 打开文件后默认进入该模式,可以进行光标移动和文本的复制、粘贴、删除。 115 | 116 | | 级别 | 操作符 | 说明 | 117 | | :----: | :----: | :--------------------------: | 118 | | 字符级 | ← 或 h | 光标向左移动一个字符 | 119 | | 字符级 | → 或 l | 光标向右移动一个字符 | 120 | | 单词级 | b | 光标移动到本单词的首字符 | 121 | | 单词级 | e | 光标移动到本单词的尾字符 | 122 | | 单词级 | w | 光标移动到下一个单词的首字符 | 123 | | 行级 | 0 | 光标移动到当前行首字符 | 124 | | 行级 | $ | 光标移动到当前行尾字符 | 125 | | 行级 | ↑ 或 k | 光标移动到上一行相同位置字符 | 126 | | 行级 | ↓ 或 j | 光标移动到下一行相同位置字符 | 127 | | 段落级 | { | 光标移动到段落首字符 | 128 | | 段落级 | } | 光标移动到段落尾字符 | 129 | | 屏幕级 | H | 光标移动到屏幕首行首字符 | 130 | | 屏幕级 | L | 光标移动到屏幕尾行首字符 | 131 | | 文档级 | **n**G | 光标移动到文档第 n 行首字符 | 132 | | 文档级 | G | 光标移动到文档尾行首字符 | 133 | 134 | | 级别 | 操作符 | 说明 | 135 | | :----: | :------: | :--------------------------------------: | 136 | | 字符级 | y← 或 yh | 复制前一个字符 | 137 | | 字符级 | y→ 或 yl | 复制当前字符 | 138 | | 单词级 | yb | 从单词首字符开始复制直到当前字符(不包括) | 139 | | 单词级 | ye 或 yw | 从当前字符开始复制直到单词尾字符(包括) | 140 | | 行级 | y0 | 从行首字符开始复制直到当前字符(不包括) | 141 | | 行级 | y$ | 从当前字符开始复制直到行尾字符(包括) | 142 | | 行级 | yy | 复制当前行 | 143 | | 行级 | y↑ 或 yk | 复制上一行和当前行 | 144 | | 行级 | y↓ 或 yj | 复制当前行和下一行 | 145 | | 行级 | **n**yy | 复制包括当前行在内的后面 n 行 | 146 | | 段落级 | y{ | 从段落首字符开始复制直到当前字符(不包括) | 147 | | 段落级 | y} | 从当前字符开始复制直到段落尾字符(包括) | 148 | | 屏幕级 | yH | 从屏幕首行开始复制直到当前行(包括) | 149 | | 屏幕级 | yL | 从当前行开始复制直到屏幕尾行(包括) | 150 | | 文档级 | yG | 从当前行开始复制直到文档尾行(包括) | 151 | | 文档级 | y**n**G | 从第 n 行开始复制直到当前行(包括) | 152 | 153 | | 操作符 | 说明 | 154 | | :----: | :------------------------------------: | 155 | | p | 将复制的内容粘贴到当前字符的下一个位置 | 156 | 157 | | 操作符 | 说明 | 158 | | :-----: | :---------------------------: | 159 | | x | 删除当前字符 | 160 | | dd | 删除当前行 | 161 | | **n**dd | 删除包括当前行在内的后面 n 行 | 162 | 163 | | 操作符 | 说明 | 164 | | :----: | :----------------: | 165 | | u | 撤销命令 | 166 | | . | 重复执行上一次命令 | 167 | | J | 合并两行内容 | 168 | | r | 快速替换当前字符 | 169 | 170 | - 插入模式(insert mode):在命令模式下输入操作符(如`i`)进入插入模式,该模式下可以修改文件内容,与 Windows 记事本的操作类似。 171 | 172 | | 操作符 | 说明 | 173 | | :----: | :------------------------------: | 174 | | a | 光标后移一位进入编辑模式 | 175 | | s | 删除当前字符进入编辑模式 | 176 | | o | 当前行之下新起一行进入编辑模式 | 177 | | A | 光标移动到当前行末尾进入编辑模式 | 178 | | I | 光标移动到当前行行首进入编辑模式 | 179 | | S | 删除当前行进入编辑模式 | 180 | | O | 当前行之上新起一行进入编辑模式 | 181 | 182 | `Esc`从编辑模式返回命令模式。 183 | 184 | - 底行模式(last line mode):在命令模式下输入`:`或`/`进入底行模式,可以进行编辑器的设置、文本查找与替换、文件保存、退出编辑器。 185 | 186 | | 操作符 | 说明 | 187 | | :-------: | :------: | 188 | | :set nu | 设置行号 | 189 | | :set nonu | 取消行号 | 190 | 191 | 底行模式下对 vi 编辑器的设置只对本次操作有效。 192 | 193 | vi 编辑器的用户配置信息存放在`~/.vimrc`文件中,可以通过修改该文件来进行长久有效的配置。 194 | 195 | ```shell 196 | $ vi ~/.vimrc 197 | ``` 198 | 199 | | 设置 | 说明 | 200 | | :-------------: | :-------------: | 201 | | set number | 设置行号 | 202 | | set autoindent | 自动对齐 | 203 | | set smartindent | 智能对齐 | 204 | | set cindent | C 语言格式对齐 | 205 | | set showmatch | 括号匹配 | 206 | | set tabstop=4 | Tab 为 4 个空格 | 207 | | set mouse=a | 鼠标支持 | 208 | 209 | | 操作符 | 说明 | 210 | | :------------------------: | :------------------------------------------: | 211 | | :**n** | 光标移动到第 n 行 | 212 | | /查找内容 | 查找指定内容,n 下一个,N 上一个 | 213 | | :s/被替换内容/替换内容/ | 替换当前行的第一个目标 | 214 | | :s/被替换内容/替换内容/g | 替换当前行的全部目标 | 215 | | :%s/被替换内容/替换内容/g | 替换整个文档的全部目标 | 216 | | :%s/被替换内容/替换内容/gc | 替换整个文档的全部目标,替换每个内容时都询问 | 217 | 218 | | 操作符 | 说明 | 219 | | :-----------: | :------------------------------------------------------------------------: | 220 | | :w [文件名] | 保存文件 | 221 | | :q | 退出编辑器 | 222 | | :wq [文件名] | 保存文件并退出编辑器 | 223 | | :w! [文件名] | 对于没有修改权限的用户强行保存对文件的修改,修改后的所有者和所属组发生变化 | 224 | | :q! | 强行退出编辑器,不保存修改 | 225 | | :wq! [文件名] | 强行保存修改并退出编辑器 | 226 | 227 | `Esc`清空底行或返回命令模式。 228 | 229 | ### 3.3. 扩展:Tmux 的使用 230 | 231 | Tmux 是一个终端复用器(terminal multiplexer),它将会话与窗口的"解绑",使二者彻底分离。非常有用,属于常用的开发工具。 232 | 233 | 详细请看:[Tmux 使用教程](http://www.ruanyifeng.com/blog/2019/10/tmux.html) 234 | 235 | ## 4. 实验准备 236 | 237 | - 请安装一个合适的 `Linux` 系统,你安装的 `Linux` 发行版及版本号是什么?内核版本号是什么? 238 | - 查看你的根目录下有哪几个子目录,每个子目录主要用来做什么用的? 239 | - 查看自己的 ip 地址,并 `ping` 一下 `baidu.com` 看网络是否连通? 240 | - 用软件安装命令下载 `build-essential`(注:在 Ubuntu 等 Debian 系的系统中下为`build-essentian`;在 CentOS / Arch 系的系统中有类似的 package,请自行安装,为后续实验作准备)。 241 | 242 | ## 5. 实验操作 243 | 244 | - Linux 命令行操作,请用你学到的 Linux 命令,实现以下操作,给出每一步你的命令行截图。 245 | - 用 wget 从`https://github.com/BUAA-SE-2021/patpat/releases/download/v0.1.3/patpat-linux-amd64`下载你们的 Linux 版 OOP 课 Java 自助评测机 patpat 246 | - 从`https://github.com/BUAA-SE-2021/sp-labs/lab01`处下载实验资料。 247 | - 解压实验资料里的`lab01.zip`。 248 | - 进入`lab01`目录,进入子目录`etc`,打印当前路径并在当前路径下创建名为`a1`的目录,并在`a1`目录中创建名为`b1`的目录。 249 | - 进入`b1`目录中,创建两个文件`a.txt`,`b.txt`。 250 | - 退回子目录`etc`,删除目录`a1`。 251 | - 将`etc`目录下所有以`tmp`开头的文件移动到`lab01`目录下的`Download`目录下的`tmp`目录中。 252 | - 查看`tmp`目录下`a1005.cpp`的内容。 253 | - 查看`tmp`目录下`a1009.cpp`的前十行和后十行。 254 | - 将`tmp`目录下的所有文件打成一个`tar`包,并命名为并命名为`tmp.tar.gz`。 255 | - 返回`lab01`目录,列出当前目录下的文件大小。 256 | - 用命令找出 lab01 下的空目录并将空目录删除。 257 | - vi 编辑器有哪几种模式?简述这几种模式间如何互相切换? 258 | - 写出以下一系列操作使用的**命令**(底行模式的命令加上`:`或`/`): 259 | - 用 vi 在当前用户家目录下新建文件`testfile`并打开。 260 | - 设置显示行号。 261 | - 进入`insert mode`,输入 3 行文本"`This is the first line.`","`This is the second line.`","`This is the third line.`"。 262 | - 返回到`command mode`,将光标移动到第 2 行,复制这一行。 263 | - 移动光标到文档最后一行,将复制内容粘贴到这一行后面。 264 | - 移动光标到第一行,删除第一行。 265 | - 从文档开头开始查找`second`,然后查找下一个。 266 | - 将全部`second`替换为`fourth`,替换过程不需要询问。 267 | - 取消显示行号。 268 | - 保存修改并退出 vi。 269 | - 再次用 vi 打开`testfile`,另存为`testfile2`。 270 | - 什么是 POSIX 标准?哪些操作系统支持 POSIX 标准? 271 | - 什么是 LSB 标准?分析它和 POSIX 标准的异同。 272 | -------------------------------------------------------------------------------- /lab01/lab01.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab01/lab01.zip -------------------------------------------------------------------------------- /lab02/answer.md: -------------------------------------------------------------------------------- 1 | # Lab02 Assignment 参考答案 2 | 3 | ## 1. GCC 4 | 5 | - GCC 将一个源程序转换为可执行文件经历了哪些主要步骤? 6 | 7 | 源⽂件->预处理->编译->汇编->链接->可执⾏⽂件 8 | 9 | - 请利用`test.c`用 GCC 命令将其将其转换为可执行程序的主要过程表示出来。 (命令和截图) 10 | 11 | ```sh 12 | gcc -E test.c -o test.i 13 | gcc -S test.i -o test.s 14 | gcc -c test.s -o test.o 15 | gcc test.o -o test 16 | ./test 17 | ``` 18 | 19 | ## 2. 静态库 20 | 21 | - 解压 lab02.zip 22 | 23 | ```sh 24 | unzip lab02.zip 25 | ``` 26 | 27 | - 使用 gcc 命令分别将`mytool1.c`和`mytool2.c` 编译成 .o 目标文件 28 | 29 | ```sh 30 | gcc -c mytool1.c -o mytool1.o 31 | gcc -c mytool2.c -o mytool2.o 32 | ``` 33 | 34 | - 说明上述两个命令完成了什么事? 35 | 36 | ```sh 37 | ar cr libmylib.a mytool2.o mytool1.o 38 | # 该命令将mytool2.o,mytool1.o这两个⽂件创建⽣成了libmylib.a这个静态库⽂件 39 | gcc -o main main.c -L. -lmylib 40 | # ⾸先将main.c编译⽣成了可执⾏⽂件main。在编译的时候链接静态库libmylib.a,表明main.c需要的函 数在该库中。 41 | ``` 42 | 43 | - 查看`main`文件大小,并记录 44 | 45 | ```sh 46 | ls -l 47 | # main此时⼤⼩为8800b 48 | ``` 49 | 50 | - 执行`./main` 51 | 52 | ```sh 53 | ./main 54 | ``` 55 | 56 | - 删除之前生成的静态库文件,重新执行`./main`命令,对比上一步骤得到的结果,你有什么发现?并解释原因。 57 | 58 | ```sh 59 | rm libmylib.a 60 | ./main 61 | ``` 62 | 63 | 运⾏结果和上⼀次的完全⼀样。这说明在⽣成可执⾏⽂件时,所需的静态库中的内容也被链接到了main中。在执⾏main的时候不再依赖静态库,所以程序运⾏不受影响。 64 | 65 | ## 3.动态库 66 | 67 | - 执行下面两个命令 68 | 69 | ```shell 70 | gcc -c -fPIC mytool2.c -o mytool2.o 71 | gcc -c -fPIC mytool1.c -o mytool1.o 72 | gcc -shared -o libmylib.so mytool2.o mytool1.o 73 | ``` 74 | 75 | ```shell 76 | gcc -o main main.c -L. -lmylib 77 | ``` 78 | 79 | - 查看`main`文件大小,并和之前的作比较,解释原因。 80 | 81 | ```sh 82 | ls -l 83 | # main⽂件的⼤⼩变为了8648b。 84 | ``` 85 | 86 | 原因:在⽣成可执⾏⽂件时,动态库不会被链接进去。所以⽣成的可执⾏⽂件⼤⼩⽐静态库的要⼩。但是此时其运⾏需要依赖动态库。 87 | 88 | - 执行命令将当前目录添加到库搜索路径中 89 | 90 | ```shell 91 | export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:; 92 | ``` 93 | 94 | - 执行`./main` 95 | 96 | ```sh 97 | ./main 98 | ``` 99 | 100 | 此时的运⾏结果和之前完全相同。 101 | 102 | - 删除之前生成的动态库文件,重新执行`./main`命令,对比上一步骤得到的结果,你有什么发现? 103 | 104 | ```sh 105 | rm libmylib.so 106 | ./main 107 | ``` 108 | 109 | - 综合实验,你觉得静态库和动态库的区别和相同点是什么?谈谈他们的优缺点。 110 | 111 | 相同点: 112 | 113 | - 静态库和动态库都能提供外界需要的封装好的模块功能,当外界需要调用某个函数时,就可以从库中复制对应的代码。 114 | 115 | 区别: 116 | 117 | - 命名上:静态库⽂件名的命名⽅式是“libxxx.a”,库名前加”lib”,后缀⽤”.a”,“xxx”为静态库名;动态库的命名⽅式与静态库类似,前缀相同,为“lib”,后缀变为“.so”。所以为“libmytime.so” 118 | - 链接上:静态库的代码是在编译过程中被载⼊程序中;动态库在编译的时候并没有被编译进⽬标代码,⽽是当你的程序执⾏到相关函数时才调⽤该函数库⾥的相应函数 119 | - 更新上:如果所使⽤的静态库发⽣更新改变,你的程序必须重新编译;动态库的改变并不影响你的程序,动态函数库升级⽐较⽅便 120 | 121 | 当同⼀个程序分别使⽤静态库,动态库两种⽅式⽣成两个可执⾏⽂件时,静态链接所⽣成的⽂件所占⽤的内存要远远⼤于动态链接所⽣成的⽂件 122 | 123 | - 内存上:静态库每⼀次编译都需要载⼊静态库代码,内存开销⼤;系统只需载⼊⼀次动态库,不同的程序可以得到内存中相同的动态库的副本,内存开销⼩ 124 | - 静态库和程序链接有关和程序运⾏⽆关;动态库和程序链接⽆关和程序运⾏有关 125 | 126 | 相同点: 127 | 128 | - 静态链接库与动态链接库都是共享代码的⽅式 129 | 130 | 静态库优点: 131 | 132 | - 在编译后的执⾏程序不再需要外部的函数库⽀持,运⾏速度相对快些; 133 | 134 | 静态库缺点: 135 | 136 | - 如果所使⽤的静态库发⽣更新改变,程序必须重新编译。 137 | 138 | 动态库优点: 139 | 140 | - 系统只需载⼊⼀次动态库,不同的程序可以得到内存中相同的动态库的副本,内存开销⼩; 141 | - 动态库的改变并不影响程序,所以动态函数库升级⽐较⽅便 142 | 143 | 动态库缺点: 144 | 145 | - 因为函数库并没有整合进程序,所以程序的运⾏环境必须提供相应的库。 146 | 147 | ## 4. GDB 148 | 149 | 粘贴各步骤结果截图 150 | 151 | (3)执行 gdb a.out 命令 152 | 153 | ![image-20210516123802596](img/image-20210516123802596.png) 154 | 155 | (5)在 main 函数处设置断点 156 | 157 | ![image-20210516123816504](img/image-20210516123816504.png) 158 | 159 | (6)输入 run 命令开始程序 160 | 161 | ![image-20210516123839754](img/image-20210516123839754.png) 162 | 163 | (7)多次输入 next 命令使程序运行到第 13 行,使用 print 命令打印 num 的值 164 | 165 | ![6](img/6.png) 166 | 167 | (8)继续调试至程序第 16 行,使用 print 命令打印 factorial 的值 168 | 169 | ![7](img/7.png) 170 | 171 | (9)使用 run 命令再次调试程序 172 | 173 | ![8](img/8.png) 174 | 175 | (10)在程序第 10 行加入断点 176 | 177 | ![9](img/9.png) 178 | 179 | (11)使用 continue 命令使程序运行到断点处 180 | 181 | ![10](img/10.png) 182 | 183 | (12)使用 next 命令 184 | 185 | ![11](img/11.png) 186 | 187 | (13)使用 print 命令打印 i 和 factorial 的值 188 | 189 | ![12](img/12.png) 190 | 191 | (14)使用 p factorial=1 命令改变 factorial 的值 192 | 193 | ![13](img/13.png) 194 | 195 | (15)使用 info locals 查看所有局部变量值 196 | 197 | ![14](img/14.png) 198 | 199 | (16)继续调试至程序结束 200 | 201 | ![15](img/15.png) 202 | 203 | (17)说明源程序中存在的错误 204 | 205 | ​ factorial 没有初始化 206 | 207 | ## 5. Makefile 208 | 209 | - 补全 makefile 文件 210 | 211 | ```shell 212 | SOURCE = $(wildcard ./src/*.c) #获取所有的.c文件 213 | # vpath %.c src 214 | OBJ = $(patsubst %.c, %.o, $(SOURCE)) #将.c文件转为.o文件 215 | 216 | INCLUDES = -I ./include #头文件路径 217 | 218 | CFLAGS = -Wall -c #编译标志位 219 | 220 | LIBS = -ldylib #库文件名字 221 | LIB_PATH = -L./lib #库文件地址 222 | 223 | TARGET = app 224 | CC = gcc 225 | 226 | $(TARGET): $(OBJ) 227 | @mkdir -p output/ 228 | $(CC) $(OBJ) $(LIB_PATH) $(LIBS) -o output/$(TARGET) 229 | export LD_LIBRARY_PATH=./lib 230 | %.o: %.c 231 | $(CC) $(CFLAGS) $(INCLUDES) $< -o $@ 232 | .PHONY: clean 233 | clean: 234 | rm -rf $(OBJ) output/ 235 | print: 236 | echo $(OBJ) 237 | ``` 238 | -------------------------------------------------------------------------------- /lab02/answer_template.md: -------------------------------------------------------------------------------- 1 | # Lab02 Assignment 2 | 3 | > 班级: 4 | > 学号: 5 | > 姓名: 6 | 7 | ## 1. GCC 8 | 9 | - GCC 将一个源程序转换为可执行文件经历了哪些主要步骤? 10 | 11 | - 请利用`test.c`用 GCC 命令将其将其转换为可执行程序的主要过程表示出来。 (命令和截图) 12 | 13 | ## 2.静态库 14 | 15 | - 解压 lab02.zip 16 | code # 17 | 18 | - 使用 gcc 命令分别将`mytool1.c`和`mytool2.c` 编译成 .o 目标文件 19 | code # 20 | 21 | - 说明上述两个命令完成了什么事? 22 | code # 23 | 24 | - 查看`main`文件大小,并记录 25 | code # 26 | 27 | - 执行`./main` 28 | code # 29 | 30 | - 删除之前生成的静态库文件,重新执行`./main`命令,对比上一步骤得到的结果,你有什么发现?并解释原因。 31 | code # 32 | 33 | ## 3.动态库 34 | 35 | - 执行下面两个命令 36 | 37 | ```shell 38 | gcc -c -fPIC mytool2.c -o mytool2.o 39 | gcc -c -fPIC mytool1.c -o mytool1.o 40 | gcc -shared -o libmylib.so mytool2.o mytool1.o 41 | ``` 42 | 43 | ```shell 44 | gcc -o main main.c -L. -lmylib 45 | ``` 46 | 47 | code # 48 | 49 | - 查看`main`文件大小,并和之前的作比较,解释原因。 50 | code # 51 | 52 | - 执行命令将当前目录添加到库搜索路径中 53 | 54 | ````shell 55 | export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:; 56 | ``` 57 | ```` 58 | 59 | code # 60 | 61 | - 执行`./main` 62 | code # 63 | 64 | - 删除之前生成的动态库文件,重新执行`./main`命令,对比上一步骤得到的结果,你有什么发现? 65 | code # 66 | 67 | - 综合实验,你觉得静态库和动态库的区别和相同点是什么?谈谈他们的优缺点。 68 | code # 69 | 70 | ## 4. GDB 71 | 72 | 粘贴各步骤结果截图 73 | 74 | (3)执行 gdb a.out 命令 75 | 76 | (5)在 main 函数处设置断点 77 | 78 | (6)输入 run 命令开始程序 79 | 80 | (7)多次输入 next 命令使程序运行到第 13 行,使用 print 命令打印 num 的值 81 | 82 | (8)继续调试至程序第 16 行,使用 print 命令打印 factorial 的值 83 | 84 | (9)使用 run 命令再次调试程序 85 | 86 | (10)在程序第 10 行加入断点 87 | 88 | (11)使用 continue 命令使程序运行到断点处 89 | 90 | (12)使用 next 命令 91 | 92 | (13)使用 print 命令打印 i 和 factorial 的值 93 | 94 | (14)使用 p factorial=1 命令改变 factorial 的值 95 | 96 | (15)使用 info locals 查看所有局部变量值 97 | 98 | (16)继续调试至程序结束 99 | 100 | (17)说明源程序中存在的错误 101 | 102 | ## 5. Makefile 103 | 104 | - 补全 makefile 文件 105 | 106 | ```shell 107 | # 粘贴你的代码 108 | ``` 109 | 110 | ## 6. 实验感想 111 | -------------------------------------------------------------------------------- /lab02/img/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab02/img/10.png -------------------------------------------------------------------------------- /lab02/img/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab02/img/11.png -------------------------------------------------------------------------------- /lab02/img/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab02/img/12.png -------------------------------------------------------------------------------- /lab02/img/13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab02/img/13.png -------------------------------------------------------------------------------- /lab02/img/14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab02/img/14.png -------------------------------------------------------------------------------- /lab02/img/15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab02/img/15.png -------------------------------------------------------------------------------- /lab02/img/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab02/img/6.png -------------------------------------------------------------------------------- /lab02/img/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab02/img/7.png -------------------------------------------------------------------------------- /lab02/img/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab02/img/8.png -------------------------------------------------------------------------------- /lab02/img/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab02/img/9.png -------------------------------------------------------------------------------- /lab02/img/image-20210321194020680.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab02/img/image-20210321194020680.png -------------------------------------------------------------------------------- /lab02/img/image-20210516123802596.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab02/img/image-20210516123802596.png -------------------------------------------------------------------------------- /lab02/img/image-20210516123816504.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab02/img/image-20210516123816504.png -------------------------------------------------------------------------------- /lab02/img/image-20210516123839754.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab02/img/image-20210516123839754.png -------------------------------------------------------------------------------- /lab02/img/image-20210516123912837.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab02/img/image-20210516123912837.png -------------------------------------------------------------------------------- /lab02/lab02.md: -------------------------------------------------------------------------------- 1 | # Lab02: GCC & GDB & Makefile 2 | 3 | [TOC] 4 | 5 | ## 1. 实验目的 6 | 7 | 通过使用 GCC & GDB & Makefile 熟悉 Linux 中三种常用工具的基本操作,深入了解 Linux 编程环境。 8 | 9 | 提示: 10 | 11 | - 开始实验前请仔细阅读书中第二章内容。 12 | 13 | ## 2. 实验内容 14 | 15 | - GCC/GDB 基本使用方法 16 | - GDB 调试功能 17 | - 静态库和动态库的创建和使用 18 | - 熟悉 make 命令的使用和 Makefile 中的规则编写 19 | 20 | ## 3. 实验指南 21 | 22 | ### 3.1. GCC 23 | 24 | - 无选项编译链接 25 | 26 | - 用法:gcc test.c 27 | 28 | 作用:将 test.c 预处理、编译、汇编并链接形成可执行文件。这里未指定输出文件,默认输出为 a.out。 29 | 30 | - 选项 -o 31 | 32 | - 用法:gcc test.c -o test 33 | 34 | 作用:将 test.c 预处理、编译、汇编并链接形成可执行文件 test。-o 选项用来指定输出文件的文件名。 35 | 36 | - 选项 -E 37 | 38 | - 用法:gcc -E test.c -o test.i 39 | 40 | 作用:将 test.c 预处理输出 test.i 文件。 41 | 42 | - 选项 -S 43 | 44 | - 用法:gcc -S test.i 45 | 46 | 作用:将预处理输出文件 test.i 编译成 test.s 文件。 47 | 48 | - 选项 -c 49 | 50 | - 用法:gcc -c test.s 51 | 52 | 作用:将汇编语言文件 test.s 汇编成目标代码 test.o 文件。 53 | 54 | - 无选项链接 55 | 56 | - 用法:gcc test.o -o test 57 | 58 | 作用:将目标代码文件 test.o 链接成最终可执行文件 test。 59 | 60 | - 选项 -O 61 | 62 | - 用法:gcc -O1 test.c -o test 63 | 64 | 作用:使用编译优化级别 1 编译程序。级别为 1~3,级别越大优化效果越好,但编译时间越长。 65 | 66 | 官方文档:[GCC, the GNU Compiler Collection](https://gcc.gnu.org/) 67 | 68 | ### 3.2. GDB 调试器的使用 69 | 70 | - 在使用 gcc 对程序编译时,需要**加上-g 参数**(产生调试信息)才能使 GDB 进行调试。 71 | 72 | - 输入 help 命令获得帮助 73 | 74 | - 输入 quit 或者按 Ctrl+D 组合键退出 GDB。 75 | 76 | - 启动程序准备调试方法 77 | - 方法一:在执行 GDB 命令时加上要调试的可执行程序名称,如“GDB yourprogram”; 78 | - 方法二:先输入 GDB,在 GDB 中输入 file yourprogram 加载需要调试的程序。最后使用 run 或者 r 命令开始执行,也可以使用 run parameter 方式传递参数 79 | 80 | | 命令 | 命令缩写 | 命令说明 | 81 | | :-------: | :------: | :------------------------------------------------------------------------------------------: | 82 | | list | l | 显示多行源代码 | 83 | | break | b | 设置断点,程序运行到断点的位置会停下来 | 84 | | info | i | 描述程序运行的状态 | 85 | | run | r | 开始运行程序 | 86 | | display | disp | 跟踪查看某个变量,每次停下来都显示它的值 | 87 | | step | s | 执行下一条语句,若该语句为函数调用,则进入函数执行其第一条语句 | 88 | | next | n | 执行下一条语句,若该语句为函数调用,不会进入函数内部执行(即不会一步一步地调试函数内部语句) | 89 | | print | p | 打印内部变量 | 90 | | continue | c | 继续程序的执行直到遇到下一个断点 | 91 | | set var | | 设置变量的值 | 92 | | start | | 开始执行程序,在 main 函数第一条语句前面停下 | 93 | | file | | 装入需要调试的文件 | 94 | | kill | k | 终止正在调试的程序 | 95 | | watch | | 监视变量值的变化 | 96 | | backtrace | bt | 查看函数调用的信息 | 97 | | frame | f | 查看栈帧 | 98 | | quit | q | 退出 GDB 环境 | 99 | | | | | 100 | 101 | - 扩展:GDB Text User Interface 102 | 103 | > The GDB Text User Interface (TUI) is a terminal interface which uses the `curses` library to show the source file, the assembly output, the program registers and GDB commands in separate text windows. The TUI mode is supported only on platforms where a suitable version of the `curses` library is available. 104 | > 105 | > The TUI mode is enabled by default when you invoke GDB as ‘gdb -tui’. You can also switch in and out of TUI mode while GDB runs by using various TUI commands and key bindings, such as `tui enable` or C-x C-a. See [TUI Commands](https://sourceware.org/gdb/current/onlinedocs/gdb/TUI-Commands.html#TUI-Commands), and [TUI Key Bindings](https://sourceware.org/gdb/current/onlinedocs/gdb/TUI-Keys.html#TUI-Keys). 106 | 107 | GDB 的更多使用方法可以参阅[**GDB User Manual**](http://sourceware.org/gdb/current/onlinedocs/gdb/) ([PDF](http://sourceware.org/gdb/current/onlinedocs/gdb.pdf)) 108 | 109 | ### 3.3. Makefile 的使用 110 | 111 | > 代码变成可执行文件,叫做[编译](http://www.ruanyifeng.com/blog/2014/11/compiler.html)(compile);先编译这个,还是先编译那个(即编译的安排), 叫做[构建](http://en.wikipedia.org/wiki/Software_build)(build)。[Make]()是最常用的构建工具,诞生于 1977 年,主要用于 C 语言的项目。但是实际上 ,任何只要某 个文件有变化,就要重新构建的项目,都可以用 Make 构建。 112 | 113 | - 当使用 make 构建项目时,具体的构建规则被写在一个叫做 Makefile 的文件中。本次课程的内容有了解 make 命令、了解 Makefile 文件的概念并学习 Makefile 文件的基本语法。还可以参考[阮一峰的 make 命令教程](http://www.ruanyifeng.com/blog/2015/02/make.html)。遇到不懂的地方可以查阅官方文档[GNU make](https://www.gnu.org/software/make/manual/make.html)。 114 | 115 | 注:教材 P49“vpath %.xyz” 中间不应该有'.',应为'vpath % xyz' 116 | 117 | ## 4. 实验习题 118 | 119 | - GCC 理解(请用命令行操作并给出截图) 120 | 121 | - 在 Linux 下创建并写入一个 C 程序文件,可以输出`hello,world!`,命名为`test.c`。 122 | - GCC 将一个源程序转换为可执行文件经历了哪些主要步骤? 123 | - 请利用`test.c`用 GCC 命令将其将其转换为可执行程序的主要过程表示出来。 124 | 125 | - 静态库和动态库的操作 126 | 127 | - 解压 lab02.zip 128 | - 静态库 129 | 130 | - 使用 gcc 命令分别将`mytool1.c`和`mytool2.c` 编译成 .o 目标文件 131 | - 执行下面两个命令 132 | 133 | ```shell 134 | ar cr libmylib.a mytool2.o mytool1.o 135 | ``` 136 | 137 | ```shell 138 | gcc -o main main.c -L. -lmylib 139 | ``` 140 | 141 | 说明上述两个命令完成了什么事? 142 | 143 | - 查看`main`文件大小,并记录 144 | - 执行`./main` 145 | - 删除之前生成的静态库文件,重新执行`./main`命令,对比上一步骤得到的结果,你有什么发现?并解释原因。 146 | 147 | - 动态库 148 | 149 | - 执行下面两个命令 150 | 151 | ```shell 152 | gcc -c -fPIC mytool2.c -o mytool2.o 153 | gcc -c -fPIC mytool1.c -o mytool1.o 154 | gcc -shared -o libmylib.so mytool2.o mytool1.o 155 | ``` 156 | 157 | ```shell 158 | gcc -o main main.c -L. -lmylib 159 | ``` 160 | 161 | - 查看`main`文件大小,并和之前的作比较,解释原因。 162 | - 执行命令将当前目录添加到库搜索路径中 163 | 164 | ```shell 165 | export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:; 166 | ``` 167 | 168 | - 执行`./main` 169 | - 删除之前生成的动态库文件,重新执行`./main`命令,对比上一步骤得到的结果,你有什么发现? 170 | 171 | - 综合实验,你觉得静态库和动态库的区别和相同点是什么?谈谈他们的优缺点。 172 | 173 | - GDB 命令操作 174 | 175 | 按顺序执行如下操作 176 | 177 | (1)输入如下程序保存为 test.c 178 | 179 | ```c 180 | #include 181 | int main() { 182 | int num; 183 | do 184 | { 185 | printf("Enter a positive integer: "); 186 | scanf("%d", &num); 187 | } 188 | while(num < 0); 189 | 190 | int factorial; 191 | for(int i = 1; i<=num; i++) 192 | factorial = factorial*i; 193 | 194 | printf("%d! = %d\n", num, factorial); 195 | return 0; 196 | } 197 | ``` 198 | 199 | (2)使用 gcc -g test.c 命令编译生成可执行文件 a.out 200 | 201 | (3)执行 gdb a.out 命令 202 | 203 | (5)在 main 函数处设置断点 204 | 205 | (6)输入 run 命令开始程序 206 | 207 | (7)多次输入 next 命令使程序运行到第 13 行,使用 print 命令打印 num 的值 208 | 209 | (8)继续调试至程序第 16 行,使用 print 命令打印 factorial 的值 210 | 211 | (9)使用 run 命令再次调试程序 212 | 213 | (10)在程序第 10 行加入断点 214 | 215 | (11)使用 continue 命令使程序运行到断点处 216 | 217 | (12)使用 next 命令 218 | 219 | (13)使用 print 命令打印 i 和 factorial 的值 220 | 221 | (14)使用 p factorial=1 命令改变 factorial 的值 222 | 223 | (15)使用 info locals 查看所有局部变量值 224 | 225 | (16)继续调试至程序结束 226 | 227 | (17)说明源程序中存在的错误 228 | 229 | - 综合实验,跟用 printf 函数打印输出相比,采用 gdb 调试的优点有哪些? 230 | 231 | - Makefile 操作 232 | 233 | 解压 `make_practice.zip`,补全 Makefile 文件,要求对主模块进行编译,包含 clean 模块可删除目标文件和中间生成文件. 234 | 235 | make_practice 文件夹的目录结构如下 236 | 237 | ```shell 238 | . 239 | ├── include 240 | │   ├── dylib.h 241 | │   ├── fun1.h 242 | │   └── fun2.h 243 | ├── lib 244 | │   └── libdylib.so 245 | ├── Makefile 246 | └── src 247 | ├── fun1.c 248 | ├── fun2.c 249 | └── main.c 250 | ``` 251 | 252 | - 综合实验,Make 工具是如何知道哪些文件需要重新生成,哪些不需要的? 253 | -------------------------------------------------------------------------------- /lab02/lab02.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab02/lab02.zip -------------------------------------------------------------------------------- /lab02/lab02/main.c: -------------------------------------------------------------------------------- 1 | #include "mytool1.h" 2 | #include "mytool2.h" 3 | int main(int argc,char **argv){ 4 | mytool1_print("hello"); 5 | mytool2_print("hello"); 6 | } -------------------------------------------------------------------------------- /lab02/lab02/mytool1.c: -------------------------------------------------------------------------------- 1 | #include "mytool1.h" 2 | #include "stdio.h" 3 | 4 | void mytool1_print(char *print_str){ 5 | printf("This is mytool1 print %s\n",print_str); 6 | printf("In mytool1\n"); 7 | printf("In mytool1\n"); 8 | printf("In mytool1\n"); 9 | printf("In mytool1\n"); 10 | } 11 | -------------------------------------------------------------------------------- /lab02/lab02/mytool1.h: -------------------------------------------------------------------------------- 1 | #ifndef _MYTOOL_1_H 2 | #define _MYTOOL_1_H 3 | void mytool1_print(char *print_str); 4 | #endif -------------------------------------------------------------------------------- /lab02/lab02/mytool2.c: -------------------------------------------------------------------------------- 1 | #include "mytool2.h" 2 | #include "stdio.h" 3 | void mytool2_print(char *print_str){ 4 | printf("This is mytool2 print %s\n",print_str); 5 | printf("In mytool2\n"); 6 | printf("In mytool2\n"); 7 | printf("In mytool2\n"); 8 | printf("In mytool2\n"); 9 | printf("In mytool2\n"); 10 | } 11 | -------------------------------------------------------------------------------- /lab02/lab02/mytool2.h: -------------------------------------------------------------------------------- 1 | #ifndef _MYTOOL_2_H 2 | #define _MYTOOL_2_H 3 | void mytool2_print(char *print_str); 4 | #endif -------------------------------------------------------------------------------- /lab02/make_practice.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab02/make_practice.zip -------------------------------------------------------------------------------- /lab03a/answer.md: -------------------------------------------------------------------------------- 1 | # Lab03a Assignment 参考答案 2 | 3 | ## 1. 文件权限 4 | 5 | - 对一个文本文件`file.txt`执行命令:`#chmod 746 file.txt`。请解释该命令的含义并写出执行该命令后该文件的权限信息。(注:这里的`#`表示在管理员权限下完成,并不是命令的前缀。在 Shell 中`#`前缀会被当作注释,从而无效) 6 | 7 | 命令含义:改变 file.txt 的用户使用权限,即文件拥有者可读、可写、可执行;用户组只可读;其他用户可读、可写、不可执行。 8 | 9 | 该文件的权限信息(截图):![1](img\1.png) 10 | 11 | ## 2. 用户权限 12 | 13 | - 假设系统中有两个用户,分别是 A 与 B ,这两个人共同支持一个名为 `project` 的用户组。假设这两个用户需要共同拥有`/srv/ahome/`目录的开发权,且该目录不许其他人进入查阅,请问该目录的权限配置应如何配置,写出配置所需的指令。并在你的机器上验证。(提示:请在 `/srv`下创建`/ahome`目录,并创建用户 A , B ,用户组`project` ,并给 `/ahome`赋予合理的访问权限。) 14 | 15 | ```bash 16 | # 粘贴你的代码,附上注释表明目的(可截图) 17 | groupadd myproject # 创建组 18 | useradd A -G myproject # 创建用户A并加入组 19 | useradd B -G myproject # 创建用户B并加入组 20 | chown A:myproject ahome/ # 改变目录所有者为A及它的组 21 | chmod 770 ahome/ # 修改权限,使得A拥有所有权限,和A同组的成员也拥有所有权限,其他用户无任何权限。 22 | ``` 23 | 24 | 验证如下: 25 | 26 | ![6](img\6.png) 27 | 28 | ## 3. 管道 29 | 30 | - 请写出命令`who | wc -l`的结果并分析其执行过程。 31 | 32 | 运行结果: 33 | 34 | ![7](img\7.png) 35 | 36 | 分析: 37 | 38 | ​ 首先执行 who 命令,将其输出作为 wc -l 的输入,而 who 输出了一行,wc -l 打印文件行数为 1。 39 | 40 | ## 4. 重定向 41 | 42 | - 解释以下命令`sh test && cat a.txt || cat b.txt >f1 &1`'若命令执行到最后一个 `cat b.txt`,`f1`中的内容为`b.txt`的内容还是`f2`的内容 43 | 44 | 答:是 b.txt 的内容。 45 | 46 | 原因:⾸先 2>&1 没影响,其次 `cat b.txt >f1` 将 `b.txt` 的内容输出到了 `f1` 文件中,⽽ `< f2` 对 `f1` ⽆影响,因此是 `b.txt` 的内容。 47 | 48 | ## 5. 环境变量 49 | 50 | - 自己添加一个环境变量,名称是`STUDENT_ID`,值为你的学号,并编写一个 C 程序来获取该环境变量,并打印出来。**请详细叙述你的操作过程以及操作过程的截图,并给出 C 程序的代码。** 51 | 52 | ```shell 53 | export STUDENT_ID=19373075 54 | ``` 55 | 56 | 截图: 57 | 58 | ![4](img\4.png) 59 | 60 | ![5](img\5.png) 61 | 62 | 代码: 63 | 64 | ```c 65 | #include 66 | int main() { 67 | printf("STUDENT_ID:%s\n",getenv("STUDENT_ID")); 68 | return 0; 69 | } 70 | ``` 71 | -------------------------------------------------------------------------------- /lab03a/answer_template.md: -------------------------------------------------------------------------------- 1 | # Lab03a Assignment 2 | 3 | > 班级: 4 | > 学号: 5 | > 姓名: 6 | 7 | ## 1. 文件权限 8 | 9 | - 对一个文本文件`file.txt`执行命令:`#chmod 746 file.txt`。请解释该命令的含义并写出执行该命令后该文件的权限信息。(注:这里的`#`表示在管理员权限下完成,并不是命令的前缀。在 Shell 中`#`前缀会被当作注释,从而无效) 10 | 11 | 命令含义: 12 | 13 | 该文件的权限信息(截图): 14 | 15 | ## 2. 用户权限 16 | 17 | - 假设系统中有两个用户,分别是 A 与 B ,这两个人共同支持一个名为 `project` 的用户组。假设这两个用户需要共同拥有`/srv/ahome/`目录的开发权,且该目录不许其他人进入查阅,请问该目录的权限配置应如何配置,写出配置所需的指令。并在你的机器上验证。(提示:请在 `/srv`下创建`/ahome`目录,并创建用户 A , B ,用户组`project` ,并给 `/ahome`赋予合理的访问权限。) 18 | 19 | ```bash 20 | # 粘贴你的代码,附上注释表明目的(可截图) 21 | ``` 22 | 23 | ## 3. 管道 24 | 25 | - 请写出命令`who | wc -l`的结果并分析其执行过程。 26 | 27 | 运行结果: 28 | 29 | ![pipe](img/fig.jpg) 30 | 31 | 分析: 32 | 33 | 我的分析是 xxx 34 | 35 | ## 4. 重定向 36 | 37 | - 解释以下命令 `sh test && cat a.txt || cat b.txt >f1 &1` 若命令执行到最后一个 `cat b.txt`,`f1`中的内容为`b.txt`的内容还是`f2`的内容 38 | 39 | 是 xxx 的内容。 40 | 41 | 原因: 42 | 43 | xxx 44 | 45 | ## 5. 环境变量 46 | 47 | - 自己添加一个环境变量,名称是`STUDENT_ID`,值为你的学号,并编写一个 C 程序来获取该环境变量,并打印出来。**请详细叙述你的操作过程以及操作过程的截图,并给出 C 程序的代码。** 48 | 49 | 截图: 50 | 51 | 代码: 52 | 53 | ```c 54 | // 粘贴你的代码,附上注释表明目的(可截图) 55 | ``` 56 | 57 | ## 6. 实验感想 58 | -------------------------------------------------------------------------------- /lab03a/img/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab03a/img/1.png -------------------------------------------------------------------------------- /lab03a/img/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab03a/img/2.png -------------------------------------------------------------------------------- /lab03a/img/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab03a/img/3.png -------------------------------------------------------------------------------- /lab03a/img/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab03a/img/4.png -------------------------------------------------------------------------------- /lab03a/img/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab03a/img/5.png -------------------------------------------------------------------------------- /lab03a/img/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab03a/img/6.png -------------------------------------------------------------------------------- /lab03a/img/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab03a/img/7.png -------------------------------------------------------------------------------- /lab03a/img/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab03a/img/8.png -------------------------------------------------------------------------------- /lab03a/img/bashrc.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab03a/img/bashrc.jpg -------------------------------------------------------------------------------- /lab03a/img/export_path.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab03a/img/export_path.jpg -------------------------------------------------------------------------------- /lab03a/img/getenv.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab03a/img/getenv.jpg -------------------------------------------------------------------------------- /lab03a/img/path.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab03a/img/path.jpg -------------------------------------------------------------------------------- /lab03a/img/sysenv.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab03a/img/sysenv.jpg -------------------------------------------------------------------------------- /lab03a/img/which.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab03a/img/which.jpg -------------------------------------------------------------------------------- /lab03a/lab03a.md: -------------------------------------------------------------------------------- 1 | # Lab03a: 权限 & 重定向 & 管道 & 环境变量 2 | 3 | [TOC] 4 | 5 | ## 1. 实验目的 6 | 7 | 掌握 Linux 中用户及文件的权限管理、重定向、管道等命令行使用技巧,了解并操作环境变量。 8 | 9 | ## 2. 实验指南 10 | 11 | ### 2.1. Linux 权限 12 | 13 | (以下关于文件的内容在下一章:文件 I/O 操作中会再次涉及) 14 | 15 | #### 2.1.1. 从`ls -l`说起 16 | 17 | 当你使用`ls -l`命令的时候, 会发现列出了目录的很多信息,并且以行为单位,下来就认识一下这些文件的基本含义。 18 | 首先 `-l` 选项代表`ls (- long)` 就是展示目录下的详细信息。 比如如下例子: 19 | 20 | ```shell 21 | [me@linuxbox ~]$ ls -l 22 | total 56 23 | drwxrwxr-x 2 me me 4096 2007-10-26 17:20 Desktop 24 | drwxrwxr-x 2 me me 4096 2007-10-26 17:20 Documents 25 | drwxrwxr-x 2 me me 4096 2007-10-26 17:20 Music 26 | drwxrwxr-x 2 me me 4096 2007-10-26 17:20 Pictures 27 | drwxrwxr-x 2 me me 4096 2007-10-26 17:20 Public 28 | drwxrwxr-x 2 me me 4096 2007-10-26 17:20 Templates 29 | ``` 30 | 31 | - 第一个字段 10 个字符表示这个文件的类型和权限,其中第一个字符表示该文件的类型,其余九个字符表示权限。第一个字符的函数如下: 32 | 33 | - `-`: 表示普通文件 34 | - `d`: 表示目录 35 | - `l`: 表示链接文件 36 | - `p`: 表示管理文件 37 | - `b`: 表示块设备文件 38 | - `c`: 表示设备文件 39 | - `s`: 表示套接字文件 40 | 41 | 其余九个字符表示这个文件的详细权限,三个字符为一组 ,总共有三组,分别代表了拥有者,拥有组,其他人对这个文件的权限。三组属性分为三段:`\[rwx]\[rwx][r-x]`,其中: 42 | 43 | - **r**(Read,读取权限):对文件而言,具有读取文件内容的权限;对目录来说,具有浏览目录的权限; 44 | - **w**(Write,写入权限):对文件而言,具有新增、修改文件内容的权限;对目录来说,具有删除、移动目录内文件的权限; 45 | - **x**(eXecute,执行权限):对文件而言,具有执行文件的权限;对目录来说,该用户具有进入目录的权限。 46 | - `-`:对应的字符为-表示没有该权限 47 | 48 | - 第二个字段表示该文件硬连接的数目。这里表示都是 2。 49 | - 第三个和第四个字段 分别表示所有人和所有组,这里表示拥有人和拥有组都是`me` 50 | - 第五个字段表示以字节数表示的文件大小。 51 | - 第六个字段表示最后修改时间。 52 | - 第七个字段表示文件名。UNIX 系统本身不要求文件名中有扩展名,但 GCC 等应用程序要求源代码文件有扩展名,比如 C 语言源文件的扩展名为`.c`,以用来区别不同编程语言编写的源程序。 53 | 54 | #### 2.1.2. 用户及用户组 55 | 56 | Linux 用户分为**管理员**和**普通用户**,普通用户又分为**系统用户**和**自定义用户**。可以查看`/etc/passwd`来查看; 57 | 58 | - 系统管理员:即 root 帐户,拥有所有系统权限,是整个系统的所有者。 59 | - 系统用户:Linux 为满足自身系统管理所内建的账号,通常在安装过程中自动创建,用于区分以其身份所运行的应用程序的权限,不用于登录操作系统。 60 | - 自定义用户:由 root 管理员创建,供用户登录系统进行操作所使用的账号,用于区分不同的用户身份。 61 | 62 | 在 Linux 中的每个用户必须属于一个组,不能独立于组外。在 Linux 中每个文件有所有者、所在组、其它组的概念。用户组的信息我们可以在`/etc/group`中查看。 63 | 64 | 用户组是具有相同特征用户的逻辑集合。简单的理解,有时我们需要让多个用户具有相同的权限,比如查看、修改某一个文件的权限,一种方法是分别对多个用户进行文件访问授权,如果有 10 个用户的话,就需要授权 10 次,那如果有 100、1000 甚至更多的用户呢? 65 | 66 | 显然,这种方法不太合理。最好的方式是建立一个组,让这个组具有查看、修改此文件的权限,然后将所有需要访问此文件的用户放入这个组中。那么,所有用户就具有了和组一样的权限,这就是用户组。 67 | 68 | 将用户分组是 Linux 系统中对用户进行管理及控制访问权限的一种手段,通过定义用户组,很大程度上简化了对用户的管理工作。 69 | 70 | #### 2.1.3. 权限操作命令 71 | 72 | - `su`: 切换到`root`,`root`账户具有最高权限。返回当前用户则使用`exit`。 73 | - `sudo`: 在指令前加上`sudo`,使得本条指令以最高权限运行。 74 | - `chmod`: 使用 `chmod`命令更改文件权限。 75 | - `chown`: 使用`chown`命令更改文件所有者。 76 | - `chgrp`: 使用`chgrp`命令更改文件的所属组。 77 | - `useradd`, `groupadd`: 添加用户/用户组 格式为 `useradd/groupadd [选项] 用户名` 78 | - `passwd`: 给用户设置密码。格式为`passwd [选项] 用户名` 79 | - `userdel`, `groupdel`: 删除用户 格式为 `userdel/groupdel [选项] 用户名` 80 | - `usermod`, `groupmod`: 用以修改用户和用户组的相关属性 81 | 82 | 如果具体的命令格式不记得了,可使用 `help` 或 `man` 命令获取提示信息。 大多数 GNU 工具都有`--help` 选项,用来显示工具的一些信息,用法。注意:`help` 命令**只能显示 Shell 内部的命令帮助信息**。 通过 `man` 指令可以查看 Linux 中的指令帮助、配置文件帮助和编程帮助等信息。`man` 查到的信息比 `help` 详细。 83 | 84 | ### 2.2. 重定向 85 | 86 | Linux 中默认输入输出分为 3 个文件: 87 | 88 | - 标准输入`stdin`。标准输入文件的编号是`0`**(牢记 Linux 万物皆文件,可以用文件表示设备)**,默认的设备是键盘,命令执行时从键盘读取数据。 89 | - 标准输出`stdout`。标号为`1`,默认的设备是显示器,命令的输出会被打印到屏幕上。 90 | - 标准错误`stderr`。标号为`2`,默认的设备是显示器,命令执行产生的错误信息会被发送到标准错误文件。 91 | 92 | 重定向的意思就是改变这三个文件实际指向,比如我们希望从某个文件中获取输入,那么就需要将标准输入指向这个文件。重定向后命令依然从标准输入获得输入,此时标准输入指向这个文件,故命令能够从这个文件获取输入。 93 | 94 | - 输入重定向 95 | 格式为`命令 < 文件名`,比如 `sort < sp.txt` , 把`sp.txt`文件中的内容作为`sort`的输入。 96 | - 输出重定向 97 | 格式为`命令 > 文件名`,比如 `cat /etc/passswd > ps.log`,`cat`会输出`/etc/passwd`中内容,但此时并不会输出到屏幕上,而是输出到`ps.log`中。 98 | - 错误重定向 99 | 格式为`命令 2> 文件名`,比如 `gcc -c test.c -o test.out 2 > error.log`,如果`gcc`编译时出现错误,则会把错误信息输出到`error.log`中。会覆盖原文件中内容,`>>` 则会将输出追加到原文件末尾。 100 | - 其他 101 | - 在重定向错误时使用了错误文件的编号`2`,其实在输入输出的时候也可以显式写`0`或`1`,通常是省略。 102 | - `&`运算符,等价于:`2>&1`。表示将标准错误从重定向到标准输出指向的文件。如 `1>/dev/null` ,然后执行`2>&1`,此时都指向空设备。 103 | 104 | ### 2.3. 管道 105 | 106 | 管道作用是将多个命令串连起来,使一个命令的输出作为另一个命令的输入。 107 | 108 | 格式为`命令1 | 命令2 | 命令3 ....| 命令n`。 109 | 110 | 比如 `ls /etc | grep init` 将会输出 `/etc` 目录下,文件名包含 `init` 的文件/目录。如果不使用管道,命令就得拆成: `ls /etc > tmp grep init < tmp rm tmp` 或 `ls /etc | grep init >> test cat test`。 111 | 112 | 其实管道是一种进程之间通信的手段,在之后的 Linux 系统编程的实验中我们还会经常遇到。 113 | 114 | ### 2.4. 环境变量和可执行文件 115 | 116 | #### 2.4.1. 环境变量 117 | 118 | 环境变量可以简单理解为,在程序运行所处的环境中,提前设定好的参数值。程序在执行过程中,会去读取这些提前设定好的参数值。 119 | 120 | 举个例子,大家在 Windows 中安装完 jdk 时,双击安装完安装包后,都要去修改`JAVA_HOME`、`CLASSPATH`、`PATH`这三个环境变量。其中当时这个`JAVA_HOME`我们设定的是一个目录,这样,当有软件需要使用 jdk 的时候,它就会尝试读取这个`JAVA_HOME`的值,从而知道系统安装的 jdk 在哪里。 121 | 122 | Linux 默认存在的环境变量有`PATH`、`HOME`等,我们可以通过下面的命令查看: 123 | 124 | ![PATH](img/path.jpg) 125 | 126 | 有没有发现对环境变量的引用与我们在 shell 脚本中引用变量的方法完全一样?因为它本质上就是已经提前定义好的变量嘛! 127 | 128 | #### 2.4.2. 设定或者修改环境变量 129 | 130 | 在 Windows 中,我们一般是直接使用 GUI 界面修改,然后重启就可以了。但在 Linux 必须使用命令行操作。 131 | 132 | 总的来说,在 Linux 中设定环境变量的语法很简单: 133 | 134 | ```bash 135 | export environment_variable=xxxxx 136 | ``` 137 | 138 | 比如: 139 | 140 | ![export path](img/export_path.jpg) 141 | 142 | 很简单吧? 143 | 144 | 但是你会发现,当你退出当前的 Terminal,再次打开一个新的 Terminal 时,将无法再次访问`system_programming`。这是因为,我们上次进行的修改,是在一个 bash 的进程中修改的,当我们关闭 Terminal,就终止了这个进程;当再次启动一个新的 Terminal 时,就重新开启了一个新的进程,这个新的进程自然是访问不到别的进程的变量的。(关于进程的概念,后续学习中会有介绍)。 145 | 146 | 那该如何“永久”设置环境变量呢?我们知道,当一个 bash 进程启动时(即,打开一个 Terminal 或者远程 SSH 登录时),该进程会读取`~/.bashrc`文件来完成初始化。因此,我们只需要把上面提到的`export`语句写到`~/.bashrc`文件中就可以了。注意,该文件前面加了`.`,也就是说这是一个隐藏文件,要使用`ls -a`才能看到。 147 | 148 | ![.bashrc](img/bashrc.jpg) 149 | 150 | 当然,这种方法只能使得当前用户,也就是我们自己访问到该环境变量,对于 root 用户,或者其他注册用户来讲,是访问不到的。为了达到所有用户都可以访问的效果,我们可以把`export`语句写到`/etc/profile`文件,当系统启动时会读取到该文件。 151 | 152 | #### 2.4.3. 使用 C 语言读取环境变量 153 | 154 | 可以使用全局变量`environ`获取所有的环境变量: 155 | 156 | ```c 157 | # include 158 | extern char** environ; 159 | int main(int argc, char const* argv[]) { 160 | char** p = environ; 161 | for (; *p != NULL; p++) { 162 | printf("%s\n", *p); 163 | } 164 | return 0; 165 | } 166 | ``` 167 | 168 | 输出大概如下图所示: 169 | 170 | ![所有环境变量](img/sysenv.jpg) 171 | 172 | 还可以使用函数`getenv()` 返回特定的环境变量的值: 173 | 174 | ```c 175 | #include 176 | #include 177 | 178 | int main(int argc, char const *argv[]) { 179 | const char* envName = "SHELL"; 180 | printf("$SHELL = %s\n", getenv(envName)); 181 | return 0; 182 | } 183 | ``` 184 | 185 | 执行结果如下: 186 | 187 | ![getenv](img/getenv.jpg) 188 | 189 | #### 2.4.4. “可执行文件” 190 | 191 | Linux 中“可以被执行的文件”分为两种,一种是二进制文件,一般是 ELF 格式的,其中包含机器指令和数据,操作系统可以直接解析这种文件,把相应的代码段和数据段加载到内存中执行。我们用 gcc 编译产生`a.out`就是这种文件。 192 | 193 | 还有一种是脚本文件,这种文件是文本文件。所谓的脚本文件,指的是该文件中是一条条的指令,操作系统在执行该脚本文件的时候,会用一种解释器(bash、Python 等)来**逐行解释执行**该文件中的指令。大家在前几周学习 bash shell 编程的时候,写的就是这些脚本文件。操作系统使用什么解释器来执行该脚本文件,是需要**人为来指定**的(当不指定时,使用不同的 shell 会有不同的策略)。这种指定可以是这样的: 194 | 195 | ```bash 196 | bash test.sh 197 | ``` 198 | 199 | 表示使用 bash 解释器来解释执行 test.sh,大家运行 Python 文件的时候使用`python test.py`,跟这个道理是相同的。(**请始终注意,文件拓展名大多数情况下都是给人看的,操作系统不会自动把`.py`结尾的文件看做是 Python 文件,你同样可以把一个 bash 脚本保存成`test.py`这样的名称,只不过这容易给人造成误解而已**)。 200 | 201 | 还有一种指定解释器的方法,就是在脚本文件的第一行通过注释的方式指明所使用的解释器。就像下面这样: 202 | 203 | ```bash 204 | #! /bin/bash 205 | 206 | echo 'Linux is cool!' 207 | ``` 208 | 209 | 这样,在执行该文件的时候,就不需要手动指定解释器了,只需要输入该文件的名称就可以了。但一般这个时候执行这个文件会报错,类似于: 210 | 211 | ```bash 212 | -bash: ./test.sh: Permission denied 213 | ``` 214 | 215 | 咦,明明是我创建的这个文件,我再来执行它为啥会“Permission denied(没有权限)”呢?这时候查看一下这个脚本文件的权限: 216 | 217 | ```bash 218 | -rw-rw-r-- 1 loheagn loheagn 27 Apr 28 21:33 test.sh 219 | ``` 220 | 221 | 发现了吧?该文件确实没有可执行的权限。 222 | 223 | 这时,我们使用`chmod`来修改文件权限就可以了,例如可以: 224 | 225 | ```bash 226 | chmod 764 test.sh 227 | ``` 228 | 229 | 或者直接: 230 | 231 | ```bash 232 | chmod +x test.sh 233 | ``` 234 | 235 | #### 2.4.5. PATH 236 | 237 | 相信大家在上学期的硬件基础课程中已经知道在 bash shell 中怎么执行一个程序了。比如,当大家在工作目录中使用 GCC 编译出来了一个可执行文件`a.out`,要运行这个程序的时候是这样进行的: 238 | 239 | ```bash 240 | ./a.out 241 | ``` 242 | 243 | 或者,直接 244 | 245 | ```bash 246 | a.out 247 | ``` 248 | 249 | 这两个都是一样的,都是**直接在 shell 中输入了要执行的文件的文件名**。而且正如上一小节所看到的,执行脚本文件也是直接输入文件名。这是在 bash shell 中执行可执行文件的唯一方式。 250 | 251 | 但如果我们不在这个可执行文件所在的目录下怎么办?如果我在一个其他目录下,想执行这个程序,就需要用相对路径或绝对路径写出整个路径前缀,这往往是一件非常麻烦的事情。这时候,`PATH`就诞生了。 252 | 253 | `PATH`是一个环境变量,这个环境变量指明了**系统默认**的查找可执行文件的路径。你可以在 bash shell 中使用`echo $PATH`打印出你当前的`PATH`,它将如下所示: 254 | 255 | ```bash 256 | /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin 257 | ``` 258 | 259 | 这一行输出实际上是几个路径之间用`:`拼接起来的。 260 | 261 | 有了`PATH`,当你在命令行输入一个程序名时,bash shell 就会去`PATH`所指定的这几个目录中去寻找该程序,如果找不到就会报错。 262 | 263 | 现在大家知道了吧,我们平常使用的指令`ls`、`mv`等指令没啥神奇的,他们只不过是操作系统预设好的一个个程序,并放在了`PATH`指定的某个路径下,我们输入指令时,其实就相当于是在执行这些程序。 264 | 265 | 比如,我们可以用`which`来查看这些“内置程序”的具体路径: 266 | 267 | ![which](img/which.jpg) 268 | 269 | 大家可以去路径`/usr/bin`下去查看验证一下。 270 | 271 | ## 3. 实验习题 272 | 273 | - 对一个文本文件`file.txt`执行命令:`#chmod 746 file.txt`。请解释该命令的含义并写出执行该命令后该文件的权限信息。(注:这里的`#`表示在管理员权限下完成,并不是命令的前缀。在 Shell 中`#`前缀会被当作注释,从而无效) 274 | - 假设系统中有两个用户,分别是 A 与 B ,这两个人共同支持一个名为 `project` 的用户组。假设这两个用户需要共同拥有`/srv/ahome/`目录的开发权,且该目录不许其他人进入查阅,请问该目录的权限配置应如何配置,写出配置所需的指令。并在你的机器上验证。(提示:请在 `/srv`下创建`/ahome`目录,并创建用户 A , B ,用户组`project` ,并给 `/ahome`赋予合理的访问权限。) 275 | - 请写出命令`who | wc -l`的结果并分析其执行过程。 276 | - 解释以下命令`sh test && cat a.txt || cat b.txt >f1 &1`'若命令执行到最后一个 `cat b.txt`,`f1`中的内容为`b.txt`的内容还是`f2`的内容 277 | - 自己添加一个环境变量,名称是`STUDENT_ID`,值为你的学号,并编写一个C程序来获取该环境变量,并打印出来。**请详细叙述你的操作过程以及操作过程的截图,并给出C程序的代码。** 278 | -------------------------------------------------------------------------------- /lab03b/answer.md: -------------------------------------------------------------------------------- 1 | # Lab03b Assignment 参考答案 2 | 3 | ## 0. (选做,不算分,不限语言)批量删除 IDEA 工程文件 4 | 5 | 参考如下的目录结构: 6 | 7 | ```shell 8 | ./sp_java/ 9 | ├── .idea 10 | │ ├── .gitignore 11 | │ ├── misc.xml 12 | │ ├── modules.xml 13 | │ └── workspace.xml 14 | ├── sp_java.iml 15 | └── src 16 | ├── Test.class 17 | └── Test.java 18 | ``` 19 | 20 | 在当前目录下可能存在**多个**类似于如上 `sp_java` 工程的文件夹。 21 | 22 | 要求批量删除**这些**文件夹中的 IDEA 工程文件,转换成如下的干净的目录结构。 23 | 24 | ```shell 25 | ./sp_java 26 | └── src 27 | └── Test.java 28 | ``` 29 | 30 | 参考答案: 31 | 32 | ```sh 33 | #!/bin/sh 34 | find . -name "*.class" -exec rm -rf {} \; 35 | find . -name "*.iml" -exec rm -rf {} \; 36 | find . -type d -name ".idea" -exec rm -rf {} \; 37 | ``` 38 | 39 | ## 1. shebang 40 | 41 | - 假如在脚本的第一行放入`#!/bin/rm`或者在普通文本文件中第一行放置`#!/bin/more`,然后将文件设为可执行权限执行,看看会发生什么,并解释为什么。 42 | 43 | - `#!/bin/rm` 44 | 45 | 执行后文件会被删除。因为该语句指明了执行的命令为 `rm` 删除。 46 | 47 | ![20210417002714](https://cdn.jsdelivr.net/gh/ZewanHuang/Img@master/Images/20210417002714.png) 48 | 49 | - `#!/bin/more` 50 | 51 | 执行后会标准输出文件内容。因为该语句指明了执行的命令为 `more` 标准显示。 52 | 53 | ![20210417002956](https://cdn.jsdelivr.net/gh/ZewanHuang/Img@master/Images/20210417002956.png) 54 | 55 | 为什么 56 | 57 | ## 2. 运行、打印 58 | 59 | - 编写一个 bash 脚本,执行该脚本文件将得到两行输出,第一行是你的学号,第二行是当前的日期(考虑使用`date`命令)。对该脚本文件的要求是 60 | - 文件名为`date-${你的学号}`,比如`date-15131049` 61 | - 用户可以在任意位置**只需要输入文件名**就可以执行该脚本文件 62 | - **不破坏除用户家目录之外的任何目录结构**,即不要在家目录之外的任何地方增删改任何文件 63 | **请详细叙述你的操作过程以及操作过程的截图,并给出你所编写的脚本文件的代码。** 64 | 65 | > 我在用户根目录下的 `~/.bash_profile` 中加入脚本文件所在目录路径,然后执行 `source ~/.bash_profile`,之后便可以在任何位置执行 `date-19373257.sh`。 66 | 67 | ```sh 68 | #!/bin/bash 69 | echo 19373257 70 | echo `date` 71 | ``` 72 | 73 | ![20210417011840](https://cdn.jsdelivr.net/gh/ZewanHuang/Img@master/Images/20210417011840.png) 74 | 75 | ![20210417011608](https://cdn.jsdelivr.net/gh/ZewanHuang/Img@master/Images/20210417011608.png) 76 | 77 | ## 3. 正则 78 | 79 | - 完成[LeetCode: 193. Valid Phone Numbers](https://leetcode.com/problems/valid-phone-numbers/) | [leetcode-cn](https://leetcode-cn.com/problems/valid-phone-numbers/) (体会`grep -E`, `grep -P`, `egrep`, `awk`等的差异) 80 | 81 | ```bash 82 | grep -P '^([0-9]{3}-|\([0-9]{3}\) )[0-9]{3}-[0-9]{4}$' file.txt 83 | grep -E '^([0-9]{3}-|\([0-9]{3}\) )[0-9]{3}-[0-9]{4}$' file.txt 84 | ``` 85 | 86 | ## 4. 文件读入 87 | 88 | - 完成[LeetCode: 195. Tenth Line](https://leetcode.com/problems/tenth-line/) | [leetcode-cn](https://leetcode-cn.com/problems/tenth-line/),给出你的代码和 AC 截图。(提示:[怎么读取每一行](http://blog.sina.com.cn/s/blog_605f5b4f0101b0sd.html)) 89 | 90 | ```bash 91 | #!/bin/bash 92 | idx=0 93 | while read -r line; do 94 | idx=$(($idx+1)) 95 | if [ $idx -eq 10 ]; then 96 | echo ${line} 97 | fi 98 | done 班级: 4 | > 学号: 5 | > 姓名: 6 | 7 | ## 0. (选做,不算分,不限语言)批量删除 IDEA 工程文件 8 | 9 | 参考如下的目录结构: 10 | 11 | ```shell 12 | ./sp_java/ 13 | ├── .idea 14 | │ ├── .gitignore 15 | │ ├── misc.xml 16 | │ ├── modules.xml 17 | │ └── workspace.xml 18 | ├── sp_java.iml 19 | └── src 20 | ├── Test.class 21 | └── Test.java 22 | ``` 23 | 24 | 在当前目录下可能存在**多个**类似于如上 `sp_java` 工程的文件夹。 25 | 26 | 要求批量删除**这些**文件夹中的 IDEA 工程文件,转换成如下的干净的目录结构。 27 | 28 | ```shell 29 | ./sp_java 30 | └── src 31 | └── Test.java 32 | ``` 33 | 34 | ## 1. shebang 35 | 36 | - 假如在脚本的第一行放入`#!/bin/rm`或者在普通文本文件中第一行放置`#!/bin/more`,然后将文件设为可执行权限执行,看看会发生什么,并解释为什么。 37 | 38 | - `#!/bin/rm` 39 | 40 | 描述发生了什么(截图) 41 | 42 | ![rm](img/rm.jpg) 43 | 44 | 为什么 45 | 46 | - `#!/bin/more` 47 | 48 | 描述发生了什么(截图) 49 | 50 | ![more](img/more.jpg) 51 | 52 | 为什么 53 | 54 | ## 2. 运行、打印 55 | 56 | - 编写一个 bash 脚本,执行该脚本文件将得到两行输出,第一行是你的学号,第二行是当前的日期(考虑使用`date`命令)。对该脚本文件的要求是 57 | - 文件名为`date-${你的学号}`,比如`date-15131049` 58 | - 用户可以在任意位置**只需要输入文件名**就可以执行该脚本文件 59 | - **不破坏除用户家目录之外的任何目录结构**,即不要在家目录之外的任何地方增删改任何文件 60 | **请详细叙述你的操作过程以及操作过程的截图,并给出你所编写的脚本文件的代码。** 61 | 62 | ## 3. 正则 63 | 64 | - 完成[LeetCode: 193. Valid Phone Numbers](https://leetcode.com/problems/valid-phone-numbers/) | [leetcode-cn](https://leetcode-cn.com/problems/valid-phone-numbers/) (体会`grep -E`, `grep -P`, `egrep`, `awk`等的差异) 65 | 66 | ```bash 67 | # 粘贴你的代码 68 | ``` 69 | 70 | AC 截图: 71 | 72 | ![lc193](img/fig.jpg) 73 | 74 | ## 4. 文件读入 75 | 76 | - 完成[LeetCode: 195. Tenth Line](https://leetcode.com/problems/tenth-line/) | [leetcode-cn](https://leetcode-cn.com/problems/tenth-line/),给出你的代码和 AC 截图。(提示:[怎么读取每一行](http://blog.sina.com.cn/s/blog_605f5b4f0101b0sd.html)) 77 | 78 | ```bash 79 | # 粘贴你的代码 80 | ``` 81 | 82 | AC 截图: 83 | 84 | ![lc195](img/fig.jpg) 85 | 86 | ## 5. 语法 87 | 88 | - 完成一个简单的交互设计,根据用户输入输出对应内容,具体交互内容随意,要求至少用上`select`,`case`和`read`。 89 | 90 | ```bash 91 | # 粘贴你的代码 92 | ``` 93 | 94 | 交互体验截图: 95 | 96 | ![inter](img/fig.jpg) 97 | 98 | ## 6. 综合实验 99 | 100 | - 编写 Shell 脚本`addowner.sh`将某目录下面所有的文件名后面加上文件所有者的名字。比如`a.txt`和`file`的所有者都为 owner,文件名修改后分别为`a[owner].txt`和`file[owner]`。 101 | - 使用用法:`./addowner 目录名称`。(若无目录名称这一参数,则默认为当前目录) 102 | - 对于子目录,名称不变。 103 | - (提示:为了测试效果,请通过`useradd`创建若干用户,并可通过`chown`改变文件所有者。另外,网上的答案是错的。) 104 | 105 | ```bash 106 | # 粘贴你的代码 107 | ``` 108 | 109 | 运行结果截图(包含目录、文件(含`.`与不含`.`),并体现出多个文件所有者): 110 | 111 | ![owner](img/fig.jpg) 112 | 113 | 完成本综合实验的过程和体会: 114 | 115 | xxx 116 | 117 | ## 7. 实验感想 118 | -------------------------------------------------------------------------------- /lab03b/lab03b.md: -------------------------------------------------------------------------------- 1 | # Lab03b: Shell 编程 2 | 3 | [TOC] 4 | 5 | ## 1. 实验目的 6 | 7 | 能够进行 Shell 基础编程。 8 | 9 | ## 2. 实验指南 10 | 11 | ### 2.1. Shell 编程 12 | 13 | #### 2.1.1. 什么是 Shell? 14 | 15 | 通俗一点的解释就是**命令解析器**,用以接收用户输入的命令,然后调用相应的应用程序。 16 | 17 | **要注意我们敲命令的地方(也就是那个黑色框框)不叫 Shell!!!那个东西叫 terminal,我们在 terminal 中输入命令,然后 Shell 接收指令**。Shell 可看成是一个进程,terminal 是这个进程的输入输出,与用户交互的部分。 18 | 19 | 详细可看[知乎:终端、Shell、tty 和控制台(console)有什么区别?](https://www.zhihu.com/question/21711307) 20 | 21 | #### 2.1.2. 实验环境 22 | 23 | 这次实验统一使用`bash`,在 Shell 中输入 `echo $SHELL`,输出应当是`/bin/bash`。如果不是,可使用`chsh`命令,之后输入`/bin/bash`将默认 Shell 更改为`bash`。 24 | 25 | #### 2.1.3. 如何运行 Shell? 26 | 27 | 以`helloworld.sh`为例 28 | 29 | ```bash 30 | #!/bin/bash 31 | echo "Hello, World!" 32 | ``` 33 | 34 | - `#!/bin/bash`被称为 shebang。具体其来源与作用见[释伴:Linux 上的 Shebang 符号(#!)](https://blog.csdn.net/u012294618/article/details/78427864) 35 | - 运行该程序需要具有可执行权限,可通过`chmod +x helloworld.sh`进行赋权。 36 | - `./helloworld.sh`即可看到输出。 37 | 38 | #### 2.1.4. 命令连接符 39 | 40 | 命令的执行是串行的,一条命令结束才能输入下一条命令,可以在命令之间加上`;`分割命令,从而可以一行输入所有命令。Shell 会挨个执行。 41 | 42 | - `&&`连接符 43 | - 命令`1 && 命令2 && 命令3` ,Shell 在判断出这个表达式的真假后就会停止执行。如果命令 1 为`false`,可以判断表达式 一定为假,执行停止。如果为`true`,那么还需要执行命令 2,一直执行到能判断真假为止或者执行完被`&&`连接的命令。 44 | - `||`连接符 45 | - 同 `&&` , 执行到能判断真假或者所有被连接命令被执行完为止。`&&`和`||`的计算方式同 c 语言中的`&& ||`。以上运算原则又被称作**短路原则** 46 | 47 | #### 2.1.5. Shell 变量 48 | 49 | - 在 Shell 中使用变量无需定义,在使用的时候创建。并且变量不分类型,Shell 统一认为是字符串,需要的时候通过一些命令进行转换。 50 | - 变量赋值: **变量名=值** ,等号左右不能够有空格。若字符串中包含空格,则需要用单/双引号括起来。 51 | - 可以使用`readonly`将变量改为只读类型。 52 | - 通过 `$` 引用变量值,`echo $SHELL`。 53 | - 输入变量,`read`变量名。 54 | 55 | | 引用格式 | 返回值 | 56 | | --------------------- | ------------------------------------------- | 57 | | `$var` | 返回变量值 | 58 | | `${#var}` | 返回变量值的长度 | 59 | | `${var:start}` | 返回从 start 下标到字符串末尾的子串 | 60 | | `${var:start:length}` | 返回从 start 下标开始,长度为 length 的子串 | 61 | 62 | 实际上还有一些空值判断、字符串替换和正则匹配拆分字符串等,为了精简篇幅,这里不再列举,可自行查阅资料。 63 | 64 | - 环境变量 (在前面已经介绍了) 65 | - 位置变量 66 | - 在执行 Shell 脚本的时候,可以传入参数,如当前有个脚本叫`test`,执行`sh test arg1 arg2 arg3` ,那么在`test`中,`$0` 代表脚本文件名,​`$1`为第一个参数:`arg1`,以此类推。 67 | - 使用`shift`可以将参数左移,此时`$1`为`arg2`,`$2`为`arg3`, `$3`为空`$#`为参数数量。 68 | 69 | | 特殊参数 | 含义 | 70 | | -------- | --------------------------------------- | 71 | | `$#` | 传递到脚本的参数数量 | 72 | | `$?` | 前一个命令执行情况,`0`成功,其他值失败 | 73 | | `$$` | 运行当前脚本的进程`id` | 74 | | `$!` | 运行脚本最后一个命令 | 75 | | `$*` | 传递给脚本或者函数的所有参数 | 76 | 77 | #### 2.1.6. 其余 Shell 语法 78 | 79 | Shell 编程还可以使用一些比如`if`语句等其余语句,这些语句和咱们学过的`C`中的对应语句功能基本一致,只是写法可能有点不一样,这里就不列举具体的功能了,仅给出一些例子以供参考。 80 | 81 | ```shell 82 | #!/bin/bash 83 | clear 84 | echo;echo 85 | echo "为 $0 添加执行权限" 86 | chmod +x $0 87 | echo 88 | 89 | # 重定向 90 | echo 输出重定向 > __re.txt 91 | cat __re.txt 92 | echo 93 | 94 | gcc tan90.file 2>__error.log 95 | cat __error.log 96 | echo 97 | 98 | # 管道 99 | ls -l /etc | grep pa 100 | echo 101 | 102 | # 定义变量 103 | var=string 104 | 105 | # 只读变量不可修改 106 | readonly test 107 | # test=6 去掉注释报错 108 | 109 | # 引用变量 110 | echo "var的值为: "$var 111 | echo "var长度为: "${#var} 112 | echo "var2~4为: "${var:2:3} 113 | echo 114 | 115 | # 传入参数 116 | echo "文件名为$0" 117 | echo '$1: '$1 118 | echo '$2: '$2 119 | echo '$1: '$3 120 | echo "传入参数有: "$* 121 | echo "传入参数数量:"$# 122 | echo 123 | 124 | shift;echo "移位后参数为:" 125 | echo '$0: '$0 126 | echo '$1: '$1 127 | echo 128 | 129 | # 运算 130 | i=1 131 | ((i+=5)) 132 | echo i=1,执行 '((i+=5))' 133 | echo 'i=: '$i 134 | echo 135 | 136 | # if 137 | echo -e "输入一个文件名,测试是否存在: \c" # -e能解释转义字符,详情man echo 138 | read filename 139 | if [[ -f $filename && -s $filename ]]; then 140 | echo "$filename 文件存在且不为空" 141 | else 142 | echo "$filename 文件不存在或为空" 143 | fi 144 | echo 145 | #====================== 146 | if [ -d /boot ]; then 147 | echo "存在/boot目录" 148 | else 149 | echo "不存在/boot目录" 150 | fi 151 | echo 152 | #================== 153 | if ls -l /boot/efi; then 154 | echo "访问/boot/efi 成功" 155 | else 156 | echo "访问/boot/efi 失败" 157 | fi 158 | echo 159 | #====== 160 | echo 161 | data=3 162 | data2=2 163 | if [ $data -lt $data2 ]; then 164 | echo $data \< $data2 165 | elif [ $data -gt $data2 ]; then 166 | echo $data \> $data2 167 | else 168 | echo $data = $data2 169 | fi 170 | 171 | echo 172 | 173 | # select 174 | echo "输入编号选择:" 175 | select subject in Math ComputerScience Chinese Moyu 176 | do 177 | echo "我最喜欢$subject" 178 | break 179 | done 180 | echo 181 | 182 | # case 183 | case $subject in 184 | 'Moyu' ) echo 'Moyu美滋滋';; #2个分号 185 | '*' ) echo "好好学习天天向上" 186 | esac 187 | echo 188 | 189 | # for 190 | for var in 1 2 3 4 5 6 191 | do 192 | echo -e $var '\c ' 193 | done 194 | echo;echo 195 | 196 | # while 197 | count=1 198 | sum=0 199 | while [ $count -lt 101 ] 200 | do 201 | ((sum+=count)) 202 | # count=`expr $count + 1` 这种写法不推荐 203 | ((count=expr+1)) 204 | done 205 | echo sum is $sum 206 | echo 207 | 208 | # until 略 209 | 210 | function helloworld # 或者 helloworld() 211 | { 212 | echo 参数个数为 $# 213 | echo 传入参数为 $* 214 | } 215 | helloworld 1 2 3 216 | 217 | 218 | # 一个综合一些的例子 219 | f[1]=1 220 | function fib () { 221 | for ((i = $1; i < $2; i++)); do 222 | if (( i > 6 && i > 8)); then 223 | break 224 | fi 225 | ((f[i] ++ )) 226 | # f[i]=$((${f[i]}+1)) 227 | # f[i]=`expr ${f[i]} + 1` 228 | # 这三种方式都可以进行赋值,但expr的方法不推荐(lint会报错) 229 | echo ${f[i]} 230 | done 231 | } 232 | fib 3 10 # $0: fib, $1: 3, $2: 10 233 | 234 | rm __error.log 235 | rm __re.txt 236 | exit 0 # 脚本成功退出 237 | ``` 238 | 239 | ## 3. 实验习题 240 | 241 | - 假如在脚本的第一行放入`#!/bin/rm`或者在普通文本文件中第一行放置`#!/bin/more`,然后将文件设为可执行权限执行,看看会发生什么,并解释为什么。 242 | - 编写一个 bash 脚本,执行该脚本文件将得到两行输出,第一行是你的学号,第二行是当前的日期(考虑使用`date`命令)。对该脚本文件的要求是 243 | - 文件名为`date-${你的学号}`,比如`date-15131049` 244 | - 用户可以在任意位置**只需要输入文件名**就可以执行该脚本文件 245 | - **不破坏除用户家目录之外的任何目录结构**,即不要在家目录之外的任何地方增删改任何文件 246 | **请详细叙述你的操作过程以及操作过程的截图,并给出你所编写的脚本文件的代码。** 247 | - 完成[LeetCode: 193. Valid Phone Numbers](https://leetcode.com/problems/valid-phone-numbers/) | [leetcode-cn](https://leetcode-cn.com/problems/valid-phone-numbers/) (体会`grep -E`, `grep -P`, `egrep`, `awk`等的差异) 248 | - 完成[LeetCode: 195. Tenth Line](https://leetcode.com/problems/tenth-line/) | [leetcode-cn](https://leetcode-cn.com/problems/tenth-line/),给出你的代码和 AC 截图。(提示:[怎么读取每一行](http://blog.sina.com.cn/s/blog_605f5b4f0101b0sd.html)) 249 | - 完成一个简单的交互设计,根据用户输入输出对应内容,具体交互内容随意,要求至少用上`select`,`case`和`read`。 250 | - 编写 Shell 脚本`addowner.sh`将某目录下面所有的文件名后面加上文件所有者的名字。比如`a.txt`和`file`的所有者都为 owner,文件名修改后分别为`a[owner].txt`和`file[owner]`。 251 | - 使用用法:`./addowner 目录名称`。(若无目录名称这一参数,则默认为当前目录) 252 | - 对于子目录,名称不变。 253 | - (提示:为了测试效果,请通过`useradd`创建若干用户,并可通过`chown`改变文件所有者。另外,网上的答案是错的。) 254 | -------------------------------------------------------------------------------- /lab04/answer.md: -------------------------------------------------------------------------------- 1 | # Lab04 Assignment 参考答案 2 | 3 | > 班级: 4 | > 学号: 5 | > 姓名: 6 | 7 | ## 1. 简述文件描述符的作用并回答问题:文件描述符和文件是一对一对应的关系吗? 8 | 9 | 文件描述符(file descriptor,简称 fd)是一个非负整数,存在于每一个进程的文件描述符表(也称打开文件描述符表)中。每个文件描述符对应一个打开文件,其范围是 0~一个进程最多可以打开的文件数量。 10 | 11 | **由于相同的文件可以被一个进程打开多次,所以不同的文件描述符可以指向同一个文件。每个文件描述符都是对于一个进程而言的,所以不同的进程的相同的文件描述符可能指向的是不同的文件。** 12 | 13 | 文件描述符实际上是一个索引值,其作用是索引到该进程的文件描述符表中的对应表项。文件描述符表的表项里有一指针,指向在内核中的打开文件表的表项,该表项中存有打开文件的文件偏移量、文件相关目录项 dentry 等相关属性信息。 14 | 15 | ## 2. 写出文件 I/O 函数`open()`、`read()`、`write()`、`close()`都分别有哪些参数,每个参数的含义是什么? 16 | 17 | ```c 18 | int open(const char* filename, int oflag,); 19 | filename为打开的文件名,oflag为文件打开标志(O_RDONLY,O_WRONLY,O_RDWR等) 20 | Int close(int fd); 21 | fd为关闭文件的文件描述符 22 | ssize_t read(int fd,void* buf,size_t count); 23 | ssize_t write(int fd, const void* buf,size_t count); 24 | fd为文件描述符,buf为读/写的数据,count为读/写的字符数量 25 | ``` 26 | 27 | ## 3. 创建一个文件,内容为你的姓名的全拼(如张三同学,文件中的内容即为`zhangsan`)。编写 c 语言程序实现以下功能:首先打开该文件并输出文件的内容,之后将文件的内容修改为`May the force be with you, ${姓名全拼}!`,比如`May the force be with you, zhangsan!`,输出修改后文件的内容,最后关闭文件。要求使用到`open()` `read()` `write()` `close()`函数。请详细叙述你的操作过程以及操作过程的截图,并给出你所编写的 C 程序的代码 28 | 29 | ```c 30 | 31 | #include 32 | #include 33 | #include 34 | #include 35 | int main(){ 36 | char* s1="May the force be with you,"; 37 | char* s2="!"; 38 | char buf[20]={0}; 39 | int fd; 40 | if((fd=open("1.txt",O_RDWR))==-1){ 41 | perror("open error"); 42 | exit(1); 43 | } 44 | 45 | ssize_t size=read(fd,buf,20); 46 | //printf("%s",buf); 47 | if(size==-1){ 48 | perror("read error"); 49 | exit(1); 50 | } 51 | lseek(fd, 0, SEEK_SET); 52 | write(fd,s1,strlen(s1)); 53 | write(fd,buf,size); 54 | write(fd,s2,strlen(s2)); 55 | close(fd); 56 | return 0; 57 | } 58 | ``` 59 | 60 | ## 4. 针对书中例 4-3 按照顺序执行下列命令 61 | 62 | ```shell 63 | ./writelock& 64 | ls-l>book.dat 65 | ``` 66 | 67 | - 查看 book.dat 数据是否发生变化,解释原因。 68 | 69 | ls -l 的内容写⼊了⽂件,因为 writelock 加的建议性锁, ls -l 的输出重定向进了book.dat 70 | 71 | - 输入书中 P110 程序 write.c,按下列方式执行程序 72 | 73 | ```shell 74 | ./writelock & 75 | ./write 76 | ./write 77 | ./write 78 | ``` 79 | 80 | - book.dat 文件数据的变化情况是什么?给出原因,给出必要的截图。 81 | 82 | ./write的内容写⼊了⽂件,因为 writelock 加的建议性锁 83 | 84 | ## 5. 创建文件`~/srcfile`,使用 `ln` 命令创建 `srcfile` 的软链接文件 `~/softlink` ,给出使用的命令;使用 `ls -l` 查看 `~` ,观察 `softlink` 的文件大小,并解释为什么;使用 `ln` 命令创建 `srcfile`的硬链接文件 `~/hardlink` ,给出使用的命令;使用 `ls -l` 观察 `srcfile` 硬链接数的变化 85 | 86 | ```sh 87 | ln -s srcfile softlink 88 | # softlink 的文件大小为7字节,软链接存储源文件的路径 89 | ln srcfile hardlink 90 | # srcfile硬链接数加1 91 | ``` 92 | 93 | ## 6. 编写一个 myls 程序,要求输入一个参数代表指定路径,打印路径下所有文件的名称 94 | 95 | 参考课本例 4-8 96 | 97 | ## 7. 创建一个文件,内容为你的姓名的全拼(如张三同学,文件中的内容即为`zhangsan`)。编写 c 语言程序实现以下功能:首先打开该文件并输出文件的内容,之后将文件的内容修改为`May the force be with you, ${姓名全拼}!`,比如`May the force be with you, zhangsan!`,输出修改后文件的内容,最后关闭文件。**要求使用到`fopen()` `fread()` `fwrite()` `fclose()`函数**。请详细叙述你的操作过程以及操作过程的截图,并给出你所编写的 C 程序的代码 98 | 99 | ```c 100 | #include 101 | #include 102 | int main(){ 103 | char* s1="May the force be with you,"; 104 | char* s2="!"; 105 | char buf[20]={0}; 106 | FILE *file; 107 | file=fopen("1.txt","r+"); 108 | fread(buf,20,1,file); 109 | //printf("%s",buf); 110 | fseek(file, 0, SEEK_SET); 111 | fwrite(s1,strlen(s1),1,file); 112 | fwrite(buf,strlen(buf),1,file); 113 | fwrite(s2,strlen(s2),1,file); 114 | fclose(file); 115 | return 0; 116 | } 117 | ``` 118 | -------------------------------------------------------------------------------- /lab04/answer_template.md: -------------------------------------------------------------------------------- 1 | # Lab04 Assignment 2 | 3 | > 班级: 4 | > 学号: 5 | > 姓名: 6 | 7 | ## 1. 简述文件描述符的作用并回答问题:文件描述符和文件是一对一对应的关系吗? 8 | 9 | ## 2. 写出文件 I/O 函数`open()`、`read()`、`write()`、`close()`都分别有哪些参数,每个参数的含义是什么? 10 | 11 | ## 3. 创建一个文件,内容为你的姓名的全拼(如张三同学,文件中的内容即为`zhangsan`)。编写 c 语言程序实现以下功能:首先打开该文件并输出文件的内容,之后将文件的内容修改为`May the force be with you, ${姓名全拼}!`,比如`May the force be with you, zhangsan!`,输出修改后文件的内容,最后关闭文件。要求使用到`open()` `read()` `write()` `close()`函数。请详细叙述你的操作过程以及操作过程的截图,并给出你所编写的 C 程序的代码 12 | 13 | ```c 14 | 15 | ``` 16 | 17 | ## 4. 针对书中例 4-3 按照顺序执行下列命令 18 | 19 | ```shell 20 | ./writelock& 21 | ls-l>book.dat 22 | ``` 23 | 24 | - 查看 book.dat 数据是否发生变化,解释原因。 25 | 26 | - 输入书中 P110 程序 write.c,按下列方式执行程序 27 | 28 | ```shell 29 | ./writelock & 30 | ./write 31 | ./write 32 | ./write 33 | ``` 34 | 35 | - book.dat 文件数据的变化情况是什么?给出原因,给出必要的截图。 36 | 37 | ## 5. 创建文件`~/srcfile`,使用 `ln` 命令创建 `srcfile` 的软链接文件 `~/softlink` ,给出使用的命令;使用 `ls -l` 查看 `~` ,观察 `softlink` 的文件大小,并解释为什么;使用 `ln` 命令创建 `srcfile`的硬链接文件 `~/hardlink` ,给出使用的命令;使用 `ls -l` 观察 `srcfile` 硬链接数的变化 38 | 39 | ```sh 40 | 41 | ``` 42 | 43 | ## 6. 编写一个 myls 程序,要求输入一个参数代表指定路径,打印路径下所有文件的名称 44 | 45 | ```c 46 | 47 | ``` 48 | 49 | ## 7. 创建一个文件,内容为你的姓名的全拼(如张三同学,文件中的内容即为`zhangsan`)。编写 c 语言程序实现以下功能:首先打开该文件并输出文件的内容,之后将文件的内容修改为`May the force be with you, ${姓名全拼}!`,比如`May the force be with you, zhangsan!`,输出修改后文件的内容,最后关闭文件。要求使用到`fopen()` `fread()` `fwrite()` `fclose()`函数。请详细叙述你的操作过程以及操作过程的截图,并给出你所编写的 C 程序的代码 50 | 51 | ```c 52 | 53 | ``` 54 | 55 | ## 8. 实验感想 56 | -------------------------------------------------------------------------------- /lab04/img/edit.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab04/img/edit.jpg -------------------------------------------------------------------------------- /lab04/img/fd1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab04/img/fd1.png -------------------------------------------------------------------------------- /lab04/img/fd2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab04/img/fd2.png -------------------------------------------------------------------------------- /lab04/img/fhs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab04/img/fhs.jpg -------------------------------------------------------------------------------- /lab04/img/inode.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab04/img/inode.jpg -------------------------------------------------------------------------------- /lab04/img/inode_num.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab04/img/inode_num.jpg -------------------------------------------------------------------------------- /lab04/img/ls.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab04/img/ls.jpg -------------------------------------------------------------------------------- /lab04/img/ls_detail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab04/img/ls_detail.jpg -------------------------------------------------------------------------------- /lab04/img/pwd.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab04/img/pwd.jpg -------------------------------------------------------------------------------- /lab04/img/tree.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab04/img/tree.jpg -------------------------------------------------------------------------------- /lab04/lab04.md: -------------------------------------------------------------------------------- 1 | # Lab04: 文件 I/O 操作 2 | 3 | [TOC] 4 | 5 | ## 1 实验目的 6 | 7 | 1. 在之前的基础上,进一步学习并掌握 Linux 系统中文件与文件系统的相关概念。 8 | 9 | 2. 学习并掌握与文件 IO 有关的 API。 10 | 11 | 3. 学习并掌握 Linux 系统中的 C 语言库函数、系统调用和链接相关的知识。 12 | 13 | ## 2 实验内容 14 | 15 | - 文件与文件系统 16 | - Linux 的目录结构 17 | - 工作目录、绝对路径与相对路径 18 | - inode、硬链接与软链接 19 | - 文件权限 20 | - 文件描述符 21 | - 常用的文件操作函数介绍 22 | 23 | ## 3 实验指南 24 | 25 | ### 3.1 文件 I/O 操作 26 | 27 | #### 3.1.1 常用的文件操作函数介绍 28 | 29 | **int create(const char \*filename, mode_t mode);** 30 | 31 | create 函数的功是创建文件,如果创建成功会返回一个文件描述符,创建失败则返回-1。 32 | 33 | **int open(const char \*pathname, int flags(,mode_t mode));** 34 | 35 | open 函数的功能是打开创者创建一个文件。如果文件打开成功,open 函数会返回一个文件描述符,以后对该文件的所有操作就可以通过对这个文件描述符进行操作来实现。open 函数有两个形式,其中 pathname 是要打开的文件名。 36 | 37 | **int close(int fd);** 38 | 39 | 关闭文件,成功调用则返回 0,否则返回-1。 40 | 41 | **int read(int fd, const void \*buf,size_t length);** 42 | 43 | 函数 read 实现从文件描述符 fd 所指定的文件中读取 length 个字节到 buf 所指向的缓冲区中,返回值为实际读取的字节数。 44 | 45 | **int write(int fd, const void \*buf,size_t length);** 46 | 47 | 函数 write 实现将 length 个字节从 buf 指向的缓冲区中写到文件描述符 fd 所指向的文件中,返回值为实际写入的字节数。 48 | 49 | **int lseek(int fd, offset_t offset, int whence);** 50 | 51 | lseek()将文件读写指针相对 whence 移动 offset 个字节。操作成功时,返回文件指针相对于文件头的位置。 52 | 53 | **int fcntl(int fd, int cmd, ...);** 54 | 55 | 用来修改已经打开文件的属性的函数 。 56 | 57 | **int stat(const char \*path,struct stat \*buf);** 58 | 59 | 用于获取文件的属性。参数 path 为文件路径。当函数调用成功之后,可以通过读取 buf 中的信息获取文件属性。 60 | 61 | **int access(const char \*pathname,int mode);** 62 | 63 | 用于测试文件是否拥有某种权限。参数 pathname 为文件名,参数 mode 可取四个值,分别表示测试文件是否具有读、写、执行权限和文件是否存在。如果满足 mode 中值所代表的条件,则返回 0,否则返回-1。 64 | 65 | **int chmod(const char \*path,mode_t mode);** 66 | 67 | 用于修改文件的访问权限。 68 | 69 | **int truncate(const char \*path,off_t length);** 70 | 71 | 用于修改文件大小。 72 | 73 | ### 3.2 目录 74 | 75 | #### 3.2.1 Linux 的目录结构 76 | 77 | **FHS**(Filesystem Hierarchy Standard):由 Linux Foundation 维护的 Linux 文件系统标准,定义了 Linux 系统中重要文件的组织方式。 78 | 79 | 不同于 Windows 的多树目录结构(每个磁盘分区都是一棵树),Linux 等 Unix 系统中只有一棵“文件树”。根目录`/`表示,是整棵文件树的“根”。 80 | 81 | 这里借用参考教材上的一张图: 82 | 83 | ![目录树结构示意图](img/tree.jpg)) 84 | 85 | 按照这样的组织结构,`test.c`文件的在操作系统内的完整路径表示是`/usr/bin/test.c`. 86 | 87 | 在整棵目录树中,在用户没有进行过修改的情况下,每个目录有自己默认的用途,我们平常使用的软件也会去这些对应的目录下面放置自己生成的文件。比如,绝大多数软件都会在`/etc`目录下放置自己的配置文件。关于每个目录默认的用途,大家可以参考第一次试验的内容,几个重要的目录也放在了这里。**但要注意的是,大多数目录的使用没有严格的明文规定,你完全可以自定义每个目录的用途,比如,你完全可以把一个软件的配置文件放在一个乱七八糟的其他地方,只不过你在使用这个配置文件的时候要手动指定其路径;比如你完全可以把可执行文件放在`/bin`或`/usr/bin`之外的任何地方,只不过你要修改系统的环境变量`PATH`。** 88 | 89 | ![FHS](img/fhs.jpg) 90 | 91 | - `/`:文件系统的根目录。 92 | - `/bin/`:重要的用户命令二进制文件,是英语 binary 的缩写,表示“二进制文件”(我们知道可执行文件是二进制的)。包含了会被所有用户使用的可执行程序。 93 | - `/etc/`:主机特定的系统配置,是 et cetera 的缩写,翻成英语就是 and so on,表示“等等”。包含系统的配置文件。至于为什么在`etc`下面存放配置文件, 按照原始的 UNIX 的说法(Linux 文件结构参考 UNIX 的教学实现 MINIX) ,这下面放的都是一堆零零碎碎的东西,就叫`etc`好了。 94 | - `/home/`:用户家目录,英语 home 表示“家”。在这个目录中放置各个用户的私人文件,类似 Windows 中的`C:\Users\`。Linux 中的每个用户(除了`root`)都在 home 目录下有自己的一个私人目录。比如我的用户名是`sysp`,那么我的私人目录就是`/home/sysp`。**Linux 中,默认使用`~`来代表当前用户的家目录,比如,对于用户`sysa`来说,`~`就代表了`/home/sysp`。** 95 | - `/lib/`:重要的共享库和内核模块,是英语 library 的缩写,表示“库”。包含被程序所调用的库文件,例如`.so`结尾的文件,在 Windows 下这样的库文件是以`.dll`结尾的。 96 | - `/mnt/`:临时挂载的文件系统的挂载点,是英语 mount 的缩写,表示“挂载”。有点类似 media,但一般用于临时挂载一些装置。 97 | - `/opt/`:附加应用程序软件包,是英语 optional application software package 的缩写,表示“可选的应用软件包”。用于安装多数第三方软件和插件。 98 | - `/tmp/`:临时文件,是英语 temporary 的缩写,表示“临时的”。普通用户和程序存放临时文件的地方。**每次重启计算机都会清空该目录下的文件。** 99 | - `/usr/`:多个用户的实用程序和应用程序,是英语 Unix Software Resource 的缩写,表示“Unix 操作系统软件资源”(也是个历史遗留的命名)。这个目录是最庞大的目录之一。有点类似 Windows 中的 C:\Windows 和 C:\Program Files 这两个文件夹的集合。在这里面安装了大部分用户要调用的程序。 100 | - `/var/`:可变文件,是英语 variable 的缩写,表示“动态的,可变的”。通常包含程序的数据,比如一些 log(日志)文件,记录电脑中发生了什么事。 101 | - `/root/`:root 用户的家目录,是英语“根”的意思。一般的用户的家目录位于/home 下。 102 | - `/proc/`:以文本文件形式记录内核和进程状态的虚拟文件系统,这里的文件实际存在位置是内存中,并不是硬盘。 103 | 104 | #### 3.2.2 工作目录、绝对路径与相对路径 105 | 106 | 一般来讲,用户执行的任何一个操作,都一定是在一个特定的“**工作目录**”中进行的。比如,在 bash 中,每一行的提示符中,主机名的冒号后面都会写明当前的工作路径,比如,当你一开始登录 Ubuntu 的时候,这里显示的是`~`,表示当前的工作目录是你的家目录,即`/home/${userName}`。使用`pwd`命令可以打印出当前的工作目录,依旧如下图所示。 107 | 108 | ![工作目录](img/pwd.jpg) 109 | 110 | **绝对路径**,从字面意义上很好理解,就是相对于根目录的路径,给定一个绝对路径,我们就能在系统中**唯一确定**一个资源。比如,上文中提到的`/usr/bin/test.c`,就是一个绝对路径。在同一个主机上,绝对路径绝对不会产生歧义。 111 | 112 | **相对路径**,就是其所表示的资源相对于当前工作目录的路径。在 Linux 中我们会用`.`来表示当前路径,会用`..`来表示上一级路径。比如说,我当前的工作目录是`/home`,`test.c`相对于我来说的相对路径就是`../usr/local/test.c`。**请注意,相对路径仅仅对于当前的工作目录有意义**。 113 | 114 | #### 3.2.3 Linux 目录结构查看命令 115 | 116 | `tree`命令:将目录内容用树形表示。 117 | 118 | - 安装(Ubuntu): 119 | 120 | ```shell 121 | $ sudo apt install tree 122 | ``` 123 | 124 | - Usage(常用): 125 | 126 | ```shell 127 | # ------- Listing options ------- 128 | -a All files are listed. 列出所有文件 129 | -d List directories only. 仅列出目录 130 | -l Follow symbolic links like directories. 追踪符号链接 131 | -f Print the full path prefix for each file. 打印完整路径 132 | -L level Descend only level directories deep. 查看的目录深度 133 | -o filename Output to file instead of stdout. 输出文件 134 | # -------- File options --------- 打印文件的其他信息 135 | -p Print the protections for each file. 权限信息(rwx) 136 | -u Displays file owner or UID number. 所有者 137 | -g Displays file group owner or GID number. 所属组 138 | -s Print the size in bytes of each file. 以字节为单位的文件大小 139 | -h Print the size in a more human readable way. 可读性更强的文件大小 140 | -D Print the date of last modification or (-c) status change. mtime/ctime 141 | --inodes Print inode number of each file. inode编号 142 | --device Print device ID number to which each file belongs. 设备号 143 | # ------- Sorting options ------- 排序标准 144 | -v Sort files alphanumerically by version. 版本号中的字母和数字 145 | -t Sort files by last modification time. mtime 146 | -c Sort files by last status change time. ctime 147 | -U Leave files unsorted. 不排序 148 | -r Reverse the order of the sort. 逆序 149 | --dirsfirst List directories before files (-U disables). 目录在前 150 | --sort X Select sort: name,version,size,mtime,ctime. 选择排序标准 151 | # ------- Graphics options ------ 152 | -i Do not print indentation lines. 不缩进 153 | -n Turn colorization off always (-C overrides). 关闭颜色显示 154 | -C Turn colorization on always. 不同类型的文件用不同颜色表示 155 | # ---- Miscellaneous options ---- 156 | --version Print version and exit. 版本号 157 | --help Print usage and this help message and exit. 帮助信息 158 | ``` 159 | 160 | #### 3.2.4 inode 161 | 162 | ![inode](img/inode.jpg) 163 | 164 | 上图**非常非常简略**的描述了 Linux 所使用的大多数文件系统对磁盘使用的抽象,仅供大家参考理解 inode 等概念。 165 | 166 | 关于 inode 的详细介绍,非常建议阅读下面的文章 (其中也包含了软硬链接的相关知识)。 167 | 168 | [理解 inode——阮一峰的网络日志](https://www.ruanyifeng.com/blog/2011/12/inode.html) 169 | 170 | #### 3.2.5 目录也是文件 171 | 172 | 目录在磁盘上的存储也是以文件的形式进行的。即,一个目录文件有自己的 inode 号,有自己的数据块,其中,这些数据块里存储的是本目录包含的文件信息。 173 | 174 | 比如,我们用`ls -li`命令查看文件的详细信息,同时列出每个文件的 inode 号: 175 | 176 | ![inode 号](img/inode_num.jpg) 177 | 178 | 我们还可以用`vim`来打开编辑一个目录文件,比如,编辑`systemProgramming`目录: 179 | 180 | ![编辑目录](img/edit.jpg) 181 | 182 | 打开的界面中,上半部分是提示信息,提示了一些操作方法,不用管;下半部分是真正目录文件中的内容。大家可以尝试编辑这些内容。 183 | 184 | #### 3.2.6 与目录有关的系统调用 185 | 186 | **DIR \*opendir(const char \*dirname);** 187 | 188 | 打开目录,返回一个指向 DIR 的指针,从而创建一个到目录的连接。 189 | 190 | **struct dirent \*readdir(DIR \*dir);** 191 | 192 | 每次从 DIR 中读取目录项信息,该目录项信息保存在结构体 dirent 中。 193 | 194 | **int closedir(DIR \* dir_ptr);** 195 | 196 | 关闭目录。 197 | 198 | **void seekdir(DIR \*dir, off_t offset);** 199 | 200 | 确定下一次调用 readdir 读取目录时的位置。 201 | 202 | **off_t telldir(DIR \*dir);** 203 | 204 | 返回当前所打开目录中下一次读取时的位置。 205 | 206 | **void rewinddir(DIR \*dir);** 207 | 208 | 系统调用重置目录流的位置至起始位置。 209 | 210 | **int mkdir(char\* pathname,mode_t mode);** 211 | 212 | 创建目录,成功创建目录时返回 0,失败时返回-1 并设置 errno 值。 213 | 214 | **int rmdir(char\* pathname);** 215 | 216 | 删除目录,要求删除时目录为空,成功时返回 0,失败时返回-1 并设置 errno 值。 217 | 218 | **int chdir(const char\* pathname,);** 219 | 220 | 切换进程自身工作目录,对其他进程工作目录没有影响。 221 | 222 | **int rename(const char \*from,const char \*to);** 223 | 224 | 对目录或文件重新命名或移动到新的位置。 225 | 226 | **char *getcwd(char *buf, size_t size);** 227 | 228 | 得到当前工作目录。 229 | 230 | ### 3.3 文件与目录的属性 231 | 232 | #### 3.3.1 Linux 文件类型 233 | 234 | Linux 文件类型: 235 | 236 | - 普通文件:包括二进制文件、文本文件、可执行文件等。 237 | - 目录文件:Linux 中目录以文件的形式存储。 238 | - 设备文件:包括块设备文件和字符设备文件。 239 | - 特殊文件:包括管道文件、套接字文件、符号链接文件。 240 | 241 | #### 3.3.2 链接文件 242 | 243 | Linux 中的链接文件类似于 Windows 中的快捷方式。 244 | 245 | 链接文件分为两种:硬链接文件和软链接文件。 246 | 247 | (1)软链接文件 248 | 249 | 一个文件的软链接文件是一个新文件,被分配一个未被占用的`inode`,它指向的是原文件(被链接文件)的数据,也就是共享原文件的数据。 250 | 251 | ```shell 252 | $ ln -s 源文件 目标文件 253 | ``` 254 | 255 | 文件的大小代表文件数据块中存储的数据的大小,软链接文件中存储的数据就是源文件的路径名。 256 | 257 | 当访问软链接文件时,系统会从它的数据块中获取源文件的路径,再到这个路径中访问源文件。 258 | 259 | (2)硬链接文件 260 | 261 | 硬链接文件与原文件共享`inode`,只是在硬链接文件的上级目录增加一项`dentry`,用来保存硬链接文件的文件名。因为`inode`是文件操作和表示的主结构体,因此,可以认为,硬件链接是一个文件,两个文件名。软件链接是两个文件,一份数据。 262 | 263 | ```shell 264 | $ ln 源文件 目标文件 265 | ``` 266 | 267 | 在创建硬链接文件时,文件的硬链接数会加`1`(可使用`ls -l`命令查看);若执行删除操作,只有在硬链接数为`1`时该文件才会真正被删除,其他时候只是删除文件路径目录项中的记录并使文件硬链接数减`1`;创建软链接文件时不会增加被链接文件的链接次数。 268 | 269 | Linux 中文件类型之一的符号链接文件只包含软链接文件,硬链接文件本质上是 Linux 中的普通文件。 270 | 271 | `inode`与`dentry`:《Linux 编程基础》P100 5.1.3 。 272 | 273 | 链接文件的更多介绍:《Linux 编程基础》P113 5.3.2 。 274 | 275 | #### 3.3.3 文件权限 276 | 277 | 大家在使用`ls -l`查看目录中文件的详细信息的时候,会看到这样一列的描述信息(红框框标出来的): 278 | 279 | ![ls](img/ls.jpg) 280 | 281 | 这些描述信息一般有 10 个字符组成。 282 | 283 | 这个字段的第一个字符代表了对象的类型: 284 | 285 | - `-` 代表是一个**普通**文件(**注意,在 Linux 中,“万物”皆为文件**) 286 | - `d` 代表目录 287 | - `l` 代表链接 288 | - `c` 代表字符型设备 289 | - `b` 代表块设备 290 | - `n` 代表网络设备 291 | 292 | 比如,`-rw-rw-r--`的第一个字符是`-`,表示是一个文件;`drwxr-xr-x`的第一个字符是`d`,表示这是一个目录。 293 | 294 | 后 9 个字符分为 3 组,分别描述了文件的所有者、文件所属的用户组的其他用户、系统的其他用户对该文件的权限。 295 | 296 | ![文件权限信息](img/ls_detail.jpg) 297 | 298 | 其中在这些描述用的字符中,`r`表示拥有读文件的权限,`w`表示拥有写文件的权限,`x`表示拥有执行文件的权限,这三个字符的位置是固定的,如果该位置上的字符为`-`,则表示该成员对该文件没有相应权限。 299 | 300 | 比如,上图中的意思就是,文件所有者对该文件有读、写、执行的权限,其他人只有读和执行的权限。 301 | 302 | 为了简便,**可以用数值来描述权限信息**,`r`对应的数值是`4`,`w`对应的数值是`2`,`x`对应的数值是`1`。每组权限信息是对这三个数值的简单相加,比如上图中的权限信息可以描述成`755`(**注意,这是一个八进制数**)。 303 | 304 | 可以使用`chmod`命令来修改一个文件的权限。下面举个例子。 305 | 306 | 首先新建一个文件: 307 | 308 | ```bash 309 | touch test 310 | ``` 311 | 312 | 它默认的权限是`rw-rw-r--`,即`664`, 可以用下面的命令修改权限,赋予每个用户对该文件的可执行权限: 313 | 314 | ```bash 315 | chmod 775 test 316 | ``` 317 | 318 | 当然,这里只是对`chmod`命令的简单介绍,可以在[这个链接](https://www.runoob.com/linux/linux-comm-chmod.html)中查看更多用法。 319 | 320 | #### 3.3.4 文件描述符 321 | 322 | 文件描述符(file descriptor,简称 fd)是一个非负整数,存在于每一个进程的文件描述符表(也称打开文件描述符表)中。每个文件描述符对应一个打开文件,其范围是 0~一个进程最多可以打开的文件数量。 323 | 324 | 由于相同的文件可以被一个进程打开多次,所以不同的文件描述符可以指向同一个文件。每个文件描述符都是对于一个进程而言的,所以不同的进程的相同的文件描述符可能指向的是不同的文件。 325 | 326 | 文件描述符实际上是一个索引值,其作用是索引到该进程的文件描述符表中的对应表项。文件描述符表的表项里有一指针,指向在内核中的打开文件表的表项,该表项中存有打开文件的文件偏移量、文件相关目录项 dentry 等相关属性信息。 327 | 328 | ![fd](./img/fd1.png) 329 | 330 | ![fd](./img/fd2.png) 331 | 332 | ### 3.4 标准文件 I/O 333 | 334 | #### 3.4.1 什么是标准 I/O 335 | 336 | 文件 I/O:文件 I/O 称之为不带缓存的 IO(unbuffered I/O)。不带缓存指的是每个 read,write 都调用内核中的一个系统调用。也就是一般所说的低级 I/O——操作系统提供的基本 IO 服务,与 os 绑定,特定于 linux 或 unix 平台。 337 | 338 | 标准 I/O:标准 I/O 是 ANSI C 建立的一个标准 I/O 模型,是一个标准函数包和 stdio.h 头文件中的定义,具有一定的可移植性。标准 I/O 库处理很多细节。例如缓存分配,以优化长度执行 I/O 等。标准的 I/O 提供了三种类型的缓存。 339 | 340 | (1)全缓存:当填满标准 I/O 缓存后才进行实际的 I/O 操作。 341 | (2)行缓存:当输入或输出中遇到新行符时,标准 I/O 库执行 I/O 操作。 342 | (3)不带缓存:stderr 就是了。 343 | 344 | #### 3.4.2 文件 I/O 和标准 I/O 的区别 345 | 346 | 文件 I/O 又称为低级磁盘 I/O,遵循 POSIX 相关标准。任何兼容 POSIX 标准的操作系统上都支持文件 I/O。标准 I/O 被称为高级磁盘 I/O,遵循 ANSI C 相关标准。只要开发环境中有标准 I/O 库,标准 I/O 就可以使用。(Linux 中使用的是 GLIBC,它是标准 C 库的超集。不仅包含 ANSI C 中定义的函数,还包括 POSIX 标准中定义的函数。因此,Linux 下既可以使用标准 I/O,也可以使用文件 I/O)。 347 | ​ 通过文件 I/O 读写文件时,每次操作都会执行相关系统调用。这样处理的好处是直接读写实际文件,坏处是频繁的系统调用会增加系统开销,标准 I/O 可以看成是在文件 I/O 的基础上封装了缓冲机制。先读写缓冲区,必要时再访问实际文件,从而减少了系统调用的次数。 348 | ​ 文件 I/O 中用文件描述符表现一个打开的文件,可以访问不同类型的文件如普通文件、设备文件和管道文件等。而标准 I/O 中用 FILE(流)表示一个打开的文件,通常只用来访问普通文件。 349 | 350 | ### 3.5 处理系统调用中的错误 351 | 352 | 以书上例 4-14 为例(书中的该代码错误已在如下程序中修正): 353 | 354 | ```c 355 | #include 356 | #include 357 | #include 358 | #include 359 | #include 360 | #include 361 | int main(void){ 362 | int fd; 363 | fd = open("/home/zhyh/no_exist_file",O_WRONLY); 364 | if(fd < 0){ 365 | perror("/home/zhyh/no_exist_file"); 366 | return 0; 367 | } 368 | } 369 | ``` 370 | 371 | 通过 `perror()`,能将在 `open` 时发生的错误打印出来,以便于调试。 372 | 373 | ```shell 374 | zhyh@ubuntu:~/splab$ gcc -o perrortest perrortest.c 375 | zhyh@ubuntu:~/splab$ ./perrortest 376 | /home/zhyh/no_exist_file: No such file or directory # 打印出了错误信息:没有这个文件/目录 377 | ``` 378 | 379 | ## 4 实验内容 380 | 381 | 1. 简述文件描述符的作用并回答问题:文件描述符和文件是一对一对应的关系吗? 382 | 383 | 2. 写出文件 I/O 函数`open()`、`read()`、`write()`、`close()`都分别有哪些参数,每个参数的含义是什么? 384 | 385 | 3. 创建一个文件,内容为你的姓名的全拼(如张三同学,文件中的内容即为`zhangsan`)。编写 c 语言程序实现以下功能:首先打开该文件并输出文件的内容,之后将文件的内容修改为`May the force be with you, ${姓名全拼}!`,比如`May the force be with you, zhangsan!`,输出修改后文件的内容,最后关闭文件。要求使用到`open()` `read()` `write()` `close()`函数。请详细叙述你的操作过程以及操作过程的截图,并给出你所编写的 C 程序的代码。 386 | 387 | 4. 针对书中例 4-3 按照顺序执行下列命令: 388 | 389 | ```shell 390 | ./writelock& 391 | ls-l>book.dat 392 | ``` 393 | 394 | 查看 book.dat 数据是否发生变化,解释原因。 395 | 396 | 输入书中 P110 程序 write.c,按下列方式执行程序 397 | 398 | ```shell 399 | ./writelock & 400 | ./write 401 | ./write 402 | ./write 403 | ``` 404 | 405 | book.dat 文件数据的变化情况是什么?给出原因,给出必要的截图。 406 | 407 | 5. 创建文件`~/srcfile`,使用 `ln` 命令创建 `srcfile` 的软链接文件 `~/softlink` ,给出使用的命令;使用 `ls -l` 查看 `~` ,观察 `softlink` 的文件大小,并解释为什么;使用 `ln` 命令创建 `srcfile`的硬链接文件 `~/hardlink` ,给出使用的命令;使用 `ls -l` 观察 `srcfile` 硬链接数的变化。 408 | 409 | 6. 编写一个myls程序,要求输入一个参数代表指定路径,打印路径下所有文件的名称。 410 | 411 | 7. 创建一个文件,内容为你的姓名的全拼(如张三同学,文件中的内容即为`zhangsan`)。编写 c 语言程序实现以下功能:首先打开该文件并输出文件的内容,之后将文件的内容修改为`May the force be with you, ${姓名全拼}!`,比如`May the force be with you, zhangsan!`,输出修改后文件的内容,最后关闭文件。**要求使用到`fopen()` `fread()` `fwrite()` `fclose()`函数**。请详细叙述你的操作过程以及操作过程的截图,并给出你所编写的 C 程序的代码。 412 | -------------------------------------------------------------------------------- /lab05/answer.md: -------------------------------------------------------------------------------- 1 | # Lab05 Assignment 参考答案 2 | 3 | ## 1. 参数传递 4 | 5 | 请写这样一个程序(不是函数):传入三个参数,传入该程序的第一个参数用以判断该程序进行有理数算术加运算还是减运算(0 表示将要进行加运算,1 表示将要进行减运算),第二个第三个参数分别是加(减)运算的第一第二个元素。(提示:`main(int argc, char*argc[])`,可获取命令行参数)。 6 | 7 | ```c 8 | #include 9 | #include 10 | 11 | int to_int(char s[]){ 12 | int len = strlen(s); 13 | int num = 0; 14 | for(int i = 0; i < len; i++){ 15 | num *= 10; 16 | num += s[i] - '0'; 17 | } 18 | return num; 19 | } 20 | int main(int argc,char *argv[]){ 21 | if(strcmp("0",argv[1])==0){ 22 | printf("%d\n",to_int(argv[2])+to_int(argv[3])); 23 | } 24 | else{ 25 | printf("%d\n",to_int(argv[2])-to_int(argv[3])); 26 | } 27 | } 28 | ``` 29 | 30 | ## 2. 进程调用 31 | 32 | 请写这样一个程序:使用创建子进程的方式调用上一个程序,进行加运算和减运算(请学习`fork`和`exec`函数的使用)。 33 | 34 | ```c 35 | #include 36 | #include 37 | #include 38 | #include 39 | #include 40 | #include 41 | #include 42 | #include 43 | #include 44 | 45 | char *s[4]; 46 | char *S[4]; 47 | 48 | int main(){ 49 | 50 | s[0] = "./a"; 51 | s[1] = "1"; 52 | s[2] = "50"; 53 | s[3] = "60"; 54 | 55 | S[0] = "./a"; 56 | S[1] = "0"; 57 | S[2] = "50"; 58 | S[3] = "60"; 59 | 60 | if (fork() == 0) execv("./a",s); 61 | else execv("./a",S); 62 | 63 | return 0; 64 | } 65 | ``` 66 | 67 | ## 3. 守护进程 68 | 69 | 守护进程有哪些特点,创建一个守护进程需要哪些步骤,每一步的意义是什么?(如果没有这一步可能有什么问题) 70 | 71 | - 特点 : 72 | 73 | 1. 守护进程是脱离于终端并且在后台运行的进程,它不受终端控制,及时终端推出,它仍然在后端运行。 74 | 2. 守护进程在执行过程中产生的信息不在任何终端显示,也不会被任何终端所产生的信息所打断。 75 | 76 | - 步骤 : 77 | 78 | 1. 创建子进程 79 | 2. 让子进程脱离控制终端 80 | 3. 改变当前目录为根目录 81 | 4. 修改文件权限掩码 82 | 5. 关闭文件描述符 83 | 84 | 每一步的详情和意义参见《Linux 编程基础》118-119 页。 85 | 86 | ## 4. 僵尸进程 87 | 88 | 僵尸进程有什么危害? 89 | 90 | - 僵尸进程没有任何代码、数据、堆栈,并不会占用多少资源,但它存在于系统的任务列表。 91 | 92 | 编写一个会产生僵尸进程的程序并运行,在终端查看当前进程。然后利用终端杀死该进程。因为系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程。 93 | 94 | ```c 95 | #include 96 | #include 97 | #include 98 | #include 99 | 100 | int main(){ 101 | pid_t pid; 102 | pid = fork(); 103 | if (pid < 0){ 104 | perror("fork error:"); 105 | exit(1); 106 | } 107 | else if (pid == 0){ 108 | printf("I am child process. I am exiting.\n"); 109 | printf("I will become a zombie!\n"); 110 | exit(0); 111 | } else sleep(60); 112 | return 0; 113 | } 114 | ``` 115 | 116 | ## 5. C 实现重定向 117 | 118 | 编写一个 C 程序:使得调用 `printf()` 时,输出直接重定向至 `log.txt`。 119 | 120 | ```c 121 | #include 122 | #include 123 | 124 | int main(){ 125 | freopen("log.txt","w",stdout); 126 | printf("Hello World!\n"); 127 | printf("Print into log.txt successfully!"); 128 | return 0; 129 | } 130 | ``` 131 | 132 | ## 6. 重定向代码分析 133 | 134 | 有以下一段代码: 135 | 136 | ```c 137 | int fd1, fd2, fd3, fd4; 138 | fd1 = open("a.txt", O_RDONLY); 139 | fd2 = open("b.txt", O_WRONLY); 140 | fd3 = dup(fd1); 141 | fd4 = dup2(fd2, 0); 142 | 143 | #include 144 | #include 145 | #include 146 | #include 147 | #include 148 | #include 149 | 150 | int main(){ 151 | int fd1, fd2, fd3, fd4; 152 | fd1 = open("a.txt", O_RDONLY); 153 | fd2 = open("b.txt", O_WRONLY); 154 | fd3 = dup(fd1); 155 | fd4 = dup2(fd2, 0); 156 | printf("%d %d %d %d",fd1, fd2, fd3, fd4); 157 | while (1); 158 | return 0; 159 | } 160 | ``` 161 | 162 | 请问,最后的 fd1, fd2, fd3, fd4 的值为多少?并解释原因。 163 | 164 | ```c 165 | fd1 = 3 166 | fd2 = 4 167 | fd3 = 5 168 | fd4 = 0 169 | ``` 170 | 171 | open 返回的是文件描述符。文件描述符是打开的文件数量减去一。因为对于每个进程,都会打开 3 个描述符,分别是标准输入(0),标准输出(1),标准错误(2)。所以 fd1 为 3,fd2 为 4。而 dup 函数的作用是复制一个和 fd1 指向同一个文件的文件描述符,并且新的文件符总是取最小的可用值,所以这时 fd3 为 5。而 dup2(fd2,0)的作用就是将 fd2 所指的文件将其拷贝到 0 之后,然后将 0 返回,所以 fd4 为 0,且 0 也是 b.txt 的文件描述符。 172 | 173 | ### 7. 管道 174 | 175 | 编写 C 语言程序实现如下功能:创建父子进程,父子进程之间通过管道进行通信,父进程向子进程发送字符串,子进程接收到该字符串后,将该字符串的最后 5 个字符发送给父进程。 176 | 177 | ```c 178 | #include 179 | #include 180 | #include 181 | int main() { 182 | int pipefds[2]; 183 | int returnstatus; 184 | int pid; 185 | returnstatus = pipe(pipefds); 186 | if (returnstatus == -1) { 187 | printf("Unable to create pipe\n"); 188 | return 1; 189 | } 190 | pid = fork(); 191 | // Child process 192 | if (pid == 0) { 193 | sleep(1);//等待父进程写入管道 194 | char childstr[20],childstr1[20]; 195 | read(pipefds[0],childstr, sizeof(childstr)); 196 | printf("子进程从管道读出:%s\n",childstr); 197 | int len=strlen(childstr); 198 | int i,j; 199 | for(i=len-5,j=0;i 班级: 4 | > 学号: 5 | > 姓名: 6 | 7 | ## 1. 参数传递 8 | 9 | 请写这样一个程序(不是函数):传入三个参数,传入该程序的第一个参数用以判断该程序进行有理数算术加运算还是减运算(0 表示将要进行加运算,1 表示将要进行减运算),第二个第三个参数分别是加(减)运算的第一第二个元素。(提示:`main(int argc, char*argv[])`,可获取命令行参数)。 10 | 11 | ```c 12 | // 代码 13 | ``` 14 | 15 | ## 2. 进程调用 16 | 17 | 请写这样一个程序:使用创建子进程的方式调用上一个程序,进行加运算和减运算(请学习`fork`和`exec`函数的使用)。 18 | 19 | ```c 20 | // 代码 21 | ``` 22 | 23 | ## 3. 守护进程 24 | 25 | 守护进程有哪些特点,创建一个守护进程需要哪些步骤,每一步的意义是什么?(如果没有这一步可能有什么问题) 26 | 27 | ## 4. 僵尸进程 28 | 29 | 僵尸进程有什么危害?编写一个会产生僵尸进程的程序并运行,在终端查看当前进程。然后利用终端杀死该进程。 30 | 31 | ## 5. C 实现重定向 32 | 33 | 编写一个 C 程序:使得调用 `printf()` 时,输出直接重定向至 `log.txt`。 34 | 35 | ```c 36 | // 代码 37 | ``` 38 | 39 | 截图: 40 | 41 | ## 6. 重定向代码分析 42 | 43 | 有以下一段代码: 44 | 45 | ```c 46 | int fd1, fd2, fd3, fd4; 47 | fd1 = open("a.txt", O_RDONLY); 48 | fd2 = open("b.txt", O_WRONLY); 49 | fd3 = dup(fd1); 50 | fd4 = dup2(fd2, 0); 51 | ``` 52 | 53 | 请问,最后的 fd1, fd2, fd3, fd4 的值为多少?并解释原因。 54 | 55 | ```c 56 | fd1 = 57 | fd2 = 58 | fd3 = 59 | fd4 = 60 | ``` 61 | 62 | ## 7. 管道 63 | 64 | 编写 C 语言程序实现如下功能:创建父子进程,父子进程之间通过管道进行通信,父进程向子进程发送字符串,子进程接收到该字符串后,将该字符串的最后 5 个字符发送给父进程。 65 | 66 | ```c 67 | // 代码 68 | ``` 69 | 70 | ## 8. 实验感想 71 | -------------------------------------------------------------------------------- /lab05/img/cpu_switch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab05/img/cpu_switch.jpg -------------------------------------------------------------------------------- /lab05/img/fork.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab05/img/fork.jpg -------------------------------------------------------------------------------- /lab05/img/pcb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab05/img/pcb.jpg -------------------------------------------------------------------------------- /lab05/img/process.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab05/img/process.jpg -------------------------------------------------------------------------------- /lab05/img/process_status.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab05/img/process_status.jpg -------------------------------------------------------------------------------- /lab05/lab05.md: -------------------------------------------------------------------------------- 1 | # Lab05: Linux 进程管理 2 | 3 | [TOC] 4 | 5 | ## 1. 实验指南 6 | 7 | ### 1.1. 进程 8 | 9 | 进程的英文是`process`,意为“做事情的过程”,操作系统中的进程就可以理解为“**操作系统执行一个程序的过程**”。 10 | 11 | 程序在编译后以二进制方式存在于外存上,执行的时候被操作系统载入内存。以 Linux 系统上的 C 语言编译出来的程序为例,载入的过程简单来说就是把编译完成的 ELF 文件的几个段的内容读取到内存指定位置,然后初始化寄存器的内容,将指令寄存器(比如`cs:ip`)指向程序入口,再初始化一些进程相关内容就完成了。在某一次时钟中断发生的时候,进程主动陷入内核态,进行进程切换的系统调用,CPU 将切换到另一个进程工作(关于具体的进程切换的细节是大家下学期学习操作系统的重点)。**总而言之,整个计算机从开机到关机,就是一个不断创建、切换、终止进程的过程。** 12 | 13 | #### 1.1.1. 进程概念的用途 14 | 15 | - 早期的计算机一次只能执行一个程序,这种程序完全控制系统,并且访问所有系统资源。 16 | - 相比之下,现代计算机系统允许“同时”加载多个应用程序到内存,以便并发(轮流)执行。 17 | - 这种改进要求:对各种程序提供更严的控制和更好的划分。这些需求导致了**进程**概念的诞生。 18 | - 进程是现代分时操作系统的工作单元,是操作系统向运行中的程序进行资源分配的单位。 19 | - 进程包括:程序代码(文本),当前活动(程序计数器,寄存器的值),堆栈,数据端,堆。 20 | 21 | ![process](img/process.jpg) 22 | 23 | - 程序与进程:程序是被动(`passive`)实体,如存储在磁盘上的可执行文件;进程是活动(`active`)实体,具有一个程序计数器用于表示下个执行命令和一组相关资源。 24 | - 当一个可执行文件被加载到内存时,这个程序就成为进程。 25 | - 两个进程可以与同一程序相关联,但当作两个单独的执行序列,虽然文本段相同,但是数据、堆、堆栈不同。 26 | 27 | #### 1.1.2. 进程状态(选读) 28 | 29 | - 进程在执行时会改变状态,每个进程可能处于以下状态: 30 | - 新的(`new`):进程正在创建。 31 | - 运行(`running`):指令正在执行。 32 | - 等待(`waiting`):进程等待发生某个事件(如`I/O`完成或收到信号)。 33 | - 就绪(`ready`):进程等待分配处理器。 34 | - 终止(`terminated`):进程已经完成执行。 35 | - 一次只有一个进程可在一个处理器上**运行**(`running`) 36 | - 可以有多个进程处于**就绪**(`ready`)或**等待**(`waiting`)状态 37 | - 进程状态图: 38 | 39 | ![进程状态图](img/process_status.jpg) 40 | 41 | #### 1.1.3. PCB(选读) 42 | 43 | 对于多任务处理系统,一般来讲,CPU 核心数远低于当前系统中同时存在的进程数,因此,某一时刻,大概率总有一些进程处于非运行时状态。但这些进程在未来的一些时刻又将会被调度执行,恢复到之前该进程被终止时候的状态。因此,有必要把一个进程被终止时的信息记录下来。这些信息被称为进程控制块 PCB(Process Control Block),主要包括进程被终止时各个寄存器的使用情况、父进程 ID、进程组 ID 等。操作系统可以通过进程的 ID,找到该进程的 PCB。 44 | 45 | PCB 可能包含的信息有: 46 | 47 | - 进程状态(`process state`):包括上面提到的五种状态。 48 | - 程序计数器(`program counter`):表示进程将要执行的下个指令的地址。 49 | - CPU 寄存器(`CPU register`):根据计算机体系结构的不同,寄存器的类型和数量也会不同,通常包括累加器、索引寄存器、堆栈指针、通用寄存器和其他条件码寄存器。在发生中断时,这些状态信息与程序计数器一起保存,以便进程以后能正确地继续执行。 50 | - CPU 调度信息(`CPU-scheduling information`):包括进程优先级、调度队列的指针和其他调度参数。 51 | - 内存管理信息(`memory-management information`):可能包括基地址和界限寄存器的值、页表或段表。 52 | - 记账信息(`accounting information`):包括 CPU 时间、实际使用时间、时间期限、记账数据、作业或进程数量等。 53 | - I/O 状态信息(`I/O status information`):包括分配给进程的`I/O`设备列表、打开文件列表等。 54 | 55 | ![PCB](img/pcb.jpg) 56 | 57 | - 进程间的 CPU 切换: 58 | 59 | ![进程间的CPU切换](img/cpu_switch.jpg) 60 | 61 | ### 1.2. 进程控制 62 | 63 | #### 1.2.1. 进程标识 64 | 65 | - 每个进程都有一个非负整型表示的唯一进程 ID——`pid`。 66 | - `pid`是可复用的,当一个进程终止后,其进程 ID 就成为复用的候选者。 67 | - 除了`pid`,每个进程还有一些其他标识符,例如:`ppid`,`uid`,`euid`,`gid`,`egid`。 68 | 69 | #### 1.2.2. 使用 `fork` 创建新进程 70 | 71 | fork 的作用是创建一个子进程,共享父进程所有内容,并且这个子进程会接着 fork 下面的代码继续执行。关于 fork 的用法之类的不在赘述,下面探讨下 fork 的原理。fork 吹嘘的最神奇的地方莫过于执行一次,返回两次,但这句话噱头成分更大一些。在一个进程中,一次函数调用肯定只能返回一次,之所以 fork 会返回两次,是因为在 fork 执行的过程中,会根据本进程克隆出一个新的子进程,在子进程中依旧会执行 fork 剩余的代码,也自然会从 fork 中返回。这样,在父进程返回一次,在子进程中返回一次,自然就是所谓的“返回两次”。 72 | 73 | ![fork](img/fork.jpg) 74 | 75 | ```c 76 | #include 77 | 78 | pid_t fork(); 79 | 80 | // 返回值:子进程返回0,父进程返回子进程ID;若出错,返回-1 81 | ``` 82 | 83 | - 一个现有的进程可以调用`fork`函数创建一个新进程。 84 | - `fork`有以下两种用法: 85 | - 一个父进程希望复制自己,使父进程和子进程同时执行不同的代码段。 86 | - 一个进程要执行一个不同的程序。在这种情况下,子进程从`fork`返回后立即调用`exec`。 87 | 88 | #### 1.2.3. 使用 exec 函数族执行新的程序 89 | 90 | `exec()`函数族包括以下七个函数: 91 | 92 | ```c 93 | #include 94 | 95 | int execl(const char *pathname, const char *arg0, ... /* (char *)0 */); 96 | 97 | int execv(const char *pathname, char *const argv[]); 98 | 99 | int execle(const char *pathname, const char *arg0, ... 100 | /* (char *)0, char *const envp[] */); 101 | 102 | int execve(const char *pathname, char *const argv[], char *const envp[]); 103 | 104 | int execlp(const char *filename, const char *arg0, ... /* (char *)0 */); 105 | 106 | int execvp(const char *filename, char *const argv[]); 107 | 108 | int fexecve(int fd, char *const argv[], char *const envp[]); // 第一个参数使用的是打开的文件描述符,而非文件路径名 109 | 110 | // 7个函数返回值:若出错,返回-1;若成功,不返回 111 | ``` 112 | 113 | exec 函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容换句话说,就是在调用进程内部执行一个可执行文件。这里的可执行文件既可以是二进制文件,也可以是任何 Linux 下可执行的脚本文件。与一般情况不同,exec 函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程 ID 等一些表面上的信息仍保持原样。只有调用失败了,它们才会返回一个 -1,从原程序的调用点接着往下执行。 114 | 115 | 这几个函数的用法大体上是一致的,只是参数格式有所不同。 116 | 117 | - " l " 代表 list 即列表,对应可变参数`argv` 以列表的形式出现 118 | - " v " 代表 vector 即矢量数组,对应可变参数`argv`以数组的形式出现 119 | - " e " 代表 environment ,对应 `envp`数组,是指给可执行文件指定环境变量。在全部 7 个函数中,只有`execle`、`execve`和`fexecve`使用了`char *envp[]`传递环境变量,其它的 4 个函数都没有这个参数,这并不意味着它们不传递环境变量,这 4 个函数将把默认的环境变量不做任何修改地传给被执行的应用程序。而它们用指定的环境变量去替代默认的那些。 120 | - " p " 代表 环境变量 PATH ,字母 p 是指在环境变量 PATH 的目录里去查找要执行的可执行文件。2 个以 p 结尾的函数`execlp`和`execvp`,看起来,和`execl`与`execv`的差别很小,事实也如此,它们的区别从第一个参数名可以看出:除 `execlp`和`execvp`之外的 4 个函数都要求,它们的第 1 个参数 path 必须是一个完整的路径,如"/bin/ls";而`execlp`和`execvp` 的第 1 个参数 file 可以仅仅只是一个文件名,如"ls",这两个函数可以自动到环境变量 PATH 指定的目录里去查找。 121 | 122 | **注意:** 123 | 124 | - 当进程调用一种`exec`函数时,该进程执行的程序完全替换为新程序,而新程序从其`main`函数开始执行。 125 | - 调用`exec`并不创建新进程,前后的进程 ID 并未改变,`exec`只是用磁盘上的一个新程序替换了当前进程的正文段、数据段、堆段和栈段。 126 | - 在很多 UNIX 实现中,**这`7`个函数只有`execve`是内核的系统调用**,另外`6`个只是库函数,它们最终都要调用该系统调用。 127 | 128 | #### 1.2.4. 使用 exit 处理进程终止 129 | 130 | ```c 131 | #include 132 | 133 | void exit(int status); 134 | 135 | void _Exit(int status); 136 | 137 | #include 138 | 139 | void _exit(int status); 140 | ``` 141 | 142 | - 有`8`种方式使进程终止,其中`5`种为正常终止。 143 | - `3`个函数用于正常终止一个程序:`_exit`和`_Exit`立即进入内核,`exit`则先执行一些清理处理,然后返回内核。 144 | 145 | #### 1.2.5. 使用 `wait/waitpid` 等待进程终止 146 | 147 | ```c 148 | #include 149 | 150 | pid_t wait(int *statloc); 151 | 152 | pid_t waitpid(pid_t pid, int *statloc, int options); 153 | 154 | // 两个函数返回值:若成功,返回进程ID;若出错,返回0或-1 155 | ``` 156 | 157 | 更多信息请阅读`manual`或《APUE》的相关章节。 158 | 159 | ### 1.3. 进程间通信 160 | 161 | 这部分内容介绍的是操作系统内部的进程间的通信(Inter-Process Communication,IPC),主要包含管道通信、SystemV IPC(消息队列、信号量、共享内存等),不包含 socket 连接等系统之间的进程间的通信。 162 | 163 | 本次实验内容只涉及管道和重定向内容。 164 | 165 | #### 1.3.1. 再谈重定向 166 | 167 | 在之前的思考题中,我们出现过 `dup` 函数,复制一个现存的文件描述符。事实上,文件描述符便与重定向密切关联。我们可以从 man 手册中了解 `dup`、`dup2` 和 `dup3()` 这三个函数。[dup(2) — Linux manual page](https://man7.org/linux/man-pages/man2/dup2.2.html) 168 | 169 | 重定向产生的原因就是文件描述符在分配时趋向于数值小的,而在用户层,stdout 这个文件指针指向的文件已经封装了,并且它的 fd 就是 1,这是不能修改的,所以我们一上来关闭了 1 号文件,然后新创建了一个文件它的文件描述符就会分配为被 1,同时此时写入时,像 `printf` 这类函数默认使用的输出流就是 stdout,但是我们知道它的 1 指向的已经是我们新生成的那个文件了,所以这就重定向的本质。 170 | 171 | #### 1.3.2. 再谈管道 172 | 173 | 管道是最基本的进程通信机制,可以想象成一个管道,两端分别连着 2 个进程,一个进程往里面写,一个进程从里面读。如果读或写管道的时候没有内容可供读或写,进程将被阻塞,直到有内容可供读写为止。 174 | 175 | 管道分为匿名管道和命名管道。 匿名管道创建后本质上是 2 个文件描述符,父子进程分别持有就能够使用管道,需要注意的是不能够共用匿名管道,也就是除了使用的进程,其他进程需要关闭文件描述符,保证管道 的 2 个描述符分别同时只有 1 个进程持有。 176 | 177 | 命名管道是根据路径来使用管道,故能够在任意进程间通信。(仅要求掌握匿名管道,命名管道作为了解) 178 | 179 | ## 2. 实验习题 180 | 181 | - 请写这样一个程序(不是函数):传入三个参数,传入该程序的第一个参数用以判断该程序进行有理数算术加运算还是减运算(0 表示将要进行加运算,1 表示将要进行减运算),第二个第三个参数分别是加(减)运算的第一第二个元素。(提示:`main(int argc, char*argv[])`,可获取命令行参数)。 182 | - 请写这样一个程序:使用创建子进程的方式调用上一个程序,进行加运算和减运算(请学习`fork`和`exec`函数的使用)。 183 | - 守护进程有哪些特点,创建一个守护进程需要哪些步骤,每一步的意义是什么?(如果没有这一步可能有什么问题) 184 | - 僵尸进程有什么危害?编写一个会产生僵尸进程的程序并运行,在终端查看当前进程。然后利用终端杀死该进程。 185 | - 编写一个 C 程序:使得调用 `printf()` 时,输出直接重定向至 `log.txt`。 186 | - 有以下一段代码: 187 | 188 | ```c 189 | int fd1, fd2, fd3, fd4; 190 | fd1 = open("a.txt", O_RDONLY); 191 | fd2 = open("b.txt", O_WRONLY); 192 | fd3 = dup(fd1); 193 | fd4 = dup2(fd2, 0); 194 | ``` 195 | 196 | 请问,最后的 fd1, fd2, fd3, fd4 的值为多少?并解释原因。 197 | 198 | - 编写 C 语言程序实现如下功能:创建父子进程,父子进程之间通过管道进行通信,父进程向子进程发送字符串,子进程接收到该字符串后,将该字符串的最后 5 个字符发送给父进程。 199 | -------------------------------------------------------------------------------- /lab06/answer.md: -------------------------------------------------------------------------------- 1 | # Lab06 Assignment 参考答案 2 | 3 | > 班级: 4 | > 学号: 5 | > 姓名: 6 | 7 | ## 1. 请简述信号什么时候处于未决状态,并简述信号存在未决状态的作用。 8 | 9 | 已经产生的信号,但是还没有传递给任何进程,此时该信号的状态就称为未决状态。 10 | 11 | 如果信号被阻塞,内核会保持信号未决,此时信号不会丢失,取消阻塞后依然可以递送信号。 12 | 13 | ## 2. 若在某信号的信号处理函数中给自己发送同一个信号,请简单描述程序的行为。 14 | 15 | 与信号是否可靠、注册信号方式有关,一般情况下程序会递归执行信号处理函数,陷入死循环。 16 | 17 | ## 3. 编写程序实现如下功能 18 | 19 | 程序 A.c 通过 sigqueue()函数按用户输入向程序 B.c 发送信号和附加数据;B.c 程序接收到该信号后,输出伴随信号的附加数据。运行过程如下: 20 | 21 | ```sh 22 | ./B & # 此时,输出进程 B 的 PID 号。 23 | ./A processB_PID sigvalue # 第一个参数表示进程 B 的 PID,第二个参数为伴随信号的附加数据(int 值即可)。 24 | ``` 25 | 26 | ```c 27 | //code of A 28 | #include 29 | #include 30 | 31 | int main(int argc, char *argv[]){ 32 | int message; 33 | pid_t B_pid; 34 | sscanf(argv[1],"%d",&B_pid); 35 | sscanf(argv[2],"%d",&message); 36 | 37 | union sigval info; 38 | info.sival_int = msg; 39 | 40 | sigqueue(B_pid, SIGINT, info); 41 | printf("A send signal with message: %d\n"info.sival_int); 42 | return 0; 43 | } 44 | ``` 45 | 46 | ```c 47 | //code of B 48 | #include 49 | #include 50 | #include 51 | void int_Handler(int signo, siginfo_t *info, void *ucontext){ 52 | printf("B recieve signal with message: %d\n", info->si_value.sival_int); 53 | } 54 | int main(){ 55 | printf("B pid: %d\n",getpid()); 56 | struct sigaction act; 57 | act.sa_flags = SA_SIGINFO; 58 | act.sa_sigaction = int_Handler; 59 | sigaction(SIGINT, &act, NULL); 60 | pause(); 61 | return 0; 62 | } 63 | ``` 64 | 65 | ## 4. 请实现这样一个程序 66 | 67 | 程序每间隔 1 秒输出你的学号,当按下 ctrl+c 后,程序询问是否退出程序(此时停止输出学号),输入 Y 或 5 秒未进行任何输入则退出程序,输入 N 程序恢复运行,继续输出学号(提示:alarm()函数设置超时时间,SIGALRM 信号处理函数作为超时处理)。 68 | 69 | ```c 70 | //code 71 | #include 72 | #include 73 | #include 74 | #include 75 | #include 76 | 77 | void SIGALRM_handler(){ 78 | exit(0); 79 | } 80 | void SIGINT_handler(){ 81 | char c[10]; 82 | printf("\nExit?(Y/N)\n"); 83 | alarm(5); 84 | scanf("%s",c); 85 | alarm(0); 86 | if(strcmp(c,"Y")==0) 87 | exit(0); 88 | } 89 | int main(){ 90 | signal(SIGINT,SIGINT_handler); 91 | signal(SIGALRM,SIGALRM_handler); 92 | while(1){ 93 | printf("18373455\n"); 94 | sleep(1); 95 | } 96 | return 0; 97 | } 98 | ``` 99 | 100 | ## 5. 请实现这样一个程序 101 | 102 | 在程序中创建一个子进程,通过信号实现父子进程交替输出,父进程输出学号,子进程输出姓名,要求父进程先输出。 103 | 104 | ```c 105 | //code 106 | #include 107 | #include 108 | #include 109 | #include 110 | #include 111 | 112 | int pid; 113 | void hand1() { 114 | printf("18373455\n"); 115 | sleep(1); 116 | kill(pid,SIGUSR1); 117 | } 118 | void hand2() { 119 | printf("李晓洲\n"); 120 | sleep(1); 121 | kill(getppid(),SIGUSR1); 122 | } 123 | int main() { 124 | sigset_t set; 125 | sigemptyset(&set); 126 | sigaddset(&set,SIGUSR1); 127 | sigprocmask(SIG_BLOCK,&set,NULL); 128 | 129 | pid=fork(); 130 | if(pid){ 131 | signal(SIGUSR1, hand1); 132 | sigprocmask(SIG_UNBLOCK,&set,NULL); 133 | while(1)pause(); 134 | } 135 | else{ 136 | signal(SIGUSR1, hand2); 137 | sigprocmask(SIG_UNBLOCK,&set,NULL); 138 | kill(getppid(),SIGUSR1); 139 | while(1)pause(); 140 | } 141 | } 142 | ``` 143 | 144 | ## 6. 父子进程 145 | 146 | 父进程等待子进程退出通常仅需调用 wait()函数,但如果子进程未退出,父进程将会一直处于阻塞态,并通过循环不断获取子进程状态,该回收子进程的方式是对 CPU 资源的浪费。子进程终止时会自动向父进程发送 SIGCHLD 信号,请通过该特性实现这样一个程序:父进程创建 5 个子进程,每个子进程输出 PID 后以不同的状态值退出,父进程使用 SIGCHLD 信号实现异步回收子进程,每回收一个子进程就输出该子进程的 PID 和退出状态值,需要保证任何情况下所有子进程都能回收(提示:SIGCHLD 是不可靠信号,不支持排队,考虑两个子进程同时结束的情况) 147 | 148 | ```c 149 | //code 150 | #include 151 | #include 152 | #include 153 | #include 154 | #include 155 | 156 | int num = 0; 157 | void child_handler() 158 | { 159 | int status; 160 | pid_t pid; 161 | while ((pid = waitpid(0, &status, WNOHANG)) > 0){ 162 | printf("Get Child %d: %d\n", pid, WEXITSTATUS(status)); 163 | num++; 164 | } 165 | } 166 | int main() 167 | { 168 | signal(SIGCHLD,child_handler); 169 | for (int i=1;i<=5;i++){ 170 | if (fork() == 0){ 171 | printf("Child %d exit: %d\n",getpid(),i); 172 | exit(i); 173 | } 174 | } 175 | while (num != 5) 176 | sleep(10); 177 | return 0; 178 | } 179 | ``` 180 | 181 | ## 7. 异步信号安全函数 182 | 183 | 异步信号安全函数(async-signal-safe function)是可以在信号处理函数中安全调用的函数,即一个函数在返回前被信号中断,并在信号处理函数中再次被调用,均可以得到正确结果。通常情况下,不可重入函数(non-reentrant function)都不是异步信号安全函数,都不应该在信号处理函数中调用。 184 | 185 | 1. 请判断下面的函数是否是异步信号安全函数,如果是请说明理由,如果不是请给出一种可能发生问题的情况。 186 | 187 | ```c 188 | int tmp; 189 | void swap1(int* x, int* y) 190 | { 191 | tmp = *x; 192 | *x = *y; 193 | *y = tmp; 194 | } 195 | ``` 196 | 197 | ```c 198 | void swap2(int* x, int* y) 199 | { 200 | int tmp; 201 | tmp = *x; 202 | *x = *y; 203 | *y = tmp; 204 | } 205 | ``` 206 | 207 | `swap1()`不是异步信号安全函数,因为使用全局变量 tmp 作为中间变量。考虑执行完`tmp=*x`语句,此时被信号中断,并在信号处理函数中再次调用`swap1()`函数,从信号处理函数退出后 tmp 值已经被更改,此时`*y`将得到错误的值。 208 | 209 | `swap2()`是异步信号安全函数,因为所有变量均为局部变量。 210 | 211 | 注意: 212 | 213 | - 异步信号安全函数、可重入函数、线程安全函数是三个不同的概念,有细微差别,具体请查阅资料 214 | - 若向`swap2()`函数传入全局变量作为参数,此时也会发生错误,但该错误一般认为是代码逻辑问题,并不认为是函数安全性问题。 215 | 216 | 2. 由于 `printf()` 函数使用全局缓冲区,因此它不是异步信号安全函数。为了避免可能发生的问题,其中一个解决方法是在调用 printf()函数前阻塞所有信号,并在调用后恢复。请用上述思路补全代码,实现 printf()的异步信号安全版本,无需实现格式化输出(提示:`sigprocmask()` 函数可用于阻塞多个信号)。 217 | 218 | ```c 219 | //code 220 | void print_safe() 221 | { 222 | //TODO:阻塞所有信号 223 | sigset_t newset, oldset; 224 | sigfillset(&newset); 225 | sigprocmask(SIG_BLOCK, &newset, &oldset); 226 | 227 | printf("safe print!\n") 228 | 229 | //TODO:恢复所有信号 230 | sigprocmask(SIG_SETMASK, &oldset, NULL); 231 | } 232 | ``` 233 | -------------------------------------------------------------------------------- /lab06/answer_template.md: -------------------------------------------------------------------------------- 1 | # Lab06 Assignment 2 | 3 | > 班级: 4 | > 学号: 5 | > 姓名: 6 | 7 | ## 1. 请简述信号什么时候处于未决状态,并简述信号存在未决状态的作用 8 | 9 | ## 2. 若在某信号的信号处理函数中给自己发送同一个信号,请简单描述程序的行为 10 | 11 | ## 3. 编写程序实现如下功能 12 | 13 | 程序 A.c 通过 sigqueue()函数按用户输入向程序 B.c 发送信号和附加数据;B.c 程序接收到该信号后,输出伴随信号的附加数据。运行过程如下: 14 | 15 | ```sh 16 | ./B & # 此时,输出进程 B 的 PID 号。 17 | ./A processB_PID sigvalue # 第一个参数表示进程 B 的 PID,第二个参数为伴随信号的附加数据(int 值即可)。 18 | ``` 19 | 20 | ```c 21 | //code of A 22 | 23 | ``` 24 | 25 | ```c 26 | //code of B 27 | 28 | ``` 29 | 30 | 截图: 31 | 32 | ## 4. 请实现这样一个程序 33 | 34 | 程序每间隔 1 秒输出你的学号,当按下 ctrl+c 后,程序询问是否退出程序(此时停止输出学号),输入 Y 或 5 秒未进行任何输入则退出程序,输入 N 程序恢复运行,继续输出学号(提示:alarm()函数设置超时时间,SIGALRM 信号处理函数作为超时处理)。 35 | 36 | ```c 37 | //code 38 | 39 | ``` 40 | 41 | ## 5. 请实现这样一个程序 42 | 43 | 在程序中创建一个子进程,通过信号实现父子进程交替输出,父进程输出学号,子进程输出姓名,要求父进程先输出。 44 | 45 | ```c 46 | //code 47 | 48 | ``` 49 | 50 | 截图: 51 | 52 | ## 6. 父子进程 53 | 54 | 父进程等待子进程退出通常仅需调用 wait()函数,但如果子进程未退出,父进程将会一直处于阻塞态,并通过循环不断获取子进程状态,该回收子进程的方式是对 CPU 资源的浪费。子进程终止时会自动向父进程发送 SIGCHLD 信号,请通过该特性实现这样一个程序:父进程创建 5 个子进程,每个子进程输出 PID 后以不同的状态值退出,父进程使用 SIGCHLD 信号实现异步回收子进程,每回收一个子进程就输出该子进程的 PID 和退出状态值,需要保证任何情况下所有子进程都能回收(提示:SIGCHLD 是不可靠信号,不支持排队,考虑两个子进程同时结束的情况) 55 | 56 | ```c 57 | //code 58 | 59 | ``` 60 | 61 | 截图: 62 | 63 | ## 7. 异步信号安全函数 64 | 65 | 异步信号安全函数(async-signal-safe function)是可以在信号处理函数中安全调用的函数,即一个函数在返回前被信号中断,并在信号处理函数中再次被调用,均可以得到正确结果。通常情况下,不可重入函数(non-reentrant function)都不是异步信号安全函数,都不应该在信号处理函数中调用。 66 | 67 | 1. 请判断下面的函数是否是异步信号安全函数,如果是请说明理由,如果不是请给出一种可能发生问题的情况。 68 | 69 | ```c 70 | int tmp; 71 | void swap1(int* x, int* y) 72 | { 73 | tmp = *x; 74 | *x = *y; 75 | *y = tmp; 76 | } 77 | ``` 78 | 79 | ```c 80 | void swap2(int* x, int* y) 81 | { 82 | int tmp; 83 | tmp = *x; 84 | *x = *y; 85 | *y = tmp; 86 | } 87 | ``` 88 | 89 | 2. 由于 printf()函数使用全局缓冲区,因此它不是异步信号安全函数。为了避免可能发生的问题,其中一个解决方法是在调用 printf()函数前阻塞所有信号,并在调用后恢复。请用上述思路补全代码,实现 printf()的异步信号安全版本,无需实现格式化输出(提示:sigprocmask()函数可用于阻塞多个信号)。 90 | 91 | ```c 92 | //code 93 | void print_safe() 94 | { 95 | //TODO:阻塞所有信号 96 | printf("safe print!\n") 97 | //TODO:恢复所有信号 98 | } 99 | ``` 100 | 101 | ## 8. 实验感想 102 | -------------------------------------------------------------------------------- /lab06/img/signal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab06/img/signal.png -------------------------------------------------------------------------------- /lab06/lab06.md: -------------------------------------------------------------------------------- 1 | # Lab06: 信号及信号处理 2 | 3 | [TOC] 4 | 5 | ## 1. 实验目的 6 | 7 | - 掌握信号基本概念。 8 | - 学习屏蔽、捕获信号的方法等与信号有关的基本操作。 9 | - 理解可重入函数、异步信号安全函数的概念。 10 | 11 | ## 2. 实验内容 12 | 13 | - 信号的含义 14 | - 信号的分类 15 | - 信号的产生 16 | - 信号捕获 17 | - 信号的屏蔽 18 | 19 | ## 3. 实验指南 20 | 21 | ### 3.1. 信号 22 | 23 | **详见人民邮电出版社《Linux 编程基础》第 6 章** 24 | 25 | #### 3.1.1. 信号的含义 26 | 27 | 软中断信号(signal,又简称为信号)用来通知进程发生了异步事件。在软件层次上是对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是进程间通信机制中唯一的异步通信机制,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。进程之间可 以互相通过系统调用 kill 发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。信号机制除了基本通知功能外,还可以传递附加信息。 28 | 29 | #### 3.1.2. 信号的分类 30 | 31 | 可以使用`kill -l`命令查看当前系统支持的所有信号: 32 | 33 | ![image-signal](img/signal.png) 34 | 35 | 信号值小于 SIGRTMIN(<=34)的信号都是不可靠信号。它的主要问题是信号可能丢失。 信号值位于 SIGRTMIN 和 SIGRTMAX 之间的信号都是可靠信号,这些信号支持排队,不会丢失。 36 | 37 | #### 3.1.3. 信号的产生 38 | 39 | - 键盘事件:ctrl+c ctrl+\ ctrl+Z 等 40 | 41 | - 非法内存:如果内存管理出错,系统就会发送一个信号进行处理 42 | 43 | - 硬件检测到异常:如段错误,除 0,总线错误等 44 | 45 | - 环境切换:比如说从用户态切换到其他态,状态的改变也会发送一个信号,这个信号会告知给系统 46 | 47 | - 系统调用:如调用`kill`,`raise`,`sigsend` ,`sigqueue`函数等,这些函数的使用具体请参见《Linux 编程基础》的 6.1 节。 48 | 49 | #### 3.1.4. 信号处理 50 | 51 | 进程可以通过三种方式响应信号: 52 | 53 | - 接受默认处理 54 | - 忽略信号(某些信号不能被忽略,如 SIGKILL 和 SIGSTOP) 55 | - 捕捉信号并执行信号处理程序 56 | 57 | ### 3.2. 信号操作 58 | 59 | #### 3.2.1. 信号的发送 60 | 61 | - kill()函数 62 | 63 | 系统调用中用于发送信号的函数有 kill(), raise(), abort() 等,其中 kill()是最常用的函数,该函数用于给指定进程发送信号,该函数的声明如下: 64 | 65 | ```c 66 | #include 67 | 68 | int kill(pid_t pid, int sig); 69 | //第一个参数pid代表接受信号的进程PID,第二个参数代表要发送的信号 70 | ``` 71 | 72 | 参数 pid 会影响 kill()函数的作用,取值分为以下四种情况 73 | 74 | 1. 若 pid>0,则发送信号 sig 给进程号为 pid 的进程。 75 | 76 | 2. 若 pid=0,则发送信号 sig 给当前进程所属进程组的所有进程。 77 | 78 | 3. 若 pid=-1,则发送信号 sig 给除 1 号进程和当前进程外的所有进程。 79 | 80 | 4. 若 pid<-1,则发送信号 sig 给属于进程组 pid 的所有进程。 81 | 82 | - sigqueue() 函数 83 | 84 | sigqueue() 函数同样用于发送信号,与 kill() 不同的是,sigqueue()函数支持发送信号的同时传递参数,需要配合 sigaction()函数一起使用。sigqueue() 函数的声明如下: 85 | 86 | ```c 87 | #include 88 | 89 | int sigqueue(pid_t pid, int sig, const union sigval value); 90 | //第一个参数pid代表接受信号的进程PID,第二个参数代表要发送的信号,第三个参数用于指定传递的数据 91 | ``` 92 | 93 | 参数 value 用于指定伴随信号传递的数据,为 sigval 联合体,该联合体的定义如下: 94 | 95 | ```c 96 | union sigval { 97 | int sival_int; 98 | void *sival_ptr; 99 | }; 100 | ``` 101 | 102 | 如果进程需要接收信号和附加数据,定义信号处理函数时首先应该接收三个参数: 103 | 104 | ```c 105 | void handler(int sig, siginfo_t *info, void *ucontext) 106 | { 107 | ... 108 | } 109 | // 第一个参数sig代表接收信号的值 110 | // 第二个参数info是指向siginfo_t类型的指针,包含了有关信号的附加信息 111 | // 第三个参数ucontext是内核保存在用户空间的信号上下文,一般不使用该参数 112 | ``` 113 | 114 | 此时如果接收进程使用 sigaction() 注册信号处理函数,并将 sa_flags 字段置为 SA_SIGINFO,那么在信号处理函数中可以通过 info 参数的`si_value`获取到发送信号伴随的数据,如`info->si_value.sival_int`或`info->si_value.sival_ptr`。 115 | 116 | ​ 具体可以参考 [sigqueue(3) — Linux manual page](https://man7.org/linux/man-pages/man3/sigqueue.3.html) 和 [sigaction(2) — Linux manual page](https://man7.org/linux/man-pages/man2/sigaction.2.html)。 117 | 118 | #### 3.2.2. 信号的捕捉 119 | 120 | 若进程捕捉某信号后,想要让其执行非默认的处理函数,则需要为该信号注册信号处理函数。进程的信号是在内核态下处理的,内核为每个进程准备了一个信号向量表,其中记录了每个信号所对应的信号处理函数。Linux 系统为用户提供了两个捕捉信号的函数,即 `signal()` 和 `sigaction()` 两个函数。 121 | 122 | ```c 123 | #include 124 | 125 | typedef void (*sighandler_t)(int); 126 | sighandler_t signal(int signum,sighandler_t handler); 127 | 128 | //第一个参数表示信号编号,第二个参数一般表示信号处理函数的函数指针,除此之外还可以为SIG_IGN和SIG_DEL 129 | ``` 130 | 131 | ```c 132 | #include 133 | 134 | int sigaction(int signum,const struct sigaction* act,const struct sigaction* oldact); 135 | 136 | //第一个参数表示信号编号,第二个为传入参数,包含自定义处理函数和其他信息,第三个参数为传出参数,包含旧处理函数等信息 137 | ``` 138 | 139 | #### 3.2.3. 信号的屏蔽 140 | 141 | 信号屏蔽机制是用于解决常规信号不可靠这一问题。在进程的 PCB 中存在两个信号集,分别为信号掩码和未决信号集。两个信号集实质上都是位图,其中每一位对应一个信号,若信号掩码某一位为 1,则其对应的信号会被屏蔽,进入阻塞状态,此时内核会修改未决信号集中该信号对应的位为 1,表示信号处于未决状态,之后除非信号被解除屏蔽,否则内核不会再向该进程发送该信号。 142 | 143 | 信号集设定函数: 144 | 145 | - `sigemptyset()`——将指定信号集清 0 146 | 147 | - `sigfillset()`——将指定信号集置 1 148 | 149 | - `sigaddset()`——将某信号加入指定信号集 150 | 151 | - `sigdelset()`——将某信号从信号集中删除 152 | 153 | - `sigismember()`——判断某信号是否已被加入指定信号集 154 | 155 | 信号集函数: 156 | 157 | ```c 158 | #include 159 | 160 | int sigprocmask(int how,const sigset_t* set,sigset_t* oldset); 161 | 162 | //第一个参数用于设置位操作方式,第二个参数一般为用户指定信号集,第三个参数用于保存原信号集 163 | //how=SIG_BLOCK:mask=mask|set 164 | //how=SIG_UNBLOCK:mask=mask&~set 165 | //how=SIG_SETMASK:mask=set 166 | ``` 167 | 168 | #### 3.2.4. 定时信号 169 | 170 | Linux 下的 `alarm()` 函数可以用来设置闹钟,该函数的原型为: 171 | 172 | ```c 173 | #include 174 | unsigned int alarm(unsigned int seconds); 175 | //第一个参数seconds用来指明时间,经过seconds秒后发送SIGALRM信号给当前进程,当参数为0则取消之前的闹钟 176 | ``` 177 | 178 | 返回值: 179 | 180 | - 如果本次调用前已有正在运行的闹钟,alarm()函数返回前一个闹钟的剩余秒数 181 | - 如果本次调用前无正在运行的闹钟,alarm()函数返回 0 182 | 183 | Linux 系统中 sleep()函数内部使用 nanosleep()函数实现,该函数与信号无关;而其他系统中可能使用 alarm()和 pause()函数实现,此时不应该混用 alarm()和 sleep()。 184 | 185 | #### 3.2.5. 计时器 186 | 187 | Linux 下的 `setitimer()` 和 `getitimer()` 系统调用可以用于访问和设置计时器,计时器在初次经过设定的时间后发出信号,也可以设置为每间隔相同的时间发出信号,该函数的原型为: 188 | 189 | ```c 190 | #include 191 | 192 | int getitimer(int which, struct itimerval *curr_value); 193 | int setitimer(int which, const struct itimerval *restrict new_value, 194 | struct itimerval *restrict old_value); 195 | ``` 196 | 197 | 通过指定 which 参数,可以设置不同的计时器,不同的计时器触发后也会发出不同的信号,一个进程同时只能有一种计时器: 198 | 199 | - ITIMER_REAL:真实计时器,计算程序运行的真实时间(墙钟时间),产生 SIGALRM 信号 200 | - ITIMER_VIRTUAL:虚拟计时器,计算当前进程处于用户态的 cpu 时间,产生 SIGVTALRM 信号 201 | - ITIMER_PROF:使用计时器,计算当前进程处于用户态和内核态的 cpu 时间,产生 SIGPROF 信号 202 | 203 | 计时器的值有以下结构体定义: 204 | 205 | ```c 206 | struct itimerval { 207 | struct timeval it_interval; //定期触发的间隔 208 | struct timeval it_value; //初次触发时间 209 | }; 210 | 211 | struct timeval { 212 | time_t tv_sec; //秒 213 | suseconds_t tv_usec; //微秒 214 | }; 215 | ``` 216 | 217 | 若 new_value.it_value 的两个字段不全为 0,则定时器初次将会在设定的时间后触发;若 new_value.it_value 的两个字段全为 0,则计时器不工作。 218 | 219 | 若 new_value.it_interval 的两个字段不全为 0,则定时器将会在初次触发后按设定的时间间隔触发;若 new_value.it_interval 的两个字段全为 0,则计时器仅初次触发一次。 220 | 221 | setitimer()函数和 alarm()函数共享同一个计时器,因此不应同时使用。 222 | 223 | ## 4. 实验习题 224 | 225 | - 请简述信号什么时候处于未决状态,并简述信号存在未决状态的作用。 226 | - 若在某信号的信号处理函数中给自己发送同一个信号,请简单描述程序的行为。 227 | - 编写程序实现如下功能:程序 A.c 通过 sigqueue()函数按用户输入向程序 B.c 发送信号和附加数据;B.c 程序接收到该信号后,输出伴随信号的附加数据。运行过程如下: 228 | 229 | ```sh 230 | ./B & # 此时,输出进程 B 的 PID 号。 231 | ./A processB_PID sigvalue # 第一个参数表示进程 B 的 PID,第二个参数为伴随信号的附加数据(int 值即可)。 232 | ``` 233 | 234 | - 请实现这样一个程序:程序每间隔 1 秒输出你的学号,当按下 ctrl+c 后,程序询问是否退出程序(此时停止输出学号),输入 Y 或 5 秒未进行任何输入则退出程序,输入 N 程序恢复运行,继续输出学号(提示:alarm()函数设置超时时间,SIGALRM 信号处理函数作为超时处理)。 235 | - 请实现这样一个程序:在程序中创建一个子进程,通过信号实现父子进程交替输出,父进程输出学号,子进程输出姓名,要求父进程先输出。 236 | - 父进程等待子进程退出通常仅需调用 wait()函数,但如果子进程未退出,父进程将会一直处于阻塞态,并通过循环不断获取子进程状态,该回收子进程的方式是对 CPU 资源的浪费。子进程终止时会自动向父进程发送 SIGCHLD 信号,请通过该特性实现这样一个程序:父进程创建 5 个子进程,每个子进程输出 PID 后以不同的状态值退出,父进程使用 SIGCHLD 信号实现异步回收子进程,每回收一个子进程就输出该子进程的 PID 和退出状态值,需要保证任何情况下所有子进程都能回收(提示:SIGCHLD 是不可靠信号,不支持排队,考虑两个子进程同时结束的情况)。 237 | - 异步信号安全函数(async-signal-safe function)是可以在信号处理函数中安全调用的函数,即一个函数在返回前被信号中断,并在信号处理函数中再次被调用,均可以得到正确结果。通常情况下,不可重入函数(non-reentrant function)都不是异步信号安全函数,都不应该在信号处理函数中调用。 238 | 239 | 1. 请判断下面的函数是否是异步信号安全函数,如果是请说明理由,如果不是请给出一种可能发生问题的情况。 240 | 241 | ```c 242 | int tmp; 243 | void swap1(int* x, int* y) 244 | { 245 | tmp = *x; 246 | *x = *y; 247 | *y = tmp; 248 | } 249 | ``` 250 | 251 | ```c 252 | void swap2(int* x, int* y) 253 | { 254 | int tmp; 255 | tmp = *x; 256 | *x = *y; 257 | *y = tmp; 258 | } 259 | ``` 260 | 261 | 2. 由于 printf()函数使用全局缓冲区,因此它不是异步信号安全函数。为了避免可能发生的问题,其中一个解决方法是在调用 printf()函数前阻塞所有信号,并在调用后恢复。请用上述思路补全代码,实现 printf()的异步信号安全版本,无需实现格式化输出(提示:sigprocmask()函数可用于阻塞多个信号)。 262 | 263 | ```c 264 | void print_safe() 265 | { 266 | //TODO:阻塞所有信号 267 | printf("safe print!\n") 268 | //TODO:恢复所有信号 269 | } 270 | ``` 271 | -------------------------------------------------------------------------------- /lab07/answer.md: -------------------------------------------------------------------------------- 1 | # Lab07 Assignment 参考答案 2 | 3 | > 班级: 4 | > 学号: 5 | > 姓名: 6 | 7 | ## 1. 请实现这样一个程序 8 | 9 | 请实现这样一个程序:客户端进程(Client)和服务器进程(Server)通过**消息队列**进行通信,消息队列共有两个,Up 和 Down,如下图所示: 10 | 11 | image-20210601103231089 12 | 13 | 客户端进程接受用户从终端的输入,并通过 Up 消息队列将消息传递给服务器进程,然后等待服务器进程从 Down 消息队列传回消息。服务器进程从 Up 接收到消息后将大小写字母转换,并通过 Down 传回给客户端进程,客户端随后输出转换后的消息。(例如:客户端通过 Up 发送'linuX', 将从 Down 接收到'LINUx')。多个客户端同时使用 Up 和 Down 消息队列时也应该能够正常工作,因此需要使用消息类型 mtype 区分来自不同客户端的消息。要求程序输出如下的效果: 14 | 15 | ![image-20210601114417916](img/2.png) 16 | 17 | ```c 18 | //client code 19 | #ifndef MSGQUE_EXAMP 20 | #define MSGQUE_EXAMP 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #define MAX_TEXT 512 29 | #define MSG_KEY_UP 335 30 | #define MSG_KEY_DOWN 336 31 | 32 | struct my_msg_st 33 | { 34 | long my_msg_type; 35 | char text[MAX_TEXT]; 36 | }; 37 | #endif 38 | 39 | int main(){ 40 | int msgid_up,msgid_down; 41 | msgid_up = msgget((key_t)MSG_KEY_UP, IPC_CREAT|0660); 42 | msgid_down = msgget((key_t)MSG_KEY_DOWN, IPC_CREAT|0660); 43 | if(msgid_down == -1 || msgid_up == -1){ 44 | perror("get message queue failed "); 45 | return -1; 46 | } 47 | 48 | while(1) { 49 | struct my_msg_st snd_data,rcv_data; 50 | snd_data.my_msg_type=getpid(); 51 | printf("Enter some text:"); 52 | fgets(snd_data.text,MAX_TEXT,stdin); 53 | if (msgsnd(msgid_up, (void *)&snd_data, MAX_TEXT, 0) == -1){ 54 | perror("msgsnd failed "); 55 | return -1; 56 | } 57 | if (msgrcv(msgid_down,(void *)&rcv_data, MAX_TEXT,getpid(),0) == -1){ 58 | perror("msgrcv failed "); 59 | return -1; 60 | } 61 | printf("Receive converted message:%s\n\n",rcv_data.text); 62 | } 63 | exit(0); 64 | } 65 | ``` 66 | 67 | ```c 68 | //server code 69 | #ifndef MSGQUE_EXAMP 70 | #define MSGQUE_EXAMP 71 | #include 72 | #include 73 | #include 74 | #include 75 | #include 76 | #include 77 | #include 78 | #define MAX_TEXT 512 79 | #define MSG_KEY_UP 335 80 | #define MSG_KEY_DOWN 336 81 | 82 | struct my_msg_st 83 | { 84 | long my_msg_type; 85 | char text[MAX_TEXT]; 86 | }; 87 | #endif 88 | 89 | int main(int argc,char **argv) { 90 | int msgid_up,msgid_down; 91 | msgid_up = msgget((key_t)MSG_KEY_UP, IPC_CREAT|0660); 92 | msgid_down = msgget((key_t)MSG_KEY_DOWN, IPC_CREAT|0660); 93 | if(msgid_down == -1 || msgid_up == -1){ 94 | perror("get message queue failed "); 95 | return -1; 96 | } 97 | 98 | while (1) { 99 | struct my_msg_st snd_data,rcv_data; 100 | if (msgrcv(msgid_up,(void *)&rcv_data, MAX_TEXT,0,0) == -1){ 101 | perror("msgrcv failed "); 102 | return -1; 103 | } 104 | char tmp[MAX_TEXT]; 105 | strcpy(tmp,rcv_data.text); 106 | 107 | for(int i=0;i='A' && tmp[i]<='Z') 109 | tmp[i]+=32; 110 | else if(tmp[i]>='a' && tmp[i]<='z') 111 | tmp[i]-=32; 112 | } 113 | 114 | strcpy(snd_data.text,tmp); 115 | snd_data.my_msg_type=rcv_data.my_msg_type; 116 | if (msgsnd(msgid_down, (void *)&snd_data, MAX_TEXT, 0) == -1){ 117 | perror("msgsnd failed "); 118 | return -1; 119 | } 120 | } 121 | } 122 | ``` 123 | 124 | ## 2. 请实现这样一个程序 125 | 126 | 请实现这样一个程序:一个进程创建 3 个子进程,每个子进程都打印你的学号,但要求每个进程都打印完这一位数字后,才能有进程开始下一位数字的打印。例如,我的学号是`18373455`,那么输出结果应该是`111888333777333444555555`。仅允许使用**信号量**作为同步工具。 127 | 128 | ```c 129 | //code 130 | #include 131 | #include 132 | #include 133 | #include 134 | #include 135 | #include 136 | 137 | 138 | char SEM_NAME1[]= "process1"; 139 | char SEM_NAME2[]= "process2"; 140 | char SEM_NAME3[]= "process3"; 141 | char id[]="18373455"; 142 | 143 | int main(int argc,char **argv) { 144 | pid_t pid; 145 | sem_t *sem1,*sem2,*sem3; 146 | int i,j; 147 | sem1 = sem_open(SEM_NAME1,O_CREAT,0777,1); 148 | sem2 = sem_open(SEM_NAME2,O_CREAT,0777,0); 149 | sem3 = sem_open(SEM_NAME3,O_CREAT,0777,0); 150 | 151 | if(sem1 == SEM_FAILED||sem2==SEM_FAILED||sem3==SEM_FAILED) { 152 | perror("unable to execute semaphore"); 153 | sem_close(sem1); 154 | sem_close(sem2); 155 | sem_close(sem3); 156 | exit(-1); 157 | } 158 | 159 | for(i=0;i<3;i++){ 160 | pid=fork(); 161 | if(pid==0)break; 162 | } 163 | 164 | if(i==0){ 165 | for(j=0;j<8;j++){ 166 | sem_wait(sem1); 167 | printf("%c",id[j]);fflush(stdout); 168 | sem_post(sem2); 169 | } 170 | exit(0); 171 | } 172 | else if(i==1){ 173 | for(j=0;j<8;j++){ 174 | sem_wait(sem2); 175 | printf("%c",id[j]);fflush(stdout); 176 | sem_post(sem3); 177 | } 178 | exit(0); 179 | } 180 | else if(i==2){ 181 | for(j=0;j<8;j++){ 182 | sem_wait(sem3); 183 | printf("%c",id[j]);fflush(stdout); 184 | sem_post(sem1); 185 | } 186 | exit(0); 187 | } 188 | else{ 189 | for(j=0;j<3;j++){ 190 | wait(0); 191 | } 192 | 193 | } 194 | 195 | sem_close(sem1); 196 | sem_close(sem2); 197 | sem_close(sem3); 198 | sem_unlink(SEM_NAME1); 199 | sem_unlink(SEM_NAME2); 200 | sem_unlink(SEM_NAME3); 201 | exit(0); 202 | } 203 | ``` 204 | 205 | ## 3. 请实现这样一个程序 206 | 207 | 在《Linux 编程基础》一书对共享内存的讲解中,其给出的例子是一个进程向共享内存写,然后终止,然后再启动一个进程从共享内存中读。请实现这样一个程序:同时使用**信号量**和**共享内存**实现一个这样的功能,同时运行两个进程,一个进程向共享内存中写入数据后阻塞,等待另一个进程读,再写,然后再读。要求程序输出如下的效果: 208 | 209 | image-20200514114549245 210 | 211 | 一共要求输出 10 组,30 行,`read`行之后有一空行,以便于明显区分组别;`write`和`read`后面的数字请不要显示明显的规律性,请使用`rand()`函数获取,并一定在调用`rand()`函数之前,使用`srand(unsigned int seed)`重置随机种子,其中,`seed`为你的学号。 212 | 213 | ```c 214 | //code 215 | #include 216 | #include 217 | #include 218 | #include 219 | #include 220 | #include 221 | #include 222 | #include 223 | #include 224 | 225 | union semu{ 226 | int val; 227 | struct semid_ds* buf; 228 | unsigned short* array; 229 | struct seminfo* _buf; 230 | }; 231 | 232 | int set_semvalue(int s_id,int index,int value){ 233 | union semu su; 234 | su.val=value; 235 | if(semctl(s_id,index,SETVAL,su)==-1) 236 | return 0; 237 | return 1; 238 | } 239 | int P(int s_id,int index){ 240 | struct sembuf ss; 241 | ss.sem_num=index; 242 | ss.sem_op=-1; 243 | ss.sem_flg=SEM_UNDO; 244 | if(semop(s_id,&ss,1)==-1){ 245 | perror("P error!"); 246 | return 0; 247 | } 248 | return 1; 249 | } 250 | int V(int s_id,int index){ 251 | struct sembuf ss; 252 | ss.sem_num=index; 253 | ss.sem_op=1; 254 | ss.sem_flg=SEM_UNDO; 255 | if(semop(s_id,&ss,1)==-1){ 256 | perror("V error!"); 257 | return 0; 258 | } 259 | return 1; 260 | } 261 | int delete_sem(int s_id){ 262 | union semu su; 263 | if(semctl(s_id,0,IPC_RMID,su)==-1||semctl(s_id,1,IPC_RMID,su)==-1) 264 | return 0; 265 | return 1; 266 | } 267 | 268 | int main(int argc, char const *argv[]) 269 | { 270 | int shm_id; 271 | struct shmid_ds buf; 272 | key_t key=ftok("./",0); 273 | int* smap; 274 | if(key==-1){ 275 | perror("ftok error!"); 276 | return -1; 277 | } 278 | shm_id=shmget(key,sizeof(int),0664|IPC_CREAT); 279 | if(shm_id==-1){ 280 | perror("shmget error!"); 281 | return -1; 282 | } 283 | smap=(int*)shmat(shm_id,NULL,0); 284 | 285 | int sem=semget(key,2,0664|IPC_CREAT); 286 | if(sem==-1){ 287 | perror("semget error!"); 288 | return -1; 289 | } 290 | if(!(set_semvalue(sem,0,1)&&set_semvalue(sem,1,0))){ 291 | perror("sem init error!"); 292 | return -1; 293 | } 294 | 295 | if(fork()==0){ 296 | srand(18373455); 297 | int i,tmp; 298 | for(i=0;i<10;i++){ 299 | P(sem,0); 300 | tmp=rand(); 301 | *smap=tmp; 302 | printf("Write:%d\n",tmp); 303 | V(sem,1); 304 | } 305 | } 306 | else{ 307 | int i,tmp; 308 | for(i=0;i<10;i++){ 309 | P(sem,1); 310 | tmp=*smap; 311 | printf("Read :%d\n\n",tmp); 312 | V(sem,0); 313 | } 314 | if(shmdt(smap)==-1){ 315 | perror("detach share memory error!"); 316 | return -1; 317 | } 318 | shmctl(shm_id,IPC_RMID,&buf); 319 | delete_sem(sem); 320 | } 321 | 322 | return 0; 323 | } 324 | ``` 325 | -------------------------------------------------------------------------------- /lab07/answer_template.md: -------------------------------------------------------------------------------- 1 | # Lab07 Assignment 2 | 3 | > 班级: 4 | > 学号: 5 | > 姓名: 6 | 7 | ## 1. 请实现这样一个程序 8 | 9 | 请实现这样一个程序:客户端进程(Client)和服务器进程(Server)通过**消息队列**进行通信,消息队列共有两个,Up 和 Down,如下图所示: 10 | 11 | image-20210601103231089 12 | 13 | 客户端进程接受用户从终端的输入,并通过 Up 消息队列将消息传递给服务器进程,然后等待服务器进程从 Down 消息队列传回消息。服务器进程从 Up 接收到消息后将大小写字母转换,并通过 Down 传回给客户端进程,客户端随后输出转换后的消息。(例如:客户端通过 Up 发送'linuX', 将从 Down 接收到'LINUx')。多个客户端同时使用 Up 和 Down 消息队列时也应该能够正常工作,因此需要使用消息类型 mtype 区分来自不同客户端的消息。要求程序输出如下的效果: 14 | 15 | ![image-20210601114417916](img/2.png) 16 | 17 | ```c 18 | //code 19 | 20 | ``` 21 | 22 | 截图: 23 | 24 | ## 2. 请实现这样一个程序 25 | 26 | 请实现这样一个程序:一个进程创建 3 个子进程,每个子进程都打印你的学号,但要求每个进程都打印完这一位数字后,才能有进程开始下一位数字的打印。例如,我的学号是`18373455`,那么输出结果应该是`111888333777333444555555`。仅允许使用**信号量**作为同步工具。 27 | 28 | ```c 29 | //code 30 | 31 | ``` 32 | 33 | 截图: 34 | 35 | ## 3. 请实现这样一个程序 36 | 37 | 在《Linux 编程基础》一书对共享内存的讲解中,其给出的例子是一个进程向共享内存写,然后终止,然后再启动一个进程从共享内存中读。请实现这样一个程序:同时使用**信号量**和**共享内存**实现一个这样的功能,同时运行两个进程,一个进程向共享内存中写入数据后阻塞,等待另一个进程读,再写,然后再读。要求程序输出如下的效果: 38 | 39 | image-20200514114549245 40 | 41 | 一共要求输出 10 组,30 行,`read`行之后有一空行,以便于明显区分组别;`write`和`read`后面的数字请不要显示明显的规律性,请使用`rand()`函数获取,并一定在调用`rand()`函数之前,使用`srand(unsigned int seed)`重置随机种子,其中,`seed`为你的学号。 42 | 43 | ```c 44 | //code 45 | 46 | ``` 47 | 48 | 截图: 49 | 50 | ## 4. 实验感想 51 | -------------------------------------------------------------------------------- /lab07/img/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab07/img/1.png -------------------------------------------------------------------------------- /lab07/img/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab07/img/2.png -------------------------------------------------------------------------------- /lab07/img/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab07/img/3.png -------------------------------------------------------------------------------- /lab07/img/shm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/lab07/img/shm.png -------------------------------------------------------------------------------- /lab07/lab07.md: -------------------------------------------------------------------------------- 1 | # Lab07: 进程间通信 2 | 3 | [TOC] 4 | 5 | ## 1. 实验目的 6 | 7 | 通过实验掌握管道、信号量、共享内存、消息队列四个进程间通信的机制 8 | 9 | ## 2. 实验内容 10 | 11 | - 匿名管道 12 | - 信号量 13 | - 共享内存 14 | - 消息队列 15 | 16 | ## 3. 实验指南 17 | 18 | 这部分内容介绍的是操作系统内部的进程间的通信(Inter-Process Communication,IPC),主要包含管道通信、SystemV IPC(消息队列、信号量、共享内存等),不包含 socket 连接等系统之间的进程间的通信。 19 | 20 | 关于这部分具体的编程方法详见《Linux 编程基础》第 7 章的内容。 21 | 22 | ### 3.1 管道 23 | 24 | 管道是最基本的进程通信机制,可以想象成一个管道,两端分别连着 2 个进程,一个进程往里面写,一个进程从里面读。如果读或写管道的时候没有内容可供读或写,进程将被阻塞,直到有内容可供读写为止。 25 | 管道分为匿名管道和命名管道。 匿名管道创建后本质上是 2 个文件描述符,父子进程分别持有就能够使用管道,需要注意的是不能够共用匿名管道,也就是除了使用的进程,其他进程需要关闭文件描述符,保证管道 的 2 个描述符分别同时只有 1 个进程持有。 26 | 命名管道是根据路径来使用管道, 故能够在任意进程间通信。(仅要求掌握匿名管道,命名管道作为了解) 27 | 28 | ### 3.2 消息队列 29 | 30 | 消息队列本质上在内核空间中开辟了一块内存空间,这块内存是其他进程可以访问到的,在其中使用链表的方式实现了一个队列,进程可以向该队列中发送数据块或读取数据块,从而达到进程间通信的目的。其中每个数据块包含两部分,首先是一个类型为 long 的 type,然后是具体的数据,其中的这个 type 就可以作为进程之间相互约定好的协议,即你发送 type 为`15131049`的消息,我接收 type 为`15131049`的消息,我确认这就是你发出来的,我信任该数据块中的数据。 31 | 32 | ### 3.3 信号量 33 | 34 | 信号量这个概念非常非常重要,大家以后会经常和它打交道。信号量(**semaphore**)跟前面说的“信号”(**signal**)没啥关系,大家在理解上千万不要搞混。信号量最好理解为“信号灯”,就相当于红绿灯 🚦 一样,用来在进程遭遇“岔路口”的时候,通知进程做怎样的工作。其本质是为了实现多个进程之间的同步。 35 | 36 | 我们设想一下下面的场景。回想做基物实验的时候,大家需要“竞争预约”实验设备,其有这样几个特征: 37 | 38 | - 实验设备是有限的,并且绝大多数情况下少于需要做实验的学生数量(系统中某一项资源是有限的,并且可用数量可能少于需要使用它的进程数量) 39 | - 实验设备是“互斥的”,即实验设备是会被一个同学独占的,实验设备一旦被获取,在实验做完之前是不能被其他同学使用的——你好不容易调好了分光仪,肯定不想让其他人抢占不是?(系统中的资源是互斥的,一个进程在获取该资源之后、在释放该资源之前,是不允许其他进程使用该资源的,否则会引起错误)。 40 | 41 | 基于以上的几个特征,我们使用如下的步骤来协调大家做实验的情况: 42 | 43 | - 每个人在做实验之前,先到选课网站上把该实验设备的可用数量`-1` 44 | - 如果`-1`操作完成之后,实验设备数量`>=0`,那么正常开始实验; 45 | - 如果`-1`操作完成之后,实验设备数量`<0`,那么表明,在进行`-1`操作之前,实验设备可用数量就已经`<=0`了;并且,**此时实验设备数量的绝对值表示当前有多少同学正在等待使用该实验设备**,那么我将==等待==,有人做完实验归还设备,系统也将把我放到一个==等待队列==中; 46 | - 如果我成功抢到了实验设备并完成了实验,在实验结束后,我将归还实验设备,并将实验设备的可用数量`+1`; 47 | - 一旦有人做完了实验并归还了实验设备,系统就会从==等待队列==中选择一名同学,让其==停止等待==,开始实验。 48 | 49 | 进程对资源的竞争使用和上述学生对实验设备的竞争基本相同。信号量就是用来标明现在还有多少实验设备可用的数据结构。(注意,**信号量要说明的是可用资源的数量,信号量本身不是资源的一部分,一种资源的使用对应一个信号量**)。 50 | 51 | 对信号量的操作分为两种,一种是`P操作`,即对信号量`-1`,就是进程要使用资源之前声明“自己将占有一份资源”;还有一种是`V操作`,即对信号量`+1`,就是进程在使用完资源后归还时声明“我完成了”。 52 | 53 | #### 3.3.1 二值信号量 54 | 55 | 如果资源只有一份,那么信号量的初始值将是`1`,**最多只能有一个进程使用该资源**。这种信号量被称为“二值信号量”。这时信号量的作用就失去了指示“当前还有多少资源可用”的意义,仅仅用来标明“当前资源是否可用”,就蜕化成了“互斥锁”的作用(当然,二值信号量与互斥锁有本质的不同),其作用就类似于大家在 Java 中学到的`synchronized`(当然,`synchronized`要实现的是线程之间的同步)。 56 | 57 | ### 3.4 共享内存 58 | 59 | 共享内存这个概念很重要,是大家找工作面试的常客(因为其中涉及到虚拟内存、进程通信等知识)。共享内存的本质就是把两个或多个进程的虚拟地址映射到同一块物理内存。这样,一个进程通过对这块内存的读写就能被其他进程访问到,从而实现进程间通信的功能。 60 | 61 | image-20200513200240929 62 | 63 | ### 3.5 补充内容 64 | 65 | - 可以使用`ipcs`查看当前系统中创建的消息队列、信号量和共享内存,以及其使用情况:[ipcs 查询进程间通信状态](https://linuxtools-rst.readthedocs.io/zh_CN/latest/tool/ipcs.html)。 66 | - 在本节提到的这些函数的使用中,如果发生错误,将返回`-1`,但这么一个干巴巴的值很难明确告诉我们到底发生了什么错误。幸运的是,在这些系统调用发生错误的时候,系统会设置`errno`的值,我们只需要查看`errno`的值就能明白具体发生了什么错误。具体请参见:[linux 系统编程之错误处理:perror,strerror 和 errno](https://www.cnblogs.com/mickole/p/3181097.html)。 67 | 68 | ## 4. 实验习题 69 | 70 | 1. 请实现这样一个程序:客户端进程(Client)和服务器进程(Server)通过**消息队列**进行通信,消息队列共有两个,Up 和 Down,如下图所示: 71 | 72 | image-20210601103231089 73 | 74 | 客户端进程接受用户从终端的输入,并通过 Up 消息队列将消息传递给服务器进程,然后等待服务器进程从 Down 消息队列传回消息。服务器进程从 Up 接收到消息后将大小写字母转换,并通过 Down 传回给客户端进程,客户端随后输出转换后的消息。(例如:客户端通过 Up 发送'linuX', 将从 Down 接收到'LINUx')。多个客户端同时使用 Up 和 Down 消息队列时也应该能够正常工作,因此需要使用消息类型 mtype 区分来自不同客户端的消息。要求程序输出如下的效果: 75 | 76 | ![image-20210601114417916](img/2.png) 77 | 78 | 2. 请实现这样一个程序:一个进程创建 3 个子进程,每个子进程都打印你的学号,但要求每个进程都打印完这一位数字后,才能有进程开始下一位数字的打印。例如,我的学号是`18373455`,那么输出结果应该是`111888333777333444555555`。仅允许使用**信号量**作为同步工具。 79 | 80 | 3. 在《Linux 编程基础》一书对共享内存的讲解中,其给出的例子是一个进程向共享内存写,然后终止,然后再启动一个进程从共享内存中读。请实现这样一个程序:同时使用**信号量**和**共享内存**实现一个这样的功能,同时运行两个进程,一个进程向共享内存中写入数据后阻塞,等待另一个进程读,再写,然后再读。要求程序输出如下的效果: 81 | 82 | image-20200514114549245 83 | 84 | 一共要求输出 10 组,30 行,`read`行之后有一空行,以便于明显区分组别;`write`和`read`后面的数字请不要显示明显的规律性,请使用`rand()`函数获取,并一定在调用`rand()`函数之前,使用`srand(unsigned int seed)`重置随机种子,其中,`seed`为你的学号。 85 | -------------------------------------------------------------------------------- /lab08/answer.md: -------------------------------------------------------------------------------- 1 | # Lab08 Assignment 2 | 3 | ## 1. 创建一个线程,分别打印该线程和原来的线程的进程号和父进程号,然后回收刚刚创建的线程 4 | 5 | ```c 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | void *create(void *arg){ 13 | printf("new:%d %d\n",getpid(),getppid()); 14 | } 15 | 16 | int main(){ 17 | pthread_t tidp; 18 | int error=pthread_create(&tidp,NULL,create,NULL); 19 | if(error){ 20 | printf("failed!"); 21 | return 0; 22 | } 23 | printf("old:%d %d\n",getpid(),getppid()); 24 | pthread_join(tidp,NULL); 25 | return 0; 26 | } 27 | ``` 28 | 29 | ## 2. 仅使用锁来实现两个线程的同步,让线程 a 不断地为公共变量 Num 增加 1,而当 Num 增加至 100 时线程 b 将 Num 归 0,不断重复上述过程。**将 Num 输出到屏幕并给出代码和操作步骤。** 30 | 31 | ```c 32 | #include 33 | #include 34 | #include 35 | 36 | #include 37 | #include 38 | #include 39 | #include 40 | #include 41 | #include 42 | #include 43 | #include 44 | #include 45 | 46 | int num = 0; 47 | pthread_mutex_t mutex; 48 | 49 | void *newPthread_A() { 50 | while (1) { 51 | pthread_mutex_lock(&mutex); 52 | if (num < 100) { 53 | num = num + 1; 54 | printf ("A: %d\n", num); 55 | } 56 | pthread_mutex_unlock(&mutex); 57 | } 58 | } 59 | 60 | void *newPthread_B() { 61 | while (1) { 62 | pthread_mutex_lock(&mutex); 63 | if (num >= 100) { 64 | num = 0; 65 | printf ("B: %d\n", num); 66 | } 67 | pthread_mutex_unlock(&mutex); 68 | } 69 | } 70 | 71 | int main() { 72 | pthread_t tidp[2]; 73 | pthread_mutex_init(&mutex, NULL); 74 | pthread_create(&tidp[0], NULL, newPthread_A, NULL); 75 | pthread_create(&tidp[1], NULL, newPthread_B, NULL); 76 | pthread_join(tidp[0], NULL); 77 | pthread_join(tidp[1], NULL); 78 | pthread_mutex_destroy(&mutex); 79 | return 0; 80 | } 81 | ``` 82 | 83 | ## 3. 在实际过程中,条件变量和互斥锁总是结合使用,因为互斥锁只能表示锁与不锁两种状态,而仅靠条件变量本身也是无法实现线程同步的。条件变量允许线程以无竞争的方式进行等待直到某条件发生,而不是总是尝试去获取锁。使用条件变量和锁重写上一问题,**给出代码和操作步骤** 84 | 85 | ```c 86 | #include 87 | #include 88 | #include 89 | 90 | #include 91 | #include 92 | #include 93 | #include 94 | #include 95 | #include 96 | #include 97 | #include 98 | #include 99 | 100 | int num = 0; 101 | pthread_mutex_t mutex; 102 | pthread_cond_t cond; 103 | 104 | void *newPthread_A() { 105 | while (1) { 106 | pthread_mutex_lock(&mutex); 107 | if (num >= 100) 108 | pthread_cond_signal(&cond); 109 | else 110 | printf ("A: %d\n", ++num); 111 | pthread_mutex_unlock(&mutex); 112 | } 113 | } 114 | 115 | void *newPthread_B() { 116 | while (1) { 117 | pthread_mutex_lock(&mutex); 118 | if (num >= 100) 119 | printf ("B: %d\n", num = 0); 120 | else 121 | pthread_cond_wait(&cond, &mutex); 122 | pthread_mutex_unlock(&mutex); 123 | } 124 | } 125 | 126 | int main() { 127 | pthread_t tidp[2]; 128 | pthread_mutex_init(&mutex, NULL); 129 | pthread_cond_init(&cond, NULL); 130 | pthread_create(&tidp[0], NULL, newPthread_A, NULL); 131 | pthread_create(&tidp[1], NULL, newPthread_B, NULL); 132 | pthread_join(tidp[0], NULL); 133 | pthread_join(tidp[1], NULL); 134 | pthread_mutex_destroy(&mutex); 135 | pthread_cond_destroy(&cond); 136 | return 0; 137 | } 138 | ``` 139 | 140 | 这两题主要是希望大家体会条件变量的优势,注意线程 A 是每次将 num+1 而不是使用 while 直接将 num 从 0 加到 100 141 | 142 | ## 4. 在一个程序中创建 8 个线程,每个线程打印你的学号中的一位数字,按照学号顺序打印出来。**不要使用`sleep`来进行同步,给出代码和操作步骤** 143 | 144 | ```c 145 | #include 146 | #include 147 | #include 148 | 149 | #include 150 | #include 151 | #include 152 | #include 153 | #include 154 | #include 155 | #include 156 | #include 157 | #include 158 | 159 | #define LENGTH 8 160 | const char *stuID = "12345678"; 161 | 162 | char *str[LENGTH]; 163 | sem_t *sems[LENGTH]; 164 | pthread_t tidp[LENGTH]; 165 | 166 | void *print(void *args) { 167 | int index = *((int*)args); 168 | sem_wait(sems[index]); 169 | printf("%c", stuID[index]); fflush(stdout); 170 | sem_post(sems[(index + 1) % LENGTH]); 171 | } 172 | 173 | int* state(int x) { 174 | int *res = malloc(sizeof (int)); 175 | *res = x; 176 | return res; 177 | } 178 | 179 | int main() { 180 | // init 181 | for (int i = 0;i < LENGTH; ++i) { 182 | str[i] = malloc(sizeof(char) * 16); 183 | sprintf(str[i], "sem%d", i); 184 | sems[i] = sem_open(str[i], O_CREAT, 0777, 0); 185 | } 186 | sem_post(sems[0]); 187 | 188 | // create 189 | for (int i = 0;i < LENGTH; ++i) 190 | pthread_create(&tidp[i], NULL, print, state(i)); 191 | 192 | // close 193 | for (int i = 0;i < LENGTH; ++i) 194 | pthread_join(tidp[i], NULL); 195 | 196 | for (int i = 0;i < LENGTH; ++i) 197 | sem_close(sems[i]), 198 | sem_unlink(str[i]); 199 | return 0; 200 | } 201 | ``` 202 | 203 | **本题目你采取的同步策略是什么,为什么采用这样的策略,这是最好的同步策略吗?** 204 | 205 | 采用的是信号量,从同步策略的使用习惯而言,一般更倾向于使用锁来体现线程之间的互斥,使用信号量来体现线程之间的同步。互斥突出的是资源的唯一性和排他性,但其本身通常无法限制访问者(本体中为线程)对资源的访问顺序。同步则是在互斥的基础上通过其他机制实现访问者对资源的有序访问。 206 | 207 | 希望大家能够通过以上三题体会锁、条件变量、信号量的用法和场景。 208 | 209 | ## 5.假设缓冲区上限为 20,生产者和消费者线程各 10 个,请编写程序实现一个生产者消费者模型。**在每次生产、消费时将当前动作类型(produce/consume)与缓冲区内容量输出到屏幕,给出代码和操作步骤。** 210 | 211 | 生产者消费者问题(Producer-consumer problem),也称有限缓冲问题(Bounded-buffer problem),是一个多线程同步问题的经典案例。该问题描述了共享固定大小缓冲区的两个线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。 212 | 213 | 生产者消费者问题主要要注意以下三点: 214 | 215 | - 在缓冲区为空时,消费者不能再进行消费 216 | - 在缓冲区为满时,生产者不能再进行生产 217 | - 在一个线程进行生产或消费时,其余线程不能再进行生产或消费等操作,即保持线程间的同步 218 | 219 | ```c 220 | #include 221 | #include 222 | #include 223 | 224 | #define SIZE 20 225 | #define MAX 100 226 | #define PRODUCE_THREAD_NUM 10 227 | #define CONSUME_THREAD_NUM 10 228 | struct Container{ 229 | int *data; 230 | int producePos; 231 | int consumePos; 232 | int productNumber; 233 | int size; 234 | } container; 235 | 236 | int indexGrow(int size, int i) { 237 | return ++i >= size ? 0 : 1; 238 | } 239 | 240 | void put(int item) { 241 | container.data[container.producePos] = item; 242 | container.producePos = indexGrow(container.size, container.producePos); 243 | container.productNumber++; 244 | printf("produce: %d\n", item); 245 | fflush(stdout); 246 | } 247 | 248 | int take() { 249 | int result = container.data[container.consumePos]; 250 | container.consumePos = indexGrow(container.size, container.consumePos); 251 | container.productNumber--; 252 | printf("consume: %d\n", result); 253 | fflush(stdout); 254 | return result; 255 | } 256 | 257 | int canConsume() { 258 | return container.productNumber >= 1; 259 | } 260 | 261 | int canProduce() { 262 | return container.productNumber < container.size; 263 | } 264 | 265 | int produceStop, consumeStop; 266 | 267 | int mCount; 268 | 269 | pthread_mutex_t mutex; 270 | 271 | pthread_cond_t canConsumeCond, canProduceCond; 272 | 273 | void init() { 274 | container.size = SIZE; 275 | container.data = (int *) malloc(container.size * sizeof(int)); 276 | container.consumePos = container.producePos = container.productNumber = 0; 277 | 278 | pthread_mutex_init(&mutex, 0); 279 | pthread_cond_init(&canConsumeCond, 0); 280 | pthread_cond_init(&canProduceCond, 0); 281 | produceStop = consumeStop = 0; 282 | mCount = 0; 283 | } 284 | 285 | void *product() { 286 | while(1) { 287 | pthread_mutex_lock(&mutex); 288 | while (!canProduce() && !produceStop) { 289 | pthread_cond_wait(&canProduceCond, &mutex); 290 | } 291 | if (produceStop) { 292 | pthread_mutex_unlock(&mutex); 293 | break; 294 | } 295 | int item = ++mCount; 296 | put(item); 297 | if (Item >= MAX) { 298 | produceStop = 1; 299 | } 300 | pthread_cond_signal(&canConsumeCond); 301 | pthread_mutex_unlock(&mutex); 302 | } 303 | return NULL; 304 | } 305 | 306 | void *consume() { 307 | while(1) { 308 | pthread_mutex_lock(&mutex); 309 | while (!canConsume() && !consumeStop) { 310 | pthread_cond_wait(&canConsumeCond, &mutex); 311 | } 312 | if (consumeStop) { 313 | pthread_mutex_unlock(&mutex); 314 | break; 315 | } 316 | int item = take(); 317 | if (item >= MAX) { 318 | consumeStop = 1; 319 | } 320 | pthread_cond_signal(&canProduceCond); 321 | pthread_mutex_unlock(&mutex); 322 | } 323 | return NULL; 324 | } 325 | 326 | void closeResources() { 327 | pthread_mutex_destroy(&mutex); 328 | pthread_cond_destroy(&canConsumeCond); 329 | pthread_cond_destroy(&canProduceCond); 330 | } 331 | 332 | int main() { 333 | init(); 334 | pthread_t produceThread[PRODUCE_THREAD_NUM]; 335 | pthread_t consumeThread[CONSUME_THREAD_NUM]; 336 | for (int i = 0; i < PRODUCE_THREAD_NUM; i++) { 337 | pthread_create(produceThread + i, 0, produce, 0); 338 | } 339 | for (int i = 0; i < CONSUME_THREAD_NUM; i++) { 340 | pthread_create(consumeThread + i, 0, consume, 0); 341 | } 342 | for (int i = 0; i < PRODUCE_THREAD_NUM; i++) { 343 | pthread_join(produceThread[i], 0); 344 | } 345 | for (int i = 0; i < CONSUME_THREAD_NUM; i++) { 346 | pthread_join(consumeThread[i], 0); 347 | } 348 | closeResources(); 349 | return 0; 350 | } 351 | ``` 352 | -------------------------------------------------------------------------------- /lab08/answer_template.md: -------------------------------------------------------------------------------- 1 | # Lab08 Assignment 2 | 3 | > 班级: 4 | > 学号: 5 | > 姓名: 6 | 7 | ## 1. 创建一个线程,分别打印该线程和原来的线程的进程号和父进程号,然后回收刚刚创建的线程 8 | 9 | ```c 10 | //code 11 | 12 | ``` 13 | 14 | 截图: 15 | 16 | ## 2. 仅使用锁来实现两个线程的同步,让线程 a 不断地为公共变量 Num 增加 1,而当 Num 增加至 100 时线程 b 将 Num 归 0,不断重复上述过程。**将 Num 输出到屏幕并给出代码和操作步骤。** 17 | 18 | ```c 19 | //code 20 | 21 | ``` 22 | 23 | 截图: 24 | 25 | ## 3. 在实际过程中,条件变量和互斥锁总是结合使用,因为互斥锁只能表示锁与不锁两种状态,而仅靠条件变量本身也是无法实现线程同步的。条件变量允许线程以无竞争的方式进行等待直到某条件发生,而不是总是尝试去获取锁。使用条件变量和锁重写上一问题,**给出代码和操作步骤** 26 | 27 | ```c 28 | //code 29 | 30 | ``` 31 | 32 | 截图: 33 | 34 | ## 4. 在一个程序中创建 8 个线程,每个线程打印你的学号中的一位数字,按照学号顺序打印出来。**不要使用`sleep`来进行同步,给出代码和操作步骤** 35 | 36 | ```c 37 | //code 38 | 39 | ``` 40 | 41 | 截图: 42 | 43 | **本题目你采取的同步策略是什么,为什么采用这样的策略,这是最好的同步策略吗?** 44 | 45 | ``` 46 | //ans 47 | 48 | ``` 49 | 50 | ## 5. 假设缓冲区上限为 20,生产者和消费者线程各 10 个,请编写程序实现一个生产者消费者模型。**在每次生产、消费时将当前动作类型(produce/consume)与缓冲区内容量输出到屏幕,给出代码和操作步骤。** 51 | 52 | 生产者消费者问题(Producer-consumer problem),也称有限缓冲问题(Bounded-buffer problem),是一个多线程同步问题的经典案例。该问题描述了共享固定大小缓冲区的两个线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。 53 | 54 | 生产者消费者问题主要要注意以下三点: 55 | 56 | - 在缓冲区为空时,消费者不能再进行消费 57 | - 在缓冲区为满时,生产者不能再进行生产 58 | - 在一个线程进行生产或消费时,其余线程不能再进行生产或消费等操作,即保持线程间的同步 59 | 60 | ```c 61 | //code 62 | 63 | ``` 64 | 65 | 截图: 66 | -------------------------------------------------------------------------------- /lab08/lab08.md: -------------------------------------------------------------------------------- 1 | # 第十五周 实验指南:线程 2 | 3 | [TOC] 4 | 5 | ## 一、实验目的 6 | 7 | 1. 掌握与线程相关的概念。 8 | 2. 掌握创建线程、挂起线程等线程相关操作。 9 | 3. 掌握线程同步的方法。 10 | 11 | ## 二、实验指南 12 | 13 | ### 线程简介 14 | 15 | #### 线程介绍 16 | 17 | 每个线程(thread)是 CPU 使用的一个基本单元;它包括线程 ID、程序计数器、寄存器组和堆栈。它与同一进程的其他线程共享代码段、数据段和其他操作系统资源,如打开文件和信号。如果一个进程具有多个控制线程,那么它能同时执行多个任务。从这个角度来看,线程就是一个进程的执行进度。一个进程可能需要同时执行多个任务,比如一个浏览器进程,一方面要渲染页面呈现给用户,另一方面,同时还要处理网络请求,从网络中接收和发送数据。 18 | 19 | 下图生动的说明了进程和线程的关系: 20 | 21 | image-20200526114712359 22 | 23 | ##### 使用线程的动机 24 | 25 | **正如前面所述**,**一个应用程序通常作为具有多个控制线程的一个进程来实现**。例如,一个 Web 浏览器可能有一个线程来显示图像和文本,另一个线程从网络接收数据。一个字处理器(比如 Word)可能有一个线程显示文字在屏幕上,另一个线程用于相应用户键盘的输入,还有一个线程在后台进行拼写和语法的检查。应用程序也可以设计成利用多核系统的能力。这些应用程序可以在一个 CPU 的多个核心上**并行**执行多个 CPU 密集型的任务。 26 | 27 | 还有一种情况,就是**一个应用程序可能需要同时执行多个相似任务**。比如大家在软工课上学到的后端的网络编程,就是开一个服务器,接收前端传过来的`request`,然后作出处理后返回`response`。在实际使用中,这个服务器肯定会**几乎同时**接收多个(甚至是数千个)来自前端的请求。如果服务器只有一个进程的话,这些请求会被放到一个队列里,挨个处理并返回数据。那前端的用户肯定要忍受很长时间的等待。所以,现代服务器的通用做法是,每接收一个前端传过来的请求,就分配(可能是创建、也有可能是复用之前的线程)一个线程用于处理该请求。从这里也可以看到,由于对于每个请求的处理逻辑都是完全相同的(很显然),所以每个线程都是使用的同一份代码,也就是说,同一个进程中,各个线程之间的代码是共享的。 28 | 29 | **实际上,上述的两种情况完全可以只使用进程,而不使用线程来解决。**比如,谁规定一个浏览器只能有一个进程,我完全可以开多个进程来**并发**处理啊;谁规定一个服务器只能有一个进程,我完全可以每接收一个前端请求就创建一个新的进程来处理啊,不就是使用同一段代码吗,我用`fork`和`exec`族函数完全可以解决啊(实际上,早期的 cgi 技术就是这样做的)。实际上,早期的 Linux 系统中也的确没有线程这个概念。 30 | 31 | **我们在上述场景中更倾向于使用线程而不是进程,是基于以下几个线程的优点。** 32 | 33 | ##### 使用线程的好处 34 | 35 | - **响应性**。试想,如果浏览器是一个只有一个线程的进程。如果此时网络发生了延迟,请求网络数据的操作处于等待状态,那整个浏览器的所有操作都将停滞(因为只有一个线程嘛),此时你点击任何位置的按钮或图片都将得不到相应,这无疑是一个非常糟糕的体验。 36 | - **资源共享**。进程只能通过共享内存和消息传递之类的技术(消息队列、管道等)共享资源。而同一个进程的线程之间天然可以共享同一个虚拟地址空间的代码、数据、全局变量等等,这相比于进程来说,更加高效和限制更小。比如,进程之间数据的共享要通过内核来进行,用户地址空间的代码访问一次内核空间(系统调用)的代价是很大的;而且共享的数据大小还要受到内核地址空间大小的限制,非常不方便。 37 | - **经济**。进程创建所需的内存和资源分配非常昂贵。由于线程能够共享它们所属进程的资源,所以创建和切换线程更加经济。虽然进程创建和管理与线程创建和管理的开销差异的实际测量较为困难,但是前者通常比后者花费更多时间。在某些系统中,进程创建比线程创建慢 30 倍,而且进程切换要比线程切换慢 5 倍。 38 | - **可伸缩性**。对于多处理器体系结构,多线程的优点更大,因为线程可在多处理核上**并行**运行。不管有多少可用 CPU,单线程进程只能运行在一个 CPU 上。对这部分内容感兴趣的同学可以阅读这篇文章:[利用多核多线程进行程序优化](https://www.ibm.com/developerworks/cn/linux/l-cn-optimization/index.html)。 39 | 40 | ##### 并发与并行 41 | 42 | 大家可能注意到了上文中的“并发”和“并行”都被高亮了,就是为了提醒大家,**并发(concurrent)和并行(parallel)虽然看起来很相近,但这是两个不同的概念**(又是翻译的锅)。这里的 parallel 本身还有“平行”的意思,可以从这个词的含义中大致理解“并行”想表达的含义。大家可以先看看下面这张图感受一下: 43 | 44 | image-20200526214913966 45 | 46 | **并发的意思是多个任务被交替执行**,只不过切换的过程很快,**让人以为它们是在同时执行**。 47 | 48 | **并行的意思是多个任务确确实实在同时执行,**这种情况出现在多核系统上。所谓“多核”,意思就是具有多个处理器核心的计算机系统,基本上大家现在用的计算机都是这种。 49 | 50 | 因此,如果没有线程,只有进程的话(也就是只包含一个线程的进程),那么该进程只能**并发**执行,也就是说,该进程在某一时刻,只能使用一个 CPU 核心执行任务,即使你有多个处理器核心,其他几个核心也是闲置的(起码不能被当前这个进程使用)。而如果该进程有多个线程,那么就可以把这多个线程比较平均的分配到多个 CPU 核心上去**并行**执行,从而使得多核计算机的性能被充分利用(比如,你打文明的时候是不是经常看着它空置多个核心而干瞪眼?)。当然,如何合理分配线程来并行执行是一个非常困难和有挑战的事情,不然也不会有很多 3A 大作到现在还是单核游戏了。 51 | 52 | ##### 线程是最小的调度单位,进程是最小的资源分配单位 53 | 54 | 还是回到开头那幅图: 55 | 56 | image-20200526114712359 57 | 58 | 在这幅图中,我们可以看到,一个线程有自己的寄存器和堆栈,这是程序执行最基本的要素。也就是说,线程本身就是程序执行的最基本的单位,或者说,**线程本身就是一个进程的执行部分**。所谓 CPU 调度的本质就是选择合适的线程放到一个 CPU 核心上来执行。因此,在每个 CPU 核心上执行的是一个个线程,而不是进程本身(不区分线程的进程也可以看做是只有一个线程的进程)。所以,我们看到的 CPU 的宣传都是什么 6 核心 12 线程(超线程技术)、64 核心 128 线程、线程撕裂者之类的,而没有什么“进程撕裂者”、“6 核心 12 进程”之类的说法。 59 | 60 | 同一个进程的线程之间又使用相同的**IO 资源、信号、内存资源**等,这些资源都是以操作系统以进程为单位分配的。于是,进程被称为是“资源分配的最小单位”。 61 | 62 | ### 线程操作(Pthreads) 63 | 64 | 本节内容介绍的是 POSIX 中描述的线程操作的标准。POSIX 是由 IEEE 指定的一系列标准,全称是**可移植操作系统接口**(Portable Operating System Interface)。从名字上可以看出,POSIX 的理想是,提供这样一套标准,以使得使用这套标准的 API 编写的代码,可以在不怎么修改的情况下,移植到任意一个实现了这套标准的操作系统上。请注意,POSIX 本身只是对 API 的描述,操作系统如果想要兼容 POSIX 标准,就需要去实现这些 API。一般的 Unix 系统和类 Unix 系统都会去实现 POSIX 标准。(虽然严格意义上来讲,Linux 系统从来没有被官方认证为 POSIX,但不妨碍我们在事实上把它作为一个 POSIX 兼容的系统来使用)。 65 | 66 | Pthreads 就是 POSIX 标准定义的线程创建与同步的 API。许多操作系统都实现了这个线程规范,其中就包括 macOS、Solaris 等(所以,这次实验的一些代码可以在 macOS 上尝试)。Linux 也通过一些内核外的库实现了 POSIX 标准。 67 | 68 | 下面列出了几个大家需要掌握的几个函数的用法(理解即可,如果考试需要的话,会给出如下所示的函数签名),具体的用法和使用场景在《Linux 编程基础》中已经讲的比较详细了,这里就不再赘述。 69 | 70 | **编译使用了`pthread.h`的头文件的代码的时候,一定加上`-lpthread`参数。比如`gcc -lpthread example.c -o example`。** 71 | 72 | #### 创建线程 73 | 74 | ```c 75 | // pthread.h 76 | 77 | int pthread_create(pthread_t _Nullable * _Nonnull __restrict, 78 | const pthread_attr_t * _Nullable __restrict, 79 | void * _Nullable (* _Nonnull)(void * _Nullable), 80 | void * _Nullable __restrict); 81 | ``` 82 | 83 | - 第一个参数为指向线程标识符的指针,当`pthread_create`成功返回时,新创建线程的线程 ID 会被设置成`tidp`指向的内存单元,并可以通过接口函数`pthread_self`获取; 84 | 85 | - 第二个参数用来设置线程属性,通常传入 NULL,表示线程的默认属性; 86 | 87 | - 第三个参数是线程运行函数的起始地址; 88 | 89 | - 最后一个参数是运行函数的参数。 90 | 91 | #### 线程退出 92 | 93 | ```c 94 | // pthread.h 95 | 96 | // 这个函数是自己干掉自己 97 | void pthread_exit(void *ral_ptr); 98 | ``` 99 | 100 | #### 线程取消 101 | 102 | ```c 103 | // pthread.h 104 | 105 | // 这个函数是自己干掉另一个线程 106 | int pthread_cancel(pthread_t tid); 107 | ``` 108 | 109 | #### 线程挂起 110 | 111 | ```c 112 | // pthread.h 113 | 114 | int pthread_join(pthread_t thread, void **rval_ptr); 115 | ``` 116 | 117 | #### 线程分离 118 | 119 | ```c 120 | // pthread.h 121 | 122 | int pthread_detach(pthread_t tid); 123 | ``` 124 | 125 | #### Java 中的线程 126 | 127 | Java 的多线程编程很重要,大家一定要认真掌握。 128 | 129 | 大家在 OOP 上应该都学过了,Java 的执行过程是这样的:Java 源代码首先被编译成`.class`字节码文件,然后这些字节码文件被加载到 Java 虚拟机中运行。在执行过程中,Java 虚拟机可以看做是一个进程,大家在 Java 中创建的线程都是针对于“Java 虚拟机”这个进程来说的。Java 虚拟机在实现的时候,会把用户创建的线程映射到操作系统的线程模型中。例如,在 Linux 中,Java 虚拟机会把用户创建的每一条线程映射到一个轻量级进程。 130 | 131 | ### 线程同步 132 | 133 | #### 线程需要同步的原因 134 | 135 | 因为一个进程中的多个线程是共享资源的,也就是说,进程的全局变量等每个线程都可以访问都。而每个线程都是由一个调度器调度的,一般情况下,我们很难判定一个线程什么时候执行、什么时候阻塞、什么时候退出,这就是多线程编程的**不确定性**。因此,在多线程编程的情况下,当一个线程使用公共资源(比如修改全局变量)的时候,不可能确定说这个变量在我使用的过程中不被其他线程修改,或者我的使用和修改不会对其他线程造成影响。因此,有必要对线程进行同步。 136 | 137 | 关于这部分的内容,希望大家结合着 Java 的多线程编程来理解和学习。两者相辅相成,大有裨益。 138 | 139 | #### 临界区 140 | 141 | 所谓临界区,就是代码中涉及对公共资源修改的那部分代码。我们看一个《Linux 编程基础》中的例子。 142 | 143 | image-20200529012930975 144 | 145 | 上图中用红框框起来的那部分就是临界区。大家可能会有疑惑:诶,没看见修改啥公共资源啊。请注意其中的`printf`,这个函数是要向 screen 中输出字符,很显然,这里的“screen”是各个线程公有的,所以是公共资源。 146 | 147 | #### 互斥锁 148 | 149 | 互斥锁的原理很简单,就是在线程要执行临界区的代码之前加锁,出临界区后释放锁。 150 | 151 | 这就好比一个只能一个人用的屋子(**临界区**),你只想在里面自己做些私密的事情,就可以在进入屋子前检查门是不是被锁了,如果没被锁,说明此时没人占用,你的使用是绝对安全的,就可以进入并**上锁**;此时,如果其他人要进入,看到门锁了,就要在外面**等待**,直到你**开锁**出门为止;相反,如果你进入之前门被锁了,说明有其他人在使用,那你也要**等待**,直到锁被打开。这里可能出现一种情况,就是很多人都想进去,但都被挡在门外面了,那么,等到锁被打开时,就要**竞争**进入屋子里,也就是一堆人去抢那把锁,能抢到的人就获得了进入的资格,其他的倒霉孩子则要接着等待。 152 | 153 | 相信大家从这个例子中就能学会互斥锁的使用原理了。 154 | 155 | 例如,上面临界区的例子中,如果用互斥锁实现对 screen 的同步使用,那么应该是下面这个样子: 156 | 157 | image-20200529123353252 158 | 159 | #### 条件变量 160 | 161 | 现在设想这样一种情况,当你需要进入这个只能被一个人独享的房间做事情时,在成功**获得锁**进入屋子(**进入临界区**)后,发现屋子里有个告示,给出了继续占有这间屋子的**条件**,说明只有学会了时间管理的人才能使用屋子中资源,你自己**判断**了一下,发现自己不符合条件,于是只能选择放弃使用临界区的资源**,而去学习时间管理。但总不能屋子空着让你白占着啊,所以此时你还要主动**释放锁**,然后才能去**等待**(注意,这里的“等待”不是等待锁,而是等待自己学会时间管理,在你**被唤醒之前**,没有资格参与对锁的竞争)。等你学会了时间管理,**满足了条件,被**通知唤醒**,就可以重新加入到抢占锁的队伍中,重新参与竞争了。 162 | 163 | 总结一下,这里的进入屋子的“条件”,就是条件变量。《Linux 编程基础》给出的这个图说明的很详细: 164 | 165 | \*\*\*\*![image-20200529015832290](http://cdn.loheagn.com/2020-05-28-175839.png) 166 | 167 | 有一点要特别注意的是,在被条件变量唤醒、重新获得锁进入后,需要**再次判断**是不是符合条件。这是因为“被唤醒”和“重新获得锁”之间不是原子操作,无法保证该线程的状态不会再次发生变化使其不满足条件。 168 | 169 | #### 信号量 170 | 171 | 信号量部分的原理可以参照上次实验。本部分内容详见《Linux 编程基础》一书。 172 | 173 | ## 三、实验习题 174 | 175 | 1. 创建一个线程,分别打印该线程和原来的线程的进程号和父进程号,然后回收刚刚创建的线程。**给出代码和操作步骤。** 176 | 177 | 2. 仅使用锁来实现两个线程的同步,让线程 a 不断地为公共变量 Num 增加 1,而当 Num 增加至 100 时线程 b 将 Num 归 0,不断重复上述过程。**将 Num 输出到屏幕并给出代码和操作步骤。** 178 | 179 | 3. 在实际过程中,条件变量和互斥锁总是结合使用,因为互斥锁只能表示锁与不锁两种状态,而仅靠条件变量本身也是无法实现线程同步的。条件变量允许线程以无竞争的方式进行等待直到某条件发生,而不是总是尝试去获取锁。使用条件变量和锁重写上一问题,**给出代码和操作步骤**。 180 | 181 | 4. 在一个程序中创建 8 个线程,每个线程打印你的学号中的一位数字,按照学号顺序打印出来。**不要使用`sleep`来进行同步,给出代码和操作步骤**。本题目你采取的同步策略是什么,为什么采用这样的策略,这是最好的同步策略吗? 182 | 183 | 5. 生产者消费者问题(Producer-consumer problem),也称有限缓冲问题(Bounded-buffer problem),是一个多线程同步问题的经典案例。该问题描述了共享固定大小缓冲区的两个线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。 184 | 185 | 生产者消费者问题主要要注意以下三点: 186 | 187 | - 在缓冲区为空时,消费者不能再进行消费 188 | 189 | - 在缓冲区为满时,生产者不能再进行生产 190 | 191 | - 在一个线程进行生产或消费时,其余线程不能再进行生产或消费等操作,即保持线程间的同步 192 | 193 | 现在假设缓冲区上限为 20,生产者和消费者线程各 10 个,请编写程序实现一个生产者消费者模型。**在每次生产、消费时将当前动作类型(produce/consume)与缓冲区内容量输出到屏幕,给出代码和操作步骤。** 194 | -------------------------------------------------------------------------------- /week02/answer.md: -------------------------------------------------------------------------------- 1 | # Week02 Assignment 参考答案 2 | 3 | ## 1. 你安装好了 GCC、GDB、Make 工具了吗?记录安装命令 4 | 5 | 我的系统是:CentOS 7.6 6 | 7 | 安装命令: 8 | 9 | ```shell 10 | yum -y install gcc 11 | yum -y install gdb 12 | ``` 13 | 14 | 我的系统是:Ubuntu 20.04 15 | 16 | 安装命令: 17 | 18 | ```shell 19 | sudo apt-get install gcc 20 | sudo apt-get install gdb 21 | sudo apt-get install make 22 | 23 | # sudo apt-get install build-essential 24 | ``` 25 | 26 | ## 2. 简述 GCC、GDB、Make 工具的作用 27 | 28 | - GCC:原 GNU C 语言编译器,后扩展为支持更多编程语言,作为编译器,它能够预处理、编译、连接和汇编 C/C++ 语言,生成系统的可执行文件 29 | - GDB:调试 C/C++ 程序,可执行程序的启动、断点处停顿、查看过程的变量状态 30 | - Make:控制可执行文件和其它一些从源代码来的非源码文件版本的软件,主要是读入 Makefile 的文件,通过执行这个文件中的指令,来构建多个程序的依赖关系,从而编译和安装这个程序 31 | 32 | 一些使用方法: 33 | 34 | ### GCC 35 | 36 | - 最简单的使用:`gcc a.cpp` 37 | - `-o name`:将生成的目标文件命名为 name 38 | - `-g`:生成便于 GDB 调试的信息,如果需要使用 GDB 则需要开启 39 | - `-D name`:用于 DEBUG,便于输出额外信息(实际上不如直接用 GDB ?) 40 | - `-E`:从 .c 生成 .i(头文件展开,去掉注释,宏替换) 41 | - `-S`:从 .i 生成 .s(C 文件变为汇编文件) 42 | 43 | - `-c`:从 .s 生成 .o(汇编文件变为二进制文件) 44 | - `-Wall`:生成所有的警告信息 45 | - `-Werror`:将警告信息视为错误 46 | - `-O,-O2,-O3`:由编译器优化生成的可执行文件 47 | 48 | ### GDB 49 | 50 | #### 启动 51 | 52 | 在终端输入 `gdb` 启动 GDB 53 | 54 | ```shell 55 | gdb 56 | ``` 57 | 58 | 在 GDB 中,通过 `file` 指令制定需要被调试的程序 59 | 60 | ```shell 61 | (gdb) file test 62 | ``` 63 | 64 | #### 运行 65 | 66 | 使用 `run` 指令运行被调试的程序,可以简化为 `r` 67 | 68 | ```shell 69 | (gdb) run 70 | ``` 71 | 72 | 当程序出现故障时,GDB 会提供相关的错误信息 73 | 74 | ![](img/errorinfo.png) 75 | 76 | 定位到错误后,就可以利用 GDB 对程序进行调试了 77 | 78 | #### 退出 79 | 80 | 使用 `quit` 退出,可简化为 `q` 81 | 82 | 如果程序正常结束,则 `q` 指令会成功退出 GDB,否则会收到如下提示: 83 | 84 | ![](img/quitinfo.png) 85 | 86 | 输入 `y` 可以强制杀死被调试的程序然后退出 87 | 88 | #### 断点 89 | 90 | 使用 `break` 设置断点,可简化为 `b`,后面跟一个行号或者函数名,之后程序在执行到这里时会中断。 91 | 92 | - 设置行号 93 | 94 | ```shell 95 | (gdb) b 7 96 | Breakpoint 1 at 0x118d: file a.c, line 7. 97 | ``` 98 | 99 | - 设置函数名 100 | 101 | ```shell 102 | (gdb) b main 103 | Breakpoint 2 at 0x1169: file a.c, line 5. 104 | ``` 105 | 106 | 另外,break 命令后面也可以在断点位置后面跟一个条件,仅当该条件为真的时候中断 107 | 108 | ```shell 109 | (gdb) b 9 if n == 5 110 | Breakpoint 3 at 0x11d0: file a.c, line 9. 111 | ``` 112 | 113 | #### 控制 114 | 115 | 使用 `continue` 命令使中断的程序继续运行,可简写为 `c` 116 | 117 | (这里只能让通过断点中断的程序继续运行,而非 运行时错误 的程序) 118 | 119 | 使用 `step` 使中断的程序执行一行,可简写为 `s` ,如果有函数调用则会进入函数,并在函数第一行中断 120 | 121 | 使用 `next` 使中断的程序执行一行,可简写为 `n`, 如果有函数调用,程序将不跟踪进入函数,而是直接在下一行中断 122 | 123 | #### 监视 124 | 125 | 使用 `print` 命令输出一个表达式的值,可简写为 `p`,这里的表达式也可以为包含了多个函数调用的复杂表达式 126 | 127 | 使用 `display` 来持续监视某个表达式的值,可简写为 `disp` 128 | 129 | #### 查看代码 130 | 131 | 使用 `list` 来查看部分代码,可简写为 `l` 。后面跟一个行号 132 | 133 | ### Make 134 | 135 | - 使用 make 前需要编写 makefile,后者用于告诉前者如何调用命令来完成目标。 136 | - makefile 由一系列规则组成,每条规则由 ``, `` `` 组成 137 | 138 | ```makefile 139 | : 140 | [tab] 141 | ``` 142 | 143 | 目标是必须的,前置条件和命令必须至少存在一个 144 | 145 | ## 3. 尝试练习使用 GDB 命令 146 | 147 | - 任意编写一个 C 程序,通过截图等方式,说明你在调试过程中的一些尝试 148 | 149 | 调试的程序如下: 150 | 151 | ```c 152 | #include 153 | 154 | void swap(int *x, int *y) { 155 | int temp = *y; 156 | *y = *x; 157 | *x = temp; 158 | return ; 159 | } 160 | 161 | int a[10010], n; 162 | 163 | int main() { 164 | scanf("%d", &n); 165 | for (int i = 1;i <= n; ++i) 166 | scanf("%d", &a[i]); 167 | for (int i = 1;i <= n; ++i) 168 | for(int j = 1;j <= n - i; ++j) 169 | if(a[j] < a[j + 1]) 170 | swap(&a[i], &a[j + 1]); 171 | for (int i = 1;i <= n; ++i) 172 | printf("%d ", a[i]); 173 | printf("\n"); 174 | return 0; 175 | } 176 | ``` 177 | 178 | 这段代码实现了一个错误的冒泡排序算法,希望实现的是从大到小排序。现在尝试用 GDB 将其错误找出来 179 | 180 | 首先编译并进入 GDB 环境 181 | 182 | ```shell 183 | gcc a.c -g -o a 184 | gdb a 185 | ``` 186 | 187 | 在没有任何修改的前提下,运行代码 188 | 189 | ```shell 190 | (gdb) r 191 | Starting program: /home/withinlover/Documents/Code/c/a 192 | 5 193 | 5 3 2 1 4 194 | 4 3 2 1 5 195 | [Inferior 1 (process 9948) exited normally] 196 | ``` 197 | 198 | 发现代码可以正常运行,所以很有可能是冒泡部分的逻辑出现了问题。 199 | 200 | 查看代码 201 | 202 | ```shell 203 | (gdb) l 5 204 | 1 #include 205 | 2 206 | 3 void swap(int *x, int *y) { 207 | 4 int temp = *y; 208 | 5 *y = *x; 209 | 6 *x = temp; 210 | 7 return ; 211 | 8 } 212 | 9 213 | 10 int a[10010], n; 214 | (gdb) l 215 | 11 216 | 12 int main() { 217 | 13 scanf("%d", &n); 218 | 14 for (int i = 1;i <= n; ++i) 219 | 15 scanf("%d", &a[i]); 220 | 16 for (int i = 1;i <= n; ++i) 221 | 17 for(int j = 1;j <= n - i; ++j) 222 | 18 if(a[j] < a[j + 1]) 223 | 19 swap(&a[i], &a[j + 1]); 224 | 20 for (int i = 1;i <= n; ++i) 225 | ``` 226 | 227 | 按照冒泡排序的思路,如果输入的是 `5 3 2 1 4` ,那么在第一轮的冒泡中,应该仅交换了 `1 4` 的位置 228 | 229 | 因此,对 `swap()` 设置断点,以检查第一轮冒泡的正确性 230 | 231 | ```shell 232 | (gdb) b swap 233 | Breakpoint 1 at 0x555555555189: file a.c, line 3. 234 | ``` 235 | 236 | 再次运行代码,输入保持不变 237 | 238 | ```shell 239 | (gdb) r 240 | Starting program: /home/withinlover/Documents/Code/c/a 241 | 5 242 | 5 3 2 1 4 243 | 244 | Breakpoint 1, swap (x=0x55555555537d <__libc_csu_init+77>, y=0x7ffff7fd15e0) 245 | at a.c:3 246 | 3 void swap(int *x, int *y) { 247 | (gdb) 248 | ``` 249 | 250 | 此时程序终止,理论上我们此时进行交换的应该是 1 和 4。输出查看一下: 251 | 252 | ```shell 253 | (gdb) p *x 254 | $1 = 29590344 255 | ``` 256 | 257 | 这个数字显然出现了异常,不过实际上是由于程序尚未将参数传进函数所致,我们先单步运行一次 258 | 259 | ```shell 260 | (gdb) n 261 | 4 int temp = *y; 262 | (gdb) p *x 263 | $2 = 5 264 | (gdb) p *y 265 | $3 = 4 266 | ``` 267 | 268 | 此时我们发现,传进来的并非 1 和 4,也就是说 `*x` 的传值是有问题的,查看对应的代码区域 269 | 270 | ```shell 271 | (gdb) l 20 272 | 15 scanf("%d", &a[i]); 273 | 16 for (int i = 1;i <= n; ++i) 274 | 17 for(int j = 1;j <= n - i; ++j) 275 | 18 if(a[j] < a[j + 1]) 276 | 19 swap(&a[i], &a[j + 1]); 277 | 20 for (int i = 1;i <= n; ++i) 278 | 21 printf("%d ", a[i]); 279 | 22 printf("\n"); 280 | 23 return 0; 281 | 24 } 282 | ``` 283 | 284 | 重点关注 19 行 swap 的第一个参数,不难发现在此处 i 和 j 写反导致出错,退出修改 285 | 286 | ```shell 287 | (gdb) q 288 | A debugging session is active. 289 | 290 | Inferior 1 [process 10709] will be killed. 291 | 292 | Quit anyway? (y or n) y 293 | ``` 294 | 295 | 修改完成后重新编译,测试 296 | 297 | ```shell 298 | (gdb) r 299 | Starting program: /home/withinlover/Documents/Code/c/a 300 | 5 301 | 5 3 2 1 4 302 | 5 4 3 2 1 303 | [Inferior 1 (process 10866) exited normally] 304 | ``` 305 | 306 | 程序已经恢复正常,bug-- 307 | 308 | ## 4. 请阐述静态链接库和动态链接库的异同点 309 | 310 | - 静态链接库:当要使用时,连接器会找出程序所需的函数,然后将它们拷贝到执行文件,由于这种拷贝是完整的,所以一旦连接成功,静态程序库也就不再需要了。 311 | 312 | - 动态库而言:某个程序在运行中要调用某个动态链接库函数的时候,操作系统首先会查看所有正在运行的程序,看在内存里是否已有此库函数的拷贝了。如果有,则让其共享那一个拷贝;只有没有才链接载入。 313 | 314 | ## 5. 请阐述 Make 命令工具如何确定哪些文件需要重新生成,而哪些不需要生成 315 | 316 | - 一是通过输入的指令,比如 make test,则会根据 test 的依赖关系进行迭代生成 317 | - 二是当发现待生成的文件已经存在时,会判断其依赖项是否发生了更新,如果没有更新则不再重复生成 318 | 319 | ## 6. 请简述 Make 中的伪目标的作用是什么 320 | 321 | 除了文件名,目标还可以是某个操作的名字,这称为"伪目标"(phony target),比如: 322 | 323 | ```makefile 324 | clean: 325 | rm *.o 326 | ``` 327 | 328 | 这段规则并不是为了生成文件,而是为了清理多余的 `*.o` 文件 329 | 330 | 但是,如果当前目录下恰好有一个名为 `clean` 的文件,在输入 `make clean` 时,便不会完成清理工作 331 | 332 | 为了避免这种情况,可以明确声明 `clean` 伪目标 333 | 334 | ```shell 335 | .PHONY: clean 336 | clean: 337 | rm *.o 338 | ``` 339 | -------------------------------------------------------------------------------- /week02/answer_template.md: -------------------------------------------------------------------------------- 1 | # Week02 Assignment 2 | 3 | > 班级: 4 | > 学号: 5 | > 姓名: 6 | 7 | ## 1. 你安装好了 GCC、GDB、Make 工具了吗?记录安装命令 8 | 9 | 我的系统是: 10 | 11 | 安装命令: 12 | 13 | ```shell 14 | 15 | ``` 16 | 17 | ## 2. 简述 GCC、GDB、Make 工具的作用 18 | 19 | ## 3. 尝试练习使用 GDB 命令 20 | 21 | - 任意编写一个 C 程序,通过截图等方式,说明你在调试过程中的一些尝试 22 | 23 | ## 4. 请阐述静态链接库和动态链接库的异同点 24 | 25 | ## 5. 请阐述 Make 命令工具如何确定哪些文件需要重新生成,而哪些不需要生成 26 | 27 | ## 6. 请简述 Make 中的伪目标的作用是什么 28 | -------------------------------------------------------------------------------- /week02/img/errorinfo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/week02/img/errorinfo.png -------------------------------------------------------------------------------- /week02/img/quitinfo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/week02/img/quitinfo.png -------------------------------------------------------------------------------- /week05/answer.md: -------------------------------------------------------------------------------- 1 | # Week05 Assignment 参考答案 2 | 3 | ## 1. 简述 shell 脚本中三个有特殊作用的字符 4 | 5 | 1. 管道 | 6 | 7 | 管道可以将多个简单的命令连接起来,使一个命令的输出作为另外一个命令的输入,由此来实现更加复杂的功能。 8 | 9 | 2. 连接符 ; 10 | 11 | 使用;连接符间隔的命令会按照先后次序依次执行。 12 | 13 | 3. 输入输出重定向 > < 14 | 15 | 用来改变 Shell 获取信息和输出信息的方向。 16 | 17 | 4. `\` 转义 18 | 5. `$` 使用变量 19 | 6. `#` 注释 20 | 21 | ## 2. Shell 脚本的变量有哪几类,各是什么情况下使用? 22 | 23 | - **环境变量** 24 | 25 | 定义和系统工作环境有关的变量,用户也可以重新定义该变量。常用于添加或修改环境变量。 26 | 27 | - **用户定义变量** 28 | 29 | 用户定义的变量,可用于代替表达 30 | 31 | - **内部变量(预定义变量)** 32 | 33 | 内部变量只能使用而不能修改或重定义,比如 `$#` 为传递给脚本参数的数量,`$$` 为当前进程的进程号,常用于作为暂存文件的名称,以保证不会重复。 34 | 35 | | 内部变量 | 说明 | 36 | | :------: | -------------------------------------------------------------------------------------------------- | 37 | | `#` | Shell 程序位置参数的个数 | 38 | | `*` | 以 `IFS` 为分隔,脚本的所有位置参数内容 | 39 | | `?` | 上一条前台命令执行后返回的状态值,命令执行成功与失败返回值不同 | 40 | | `$` | 当前 Shell 进程的进程 ID 号 | 41 | | `!` | 最后一个后台运行命令的进程号 | 42 | | `0` | 当前执行的 Shell 程序的名称 | 43 | | `@` | 脚本的位置参数内容,从 1 开始,`$@` 被扩展为 `$1`、`$2` 等,分别表示第一个、第二个等的位置参数内容 | 44 | | `_` | Shell 启动时,为正在执行的 Shell 程序的绝对路径;启动后,为上一条命令的最后一个参数 | 45 | 46 | 除此之外,常见的变量语法有参数置换变量,能根据不同条件给变量赋不同的值。常与位置参数变量结合使用。 47 | 48 | - `变量=${parameter:-word}`:如果 `parameter` 未定义或为 `null`,则用 `word` 置换变量的值,否则用 `parameter` 置换变量的值。 49 | - `变量=${parameter:=word}`:如果 `parameter` 未定义或为 `null`,则用 `word` 置换 `parameter` 的值然后置换变量的值,否则用 `parameter` 替换变量的值。 50 | - `变量=${parameter:?word}`:如果 `parameter` 未定义或为 `null`,`word` 被写至标准出错(默认情况下在屏幕显示 `word` 信息)然后退出,否则用 `parameter` 置换变量的值。 51 | - `变量=${parameter:+word}`:如果 `parameter` 未定义或为 `null`,则不进行替换,即变量值也为 `null`,否则用 `word` 置换变量的值。 52 | 53 | ## 3. Shell 脚本分支语句有几类,各是什么情况下使用? 54 | 55 | 1. if 语句 56 | 57 | Shell 中的 if 条件语句分为:单分支 if 语句、双分支 if 语句进而多分支 if 语句,其结构大体与其他程序设计语言的条件语句相同。 58 | 59 | 2. select 语句 60 | 61 | Shell 中的 select 语句可以将选项列表做出类似目录的形式,以交互的形式选择列表中的数据,传入 select 语句中的主体部分加以执行。 62 | 63 | 3. case 语句 64 | 65 | case 语句可以将一个变量的内容与多个选项进行匹配,若匹配成功,则执行该条件下对应的语句。 66 | 67 | ## 4. Shell 脚本循环语句有几类,各是什么情况下使用? 68 | 69 | 1. for 循环 70 | 71 | 对一个集合中的对象进行循环,并进行相应操作。 72 | 73 | 2. while 循环 74 | 75 | while 循环在表达式的值为假时停止循环,否则循环一直进行。 76 | 77 | 3. until 循环 78 | 79 | 与 while 循环的格式基本相同,不过 until 循环是在表达式值为真时停止循环,否则循环一直进行。 80 | 81 | ## 5. 简述 shell 脚本的调试方法 82 | 83 | 1. 使用大量 echo 语句来打印显示程序内部执行情况。 84 | 2. 用 trap 命令捕获程序退出的信号并执行相应的动作,格式为 trap 操作 信号名。 85 | 3. sh 命令:`sh` 命令可以执行 Shell 程序,在使用它时,通过使用参数可以查看每条命令的执行情况。 86 | | sh 的参数 | 说明 | 87 | | :-: | - | 88 | | `-n` | 检测语法错误,但无法检查出所有错误 | 89 | | `-v` | 执行每一条命令前,显示该命令 | 90 | | `-x` | 打印每条命令的结果 | 91 | 4. tee: `tee` 会将输入分到 stdout 和输出文件中去,这个特性可以帮助为管道命令 92 | 5. 调试钩子:本质就是用`if`语句来包裹调试信息,到了交付的时候设置`DEBUG`变量为`false`即可。 93 | -------------------------------------------------------------------------------- /week05/answer_template.md: -------------------------------------------------------------------------------- 1 | # Week05 Assignment 2 | 3 | > 班级: 4 | > 学号: 5 | > 姓名: 6 | 7 | ## 1. 简述 shell 脚本中三个有特殊作用的字符 8 | 9 | ## 2. Shell 脚本的变量有哪几类,各是什么情况下使用? 10 | 11 | ## 3. Shell 脚本分支语句有几类,各是什么情况下使用? 12 | 13 | ## 4. Shell 脚本循环语句有几类,各是什么情况下使用? 14 | 15 | ## 5. 简述 shell 脚本的调试方法 16 | -------------------------------------------------------------------------------- /week06/answer.md: -------------------------------------------------------------------------------- 1 | # Week06 Assignment 参考答案 2 | 3 | ## 1. 文件和目录的作用有什么不同? 4 | 5 | LINUX 有四种基本文件系统类型:普通文件、目录文件、连接文件和特殊文件,可用 file 命令来识别。 6 | 7 | - 普通文件 8 | - 如文本文件、C 语言元代码、SHELL 脚本、二进制的可执行文件等,可用 cat、less、more、vi、emacs 来察看内容,用 mv 来改名。 9 | - LINUX 的一些设备如磁盘、终端、打印机等都在文件系统中表示出来,则一类文件就是特殊文件,常放在/dev 目录内。例如,软驱 A 称为/dev/fd0。LINUX 无 C:的概念,而是用/dev/had 来自第一硬盘。 10 | - 目录文件 11 | - 包括文件名、子目录名及其指针。它是 LINUX 储存文件名的唯一地方,可用 ls 列出目录文件。 12 | 13 | ## 2. 简述标准文件 I/O 和系统调用 I/O 的各自优缺点是什么? 14 | 15 | - 系统调用 I/O 读写文件时,每次操作都会执行相关系统调用,这样处理的好处是直接读写实际文件,坏处是频繁地系统调用会增加系统开销 16 | - 标准文件 I/O 可以看成是在文件 I/O 的基础上封装了缓冲机制,先读写缓冲区,必要时再访问实际文件出,从而减少系统调用的次数 17 | 18 | ## 3. 符号链接和硬链接的异同点是什么? 19 | 20 | ### 相同点 21 | 22 | - 都建立了对同一个文件的链接 23 | - 删除链接对源文件均无影响 24 | 25 | ### 不同点 26 | 27 | - 含义不同:符号链接是一类特殊的文件,其包含有一条以绝对路径或相对路径的形式指向其它文件或者目录的引用;硬链接是一个文件的一个或多个文件名 28 | - 删除文件性质不同: 29 | - 在对符号链接进行读写时,系统会自动把该操作转换为对源文件的操作,但删除链接文件时,系统仅仅删除链接文件,而不删除源文件本身;如果目标文件被移动、重命名或删除,任何指向它的符号链接仍然存在,但会指向一个不复存在的文件 30 | - 而移动或删除原始文件时,硬链接不会被破坏,因为它所引用的是文件的物理数据 31 | - 硬链接的文件不需要用户有访问原始文件的权限,也不会显示原始文件的位置 32 | 33 | ## 4. 查看权限 34 | 35 | 假设目前系统的 umask 为 0022,执行`CREAT("a.txt", 765)`代码后,请写出文件`a.txt`所有者、组用户及其他用户对该文件的权限。 36 | 37 | ![fig](img\right.png) 38 | 39 | 所有者:不能读、能写、能执行。111 40 | 41 | 组用户:能读、不能写、不能执行。100 42 | 43 | 其他用户:能读、不能写、不能执行。101 44 | 45 | ## 5. 文件锁的使用 46 | 47 | 关于文件锁的使用,当某进程对某个文件上了读取锁或者写入锁后,请就下面几种情况分别编写程序进行分析,并解释原因。 48 | 49 | (1,2) 创建子进程,请问子进程对该文件是否拥有同样的锁?该进程执行 exec 调用,请问该进程对文件的锁是否依然有效? 50 | 51 | 我先引用 man 页面的原话如下: 52 | 53 | > Record locks are not inherited by a child created via fork(2), but are preserved across an execve(2). 54 | > 也就是说,子进程不能拥有同样的锁,这是合理的,因为文件锁是相对于进程而言的。 55 | > exec 调用,可以理解为还是父进程。 56 | 57 | 首先我要先介绍一下我对`fcntl`这个复杂的函数的封装。 58 | 59 | 我在`kaiyan.h`里面将 fcntl 函数封装成了四个函数: 60 | 61 | ```c 62 | bool fcntlReadable(int fd); //能否加读锁 63 | bool fcntlWritable(int fd); //能否加写锁 64 | int fcntlSetReadLock(int fd); //成功0,失败-1 65 | int fcntlSetWriteLock(int fd); //成功0,失败-1 66 | ``` 67 | 68 | 下面是测试: 69 | 70 | ```c 71 | # include "kaiyan.h" 72 | 73 | void confirmClose(int fd){ 74 | //由于关闭文件会导致所有锁丢失,这里用read来主动关闭文件 75 | while(1){ 76 | char msg[] = "是否关闭文件并退出?(y/n)"; 77 | write(0, msg, strlen(msg)); 78 | char quit[] = "y"; 79 | char buffer[] = "123456789123456789"; 80 | read(0, buffer, strlen(quit)); 81 | if(strncmp(buffer, quit, 1) == 0){ 82 | closeThrow(fd); 83 | exit(0); 84 | } 85 | } 86 | } 87 | //测试锁的冲突性 88 | void test1(int fd){ 89 | if(forkThrow() == 0){ //子进程 90 | if(fcntlWritable(fd))printf("OK\n"); 91 | else printf("Can not\n"); 92 | }else{ //父进程 93 | confirmClose(fd); 94 | } 95 | } 96 | int main(){ 97 | int fd = openThrow("file.txt"); 98 | if(fcntlSetWriteLock(fd) == 0){ 99 | test1(fd); 100 | }else{ 101 | printf("一开始加锁就失败了"); 102 | confirmClose(fd); 103 | } 104 | return 0; 105 | } 106 | ``` 107 | 108 | 分别手动改函数名,上面的测试的结果如下: 109 | 110 | ```c 111 | /* 112 | * 2021-4-11, Linux 113 | * 父进程 子进程 测试结果 114 | * ------|------|-------- 115 | * 有读锁 加读锁 OK 116 | * 有读锁 加写锁 Can not 117 | * 有写锁 加写锁 Can not 118 | * 有写锁 加读锁 Can not 119 | */ 120 | ``` 121 | 122 | 可见写锁的独占性和读锁的共享性。 123 | ~~exec 我就不测试了,太累了。。。~~ 124 | 125 | --- 126 | 127 | (3) 当该进程对文件建立锁的文件描述符关闭后,这些锁是否仍然有效? 128 | 引用 man 页面的原话如下: 129 | 130 | > If a process closes any file descriptor referring to a file, then all of the process's locks on that file are released, regardless of the file descriptor(s) on which the locks were obtained. This is bad: it means that a process can lose its locks on a file such as /etc/passwd or /etc/mtab when for some reason a library function decides to open, read, and close the same file. 131 | 132 | 可见,如果关闭了这个文件,那么所有所有的进程对这个文件上的锁就会被释放。 133 | 也就是说,如果一个恶意的病毒想要破坏我们对某个文件上的锁,那个病毒只需要打开这个文件,然后再关闭这个文件就行了。。。 134 | 135 | 测试: 136 | 137 | ```c 138 | //测试关闭文件是否能松开锁 139 | void test2(int fd){ 140 | if(forkThrow() == 0){ //子进程 141 | //等待父进程关闭文件大概3秒 142 | sleep(3); 143 | if(fcntlReadable(fd))printf("OK\n"); 144 | else printf("Can not\n"); 145 | }else{ //父进程 146 | closeThrow(fd); 147 | } 148 | } 149 | ``` 150 | 151 | 上面的测试给出: 152 | 153 | ```plain 154 | /* 155 | * 2021-4-11, Linux, test2 156 | * 父进程 子进程 测试结果 157 | * --------------|--------|-------- 158 | * 有读锁后close 加读锁 OK 159 | * 有读锁后close 加写锁 OK 160 | * 有写锁后close 加写锁 OK 161 | * 有写锁后close 加读锁 OK 162 | */ 163 | ``` 164 | 165 | --- 166 | 167 | (3) 如果对这些文件描述符执行 dup 操作,锁是否对新的文件描述符仍然有效? 168 | 169 | 关于`Open file descriptions`,man 页面的原话是: 170 | 171 | > The term open file description is...in kernel-developer a struct file...a child process created via fork(2) inherits duplicates of its parent's file descriptors, and those duplicates refer to the same open file descriptions...Each open() of a file creates a new open file description; thus, there may be multiple open file descriptions corresponding to a file inode... 172 | > 也就是说,多个文件描述符对应同一个`struct file`,多个`struct file`对应一个 inode,一个 inode 对应一个真实存在的文件。 173 | 174 | 测试: 175 | 176 | ```c 177 | //测试dup能否把fcntl的锁也dup出来 178 | void test3(int fd){ 179 | int fdNew = dup(fd); 180 | if(forkThrow() == 0){ //子进程 181 | if(fcntlReadable(fdNew))printf("OK\n"); 182 | else printf("Can not\n"); 183 | }else{ //父进程 184 | confirmClose(fd); 185 | } 186 | } 187 | ``` 188 | 189 | 测试结果: 子进程仍然无法对新的 fd 上锁,说明 dup 会把锁也 dup 了 190 | 191 | ```plain 192 | /* 193 | * 2021-4-11, Linux, test3 194 | * 父进程 子进程 测试结果 195 | * -------|-------------|-------- 196 | * 有读锁 给dup加读锁 OK 197 | * 有读锁 给dup加写锁 Can not 198 | * 有写锁 给dup加写锁 Can not 199 | * 有写锁 给dup加读锁 Can not 200 | */ 201 | ``` 202 | 203 | --- 204 | 205 | (4) 假设该进程对某个文件加了读取锁/写入锁,然后又同时对该进程进行写入/读取操作,将会发生什么情况? 206 | 207 | 用`F_SETLK`等等设置的锁是建议性的"Advisory record locking",意思是如果你遵循良好的编程习惯,你那么大概率能帮助你解决锁冲突的问题,但是你仍然可以进行写入和读取操作。 208 | 209 | Linux 号称自己能实现内核级别的"强制锁(Mandatory locking)",也就是说能从内核底层上自动识别危险的`read`和`write`操作,这涉及到`O_NONBLOCK`选项。 210 | 211 | > If the O_NONBLOCK flag is not enabled, then the system call is blocked until the lock is removed or converted to a mode that is compatible with the access. If the O_NONBLOCK flag is enabled, then the system call fails with the error EAGAIN... 212 | > 然而我在 man 页面却看到了满满的 BUG 警告: 213 | > Warning: the Linux implementation of mandatory locking is unreliable. See BUGS below. Because of these bugs, and the fact that the feature is believed to be little used... 214 | > 之后还说如果要想让强制锁发挥最大作用,需要在`mount`的时候加`-o`选项等等。。。看着就特别复杂。 215 | > 不过还好"强制锁"是"non-POSIX"的,也就是说属于一个"超纲"的功能,也不能太怪 Linux 了,毕竟人家免费。。。 216 | 217 | ~~演示就不演示了。。。~~ 218 | 219 | --- 220 | 221 | 题外话: 222 | 223 | 下面是我测试的脚本全部内容: 224 | 225 | ```c 226 | # include "kaiyan.h" 227 | 228 | void confirmClose(int fd){ 229 | //由于关闭文件会导致所有锁丢失,这里用read来主动关闭文件 230 | while(1){ 231 | char msg[] = "是否关闭文件并退出?(y/n)"; 232 | write(0, msg, strlen(msg)); 233 | char quit[] = "y"; 234 | char buffer[] = "123456789123456789"; 235 | read(0, buffer, strlen(quit)); 236 | if(strncmp(buffer, quit, 1) == 0){ 237 | closeThrow(fd); 238 | exit(0); 239 | } 240 | } 241 | } 242 | 243 | //测试锁的冲突性 244 | void test1(int fd){ 245 | if(forkThrow() == 0){ //子进程 246 | if(fcntlWritable(fd))printf("OK\n"); 247 | else printf("Can not\n"); 248 | }else{ //父进程 249 | confirmClose(fd); 250 | } 251 | } 252 | 253 | //测试关闭文件是否能松开锁 254 | void test2(int fd){ 255 | if(forkThrow() == 0){ //子进程 256 | //等待父进程关闭文件大概3秒 257 | sleep(3); 258 | if(fcntlReadable(fd))printf("OK\n"); 259 | else printf("Can not\n"); 260 | }else{ //父进程 261 | closeThrow(fd); 262 | } 263 | } 264 | 265 | //测试dup能否把fcntl的锁也dup出来 266 | void test3(int fd){ 267 | int fdNew = dup(fd); 268 | if(forkThrow() == 0){ //子进程 269 | if(fcntlReadable(fdNew))printf("OK\n"); 270 | else printf("Can not\n"); 271 | }else{ //父进程 272 | confirmClose(fd); 273 | } 274 | } 275 | 276 | int main(){ 277 | int fd = openThrow("file.txt"); 278 | if(fcntlSetReadLock(fd) == 0){ 279 | test2(fd); 280 | }else{ 281 | printf("一开始加锁就失败了"); 282 | confirmClose(fd); 283 | } 284 | return 0; 285 | } 286 | ``` 287 | 288 | 下面是我的`kaiyan.h`的全部内容: 289 | 290 | ```c 291 | //kaiyan.h 292 | #include 293 | #include 294 | #include 295 | #include 296 | #include 297 | #include 298 | #include 299 | #include 300 | #include 301 | 302 | void perrorExit(const char* s){ 303 | printf("errno = %d:", errno); 304 | perror(s); 305 | exit(-1); 306 | } 307 | 308 | int openThrow(const char* file){ 309 | int fd = open(file, O_RDWR | O_CREAT, S_IRWXU | S_IRWXG | S_IRWXO); 310 | if(fd < 0) perrorExit("openThrow"); 311 | return fd; 312 | } 313 | 314 | void closeThrow(int fd){ 315 | if(close(fd) < 0)perrorExit("closeThrow"); 316 | } 317 | 318 | int forkThrow(){ 319 | int pid = fork(); 320 | if(pid < 0) perrorExit("forkThrow"); 321 | return pid; 322 | } 323 | 324 | //>>>>>>>>>>> fcntl相关 >>>>>>>>>>> 325 | //这几个函数是高度封装的,可以理解为public 326 | //所有的函数都不会向stdout输出 327 | bool fcntlReadable(int fd); //能否加读锁 328 | bool fcntlWritable(int fd); //能否加写锁 329 | int fcntlSetReadLock(int fd); //成功0,失败-1 330 | int fcntlSetWriteLock(int fd); //成功0,失败-1 331 | 332 | //剩下的函数都是辅助函数,可以理解为private 333 | int fcntlThrow(int fd, int cmd, struct flock* ptr){ 334 | int eax = fcntl(fd, cmd, ptr); 335 | if(eax < 0){ 336 | perror("fcntlThrow"); 337 | exit(0); 338 | } 339 | return eax; 340 | } 341 | 342 | struct flock flockInit(short type){ 343 | struct flock lock; 344 | lock.l_type = type; //这是最关键的参数 345 | //下面的参数一般没有必要自己设置 346 | lock.l_whence = SEEK_SET; //一般我们只想从文件开头开始锁 347 | lock.l_start = 0; //一般我们只想从文件开头开始锁 348 | lock.l_len = 0; //一般我们只想锁住整个文件 349 | lock.l_pid = getpid(); //一般我们只想查看自己能否上锁 350 | return lock; 351 | } 352 | 353 | short fcntlGetLockType(int fd, short type){ 354 | struct flock lock = flockInit(type); 355 | fcntlThrow(fd, F_GETLK, &lock); 356 | return lock.l_type; 357 | } 358 | 359 | bool fcntlReadable(int fd){ //能否加读锁 360 | return fcntlGetLockType(fd, F_RDLCK) == F_UNLCK; 361 | } 362 | 363 | bool fcntlWritable(int fd){ //能否加写锁 364 | return fcntlGetLockType(fd, F_WRLCK) == F_UNLCK; 365 | } 366 | 367 | void fcntlSetLock(int fd, short type){ 368 | //不检查能否加锁 369 | struct flock lock = flockInit(type); 370 | fcntlThrow(fd, F_SETLK, &lock); 371 | } 372 | 373 | int fcntlSetReadLock(int fd){ 374 | if(fcntlReadable(fd)){ 375 | fcntlSetLock(fd, F_RDLCK); 376 | return 0; 377 | } 378 | return -1; 379 | } 380 | 381 | int fcntlSetWriteLock(int fd){ 382 | if(fcntlWritable(fd)){ 383 | fcntlSetLock(fd, F_WRLCK); 384 | return 0; 385 | } 386 | return -1; 387 | } 388 | 389 | //<<<<<<<<<<< fcntl相关 <<<<<<<<<<< 390 | ``` 391 | -------------------------------------------------------------------------------- /week06/answer_template.md: -------------------------------------------------------------------------------- 1 | # Week06 Assignment 2 | 3 | > 班级: 4 | > 学号: 5 | > 姓名: 6 | 7 | ## 1. 文件和目录的作用有什么不同? 8 | 9 | ## 2. 简述标准文件 I/O 和系统调用 I/O 的各自优缺点是什么? 10 | 11 | ## 3. 符号链接和硬链接的异同点是什么? 12 | 13 | ## 4. 查看权限 14 | 15 | 假设目前系统的 umask 为 0022,执行`creat("a.txt", 765)`代码后,请写出文件`a.txt`所有者、组用户及其他用户对该文件的权限。 16 | 17 | ## 5. 文件锁的使用 18 | 19 | 关于文件锁的使用,当某进程对某个文件上了读取锁或者写入锁后,请就下面几种情况分别编写程序进行分析,并解释原因。 20 | 21 | (1) 创建子进程,请问子进程对该文件是否拥有同样的锁? 22 | 23 | (2) 该进程执行 exec 调用,请问该进程对文件的锁是否依然有效? 24 | 25 | (3) 当该进程对文件建立锁的文件描述符关闭后,这些锁是否仍然有效?如果对这些文件描述符执行 dup 操作,锁是否对新的文件描述符仍然有效? 26 | 27 | (4) 假设该进程对某个文件加了读取锁/写入锁,然后又同时对该进程进行写入/读取操作,将会发生什么情况? 28 | -------------------------------------------------------------------------------- /week06/img/right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/week06/img/right.png -------------------------------------------------------------------------------- /week09/answer.md: -------------------------------------------------------------------------------- 1 | # Week09 Assignment 参考答案 2 | 3 | ## 1. 请给出下列代码输出行数,并解释原因 4 | 5 | ```c 6 | #include 7 | #include 8 | int main(){ 9 | printf("my pid is %d\n", getpid()); 10 | fork(); 11 | fork(); 12 | fork(); 13 | printf("my pid is %d\n", getpid()); 14 | } 15 | ``` 16 | 17 | 输出 9 行,主进程在 `fork()` 前输出 1 行,`fork()` 三次得到八个进程,执行程序最后一行输出 8 行,共 9 行 18 | 19 | ## 2. 画出上述进程的创建过程 20 | 21 | ![fig](img/2021-06-17-20-14-32.png) 22 | 23 | ## 3. 编写一个程序,实现下图示的进程之间的关系 24 | 25 | 进程 A -> 进程 B -> 进程 C 26 | 27 | 其中箭头的方向表明它们之间的生成关系,即 A 是祖先进程,C 是孙子进程。 28 | 29 | 秦喆答案: 30 | 31 | ```c 32 | #include 33 | #include 34 | #include 35 | #include 36 | int main(void) { 37 | pid_t pid; 38 | pid = fork(); 39 | if (pid < 0) { 40 | perror("fork failed"); 41 | exit(1); 42 | } 43 | if (pid == 0) { 44 | printf("This is the son B\n"); 45 | pid_t p = fork(); 46 | if (p == 0) { 47 | printf("This is the grandson C\n"); 48 | } 49 | } else { 50 | printf("This is the parent A\n"); 51 | } 52 | return 0; 53 | } 54 | ``` 55 | 56 | 蒋博文答案: 57 | 58 | ```c 59 | #include 60 | #include 61 | #include 62 | int main() { 63 | printf("this is parent A,pid=%d\n", getpid()); 64 | if (fork() == 0) { 65 | printf("this is child B,pid=%d\n", getpid()); 66 | if (fork() == 0) { 67 | printf("this is grandchild C,pid=%d\n", getpid()); 68 | } 69 | } 70 | return 0; 71 | } 72 | ``` 73 | 74 | ## 4. 编写一个程序,创建两个子进程,父进程在屏幕上输出 10 个字符 'A',两个子进程分别输出 10 个 'B' 和 'C' 75 | 76 | > 要求父进程在两个子进程输出完字符后再输出自己的字符。 77 | 78 | 秦喆答案: 79 | 80 | ```c 81 | #include 82 | #include 83 | #include 84 | #include 85 | int main(void) { 86 | pid_t pid; 87 | pid = fork(); 88 | if (pid < 0) { 89 | perror("fork failed"); 90 | exit(1); 91 | } 92 | if (pid == 0) { 93 | printf("BBBBBBBBBB"); 94 | } else { 95 | pid_t p = fork(); 96 | if (p == 0) 97 | printf("CCCCCCCCCC"); 98 | else { 99 | sleep(2); 100 | printf("AAAAAAAAAA"); 101 | } 102 | } 103 | return 0; 104 | } 105 | ``` 106 | 107 | 蒋博文答案: 108 | 109 | ```c 110 | #include 111 | #include 112 | #include 113 | int main() { 114 | int i = 2; 115 | char c = 'A'; 116 | while (i--) { 117 | if (fork() == 0) { 118 | int j; 119 | printf("this is the %dst child,pid=%d\n", 2 - i, getpid()); 120 | for (j = 0; j < 10; j++) { 121 | printf("%c", c + 2 - i); 122 | } 123 | printf("\n"); 124 | exit(1); 125 | } 126 | } 127 | sleep(1); 128 | int j; 129 | printf("this is the parent,pid=%d\n", getpid()); 130 | for (j = 0; j < 10; j++) { 131 | printf("%c", c); 132 | } 133 | return 0; 134 | } 135 | ``` 136 | -------------------------------------------------------------------------------- /week09/answer_template.md: -------------------------------------------------------------------------------- 1 | # Week09 Assignment 2 | 3 | > 班级: 4 | > 学号: 5 | > 姓名: 6 | 7 | ## 1. 请给出下列代码输出行数,并解释原因 8 | 9 | ```c 10 | #include 11 | #include 12 | int main(){ 13 | printf("my pid is %d\n", getpid()); 14 | fork(); 15 | fork(); 16 | fork(); 17 | printf("my pid is %d\n", getpid()); 18 | } 19 | ``` 20 | 21 | ## 2. 画出上述进程的创建过程 22 | 23 | ## 3. 编写一个程序,实现下图示的进程之间的关系 24 | 25 | 进程 A -> 进程 B -> 进程 C 26 | 27 | 其中箭头的方向表明它们之间的生成关系,即 A 是祖先进程,C 是孙子进程。 28 | 29 | ## 4. 编写一个程序,创建两个子进程,父进程在屏幕上输出 10 个字符 'A',两个子进程分别输出 10 个 'B' 和 'C' 30 | 31 | > 要求父进程在两个子进程输出完字符后再输出自己的字符。 32 | -------------------------------------------------------------------------------- /week09/img/2021-06-17-20-14-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/week09/img/2021-06-17-20-14-32.png -------------------------------------------------------------------------------- /week10/answer.md: -------------------------------------------------------------------------------- 1 | # Week10 Assignment 参考答案 2 | 3 | 感谢郝泽钰同学! 4 | 5 | ## 1. 编写一个程序,实现这样的功能:搜索 2~65535 之间所有的素数并保存到数组中,用户输入^C 信号时,程序打印出最近找到的素数 6 | 7 | ```c 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | int isPrime(int n) { 14 | for (int i = 2; i < n; ++i) 15 | if (n % i == 0) return false; 16 | return true; 17 | } 18 | int prime[65536], pcnt; 19 | void sigHandler(int signalNum) { 20 | printf("The nearest prime is: %d\n", prime[pcnt]); 21 | } 22 | int main() { 23 | signal(SIGINT, sigHandler); 24 | for (int i = 1; i <= 65536; ++i) 25 | if (isPrime(i)) prime[++pcnt] = i; 26 | return 0; 27 | } 28 | ``` 29 | 30 | ![fig](img/2021-06-17-20-21-12.png) 31 | 32 | ## 2. 简述什么是可靠信号和不可靠信号,并实验 SIGINT 信号是可靠的还是不可靠的 33 | 34 | - 可靠信号:多个信号发送到进程时,没来得及处理的信号排入进程的队列,依次处理; 35 | - 不可靠信号:多个信号产生时,无法来得及处理,造成信号丢失。 36 | 37 | ![fig](img/2021-06-17-20-21-44.png) 38 | 39 | 对于多次输⼊的 SIGINT 信号,只对其中⼀个作出了反应。 40 | 41 | ```c 42 | #include 43 | #include 44 | #include 45 | #include 46 | void sig_Handler(int, siginfo_t*, void*); 47 | int main() { 48 | struct sigaction act; 49 | sigset_t newmask, oldmask; 50 | int rc; 51 | sigemptyset(&newmask); 52 | sigaddset(&newmask, SIGINT); 53 | sigaddset(&newmask, SIGRTMIN); 54 | sigprocmask(SIG_BLOCK, &newmask, &oldmask); 55 | act.sa_sigaction = sig_Handler; 56 | act.sa_flags = SA_SIGINFO; 57 | if (sigaction(SIGINT, &act, NULL) < 0) { 58 | printf("install sigalerror\n"); 59 | } 60 | if (sigaction(SIGRTMIN, &act, NULL) < 0) { 61 | printf("install sigalerror\n"); 62 | } 63 | printf("my pid = %d\n", getpid()); 64 | sleep(10); 65 | sigprocmask(SIG_SETMASK, &oldmask, NULL); 66 | return 0; 67 | } 68 | void sig_Handler(int signum, siginfo_t* info, void* mask) { 69 | if (signum == SIGINT) printf("Got SIGINT\n"); 70 | if (signum == SIGRTMIN) printf("Got SIGRTMIN\n"); 71 | } 72 | ``` 73 | 74 | 黄泽桓答案: 75 | 76 | 信号值小于 SIGRTMIN=31 为不可靠信号,在 SIGRTMIN-SIGRTMAX=63 之间的为可靠信号。 77 | 78 | ![fig](img/2021-06-17-20-28-04.png) 79 | 80 | ## 3. 在执行 `ping http://www.people.com.cn` 时,假设该网站是可 ping 通的,但是在输入^\时,ping 命令并没有结束而是显示 ping 的成功率,但是输入^C 时,ping 程序却被退出,请解释发生这一现象的原因 81 | 82 | ![fig](img/2021-06-17-20-23-21.png) 83 | 84 | 推测原因是 ping 命令实现过程中重写了对于 ^\ ^C 信号的处理 85 | 86 | 黄泽桓答案: 87 | 88 | ping的中断处理模块: 89 | 90 | ```c 91 | set_signal(SIGINT, finish); 92 | void finish(void){...} 93 | ``` 94 | 95 | ping的SIGQUIT模块: 96 | 97 | ```c 98 | set_signal(SIGQUIT, sigstatus); 99 | ``` 100 | 101 | 在ping模块中,两个信号处理的函数不同,SIGINT中断,SIGQUIT显示成功率。 102 | 103 | ## 4. 编写程序实现如下功能:程序 A.c 按照用户输入的参数定时向程序 B.c 发送信号,B.c 程序接收到该信号后,打印输出一条消息 104 | 105 | 运行过程如下: 106 | 107 | ```sh 108 | ./B value& # 此时,输出进程 B 的 PID 号,value 表示要输出的参数。 109 | ./A processBPID timerVal # 第一个参数表示进程 B 的 PID,第二个参数为定时时间。 110 | ``` 111 | 112 | 效果如下图: 113 | 114 | ![fig](img/2021-06-17-20-24-15.png) 115 | 116 | ```c 117 | // A.c 118 | #include 119 | #include 120 | #include 121 | #include 122 | #include 123 | int main(int argc, char *argv[]) { 124 | int pid; 125 | sscanf(argv[1], "%d", &pid); 126 | int time; 127 | sscanf(argv[2], "%d", &time); 128 | while (true) { 129 | kill(pid, 2); 130 | sleep(time); 131 | } 132 | return 0; 133 | } 134 | 135 | // B.c 136 | #include 137 | #include 138 | #include 139 | #include 140 | #include 141 | char value[110]; 142 | void sig_Handler(int sigNum, siginfo_t *info, void *mask) { 143 | printf("%s\n", value); 144 | } 145 | int main(int argc, char *argv[]) { 146 | strcpy(value, argv[1]); 147 | struct sigaction act; 148 | sigset_t newmask, oldmask; 149 | int rc; 150 | sigemptyset(&newmask); 151 | sigaddset(&newmask, SIGINT); 152 | sigprocmask(SIG_BLOCK, &newmask, &oldmask); 153 | act.sa_sigaction = sig_Handler; 154 | act.sa_flags = SA_SIGINFO; 155 | if (sigaction(SIGINT, &act, NULL) < 0) { 156 | printf("install sigalerror\n"); 157 | } 158 | if (sigaction(SIGRTMIN, &act, NULL) < 0) { 159 | printf("install sigalerror\n"); 160 | } 161 | printf("my pid = %d\n", getpid()); 162 | sigprocmask(SIG_SETMASK, &oldmask, NULL); 163 | while (true) { 164 | sleep(1); 165 | } 166 | return 0; 167 | } 168 | ``` 169 | -------------------------------------------------------------------------------- /week10/answer_template.md: -------------------------------------------------------------------------------- 1 | # Week10 Assignment 2 | 3 | > 班级: 4 | > 学号: 5 | > 姓名: 6 | 7 | ## 1. 编写一个程序,实现这样的功能:搜索 2~65535 之间所有的素数并保存到数组中,用户输入^C 信号时,程序打印出最近找到的素数 8 | 9 | ## 2. 简述什么是可靠信号和不可靠信号,并实验 SIGINT 信号是可靠的还是不可靠的 10 | 11 | ## 3. 在执行 `ping http://www.people.com.cn` 时,假设该网站是可 ping 通的,但是在输入^\时,ping 命令并没有结束而是显示 ping 的成功率,但是输入^C 时,ping 程序却被退出,请解释发生这一现象的原因 12 | 13 | ## 4. 编写程序实现如下功能:程序 A.c 按照用户输入的参数定时向程序 B.c 发送信号,B.c 程序接收到该信号后,打印输出一条消息 14 | 15 | 运行过程如下: 16 | 17 | ```sh 18 | ./B value& # 此时,输出进程 B 的 PID 号,value 表示要输出的参数。 19 | ./A processBPID timerVal # 第一个参数表示进程 B 的 PID,第二个参数为定时时间。 20 | ``` 21 | -------------------------------------------------------------------------------- /week10/img/2021-06-17-20-21-12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/week10/img/2021-06-17-20-21-12.png -------------------------------------------------------------------------------- /week10/img/2021-06-17-20-21-44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/week10/img/2021-06-17-20-21-44.png -------------------------------------------------------------------------------- /week10/img/2021-06-17-20-23-21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/week10/img/2021-06-17-20-23-21.png -------------------------------------------------------------------------------- /week10/img/2021-06-17-20-24-15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/week10/img/2021-06-17-20-24-15.png -------------------------------------------------------------------------------- /week10/img/2021-06-17-20-28-04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/week10/img/2021-06-17-20-28-04.png -------------------------------------------------------------------------------- /week13/answer.md: -------------------------------------------------------------------------------- 1 | # Week13 Assignment 参考答案 2 | 3 | ## 1. 简述信号量的作用,如何利用信号量实现同步和互斥? 4 | 5 | - 作用:信号量是用来解决进程间的同步与互斥问题的一种进程间通信机制,包括一个称为信号量的变量和在该信号量下等待资源的进程等待队列,以及对信号量进行的两个原子操作(P/V 操作)。 6 | - 同步:由一个无条件执行的进程开始,信号量资源初始值为 0,进程结束时 V(S)操作,信号量资源+1,此时 7 | 通知别的进程。需要该资源的进程,打破阻塞,P(S)操作,去访问临界资源。 8 | - 互斥:P(S)操作,资源-1,则其余进程不可再访问该临界资源。V(S)操作结束后,资源+1,此时别的进程才可 9 | 进行是否可以访问判断。 10 | 11 | ## 2. 简述共享内存的作用和用法,共享内存为什么需要与信号量一起使用? 12 | 13 | - 作用:省去当进程向队列/管道写⼊数据时和读取消息时数据从用户地址向内核地址空间复制的操作 14 | - 用法: 15 | - 创建:从内存中获得一段共享内存区域 shmget() 16 | - 映射:shmat() 17 | - 撤销映射:shmdt() 18 | - 与信号量一起使用:保证某块共享内存在被其他进程进行读或写时不会被其他进程的操作干扰,需要信号量来加上对应的锁 19 | 20 | ## 3. 简述消息队列的作用和用法,并将其与管道进行异同点对比 21 | 22 | - 作用:将消息看作一个记录并按相应的优先级和格式以一个链表的形式存储起来 23 | - 用法: 24 | - 创建消息队列:msgsnd() 25 | - 发送数据到消息队列:msgsnd() 26 | - 从消息队列读取:msgrcv() 27 | - 删除队列 28 | - 与管道的对比:消息队列更灵活,提供有格式字节流,有利于减少开发⼈员工作量,其次,消息具有类型,在实际应用中,可为不同类型的消息分配不同的优先级 29 | 30 | ## 4. 请查阅资料简述进程间通信的 System V、POSIX 两种标准之间的差异性 31 | 32 | | 指标 | System V | POSIX | 33 | | --------------------- | ------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | 34 | | 效率性能 | PSYSTEM V 在同步互斥手段方面的无竞争条件下是无论何时都会陷⼊内核,性能稍低 | POSIX 在在同步互斥手段方面的无竞争条件下是不会陷⼊内核,性能较⾼ | 35 | | 冗余可靠性 | System V 提供了 SEM_UNDO 选项可以解决成功获取信号量后,进程如果意外终止,将无法-释放-信号量个问题,可靠性高。 | sem_wait 函数成功获取信号量后,进程如果意外终⽌,将无法释放信号量,可靠性差 | 36 | | 操作系统 | System V 操作系统实现相当⼴泛 | 可能有小部分操作系统没有实现 POSIX 标准 | 37 | | 移植性 | 不同操作系统 System V 存在一些差异 | 可移植性 | 38 | | 进程间&线程间通信同步 | 进程间通信,线程间使用较少。线程相对于进程是轻量级的。每次调用都会陷⼊内核的接口,会丧失线程的轻量优势 | 优 | 39 | 40 | ## 5. 请编写一个管道通信程序实现文件的传输,并请思考采用其他进程通信方式是否可方便实现进程间的文件传输 41 | 42 | 黄泽桓答案: 43 | 44 | ```c 45 | #include 46 | #include 47 | #include 48 | #include 49 | #include 50 | #include 51 | #include 52 | #include 53 | int main() 54 | { 55 | int pid; 56 | int fd; 57 | int fd1; 58 | int fd2; 59 | mkfifo("./pipe", 0777); 60 | pid = fork(); 61 | if(pid > 0) 62 | { 63 | fd = open("./test.txt", O_RDONLY); 64 | fd1=open("./pipe",O_WRONLY); 65 | char buf[200]; 66 | read(fd, buf, 1024); 67 | write(fd1,buf,strlen(buf)+1); 68 | close(fd); 69 | close(fd1); 70 | } 71 | else if(pid == 0) 72 | { 73 | fd = open("./pipe", O_RDONLY); 74 | FILE *f=fopen("./copy.txt","w"); 75 | fclose(f); 76 | char buf2[200]; 77 | fd2=open("./copy.txt",O_WRONLY); 78 | read(fd, buf2, 1024); 79 | write(fd2,buf2,strlen(buf2)); 80 | close(fd); 81 | close(fd2); 82 | } 83 | return 0; 84 | } 85 | ``` 86 | 87 | 使⽤共享内存可以不新建一个临时文件存储数据,而是可以直接对共享内存读取。 88 | 89 | 蒋博文答案: 90 | 91 | ```c 92 | //q5.c 93 | #include 94 | #include 95 | #include 96 | #include 97 | #include 98 | #include 99 | #include 100 | 101 | int main(){ 102 | char buf[100]; 103 | int fd=open("in", O_RDONLY); 104 | int fd1=open("guandao",O_WRONLY);//命名管道 105 | while(read(fd,buf,1)>0) 106 | write(fd1,buf,1); 107 | close(fd);close(fd1); 108 | } 109 | 110 | //q5_2.c 111 | #include 112 | #include 113 | #include 114 | #include 115 | #include 116 | #include 117 | #include 118 | 119 | int main(){ 120 | char buf[100]; 121 | int fd=open("out", O_WRONLY); 122 | int fd1=open("guandao",O_RDONLY); 123 | while(read(fd1,buf,1)) 124 | write(fd,buf,1); 125 | close(fd);close(fd1); 126 | } 127 | ``` 128 | 129 | ![fig](img/jbw_res.png) 130 | 131 | 可以通过其他进程实现数据传输,比如使用共享内存的方法,一个进程将文件里的内容放入共享内存中,另一个进程从共享内容中读取内容并输入到目标文件中。 132 | 133 | 周海涛答案: 134 | 135 | 写入管道: 136 | 137 | ```c 138 | #include 139 | #include 140 | #include 141 | #include 142 | #include 143 | #include 144 | #include 145 | 146 | #define N 32 147 | #define FIFOPATH "/home/oem/Desktop/Code/ripe" 148 | 149 | int main(int argc, const char *argv[]){ 150 | int fd_fifo,fd_src,ret; // 151 | char buf[N] = {}; 152 | 153 | if(mkfifo(FIFOPATH,0666) != 0) 154 | if(errno != EEXIST){ 155 | perror("fail to mkfifo"); 156 | return -1; 157 | } 158 | 159 | fd_src = open(argv[1],O_RDONLY); 160 | fd_fifo = open(FIFOPATH,O_WRONLY); 161 | 162 | while(1){ 163 | ret = read(fd_src,buf,N); 164 | if(ret == 0) break; 165 | write(fd_fifo,buf,ret); 166 | } 167 | close(fd_fifo); 168 | close(fd_src); 169 | return 0; 170 | } 171 | ``` 172 | 173 | 从管道中读取: 174 | 175 | ```c 176 | #include 177 | #include 178 | #include 179 | #include 180 | #include 181 | #include 182 | #include 183 | 184 | #define N 32 185 | #define FIFOPATH "/home/oem/Desktop/Code/ripe" 186 | 187 | int main(int argc, const char *argv[]){ 188 | int fd_fifo,fd_dest,ret; 189 | char buf[128]; 190 | 191 | if(mkfifo(FIFOPATH,0666) != 0) 192 | if(errno != EEXIST){ 193 | perror("fail to mkfifo"); 194 | return -1; 195 | } 196 | 197 | fd_fifo = open(FIFOPATH, O_RDONLY); 198 | fd_dest = open("out.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666); 199 | 200 | while(1){ 201 | ret = read(fd_fifo,buf,N); 202 | if(ret == 0) break; 203 | write(fd_dest,buf,ret); 204 | } 205 | close(fd_fifo); 206 | close(fd_dest); 207 | return 0; 208 | } 209 | ``` 210 | -------------------------------------------------------------------------------- /week13/answer_template.md: -------------------------------------------------------------------------------- 1 | # Week13 Assignment 2 | 3 | > 班级: 4 | > 学号: 5 | > 姓名: 6 | 7 | ## 1. 简述信号量的作用,如何利用信号量实现同步和互斥? 8 | 9 | ## 2.简述共享内存的作用和用法,共享内存为什么需要与信号量一起使用? 10 | 11 | ## 3. 简述消息队列的作用和用法,并将其与管道进行异同点对比 12 | 13 | ## 4. 请查阅资料简述进程间通信的 System V、POSIX 两种标准之间的差异性 14 | 15 | ## 5. 请编写一个管道通信程序实现文件的传输,并请思考采用其他进程通信方式是否可方便实现进程间的文件传输 16 | -------------------------------------------------------------------------------- /week13/img/jbw_res.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BUAA-SE-2021/sp-labs/1dc870aee9c945a62ed525cd5941c854d81bf376/week13/img/jbw_res.png -------------------------------------------------------------------------------- /week14/answer.md: -------------------------------------------------------------------------------- 1 | # Week14 Assignment 参考答案 2 | 3 | ## 1. 在进程部分,有父子进程的说法,有没有父子线程的说法,能说说理由吗? 4 | 5 | 因为在进程部分,父子进程存在着严格的关系,当父进程消失了,子进程就变成了孤儿进程,同时父子进程使用的是独立的资源区域。而在线程部分,当父子线程可以并发执行,父线程结束了子线程照常执行,同时父子线程共享相同的资源,并无严格关系,因此一般也就没有父子线程的说法。 6 | 7 | ## 2. 进程和多线程,都有同步和互斥的问题,分析下异同点。对比下两者同步和互斥机制的特点及异同之处 8 | 9 | - 同步 10 | - 线程同步:当多个线程相互协作,存在相互依赖的关系;当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。 11 | - 进程同步:它是指为了完成某个任务而建立起的两个或多个进程,这些进程在完成任务时,需要协调它们之间的执行顺序。 12 | 13 | 主要区别在于进程一般需要协调执行顺序,而线程没有,且线程(或进程)之间的通信方式不同。 14 | 15 | - 互斥 16 | - 线程互斥:包括临界资源等的访问,相互线程之间是互斥访问有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。 17 | - 进程互斥:当一个进程访问临界资源时,另一个进程想访问临界资源就必须等待。等第一个进程访问完,第二个进程才能去访问。 18 | 19 | 进程互斥需遵守空闲让进、忙则等待、有限等待、让权等待的规则。 20 | -------------------------------------------------------------------------------- /week14/answer_template.md: -------------------------------------------------------------------------------- 1 | # Week14 Assignment 2 | 3 | > 班级: 4 | > 学号: 5 | > 姓名: 6 | 7 | ## 1. 在进程部分,有父子进程的说法,有没有父子线程的说法,能说说理由吗? 8 | 9 | ## 2. 进程和多线程,都有同步和互斥的问题,分析下异同点。对比下两者同步和互斥机制的特点及异同之处 10 | --------------------------------------------------------------------------------