My notes and solutions for The Linux Programing Interface (TLPI)
《UNIX/LINUX 系统编程手册》笔记和解答
(代码可以从 README 的超链接或者/Exercise 文件夹下对应章节数字找到)
- 环境搭建
- CH3 系统编程概念
- CH4 文件 I/O:通用的 I/O 模型
- CH5 深入探究文件 I/O
- CH6 进程
- CH7 内存分配
- CH8 用户和组
- CH9 进程凭证
- CH10 时间
- CH11 系统限制和选项
- CH12 系统和进程信息
- CH13 文件 I/O 缓冲
- CH14 文件系统
- CH15 文件属性
- CH16 扩展属性
- CH17 访问控制列表
- CH18 目录与链接
- CH19 监控文件事件
- CH20 信号:基础概念
- CH21 信号:信号处理函数
- CH22 信号:高级特性
- CH23 定时器与休眠
- CH24 进程的创建
- CH25 进程的终止
- CH26 监控子进程
- CH27 程序的执行
- CH28 详述进程创建和程序执行
- CH29 线程:介绍
- CH30 线程:线程同步
- CH31 线程:线程安全和每线程存储
- CH33 线程:更多细节
- CH34 进程组、会话和作业控制
- CH35 进程优先级和调度
- CH36 进程资源
- CH37 DAEMON
- CH38 编写安全的特权程序
- CH39 能力
- CH40 登录记账
- CH41 共享库基础
- CH42 共享库高级特性
- CH43 进程间通信简介
- CH44 管道和 FIFO
- CH45 System V IPC 介绍
- CH46 System V 消息队列
- CH47 System V 信号量
- CH48 System V 共享内存
- CH49 内存映射
- CH50 虚拟内存操作
- CH52 POSIX 消息队列
- CH53 POSIX 信号量
- CH54 POSIX 共享内存
- CH55 文件加锁
- CH57 SOCKET: UNIX DOMAIN
- CH59 SOCKET: Internet DOMAIN
- CH60 SOCKET 服务器设计
- CH61 SOCKET 高级主题
- CH62 终端
- CH63 其他备选的 I/O 模型
- CH64 伪终端
wget https://man7.org/tlpi/code/download/tlpi-201025-dist.tar.gz
tar -zxvf tlpi-201025-dist.tar.gz
cd tlpi-dist/
make
cd lib/
sudo cp tlpi_hdr.h /usr/local/include/
sudo cp get_num.h /usr/local/include/
sudo cp error_functions.h /usr/local/include/
sudo cp ename.c.inc /usr/local/include/
如果没有静态库的话需要手动创建,现在的新版本并不需要这一步,所以这一步不做
g++ -c get_num.c error_functions.c
ar -crv libtlpi.a get_num.o error_functions.o
sudo cp libtlpi.a /usr/local/lib
运行需要链接 libtlpi.a 静态库
g++ main.cpp -o main -l tlpi
#include <unistd.h>
#include <sys/reboot.h>
int main(){
reboot(RB_AUTOBOOT);
return 0;
}
注意使用 root 权限。RB_AUTOBOOT 是定义在 reboot.h 头文件中的。如果我们换成 RB_POWER_OFF 则系统直接关机。
在其他的 Linux 中,我们也可以使用如下的程序进行重启。(然而我的 Ubuntu 20.04 发行版运行不了,只可以上面方式运行,不过这是发行版和具体版本的问题,毕竟书都过了六七年了。)
#include <unistd.h>
#include <linux/reboot.h>
int main(){
reboot(LINUX_REBOOT_MAGIC1, LINUX_REBOOT_MAGIC2A, LINUX_REBOOT_CMD_RESTART, 0);
return 0;
}
这里的 LINUX_REBOOT_MAGIC1、LINUX_REBOOT_MAGIC2A 参数在 linux/reboot.h 中被定义,这些参数是 Linux 的作者 Linus Torvalds 自己和他三个女儿的生日。
我又稍微研究了一下,发现 warning: implicit declaration of function,linux/reboot.h 这个头文件没有声明 reboot 函数,出于别的考虑吧,再说应该用 glibc 的 reboot 比较好吧。写下声明,这样 main 函数内的调用就能找到 reboot 函数了。
#include <unistd.h>
#include <linux/reboot.h>
int reboot(int);
int main()
{
reboot(LINUX_REBOOT_CMD_RESTART);
return 0;
}
没什么难度,就是 getopt 函数学了半天,恼了
有一个问题,就是无法真正的区分源文件中的连续 0 或是真实的连续空洞。以前的系统中的 cp 命令类似这样实现的,并没有真正的做到复制空洞。但是现在的(我用的 Ubuntu 20.04)已经能处理这种情况,完成正常复制空洞文件了,但是作为练习这种程度已经差不多了,我现在也看不懂源码。。。
首先说下第五章开头那句所有系统调用都以原子方式执行这句话是是错的,只是大部分完全符合原子性,另外一些需要别的措施来保证同步,还有一些系统调用会被信号中断,还有部分中断的系统调用恢复的情况,不能一概而论。而且原子性需要看语义上下文,可以看这个链接有帮助。
https://www.zhihu.com/question/46552411/answer/130482168
要么在所有头文件前(头文件里可能对这个宏有需要)#define _FILE_OFFSET_BITS 64,要么编译的时候-D_FILE_OFFSET_BITS 64 一样的效果,由于电脑本身就是 64 位,所以看不出效果,如果是 32 位的系统那么就可以突破文件大小 2GB 的限制了。
当然会在最后写入咯,因为 APPEND 之后 write 当然是先改变偏移量到最后,再写入,当然这是一组原子性的动作。(当然 read 不受影响)
多个进程同时写同一个文件的情况,如果没有 APPEND,那么一个进程刚刚 write 后但在更新偏移量前,另一个进程可能在当前偏移量写,从而覆盖,造成文件变小。(代码太简单就没写)
话说 APUE 也有这道题欸,能用 fcntl 果然舒服多了,虽然那个是直接用 dup 貌似更简单。。。
当然共享,太简单不做了。
书上印刷错误,是" world",所以是 Gidday world
- open 文件 O_CREAT 要用八进制或是直接用宏,我一直用十进制。。。服了我自己了。。。🤮
- 别再误删了。。。不要直接用 vscode 在 ssh 下删除,因为真的找不回,最好 alias rm 变成移动到某个文件夹,因为误删真的太恐怖了,多 commit,不用 push
- 实现倒是很简单,没啥要说的,另外用不用 tlpi_hdr.h 根本无所谓,我单纯是为了少些几行代码
bss 区只记录需要多大的空间,但是并不实际分配空间给它,我 ll 和 du 看了,确实占用的空间不关 bss 的事。
拓展:而数据段是要实际分配空间的,我给全局变量int a[100000000] = {1}
,虽然只有第一个元素是 1,但这个数组也算是初始化了(不过={0}仍然算在 bss 区,不过这个无所谓啦),然后 ll 和 du 了一些可执行文件,确实变的很大。
#include <stdio.h>
#include <setjmp.h>
jmp_buf env;
void func()
{
longjmp(env, 1);
}
int main()
{
if (setjmp(env) == 0)
{
func();
}
return 0;
}
可以看见增长幅度挺大的,多次 malloc 只需执行一次 sbrk 系统调用,来提高效率,这种思路非常非常常见。(比如 vector 等等)
这个完全就是 CMU15-213 的 malloc lab,在那个 lab 中要求你编写一个动态内存申请器(malloc,free,realloc),我的实现代码。 我的思路是隐式分离空闲链表,所谓隐式的意思是使用大小来定位前后的块(指针形式叫显式),分离的意思是有多个有着不同大小范围的链表,每个链表维持着一组在一定范围大小的(2^n-1~2^n)的块,使用 2 的幂,<<1 来快速找到合适的。 合并的思路是看空闲标志位,free 的时候也许前后都是空闲,来合并,根据合并后的大小插入合适的链表中。
显然这个问题本身错了,作者也在勘误中修改了,因为这里的(long)getpwnam("name")->pw_uid 是传递值直接给%ld 了,后面的又不会改变这个值。
拓展一下:
当一个函数带有多个参数时,C/C++语言没有规定在函数调用时实参的求值顺序。而编译器根据对代码进行优化的需要自行规定对实参的求值顺序。
有的编译器规定自左至右,有的编译器规定自右至左,这种对求值顺序的不同规定,对一般参数来讲没有影响。但是,如果实参表达式中带有副作
用的运算符时,就有可能产生由于求值顺序不同而造成了二义性。
同样的对于参数有多个函数调用的情况,函数调用的顺序也未定义。
非常简单,代码实现。
real effective saved fs
a. 1000 0 0 0
b. 1000 2000 2000 2000
c. 1000 2000 0 2000
d. 1000 0 0 2000
e. 1000 2000 3000 2000
不具有,因为特权只看有效用户 ID(不过因为 real 是 0 很容易回到特权模式)
使用set-root-user-ID,来让程序有权限读取密码文件
gcc 9_3.c -Wall
sudo chown root:water a.out
sudo chmod u+s a.out
这个很有意思,因为 get 和 setgroups()函数针对的是调用者 ID(即属主即实际的 ID),然后 initgroups 还会指定用户名,所以说进程的切换也是挺有意思的,不过 C 风格字符串的操作真是让人头大。
int old_eff = geteuid();
(1)
seteuid(getuid()); // 暂时有效切换到实际
seteuid(old_eff); // 恢复,因为保存UID的值允许
// 注意多种实现完全都可以,另外setuid和getuid完全作用不同
(2)
setreuid(getuid(),getuid()) // 永久放弃,r不是-1(即使值没变)
// 也会修改保存UID变成有效UID(当然是设置完后的新有效UID)
int old_eff = geteuid();
(1)
seteuid(getuid()); // 暂时有效切换到实际
seteuid(old_eff); // 恢复,因为保存UID的值允许
//此时setuid不能了,因为是特权用户,会把保存UID也改了,所以最好一直用seteuid好用
(2)
setreuid(getuid(),getuid()) // 永久放弃,r不是-1(即使值没变)
// 也会修改保存UID变成有效UID(当然是设置完后的新有效UID)
times 的单位是 sysconf(_SC_CLK_TCK), (2^32 - x) / sysconf(_SC_CLK_TCK)
clock 的单位是 CLOCKS_PER_SEC, (2^32 - x) / CLOCKS_PER_SEC
孩子只有 linux 系统(ㄒ o ㄒ)
ext4 file system:
_PC_NAME_MAX, 255
_PC_PATH_MAX, 4096
_PC_PIPE_BUF, 4096
其实如果不是读取/proc 下的文件的话,sysinfo/procfs_pidmax.c 是有 bug 的,因为 write 的字节数如果少于之前的,那么只是单纯覆盖一部分,而剩下的仍然是原来的值,比如 99999 写 123,变成 12399;当然这里并不是 bug,因为读取的/proc 其实是个伪文件系统,并不是普通的文件。不过这写法真的是让我困扰了好一阵。。。不过这个写法真的恶心人啊。。
代码实现本身不难,但是 C 的处理字符串和繁杂程度倒是有点恶心,另外量有点多。。。
代码实现在上一问的基础上改了改完成,另外想要做的好看太难了。
代码实现又是在上一问的基础上修修改改,读写目录,读写链接技能 get🤮。
代码实现
使用 O_SYNC 使得 write 需要等待内核把数据从高速缓存刷新到磁盘之后才返回,因为实在是太慢了所以直接从缓存大小 1024 开始了
简单 time 测试即可
fflush(fp);
fsync(fileno(fp));
fflush 是把 fp 流对应的 stdio 缓冲区全部刷新进入内核高速缓存,然后 fsync 是把 fp 对应的 fd 所对应的内核高速缓存(这次 fflush 放入的+之前放入还没刷新的)刷新到磁盘上;如果没有 fflush 只有 fsync 那么只是刷新 fp 对应的内核高速缓存到磁盘,而 fp 对应的 stdio 缓冲区没刷新。
printf("aaa\n");
write(STDOUT_FILENO,"bbb\n",xxx);
(1)未重定向 printf 指向终端是所以 stdio 缓冲区是行缓冲,而且有\n,那么 printf 直接刷入内核高速缓存显示,然后 write 写入内核高速缓冲显示,所以结果是
aaa
bbb
(2)定向到磁盘文件 printf 指向不是终端而是磁盘文件,所以 stdio 缓冲区是全缓冲,那么 printf 放到 stdio 缓冲区,然后 write 写入内核高速缓冲区显示,这里 stdio 缓冲区也没满,是 return 销毁 main 栈帧后调用 exit,这才刷新 stdio 缓冲区才放到内核高速缓冲的,所以这个顺序的结果是
bbb
aaa
代码实现 本来以为要 debug 好几次的,结果发现写的竟然直接能跑,惊喜
lseek 快速定位到文件最后-一个 bufsize 的位置,读入这一个 bufsize,然后倒着顺序遍历检查里面多少个'\n',不够那么再循环-bufsize,知道 lseek 定位到对应倒数对应行数开头,(文件位置是属于打开文件描述的属性,所以之后直接 write 就是从这个位置属性后开始的),然后 write 输出即可
我只有 ext4 和 tmpfs,tmpfs 是/run 路径挂载的
more /proc/mounts | grep tmpfs
发现有 none /run tmpfs rw,nosuid,noexec,noatime,mode=755 0 0
发现有序建立再有序删除比随机建立再有序删除快
因为是 tmpfs 是建立再内存上而不是磁盘上的文件系统,所以快非常多,同样符合有序建立再有序删除比随机建立再有序删除快
NOTE: 一个比较有意思的关于 setuid 和 seteuid 的区别,就是 setuid 会把保存用户 id 也设置,这样特权用户就无法恢复了,而 seteuid 不改变保存用户 id 可以恢复,对应的组 id 也是这样
a)只匹配一个,如果用户对应就不管有无权限,不会再检查组和其他的了。
c)
目录 文件
创建文件: wx -
打开读: x r
打开写: x w
删除文件: wx -
重命名: wx -
重命名存在: wx - (会覆盖)
sticky下特殊就是除了要有权限,还要是属主才行
当然不会,因为我们是通过 stat 获取信息的,包括获取时间信息,如果改了那么就没意义了。
这是代码 简单地暂时把有效改成实际,检测完再改回来即可。
mode_t old_mode = umask(0);
umask(old_mode);
不过这里中间改变了一次,不具有原子性,可以通过加锁来得到。
Note: open 自动解引用,如果不想解引用那么 O_NOFOLLOW 参数,这样如果是符号链接就会返回 ELOOP 错误,阻止打开,所以说 open 产生的 fd 不可能是符号链接文件的。
代码实现 这个是真的够恶心了,首先是确认是数字才能数值转字符串,然后转了但找不到根据 errno 判断是错误还是原本的数字就是名字;然后 ACL 的判断十分复杂恶心,然后需要文件属主和考虑掩码的问题,使用 ACL 的 API,最后根据不同情况判断权限还不能忘记一些情况要考虑掩码问题。
可以发现 inode 是变了的,说明不是修改了正在执行的二进制文件,而是 unlink 了之前的目录项(当然虽然名称是立即移除了,原进程仍然正常执行,文件是仍然存在的,直到运行结束删除),然后用了同一个名字(因为刚才刚 unlink 了所以可以)再建立了一个新的文件而已。
symlink()的第一个参数就是要设置给符号链接的内容,然后解释符号链接文件是以链接的路径为基础的,与当前所在目录无关,所以说这个地址应该是以符号链接文件所在位置为基准的地址。
代码实现 我才直到 fchdir,和 openat 之类的函数的 fd 是只能目录 fd,没我想的那么智能。。。所以 dirname 获取目录,然后得到目录 fd 即可。
代码实现 无语了,readdir_r 已经弃用了,这里只是为了做题用了下。"This function is deprecated; use readdir(3) instead. -- man page"
代码实现 C 的字符串处理和没有 STL 容器真恶心,不过没想到标准库里实现了 strrctr 倒序查找。
所谓的先序和后序指分别指的是遇到目录直接处理目录,然后直接进入下一级目录递归处理(这一层不先处理),处理完最子层之后回到上一层处理,这样;而后序指的是遇到目录则直接进入目录,但先不处理目录,等到下一层全部处理完回到该层的时候再处理目录,这两个都是深度遍历。
代码实现 需要至少#define _XOPEN_SOURCE 600 才有 nftw(),另外注意 nftw 一个目录时,里面的..父目录是不含遍历对象中的(显然父目录也不在目标目录内),而目录自身是含着的(名字是目录真的名字不是'.')。 (这与 readdir 不同,那个分别是'.'和'..')
代码实现 递归处理绝对路径和相对路径,处理. .. 目录项,真的阴间。
fchdir()在重复调用的情况下速度快,因为 chdir 首先需要在用户态和内核态传递数据即路径名字符串,注意这可不是函数传参传个指针这样,首先系统调用要陷入内核态,在内核态是内核页表,显然访问不到用户态的虚拟内存,所以会传递数据(这样的参数传递数据就是拷贝移动,不过对于大量数据传输的情况下,可以内存映射实现内核空间和用户空间的共享内存完成 IPC);另外 chdir 需要解析路径名得到 inode,虽然有高速缓存,但显然也要多做一些工作。
(关于用户态和内核态数据传输,举个最简单的例子,就是 write 把数据写入内核页缓冲,然后内核才能访问这样要写入磁盘文件的的数据,然后刷新线程之后会刷入磁盘,write 就是把用户态的数据(实参数据)传递给了内核(写入内核缓冲区,内核页表))
代码实现 注意 read 得到的一个 event 里面是可以同时代表多个事件的,这是因为多个同 wd,mask,cookie 的 event 可以合并,这样在生成速度大于消除速度时,可以提高效率减少占用内存,所以是别忘了把每一个要检查的位都要与一下,可能多个符合。
代码实现注意下不要忘了一定要用那两个函数初始化 sa_mask,因为首先自动变量里面是随机值,另外处于实现问题,里面初始化在不同系统实现中可能要求并不是全为 0 的这种初始化,所以 memset 和静态初始化都不行,只能调用那两个函数来初始化信号集变量。
SA_RESETHAND: 代码实现
注意下 SA_RESETHAND 调用 handler 前(调用前的意思是改完了还会调用一次 handler)改为 SIG_DEF,因而只调用一次 handler,下一次恢复了 SIG_DEF;但是对于 SIG_IGN 是保持不变,不会改为 SIG_DEF
SA_NODEFER: 代码实现
这个是 SA_NODEFER 使得可以在信号处理函数中递归调用 hander 自身,执行完返回上一个中断点(在上个调用 hander 的过程中中断的地方),恢复执行
这是 sa_flag=0 情况
原理图:
sa_flag=0 情况
SA_NODEFER 情况
代码实现 我的实现没有考虑刷新和关闭 stdio 缓冲区,因为看了别人的实现不知道要不要关 STDIN,有点茫然,以后补上。
代码实现 注意:只有信号处理函数调用(和终止)才能中断一个系统调用(前提是这个系统调用可以被中断),其他如 ignore、stop、cont 不会中断(eg: pause()后 kill-STOP 和 kill-CONT 不会中断 pause,pause 会继续等待被下一个信号中断)
代码实现 标准信号在实时信号前,标准信号顺序非从小到大,实时信号保持从小到大的先后顺序。
代码实现 System V signal API 已废止
NOTE: 400 页书上的代码那里 maxSigs 代码块印刷位置有问题,这种一眼看出来的错都没审出来,有点牛了。
代码实现 ITIMER_REAL 到时也是产生 SIGALARM 信号正好,不用手动处理信号再 raise 一个 SIGALARM 了。
代码实现 第一个使用相对时间,显然高频接受信号导致不断重启的情况下,重启的耗时(这里除了重启还有一些 printf 打印)不消耗剩余时间,造成嗜睡的现象,时间差距是非常非常大的,甚至如果是非常高频(就像这里,由于计时器取整问题,反而 remain 的时间会不断增加,永远也结束不了),是根本性错误,一定要注意;而采用了 TIMER_ABSTIME 绝对时间,就是自纪元后的一个具体时间点(一个秒数值这样的),用的是现实中的一个绝对时间点,这样重启的时间就被算入消耗了(因为真实时间在流动)。
另外 ABSTIME 轻微的延迟是因为唤醒之后等到 cpu 下次调度到这个进程也要一点点微小的时间。
代码实现 sigwaitinfo 来同步使用信号(挂起,让信号发生的位置固定,不再是异步信号那样不知道会再哪里处理(从而可能诱发竞争等问题))
7 个
一般先父进程调度,不过这个无法依赖,不同内核版本、不同操作系统的实现都可以是不一样的,而且有可能刚执行父进程有可能就调度了,反而看起来是子进程先行,再强调下无法依赖顺序。
终于明白了 exit 和从 main 中 return 的具体差距,那就是 return 是先析构 main 函数的栈,然后再隐式调用 exit 的,之后 exit 如果再使用 main 函数中的栈的内存就会未定义(比如段错误或者内存对应的值和预期不一样) <这里举个例子就是 return 后析构了 main 的栈帧,然后 exit 会调用退出处理函数,如果这个函数里面用到了之前析构的栈帧的局部变量就会未定义>; exit 直接在 main 中执行,此时 main 的栈仍然正常存在,而 exit 直接完成退出,没有 return 那一步,因此这里没有摧毁 main 栈帧;无论哪种形式退出后,最后再由操作系统来回收资源,如果没有 return 通过改变栈帧相关的寄存器从而释放栈帧,那么这里的 os 来释放所有资源。
举例这里用 vfork 举下例子,父子进程共享全部内存,这里头包括栈帧,因此子进程 return 析构了 main 的栈帧,那么父进程如果 return(重新释放一次 main 的栈帧),或者使用 main 中的自动变量都会未定义。 所以一定要注意,vfork 的子进程中最好不要使用函数返回 return 改变父进程的栈帧导致未定义。
代码实现返回 255,wait 历史原因一直使用 16 位(返回的 status 状态只有低 16 位用到了),正常返回的情况下只使用高 8 位,-1 的 16 位是 0x1111 也是 wait 返回的 status 值,但是注意虽然 wait 内部运算返回是 16 位的-1 值的补码,但 status 本身类型是 int,因此在程序看来决定符号的是第 32 位,(((status) & 0xff00) >> 8 得到结果是 0x11,这是 int 最高位是 32 位是 0,因此是正数,所以是 255
父进程被终止变成僵尸进程时子进程被收养。(这是必要的,因为父进程处于僵尸进程的时候是无法 wait 等待子进程的,如果父进程一直处在僵尸进程,那么子进程就算变成僵尸进程也无法被 wait 回收,因此当父进程变成僵尸时,子进程就是孤儿进程了,然后被 init 进程收养,然后 init 进程定期回收已终止的孤儿进程) (ps.写的代码 git 忘提交后误删了,吐了,实现很简单就是 sleep 打个时间差,然后在不同阶段让子进程打印父进程 id 即可)
代码实现看了看内部是一个联合 union 节省不同情况下的结构空间,有点意思,另外 si_status 是一个宏,难怪 vscode 没有自动填充,另外这个宏覆盖了我手动写的 si_status,导致出错,吐了 🤮,代码倒是很简单
sleep 确实是不安全的,因为如果在高负载或者特殊情况下,是有可能到了时间另外的进程也没执行完的。(另外就是 sleep 这个方式实际生产中也根本没用,因为休眠太浪费资源了,时间不好把控,而且还不安全)
代码实现
有点意思的就是通过全局变量标志来忽略其他信号,只有 SIGCHLD 信号到达才退出循环往下运行。
exclp()调用,xyz 中不含有/所以是只查找 PATH 路径,PATH 路径的查找方式是按顺序查找到第一个地方,之后就不查找了,所以这个查找到的是在 PATH 里面靠前的./dir1/xyz,因为权限没有 x,所以无法执行,错误退出 (ps.重点就是 PATH 中顺序查找,找到第一个就完成了,不会再找第二个)
与单次 fork 相比,最大的好处就是产生了一个被 init 收养自动被回收的进程,这样就可以不用考虑子进程的回收问题了,一些情况下可以大大降低问题的难度。两次 fork(父、子、孙进程)通过子进程的退出,使得孙进程被 init 收养,终止后自动被回收,父进程因为子进程直接退出也不用考虑在一些情况下会变得复杂的 wait 回收。
printf 没有重定向的情况下与终端关联,是行缓存的,所以先缓存到了用户态的 stdio 缓冲区,后面的 execlp 是加载新的程序到内存里面,会把当前进程的各种内存给覆盖掉,包括这个用户态的库函数里面的这个 stdio 缓冲区也被覆盖了,所以之前的缓存区的内容也没了。
代码实现 wait 自然会回收终止的僵尸子进程,解除阻塞后 pending 的 SIGCHLD 就会让进程接受 SIGCHLD 信号了。调用 system 的程序里面阻塞了 SIGCHLD 来完成正确的子进程回收,之后解除阻塞后调用 system 的程序会收到之前阻塞的一个 SIGCHLD 信号(pending 没有计数,无论阻塞过程中收到多少,pending 只是标志了收到过),这里需要明确并做好相应的信号处理。
注意:exec 执行脚本的参数好奇怪:
对于执行解释器脚本的exec系统调用,自动舍弃argv[0],而使用的是脚本名作为第三个参数,
第一第二个分别是解释器的绝对路径(从#!起始行获取),和起始行(#!这行的参数),
这三个参数都是操作系统自动实现的,当exec发现#!就知道这是解释器脚本要用解释器运行了
如果没#!写明要用的解释器的位置,那么自动使用/bin/sh解释器来解释这个脚本
注意:感觉这个好重要,书上怎么只说了子进程终止会发,没说其他情况会不会发。
子进程状态变更了,例如停止、继续、退出等,都会发送SIGCHLD信号通知父进程。
ERROR [EDEADLK/EDEADLOCK Resource deadlock avoided]这是在 linux 下发生的;当然其他 UNIX 系统可能会发生死锁(因为 pthread_join(pthread_self(),NULL)会阻塞直到等待的线程退出,而这个等待的线程正是它本身。它本身需要执行完 pthread_join 才可以退出,而 pthread_join 解除阻塞又需要退出,显然矛盾,发生死锁。)
if(!pthread_equal(tid, pthread_self())) // 确保tid不是该线程自己的POSIX thread id
pthread_join(tid, NULL); // 另外一定是要用pthread_equal来确保可移植性
主线程退出后主线程栈的内存之后会被重用,而创建的线程使用的指针指向已经被重用的原主线程栈的内存空间,这种行为未定义,可能造成严重后果
代码实现单纯熟练下 API,随便写写
未完成代码写楞了,怎么保证线程安全啊。。越想越搞不懂,也没写测试,只是写了写大概思路,等以后再搞一下。
代码实现利用互斥量实现 pthread_once
代码实现使用线程特有数据,避免了多个线程共享传入的 pathname 内存导致线程不安全。(由于 basenae 函数和 dirname 的代码大体一样,所以就写了一个,不过使用线程局部数据简单代码简单多得多) 简单试了试线程特有数据和线程局部存储这种方式,通过宏的方式都测试了下,写了个 Makefile(以后有空再修改下,这个写的太垃圾了,头疼。。)
待做,部分前置章节没看完。
不会,因为信号发送目标是进程,而 NPTL 已经非常接近 POSIX 标准,因而各线程共享进程 ID,因而某一个线程 fork 产生的子进程,发送 SIGCHLD 信号目标是父进程 ID,而线程共享这个进程 ID,因而可能发给该进程下的所有线程。
注意挂起(Ctrl+z)/阻塞(比如 I/O 处理)/休眠状态(sleep)是 S(这三个是一个东西),而停止(SIGSTOP)状态是 T,完全是另外一种东西;前者仍能处理信号,而后者不能,信号到来会放到 pending,只有 SIGCONT 恢复后才可以从 pending 取出信号处理。
因为停止的时候不能处理信号,这样之前处于停止的进程,当会话控制进程断开与控制终端联系,内核发给控制进程 SIGHUP 和 SIGCONT,控制进程一般是 shell,会处理这个 SIGHUP 信号,会发给会话前台进程租的所有进程,而如果之前处于停止的进程收到 SIGHUP 会放到 pending 而无法处理,所以不能单发一个 SIGHUP,而是发 SIGHUP 和 SIGCONT 来处理这种情况。
如果使用管道,那么这几个进程都是同一个进程组的,向前台进程组发会发给管道连接的另外几个进程造成误差。可以通过保持个子进程 pid 数字来一个一个对进程发信号。
代码实现 这个我可能没理解对题意。。
设置 SIG_IGN 后 raise 前来了个 SIGTSTP 会停止,SIGCONT 后立即又 raise(SIGTSTP)导致又停止,需要两个 SIGCONT;或是 raise(SIGTSTP)后加掩码前又来 SIGTSTP,这样也是需要两个 SIGCONT。
代码实现 当一个进程组变成孤儿进程组的时候,会成为后台进程组,从而如果 read 终端会 EIO。
外面比较的是 CPU 时间,现实时间根本和进程能占据 CPU 使用 CPU 的时间没关系,因为不同载荷的情况下现实时间一点作用都没有,所以用 CPU 时间看性能。
代码实现 CPU 时间是用户进程 CPU 时间和内核 CPU 时间之和,注意这是这个进程的 CPU 时间,不是实际时间,因为 Linux 是分时多任务系统,所以显然现实世界的一块时间的一小部分才是这个进程占据 CPU 为己所用的时间。
我真是弱智,忘记了多核处理器并行的问题,确实实时的 FIFO 调度策略占据了一个核心,但是还有另外 7 个处理器核心呢,就是普通的 OTHER 进程也正常地运行着呢,另外也不想想如果真的只有一个核心跑了那个进程,那我不就死机了吗,无语,所以说别忘了设置 CPU 亲和力。
代码实现 管道(匿名)或者 FIFO 文件就是一块内存,和硬盘没任何关系,和共享内存本质基本一样,只是外在行为不一样。
同 CPU 这样 CPU 高数缓冲器更好的利用,不同 CPU 为了一致性还需要失效缓冲的内容,降低效率。
注意,比较的时间一定是花费的 CPU 时间,因为不同 CPU 跑的时候,并行当然更快,花费的现实时间当然少,但是 CPU 时间是增加的。(就是效率低了,但多用了一个 CPU 核心并行,本质上是浪费效率了(因为原本多一个 CPU 可以跑别的,原本是 1 个跑 1.5s,现在是两个各跑 1s,少了 0.5sCPU 时间这样))
注意: RUSAGE_CHILDREN Return resource usage statistics for all children of the calling process that have terminated and been waited for. These statistics will include the resources used by grandchildren, and further removed descendants, if all of the intervening descendants waited on their terminated children. manpage 上写的多清楚,书上一开始写的有点误导(虽然后面仔细说明了)
代码实现
可以非常明显地看出子进程的资源使用信息,是在它被父进程 wait 调用的时候更新的,因而无论是正在运行还是终止都不会记录信息,只有被 wait 后才有(SIG_IGN 处理 SIGCHLD 也不会有信息)。
代码实现
可以发现超出文件大小软限制的情况下 write 并没有报错,errno 也没有设置出错,但是写入的大小超出 RLIMIT_FSIZE 的部分被截断舍去。
代码实现 注意下不同 linux 发行版的默认日志路径不一样,我的 ubuntu 20.04 是在 /var/log 里面的 syslog。
代码实现 发现修改过文件后不再是 set-user-ID 程序了,文件被一个非特权用户修改之后,内核会清除 set-user/group-ID 位(强制式加锁是 set-group-ID 和禁止执行位)
直接给我整蒙了,原来现在库函数改了,要链接-lcrypt 库,然后#include <crypt.h>。
代码实现 getpwnam()和 getspnam()然后与用相同密钥单向加密 crypt()比较,可以实现登录功能,真的神奇。
代码实现 别忘了给文件赋予 cap_sys_nice 的能力。。。
代码实现 里面使用了 ttyname,getutxent 都是不可重入函数,所以调用这个答案会影响这两个静态区,同样这个答案也是不可重入的。
代码实现 搞不懂那个 non_string 属性,不过写完了
logwtmp 函数
另外登录登出有点复杂暂略(不过里面更新登录记账日志的部分很简单,复杂的内容和本章无太大关系)
代码实现 WSL 下和 who 有点差别,不知道实际有没有
首先使用共享库,是要先执行静态链接(编译生成可执行文件的时候)再执行动态链接(运行的时候)的,而静态库则只有静态链接。
然后注意静态链接的时候要(-L)指定链接库路径(不管是静态还是动态库,默认搜索路径是/usr/lib /lib /usr/local/lib(ubuntu 可以)等);然后要运行程序动态链接的时候也要指定动态库的路径(这个自带的路径挺奇葩的,/usr/lib /lib 都有,但我电脑/usr/local/lib 却没有,明明静态链接的默认搜索路径是包含的,说明这两个没啥关系),当然还有个写在/etc/ld.so.conf 里面的也是默认搜索,不过注意这个能找到前提是你先运行过 ldconfig 生成好索引了,我把/usr/local/lib 写入 ld.so.conf 然后运行了下 ldconfig 就好了。
代码实现 为了方便,没有弄 soname 和链接器名字等一堆事情
代码实现 别忘了开个 GLOBAL 让后面 mod2 可以依赖 mod1,以前 dlopen 是自动导入共享库所依赖的共享库的,现在改了不行了无语
代码实现 gettimeofday 做差提供高精度的时间差,用于计时挺好用的。
to do
代码实现 写了半天,C 风格字符串真的恶心,总是不知道有没有必要处理'\0',吐了。。一不小心把 pipe 写在 fork 后面,分别创建了两个管道,我还傻傻查了半天错。。。
代码实现 在库函数里面用了全局变量来处理该等待哪个子进程,static 用了
代码实现 把代码放到 fifo_seqnum_server.c 后,make all 生成可执行文件。
代码实现 复习一下 unlink,unlink 的作用是立即删除硬链接(文件表项),并减少 inode(文件本身)的引用计数(只有硬链接算,软链接不是),当 inode 引用计数为 0 时真正删除文件本身。但是注意即使引用计数为 0,但如果仍有打开文件描述符(本质是通过 open 系统调用)指向 inode 项,那么先不删除 inode(因为删除 inode 之后打开描述符就什么也干不了),这样不会影响已经打开的描述符,直到所有打开描述符关闭后,inode 才被删除。(ps. inode(文件本身)的引用计数是硬链接)
另外因为 SIGINT 和 SIGTERM 两个信号用了同一个信号处理函数,为了避免一个信号中断调用 handler 的时候又被另一个信号中断调用同一个 handler 造成一些困扰,最典型的是造成竞争条件(虽然 handler 本身要求最好是可重入,这种情况不受影响),或者因为要退出并做一些清理工作,会造成退出前做了多次清理工作的 bug,因此为了避免这个当一个信号中断的时候要把掩码加上别的共用同一个 handler 的信号。
unlink(2) — Linux manual page:
unlink() deletes a name from the filesystem. If that name was
the last link to a file and no processes have the file open, the
file is deleted and the space it was using is made available for
reuse.
If the name was the last link to a file but any processes still
have the file open, the file will remain in existence until the
last file descriptor referring to it is closed.
If the name referred to a symbolic link, the link is removed.
If the name referred to a socket, FIFO, or device, the name for
it is removed but processes which have the object open may
continue to use it. (这种情况下和对普通的file操作没区别)
在服务器关闭和重新打开 server 的 FIFO 之间,如果 client 写打开 FIFO 会错误,导致请求失败。
代码实现 阻塞攻击代码 使用轮询,正常情况下即使 server 在 client open FIFO 前打开,也会在 20000 次以内 client open FIFO,阻塞攻击的情况下,20000 次仍失败则处理下一个请求,通过打印基本可以肯定在 20000 时几乎没问题,这样超出 20000 我们认为是攻击会退出后处理下一个请求(可以设大一点因为打开成功后就 break 实际上不会跑这么多次,对于阻塞攻击多给些时间来确保也是正确的)
代码实现 这个简单,舒服了
note: 只有 exit()和从 main 中 return 调用退出注册函数,被信号异常终止和_exit()不调用。
代码实现 非常容易,顺便复习下 stat 写法
代码实现 和练习一没啥区别
首先注意一下如果是 IPC_PRIVATE 作为 key 创建的标识符显示是 0. 代码实现 公式符合,但是 seq 字段的行为不同,是因为 linux 内核升级改变了计算的方式,吐了,我还以为我错了。 发现 ipcrm 的参数也变了,变了不少。。。
注意: A successful close does not guarantee that the data has been successfully saved to disk, as the kernel uses the buffer cache to defer writes. 所以说 close 不保证立即写回磁盘,记得 O_SYNC 之类的标志。
注意:自定义类型中 long 之后可能出现字节对齐导致的 padding bytes,这种情况下通过 offsetof 或者整个 sizeof - sizeof(long)可以得到真实的剩余部分的所占用空间,在自定义消息类型的 mtext 的大小十分关键。 举例
注意:打开作用的 msgget 的权限完全可以不写,只是单纯检测是否拥有权限,对后面实际拥有的权限没有任何影响,不过这个检测挺方便的。
注意:发现个有意思的地方,那就是命令行中输入的参数如果是"a"会自动忽略"",要"转义才不会被 shell 忽略。
因为有些参数的功能使用的消息类型来确定选择顺序,当然不能用了。
to do(仅完成第一问)
to do
to do
代码 a
代码 b
可以看出来其实 UNDO 本质上就是一次取相反值的 semop 操作,因为进程终止的时候做的撤回操作,同样会改变 SEMPID 的值。
代码实现 fcntl 设置 FL 是文件状态标志,文件描述符标志才是 FD 而且现在就一个 CLOSEXEC 这一个
代码实现 和 System V 消息队列基本一样用法,SEM_INFO 前两个参数都设成 0,因为这是总体的资源限制一个属性,放到 arg.__buf,返回是最大正在用的下标;SEM_STAT 把信号量集的相关结构放到 arg.buf,里面有对应该信号量集的属性,参数是所在下标,返回标识符;从 0 开始到最大下标遍历,中间可能有不存在 EINVAL(因为中途删除了该下标,同时还没来得及被重用的时候)和 EACESS(没有读权限(某些操作系统连读权限都不需要)),记得忽略这两个错误,根据 SEM_STAT 获取相关下标元素的属性来打印。
代码实现 没想到 tlpi 里面有 eventFlag 和我的头文件名字相同,makefile 没写明白一开始用的是他的实现,无语了,虽然结果一样,后来手动链接的。。。我一定以后好好学一些 c/c++的链接问题。。。
因为 bytes += shmp->cnt 这个操作放在了信号量保护的区域之外,而 shmp->cnt 是被多个进程共享的,如果没有同步保护机制会发生竞争条件。具体来说这里就是当 write 后释放了写者信号量,如果在更新 bytes 前调度到另外的进程,更新了 shmp->cnt 这个共享变量,当再度返回更新 bytes 时就会使用错误的 shmp->cnt 的值,出现竞争错误。
to do
代码实现 和之前的 System V 的消息队列和信号量的操作基本一样,就是别忘了忽略 EINVAL 和 EACESS 这两个错误。
代码实现 注意超过全部界限不一定是 SIGSEGV,这是未定义的(看你具体访问到哪个地方了)
note: 千万注意要用 mq_的系列函数,比如 mq_unlink 才会删除,而 unlink 是不会删除的。
note: timespec 是秒和纳秒,clock_gettime 对应 timespec,注意如果是用绝对时间做定时,要把定时的时间加上当前时间。
note: 一个消息队列对象是发送和接受同一种类型的(这样才能正确解析),所以 msgsize 可以设置成要发送的消息的大小,同时用 attr.msgsize 作为 receive 的参数(必须是大于等于这个的,否则会 EMSGSIZE 错误,因为可能无法接收可能到来的最大的消息);不同消息队列的对象可能用的类型不一样,比如 server 客户发送请求和服务器解析请求,client 服务器发送响应,客户接受响应,注意同一个消息队列对象的发送和接受的类型是一样的。
to do
linux 中实现是创建新线程来处理通知,如果一个线程处理完前,多个通知到来,导致多个线程并发处理通知,但由于使用了共享的全局变量 buffer,会导致竞争问题。
代码实现 别忘了删除(unlink 或者在/dev/shm 删除对应(注意我们的/name 会自动转换未 sem.name 的虚拟文件名))
to do
代码实现 Machine: AliCloud-ECS Linux-Ubuntu 20.04 2G 4cores thr-num mutex ratio posix ratio systemV 1 0.002 1.5 0.003 39 0.117 4 0.013 8.1 0.105 36 3.826 16 0.079 8.3 0.659 2.4 16.05 可以发现,当线程并发程度增加(冲突的比例上升)的时候,posix 和 system V 信号量的比例在下降,这是因为 posix 只是在出现冲突的时候调用系统调用,而 system V 总是调用系统调用,当并发程度上升,posix 调用系统调用的比例上升(即轻松的工作比例下降,重活比例上升),而 system V 一直是调用系统调用(全是重活的比例),所以会导致比值下降。
NOTE:注意 mmap 的读需要用读方式打开,写则要求读写模式打开共享内存段,而 ftruncate 与访问权限无关。 NOTE:注意 ftruncate 是对共享内存段操作,所以说 shm_open 创建的时候初始化共享段大小,之后打开是根本不需要的。 NOTE:有点不适的是对于 shm_open 关闭是 close,而删除又是 shm_unlink,虽然我知道这是因为 close 实现已有,但还是感觉有点乱。(例如 xx_unlink 不同与 unlink 哦,普通的 unlink 删除无效的)
代码实现 注意 mmap 的权限是 PROT_XX 的格式,打开权限是 O_XX,文件模式是 S_IXX,这些宏的值都不一样,别再搞错了。。。
mount -o remount,xxx /dir 这种形式,如果需要清除后面加的就把,xxx 去为空即可。
-
55.1.a 代码实现 可见连续不断的读(共享锁会造成写(互斥锁)饥饿)
-
55.1.b 本质上只是说获取的顺序是取决于调度策略,非 FIFO,取决于调度策略意味着如果改成实时策略,改了不同优先级,自然后面优先级高的先获取锁,是取决于调度的顺序获取锁,而不是先请求先获取的 FIFO。
-
55.1.c LINUX 和大部分 UNIX 都是这样,至于少部分的 UNIX 我是不知道了。
代码实现 证明确实 flock 文件锁不像记录锁一样会检查死锁
发生死锁了,休眠只有 3s,但已经过了好几分钟了
代码实现 运行可以看出 flock 文件锁和 fcntl 记录锁互相是不可见的(在 linux 上),一点相互影响都没有(当然一些 UNIX 用 fcntl 实现 flock 的不是这样)。
我犯了个弱智错误,flock 文件锁 open 给个 read 权限就都行了,互斥锁不需要;而 fcntl 记录的读、写锁需要对应 open file 的权限,不然会 EABDF 错误。
代码实现 循环用来放大倍速便于观察,发现的确是线性
代码实现 可见连续不断的读(共享锁会造成写(互斥锁)饥饿)
无 UNIX 环境,所以没做,不过 UNIX 平台实在太多了,各有不同,谁知道会发生什么呢。。。
代码实现 三个文件的依赖链导致死锁 发生死锁
代码实现 我挂载了-B -o mand 但是没用,因为我第一次用了 flock 锁,flock 锁不支持强制式锁,fcntl 记录锁才能支持(第一次挂载我 mand 属性也没加上,后面-o remount,mand 成功了)。
注意 g+s,g-x 是加到文件的属性,是文件的权限位,不是可执行文件的权限!
未开启强制式文件锁,未死锁
挂载 mond 属性,开启强制式文件锁,死锁
没看懂要我实现什么,暂时不做了。。。
代码实现 这是我随便试试 backlog 未决连接的作用。
代码实现 发送速率大于接受的时候,前面的是直接成功(未决数据报和之前的未决请求类似,由内核保持在未决数据报的队列),然后阻塞后面的发送,直到队列处理的一堆后再发送一堆(注意不是处理一个发送一个,而是处理一堆未决数据报之后再一次性发送一堆,这点和未决 connect 不一样,不过很容易理解,为了效率)
代码实现 使用 linux 抽象 socket 名空间非常简单,单纯把地址的 path 改成第一个是'\0'即可,当然是所有使用该地址的地方都要这样改,不只是 bind。
代码实现 因为 socket file(包括 linux 抽象 socket 名空间)与 socket 是一对一的,一个 socket 只能 bind 一个 socket file,而一个 socket file 也只能被一个 socket 绑定,所以会出现 ERROR [EADDRINUSE Address already in use] 错误
NOTE: 注意下,链接要有.o 或者静态库、动态库代码,别犯傻了。 NOTE: vscode 的 debug 本质是 gdb 套壳,gdb 使用一定要有-g 提供符号等信息,如果没有-g 的话 gdb 体现是没有各种代码信息,vscode 的体现是无法打开函数对应的源文件单步调试,尤其是注意.o/.a/.so 的-g 也是如此,不然是无法在对应的代码里面单步调试的。 NOTE: 注意下是不是调用的是原本就有的静态、共享库代码,根本就不是自己的,而自己的忘记编译了的弱智错误。。。 NOTE: 若 connect 失败则该套接字不再可用,必须关闭,我们不能对这样的套接字再次调用 connect 函数。在每次 connect 失败后,都必须 close 当前套接字描述符并重新调用 socket 。 NOTE: fflush 不一定实现刷新输入流,所以用 while((c = getchar()) != '\n' && c != EOF)最好
代码实现 结果是会被忽略,因为 a connect b 之后,a 的对等就是 b,internet domain 数据报的情况下会丢弃非对等 socket 发来的数据报
NOTE: ubuntu 没有默认安装 inetd,是一个进程监听一个 socket 服务的,从 ps -A 可以看出来一堆这样的进程。 NOTE: conf 没有服务启用的情况下 sudo /etc/init.d/openbsd-inetd start 启动不了正常
代码实现 connect 会阻塞到 accept
为了方便我已经生成了这个共享库放到/usr/local/lib,和.h 放到/usr/local/include 了。
#include <inet_sockets.h> // 是我们的第三方库,不是标准库
如果 write 写入非常大量的数据,服务端会把从发向服务器的缓冲区数据读取并写入发向客户端的缓冲区,但发向客户端的缓冲区的数据要等到 write 完成,如果 write 写入非常大量数据,那么最终会把双向的缓冲区都填满,导致死锁。
代码实现 socketpair 只能用于 UNIX 域,因为是在同一个进程创建两个端点,所以说这样才有意义。
代码实现 题目有点无语的是,这样实现只是模仿外在行为而已,根本没有比 read、write 性能提高。。。另外实际的 sendfile 的目的还必须是 socket,而这道题显然没必要,就随便写了。
代码实现 差点翻车,这些函数真恶心。
代码实现 这道题阴沟太多,翻车。。
- 在子进程一定要调用_exit(),除了 I/O 缓冲区的问题,更重要的是不做清理工作,比如临时文件,如果是 cpp 就更加重要,设计到一些析构函数的正确运行问题,所以要用_exit()。
- 遇到第二个问题就是我下意识在重定向之后使用了标准 I/O 库函数,而我忘记重定向到 socket 文件之后,现在不再是行缓存而是全缓冲了,甚至因为_exit 即使子进程退出也不会刷新缓冲区,自然不会实际发送过去。
- 经验之谈,子进程慎用标准 I/O 库函数。
- 带外数据最多 1 字节,如果发送多字节那么就只有最后一个字节会作为带外数据发过去(本题无关)。
- 这道题服务器要维护两个连接套接字,如果要同时检测的话,使用 I/O 多路复用、信号 I/O、epoll 模型可以实现。
- 这道题我是在回环地址上才能正常运行,如果客户在 NAT 后面,那么这个是无法运行的。
- 最搞笑的是我一直忘记处理僵尸进程了。。。之前的题也是。。。别忘了处理僵尸进程。。。
代码实现 查了半天没搞懂怎么通过 fd 和 rdev 找,结果看了看 ttyname 源码发现很简单。。。另外注意终端是字符设备,这很显然吧。。。
我用了 nftw 遍历,真的是为了方便反而更麻烦了,首先是 API 难用,然后是 level 要控制在 1 还损耗效率,然后离谱的是 nftw 自动是 stat,我还要再一次通过目录项名字来 lstat 判断链接文件,无语了,用 readdir 这种简单的 API 反而更方便,效率高。
代码实现 给使用 getpass 的程序重定向 STDIN_FILENO 发现仍然要从终端读,应该通过/dev/tty 的方式,但是那个行为和预期的不一样,搞不懂。
1.我傻了,原来 gdb 不能 SUID 的(set-user-id),这个是无用的 2.vsc 的 debugger 真弱鸡,printf 能打印找错最好,实在不行就上 gdb 吧。 3.read 读终端是行缓冲的,意思是读到\n,read 的返回值体现了这一点,确实是读入一行的读入长度;注意一下误区,那就是预读机制导致读到 buf 缓冲区的其实是能读多少读多少,这是为了效率,这个是对用户隐藏的,我们看到的应该是实际应该读入的,即 read 的返回值,对终端确实是行缓冲的。
代码实现 题干错了,应该是 63-1 的题,因为 63-2 程序本来就用的是 poll
代码实现 上道题用了 poll,所以这次就用 select 玩玩(别忘了循环 select 的话要不停地重新 FD_SET(),这个最容易出 bug)。
本质就是一个采用 I/O 多路复用的并发服务器模型,我突然想起来当时 CSAPP 做了 web proxy 的那个 lab 就是这个思想,不过当时没有 I/O 多路复用的函数库,是手动实现的,挺有意思。
比较有意思的点:
1.服务器同时开一个监听流式套接字和一个绑定到同一个端口号的两个端口(虽然同一个值,但是一个是 TCP 的端口,一个是 UDP 的端口,是两个完全没关系的端口,当然可以分别 bind)的数据报套接字。
2.然后通过 select 监听这两个 fd。
3.对于 UDP 请求很简单,直接回复发过去即可,但为了避免饥饿,所以仍然是一次只做一次(或适当的)I/O 而不是循环。
4.对于 TCP 这个比较复杂,监听套接字需要增加一个新的连接流式套接字,需要维护一个连接套接字的数据结构,加入监听集。
5.连接套接字就是具体服务代码,服务结束后(read 返回 0 说明收到 EOF,说明对面 shutdown 或 close(半)关闭),需要从监听集掩码和维护的连接套接字数据结构中去除,同时别忘了 close 掉 connfd 收回资源,这样就完成了一个回射服务。
6.实现一个 UDP 的请求客户端,一个 TCP 的请求客户端,然后降低下 BUF_SIZE 让一次 read 不完,更好地体现通过 I/O 多路复用技术实现的并发服务。
代码实现 有意思的是现在体现出来 POSIX 的好用之处了,因为 linux 和其他一些 UNIX 下,POSIX 消息队列用的就是 fd,可以被 I/O 多路复用函数监测,而 system V 的不可以。
学到了个阴间技巧,通过 poll 检测 pipe 对面的读端是否关闭(对端已关闭写端,本地已关闭读端),而不用写任何数据,注意不能 write 0,这个只会返回 0,poll 用 POLLOUT 当写不阻塞为就绪,然后如果返回-1 那么 POLLERR 设置,只需要让 pfd.revents & POLLERR 可知是错误还是正常写,检测而不用写入任何实际内容。
system V 这个消息队列发送 mtype 必须大于 0,吐了。。。
我真是弱智,忘了 select 会清除那些这次没就绪的,没在循环里重新 FD_SET()。
这里有个竞争条件,如果处理对应代码的时候,来了新信号向 pipe 写入新数据,只会的读管道会把这个新数据也清除掉。
代码实现 要把用不到的 pollfd 的 fd 设为-1(虽然这个程序没体现),另外 calloc 初始化为 0,避免忘记初始化 event 导致错误。
这样的兴趣列表为空的 epoll 实例上调用 epoll_wait()也会阻塞,这种是有用的,在多线程程序中,别的线程可以给阻塞的这个 epoll 实例添加兴趣列表,这样动态地增删兴趣列表是非常有用的。
代码实现 当 maxevents 比较小(指申请用来返回结果的 evlist 数组不够大)的情况下,而多个(比 maxevents 多)监测的 fd 都处于就绪态,一次 epoll_wait()返回不了的情况下,那么 evlist 返回的结果不是固定的也不是随机的,而是轮询遍历返回所有就绪的 fd,这样就让每个就绪的 fd 都有机会获得处理,这样避免了饥饿的发生。
该程序设计 evlist 大小为 2,一次返回两个就绪 ev,而 argv[1]是保持一直就绪的个数:
代码实现 注意一下,终端属性是一个进程属性,_GNU_SOURCE 特性测试宏加了才能用 F_SETSIG。
伪终端的意义:伪终端本质是一个 IPC,和管道没有本质区别,但是外在区别非常大,因为从设备提供了对终端的抽象,可以让面向终端(涉及到终端操作)的程序使用(vi, vim, shell 等等),而例如管道这种普通进程,不是终端,自然无法被当作终端用; 如果是对不是面向终端的程序的话,那么用管道好,更简单易用,但比如 shell 程序有些是涉及管道的,那这就要用伪终端,除非知道不会涉及面向终端的操作。
注意一开始 tcgetattr(STDIN_FILENO, &ttyOrig)设置了进程的终端属性,然后子进程是这个属性(这个模式下(一开始终端位于的模式))是会解释特殊字符的,而父进程之后把父进程自己的终端属性设置成原始模式,不会解释特殊字符。
所以终端发给父进程(script 功能部分)不会解释^D,作为一个字面值发给了主设备,然后子进程从设备会读取,这次解释^D 导致子进程即 shell 程序退出,从设备关闭,而再之后显然父进程从主设备读取就会错误(select 发现 masterfd 就绪(read 会返回-1 的这种就绪)),从而终止程序。
代码实现 很有趣的,设置了 SIGWINCH 信号后,终端大小改变(用鼠标拉大拉小这样),就会发出一个 SIGWINCH 信号。
1.注意要设置 SA_RESTART,但对于低速系统调用无法 SA_RESTART 自动重启,要处理 EINTR 的错误。
2.要说明终端窗口大小改变之后,如果终端和 shell 是直接连的情况下,会自动让 shell 适应;但使用伪终端,发给的是主进程,实际的 shell 进程没有处理。这样比如 vim 这样的程序,正常情况下比如会一行显示不过来自动显示到下一行(实际不换行,行号也不变,只是显示这样),而伪终端不处理的话,shell 是不知道终端的窗口大小属性变了的。
3.一定要用阻塞,避免在无限循环里面疯狂自旋,浪费 CPU
代码实现 换 select 成两个进程处理,文艺复兴,忆苦思甜这是,幸好这还只有两个 fd 需要同时检测,体现出 I/O 多路复用技术的好。
注意这时候^D 检测 STDIN_FILENO 的会一直阻塞,不能退出,这个时候通过子进程终止发来的 SIGCHLD 信号来知道该终止了。(如果是子进程监听 STDIN_FILENO 的话,那么父进程终止前可以通过 kill 发送信号通知,当然其他的 IPC 手段都行,不过信号是最简单的)
注意这里子 shell 解释特殊字符^D 终止后,父进程接受到 SIGCHLD 立即终止,由于父进程在第一个终止的子进程终止后立即终止,所以懒得写 waitpid 了,让 init 收养回收即可,但要记住,如果子进程终止变成僵尸进程后,父进程没有立即终止,那么就一定要让父进程 waitpid 回收避免浪费资源了。
不用担心子 shell 在 sigaction()设置完 SIGCHLD 的处理函数前终止,因为向主设备写的操作是在 sigaction() 之后的,所以安全(详细过程:从终端向主设备写,之后主设备到从设备,子 shell 再从从设备读)
代码实现 本质很简单,script 给一个文件写入时间戳和从 STDIN_FILENO 读入的命令(注意主进程设置了它的终端属性是原始的,所以特殊字符是一个正常的 char 值,而不会被解释);然后 replay 的时候,单纯地把从 STDIN_FILENO 读入的变成从文件读入并按时间戳(这个是实际时间)读入即可,注意主进程设置终端属性是原始,只有子 shell 才会解释特殊字符。
1.毫秒级的话没法用 time()了,用 gettimeofday()好用。
2.从设备表现的像一个终端,然后子 shell 进程是从设备伪终端的控制进程,子 shell 之前获取原终端属性并设置了从设备这个伪终端的属性,所以这个从设备也会回显,这是内核的工作,会自动把从主设备写入从设备的数据放到向主设备输出的队列里一份,然后我们的程序就会随之输出到终端完成回显。
3.注意这里是父进程设置的终端(真正终端)属性是原始模式,是无缓冲的,要内部加个缓冲区以\n 做标记存命令。
4.注意原始模式下终端,你按回车读入的是'\r',好奇怪,而且别忘了把'\r'改成'\n',因为这个才是 linux 下解释文本文件换行的控制字符,不然会解释出错。
吐了,这是我遇到奇葩错误最多的练习题。。。而且多个进程相互之间的关系,这个感觉是这本书第二难的习题,思路不难,就是对终端陌生导致的弱智错误一堆。
代码实现 思路并没有多难,由于在不同机器上,所以一台机器的终端无法直接与另一台机器的 shell 相连,而且还要提供中间层来支持登录等功能,所以需要伪终端。客户机的终端通过 socket 到服务器,服务器的 socket 接受数据,然后让 ssh 中间层处理,然后中间层拥有着主设备,然后从设备被 shell 拥有。
我们要做的不过是在中间层提供并发(下一道题提供登录记账功能),对每一个到来的连接,让他们都拥有一个独立的伪终端作为客户端终端与服务器 shell 连接的桥梁。
注意我们在子进程不是直接打开 shell 进程,而是打开 login 进程,login 会要求先输入账号和密码,login 登录成功后会自动打开对应用户的 shell 进程,无需手动打开,登录功能由 login 提供,无需我们自己写。
1.客户端 shutdown 半关闭,既能通知对端 FIN 已经结束,对端读会 EOF,又能不实际关机客户进程,继续做一些事情,尤其是读还没读完的对端的数据然后做一些事情这样,命令发完,但结果还没接受完,这种情况就用 shutdown 非常方便。
2.注意这里的主设备对应的终端是服务器本身上的终端,而这个程序是用不到的,所以不要设置原始模式,这样如果服务器终端收到信号字符^C 也能解释终止程序,我们根本不需要这个,只是从设备要模仿终端需要一些信息罢了。
3.这里客户端又多提供了一层中间层,这里会处理客户端终端(实际用户用的终端)的输入,通过 TCP/IP 传过去就成了单纯的命令字符串了,这里无法让服务器主设备那个进程收到特殊字符(因为被客户端进程处理了),如果不想这样,那么需要设置客户端进程的终端属性成原始模式,不进行解释,把特殊字符作为一个普通值也传过去给主设备进程。
4.同理,如果想要改变伪终端的窗口属性,那么需要通过 socket 把客户进程的终端属性发过去,然后客户端进程需要捕获 SIGWINCH 信号(这里应该需要让这个进程变成终端的控制进程才可以捕获到),知道需要处理。
5.这是个服务器,是要长期运行的,千万别忘了回收僵尸进程,另外从设备那个进程是新会话,父进程改成 init 了。
6.这个不要用 SUID 了,因为如果属于 root,那么 utmp 有点问题,所以直接 sudo 即可。
我们的是通过 login 实现登录功能,而 login 已经自动更新登陆记账的信息了,不再需要我们自己更新。如果需要手动更改,那么可以参照logwtmp 函数更改登陆记账。
代码实现 注意要关闭回显,这里要关闭回显的是从设备,所以需要从设备 fd,所以把 pty_fork()拿了出来修改。
没多大意义,以后有空再写