UIUC CS241 讲义:众包系统编程书(7)https://developer.aliyun.com/article/1427165
如何保护我的数据免受磁盘故障?
很简单!数据存储两次!这是“RAID-1”磁盘阵列的主要原则。RAID 是廉价磁盘冗余阵列的缩写。通过将写入复制到一个磁盘并将写入复制到另一个磁盘(备份磁盘),数据恰好有两份副本。如果一个磁盘故障,另一个磁盘将作为唯一副本,直到可以重新克隆。读取数据更快(因为数据可以从任一磁盘请求),但写入可能会慢两倍(现在每个磁盘块写入需要发出两个写命令),并且与使用单个磁盘相比,每字节存储成本翻了一番。
另一个常见的 RAID 方案是 RAID-0,意味着文件可以分割在两个磁盘中,但如果任何一个磁盘故障,那么文件将无法恢复。这样做的好处是可以将写入时间减半,因为文件的一部分可以写入硬盘一,另一部分可以写入硬盘二。
还常常将这些系统结合在一起。如果你有很多硬盘,考虑 RAID-10。这是指有两个 RAID-1 系统,但这些系统在彼此之间以 RAID-0 连接。这意味着你可以从减速中获得大致相同的速度,但现在任何一个磁盘都可以故障,你可以恢复该磁盘。(如果来自相对 RAID 分区的两个磁盘故障,有可能进行恢复,尽管我们大多数时候不依赖它)。
RAID-3 是什么?
RAID-3 使用奇偶校验码而不是镜像数据。对于每 N 位写入,我们将写入一个额外的位,即“奇偶校验位”,以确保写入的 1 的总数是偶数。奇偶校验位被写入到额外的磁盘上。如果任何一个磁盘(包括奇偶校验磁盘)丢失,那么它的内容仍然可以使用其他磁盘的内容计算出来。
RAID-3 的一个缺点是每当写入一个磁盘块时,奇偶校验块也总是会被写入。这意味着实际上有一个单独的磁盘瓶颈。实际上,这更有可能导致故障,因为一个磁盘被 100%使用,一旦该磁盘故障,其他磁盘更容易发生故障。
RAID-3 对数据丢失有多安全?
单个磁盘故障不会导致数据丢失(因为有足够的数据可以从剩余的磁盘重建阵列)。当两个磁盘不可用时,由于不再有足够的数据来重建阵列,数据丢失将发生。我们可以根据修复时间计算两个磁盘故障的概率,这不仅包括插入新磁盘的时间,还包括重建整个阵列内容所需的时间。
MTTF = mean time to failure MTTR = mean time to repair N = number of original disks p = MTTR / (MTTF-one-disk / (N-1))
使用典型数字(MTTR=1 天,MTTF=1000 天,N-1=9,p=0.009
在重建过程中,另一块驱动器出现故障的概率为 1%(在这一点上,你最好希望你仍然有原始数据的可访问备份)。
在实践中,修复过程中第二次故障的概率可能更高,因为重建阵列是 I/O 密集型的(并且在正常 I/O 请求活动之上)。这种更高的 I/O 负载也会对磁盘阵列造成压力
RAID-5 是什么?
RAID-5 类似于 RAID-3,只是检查块(奇偶校验信息)分配给不同的磁盘用于不同的块。检查块在磁盘阵列中“旋转”。RAID-5 提供比 RAID-3 更好的读写性能,因为不再有单个奇偶校验磁盘的瓶颈。唯一的缺点是你需要更多的磁盘来设置这个,并且需要使用更复杂的算法。
分布式存储
故障是常见情况,谷歌报告称每年有 2-10%的磁盘故障,现在将这个数字乘以单个仓库中的 60,000 多个磁盘…必须经受住不仅是磁盘的故障,还有服务器机架或整个数据中心的故障
解决方案简单冗余(每个文件有 2 或 3 个副本),例如,谷歌 GFS(2001 年)更有效的冗余(类似于 RAID 3++),例如,Google Colossus 文件系统(约 2010 年):可定制的复制,包括带有 1.5 倍冗余的 Reed-Solomon 编码
文件系统,第八部分:从安卓设备中删除预装的恶意软件
案例研究:从安卓设备中删除恶意软件
本节利用本 wikibook 中讨论的文件系统特性和系统编程工具来查找并删除安卓平板电脑中的不需要的恶意软件。
免责声明。在尝试修改您的平板电脑之前,请确保备份设备上的任何有价值的信息。不建议修改系统设置和系统文件。尝试使用本案例研究指南修改设备可能导致您的平板电脑共享、丢失或损坏数据。此外,您的平板电脑可能会出现功能异常或完全停止工作。请自行承担使用本案例研究的风险。作者对这些指南中包含的指令的正确性或完整性不承担任何责任并不提供任何保证。作者对本指南中描述或链接的任何软件,包括外部第三方软件,不承担任何责任并不提供任何保证。
背景
从亚马逊购买的 E97 安卓平板电脑出现了一些奇怪的毛病。最明显的是,浏览器应用程序总是在 gotoamazing.com 打开一个网站,而不是在应用程序的首选项中设置的主页(称为浏览器“劫持”)。我们能否利用这本 wikibook 中的知识来理解这种不需要的行为是如何发生的,还能从设备中删除不需要的预装应用程序?
使用的工具
虽然可能可以使用远程连接的 USB 设备上安装的安卓开发工具,但本指南仅使用平板电脑上的系统工具。安装了以下应用程序 -
- Malwarebytes - 一个免费的漏洞和恶意软件工具。
- 终端模拟器 - 一个简单的终端窗口,让我们在平板电脑上获得 shell 访问权限。
- KingRoot - 一个利用 Linux 内核中已知漏洞获取 root 权限的工具。
安装任何应用都可能允许任意代码执行,如果它能够突破安卓安全模型。在上面提到的应用中,KingRoot 是最极端的例子,因为它利用系统漏洞来获取我们的目的的 root 权限。然而,在这样做的同时,它也可能是最有问题的工具之一,我们要相信它不会安装任何自己的恶意软件。一个潜在更安全的选择是使用github.com/android-rooting-tools/
终端概述
最有用的命令是su grep mount
和安卓的包管理器工具pm
。
- grep -s abc * /(在当前目录和直接子目录中搜索
abc
) - su(又名“切换用户”成为 root - 需要一个已 root 的设备)
- mount -o rw,remount /system(允许/system 分区可写)
- pm disable(又名“包管理器”禁用安卓应用程序包)
文件系统布局概述
在运行安卓 4.4.2 的这个特定平板电脑上,预装的应用程序是不可修改的,并且位于
/system/app/ /system/priv-app/
偏好设置和应用数据存储在/data
分区中。每个应用程序通常打包在一个 apk 文件中,这本质上是一个 zip 文件。当应用程序安装时,代码会被扩展成一个可以被安卓虚拟机直接解析的文件。二进制代码(至少对于这个特定的虚拟机)具有 odex 扩展名。
我们可以搜索已安装的系统应用程序的代码,查找字符串’gotoamazing’
grep -s gotoamazing /system/app/* /system/priv-app/*
这没有找到任何东西;看来这个字符串没有硬编码到给定系统应用程序的源代码中。为了验证我们是否能找到
让我们检查所有已安装应用的数据区域
cd /data/data grep -s gotoamazing * */* */*/*
产生了以下结果
data/com.android.browser/shared_prefs/xbservice.xml: <string name="URL">http://www.gotoamazing...
-s 选项“静默选项”可以阻止 grep 抱怨尝试 grep 目录和其他无效文件。请注意,我们也可以使用-r 来递归搜索目录,但使用文件通配符(shell 的*通配符扩展)很有趣。
现在我们有了进展!看起来这个字符串是’app’com.android.browser’的一部分,但让我们也找出哪个应用程序二进制代码打开了’xbservice’首选项。也许这个不受欢迎的服务隐藏在另一个应用程序中,并且成功地作为浏览器的扩展秘密加载?
让我们寻找包含 xbservice 的任何文件。这次,我们将在包括’app’的/system 目录中递归搜索
grep -r -s xbservice /system/*app* Binary file /system/app/Browser.odex matches
最后 - 看起来出厂浏览器已经预装了主页劫持。让我们卸载它。为此,让我们成为 root。
$ su
pm list packages -s
Android 的包管理器有许多命令和选项。上面的例子列出了当前安装的所有系统应用程序。我们可以使用以下命令卸载浏览器应用程序
pm disable com.android.browser pm uninstall com.android.browser
使用pm list packages
可以列出所有安装的软件包(使用-s
选项只查看系统软件包)。我们禁用了以下系统应用程序。当然,我们无法保证我们成功删除了所有不需要的软件,或者其中一个是误报。因此,我们不建议在这样的平板电脑上存储敏感信息。
- com.android.browser
- com.adups.fota.sysoper
- elink.com
- com.google.android.apps.cloudprint
- com.mediatek.CrashService
- com.get.googleApps
- com.adups.fota(可以在将来安装任意项目的远程包)。
- com.mediatek.appguide.plugin
很可能你可以使用pm enable package-name
或pm install
和/system/app 或/system/priv-app 中的相关.apk 文件来重新启用软件包。
文件系统,第九部分:磁盘块示例
正在建设中
请问您能解释一下基于简单 i-node 的文件系统中文件内容是如何存储的吗?
当然!为了回答这个问题,我们将构建一个虚拟磁盘,然后编写一些 C 代码来访问其内容。我们的文件系统将把可用的字节划分为 inode 的空间和一个更大的磁盘块空间。每个磁盘块将是 4096 字节-
// Disk size: #define MAX_INODE (1024) #define MAX_BLOCK (1024*1024) // Each block is 4096 bytes: typedef char[4096] block_t; // A disk is an array of inodes and an array of disk blocks: struct inode[MAX_INODE] inodes; block[MAX_BLOCK] blocks;
为了清晰起见,我们在这个代码示例中不会使用’unsigned’。我们的固定大小的 inode 将包含文件的字节大小,权限,用户,组信息,时间元数据。对于手头的问题最相关的是,它还将包括十个指向磁盘块的指针,我们将用它们来引用实际文件的内容!
struct inode { int[10] directblocks; // indices for the block array i.e. where to the find the file's content long size; // ... standard inode meta-data e.g. int mode, userid,groupid; time_t ctime,atime,mtime; }
现在我们可以解决如何读取文件偏移量position
处的一个字节:
char readbyte(inode*inode,long position) { if(position <0 || position >= inode->size) return -1; // invalid offset int block_count = position / 4096,offset = position % 4096; // block count better be 0..9 ! int physical_idx = lookup_physical_block_index(inode, block_count ); // sanity check that the disk block index is reasonable... assert(physical_idx >=0 && physical_idx < MAX_BLOCK); // read the disk block from our virtual disk 'blocks' and return the specific byte return blocks[physical_idx][offset]; }
我们的 lookup_physical_block 的初始版本很简单-我们可以使用我们的 10 个直接块的表!
int lookup_physical_block_index(inode*inode, int block_count) { assert(block_count>=0 && block_count < 10); return inode->directblocks[ block_count ]; // returns an index value between [0,MAX_BLOCK) }
这种简单的表示是合理的,只要我们可以用十个块来表示所有可能的文件,即最多 40KB。那么更大的文件呢?我们需要 inode 结构始终保持相同的大小,因此只是将现有的直接块数组增加到 20 个,大致会使我们的 inode 大小翻倍。如果我们大多数的文件需要少于 10 个块,那么我们的 inode 存储现在就是浪费的。为了解决这个问题,我们将使用一个称为间接块的磁盘块来扩展我们可以使用的指针数组。我们只需要这个来处理大于 40KB 的文件。
struct inode { int[10] directblocks; // if size<4KB then only the first one is valid int indirectblock; // valid value when size >= 40KB int size; ... }
间接块只是一个普通的磁盘块,但我们将用它来保存指向磁盘块的指针。在这种情况下,我们的指针只是整数,因此我们需要将指针转换为整数指针:
int lookup_physical_block_index(inode*inode, int block_count) { assert(sizeof(int)==4); // Warning this code assumes an index is 4 bytes! assert(block_count>=0 && block_count < 1024 + 10); // 0 <= block_count< 1034 if( block_count < 10) return inode->directblocks[ block_count ]; // read the indirect block from disk: block_t* oneblock = & blocks[ inode->indirectblock ]; // Treat the 4KB as an array of 1024 pointers to other disk blocks int* table = (int*) oneblock; // Look up the correct entry in the table // Offset by 10 because the first 10 blocks of data are already // accounted for return table[ block_count - 10 ]; }
对于典型的文件系统,我们的索引值是 32 位,即 4 字节。因此,在 4096 字节中,我们可以存储 4096 / 4 = 1024 个条目。这意味着我们的间接块可以引用 1024 * 4KB = 4MB 的数据。通过前面的十个直接块,因此我们可以容纳文件大小达到 40KB + 1024 * 4KB= 4136KB。对于小于这个大小的文件,一些后面的表条目可能无效。
对于更大的文件,我们可以使用两个间接块。然而,有一个更好的选择,可以让我们有效地扩展到大文件。我们将包括一个双间接指针,如果这还不够,还有一个三重间接指针。双间接指针意味着我们有一个包含用作 1024 个条目的磁盘块的 1024 个条目的表。这意味着我们可以引用 1024*1024 个数据块。
(来源:uw714doc.sco.com/en/FS_admin/graphics/s5chain.gif
)
int lookup_physical_block_index(inode*inode, int block_count) { if( block_count < 10) return inode->directblocks[ block_count ]; // Use indirect block for the next 1024 blocks: // Assumes 1024 ints can fit inside each block! if( block_count < 1024 + 10) { int* table = (int*) & blocks[ inode->indirectblock ]; return table[ block_count - 10 ]; } // For huge files we will use a table of tables int i = (block_count - 1034) / 1024 , j = (block_count - 1034) % 1024; assert(i<1024); // triple-indirect is not implemented here! int* table1 = (int*) & blocks[ inode->doubleindirectblock ]; // The first table tells us where to read the second table ... int* table2 = (int*) & blocks[ table1[i] ]; return table2[j]; // For gigantic files we will need to implement triple-indirect (table of tables of tables) }
请注意,使用双间接读取一个字节需要 3 次磁盘块读取(两个表和实际数据块)。
文件系统复习问题
主题
- 超级块
- 数据块
- 索引节点
- 相对路径
- 文件元数据
- 硬链接和软链接
- 权限位
- 与目录一起工作
- 虚拟文件系统
- 可靠的文件系统
- RAID
问题
- 15 个直接块,2 个双间接块,3 个三重间接块,4kb 块和 4 字节条目的文件系统上文件可以有多大?(假设有足够的无限块)
- 超级块是什么?索引节点?数据块?
- 如何简化
/./proc/../dev/./random
/ - 在 ext2 中,索引节点中存储了什么,目录条目中存储了什么?
- /sys,/proc,/dev/random 和/dev/urandom 是什么?
- 权限位是什么?
- 如何使用 chmod 设置用户/组/所有者的读/写/执行权限?
- “dd”命令是做什么的?
- 硬链接和符号链接之间有什么区别?文件需要存在吗?
- "ls -l"显示目录中每个文件的大小。大小存储在目录中还是文件的索引节点中?
十、信号
进程控制,第一部分:使用信号的等待宏
等待宏
我能找出我的子进程的退出值吗?
您可以找到子进程退出值的最低 8 位(main()
的返回值或包含在exit()
中的值):使用“等待宏” - 通常会使用“WIFEXITED”和“WEXITSTATUS”。有关更多信息,请参阅wait
/waitpid
手册页。
int status; pid_t child = fork(); if (child == -1) return 1; //Failed if (child > 0) { /* I am the parent - wait for the child to finish */ pid_t pid = waitpid(child, &status, 0); if (pid != -1 && WIFEXITED(status)) { int low8bits = WEXITSTATUS(status); printf("Process %d returned %d" , pid, low8bits); } } else { /* I am the child */ // do something interesting execl("/bin/ls", "/bin/ls", ".", (char *) NULL); // "ls ." }
一个进程只能有 256 个返回值,其余的位是信息性的。
位移
请注意,没有必要记住这一点,这只是对状态变量内部存储信息的高级概述。
/如果 WIFEXITED(STATUS),则为状态的低 8 位。/
#define __WEXITSTATUS(status) (((status) & 0xff00) >> 8)
/如果 WIFSIGNALED(STATUS),则为终止信号。/
#define __WTERMSIG(status) ((status) & 0x7f)
/如果 WIFSTOPPED(STATUS),则为停止子进程的信号。/
#define __WSTOPSIG(status) __WEXITSTATUS(status)
/如果 STATUS 指示正常终止,则为非零。/
#define __WIFEXITED(status) (__WTERMSIG(status) == 0)
内核有一种内部方式来跟踪发出信号、退出或停止的情况。该 API 被抽象化,以便内核开发人员可以随意更改。
小心。
请记住,如果前提条件得到满足,那么宏才有意义。这意味着如果进程被发出信号,进程的退出状态将不会被定义。宏不会为您进行检查,因此需要编程来确保逻辑正确。
信号
什么是信号?
信号是内核为我们提供的一种构造。它允许一个进程异步地向另一个进程发送信号(想象一条消息)。如果该进程想要接受信号,它可以,然后对于大多数信号,可以决定如何处理该信号。这里是一个信号的简短列表(非全面)。
名称 | 默认操作 | 通常用例 |
SIGINT |
终止进程(可捕获) | 告诉进程停止 |
SIGQUIT |
终止进程(可捕获) | 告诉进程停止 |
SIGSTOP |
停止进程(无法捕获) | 停止进程以便继续 |
SIGCONT |
继续进程 | 继续运行进程 |
SIGKILL |
终止进程(无法忽略) | 你想让你的进程消失 |
我能暂停我的子进程吗?
是的!您可以通过发送 SIGSTOP 信号来暂时暂停运行中的进程。如果成功,它将冻结一个进程;即进程将不再分配任何 CPU 时间。
要允许进程恢复执行,请发送 SIGCONT 信号。
例如,这是一个每秒慢慢打印一个点的程序,最多 59 个点。
#include <unistd.h> #include <stdio.h> int main() { printf("My pid is %d\n", getpid() ); int i = 60; while(--i) { write(1, ".",1); sleep(1); } write(1, "Done!",5); return 0; }
我们将首先在后台启动进程(注意末尾的&)。然后通过使用 kill 命令从 shell 进程发送信号给它。
>./program & My pid is 403 ... >kill -SIGSTOP 403 >kill -SIGCONT 403
如何从 C 中杀死/停止/暂停我的子进程?
在 C 中,使用kill
POSIX 调用向子进程发送信号,
kill(child, SIGUSR1); // Send a user-defined signal kill(child, SIGSTOP); // Stop the child process (the child cannot prevent this) kill(child, SIGTERM); // Terminate the child process (the child can prevent this) kill(child, SIGINT); // Equivalent to CTRL-C (by default closes the process)
正如我们上面看到的,在 shell 中也有一个 kill 命令,例如获取正在运行的进程列表,然后终止进程 45 和进程 46
ps kill -l kill -9 45 kill -s TERM 46
如何检测“CTRL-C”并优雅地清理?
我们将在后面回到信号 - 这只是一个简短的介绍。在 Linux 系统上,如果您有兴趣了解更多信息,请参阅man -s7 signal
(例如系统和库调用的异步信号安全列表)。
信号处理程序内部的可执行代码有严格的限制。大多数库和系统调用都不是“异步信号安全”的 - 它们不能在信号处理程序内部使用,因为它们不是可重入安全的。在单线程程序中,信号处理瞬间中断程序执行,以执行信号处理程序代码。假设您的原始程序在执行malloc
库代码时被中断;malloc 使用的内存结构将不处于一致状态。在信号处理程序中调用printf
(它使用malloc
)是不安全的,并将导致“未定义行为”,即不再是一个有用的、可预测的程序。实际上,您的程序可能会崩溃,计算或生成不正确的结果,或者停止运行(“死锁”),具体取决于在执行信号处理程序代码时您的程序正在执行什么。
信号处理程序的一个常见用途是设置一个布尔标志,该标志偶尔被轮询(读取),作为程序正常运行的一部分。例如,
int pleaseStop ; // See notes on why "volatile sig_atomic_t" is better void handle_sigint(int signal) { pleaseStop = 1; } int main() { signal(SIGINT, handle_sigint); pleaseStop = 0; while ( ! pleaseStop) { /* application logic here */ } /* cleanup code here */ }
上述代码在纸上看起来可能是正确的。但是,我们需要向编译器和将执行main()
循环的 CPU 核心提供提示。我们需要防止编译器优化:表达式! pleaseStop
似乎是一个循环不变量,即永远为真,因此可以简化为true
。其次,我们需要确保pleaseStop
的值不是使用 CPU 寄存器缓存的,而是始终从主存中读取和写入。sig_atomic_t
类型意味着变量的所有位可以被读取或修改为“原子操作” - 一个不可中断的操作。不可能读取由一些新位值和旧位值组成的值。
通过使用正确类型的volatile sig_atomic_t
指定pleaseStop
,我们可以编写可移植的代码,其中主循环将在信号处理程序返回后退出。在大多数现代平台上,sig_atomic_t
类型可以与int
一样大,但在嵌入式系统上,它可以与char
一样小,并且只能表示(-127 至 127)的值。
volatile sig_atomic_t pleaseStop;
这种模式的两个示例可以在“COMP”中找到,这是一个基于终端的 1Hz 4 位计算机(github.com/gto76/comp-cpp/blob/1bf9a77eaf8f57f7358a316e5bbada97f2dc8987/src/output.c#L121
)。使用了两个布尔标志。一个用于标记SIGINT
(CTRL-C)的传递,并优雅地关闭程序,另一个用于标记SIGWINCH
信号以检测终端调整大小并重新绘制整个显示。
信号,第二部分:未决信号和信号掩码
信号深入解析
我如何了解更多关于信号的信息?
Linux 手册中讨论了第 2 节中的信号系统调用。第 7 节中还有一篇较长的文章(尽管在 OSX/BSD 中没有):
man -s7 signal
信号术语
- 生成-信号是由 kill 系统调用在内核中创建的。
- 未决-尚未传递,但即将传递
- 已屏蔽-因为没有信号处理方式允许信号被传递,所以尚未传递
- 已传递-传递到进程,正在执行描述的操作
- 捕获-当进程阻止信号摧毁它并做其他事情时
进程的信号处理方式是什么?
对于每个进程,每个信号都有一个处理方式,这意味着当信号传递到进程时将发生什么操作。例如,默认的 SIGINT 处理方式是终止它。信号处理方式可以通过调用 signal()(这很简单,但在不同的 POSIX 架构上实现上有微妙的变化,也不建议用于多线程程序)或sigaction
(稍后讨论)来更改。您可以将进程对所有可能信号的处理方式想象成一个函数指针条目表(每个可能信号一个)。
信号的默认处理方式可以是忽略信号、停止进程、继续已停止的进程、终止进程,或者终止进程并转储一个“核心”文件。请注意,核心文件是进程内存状态的表示,可以使用调试器进行检查。
可以排队多个信号吗?
不是-但是可能有信号处于未决状态。如果信号处于未决状态,这意味着它尚未传递到进程。信号处于未决状态的最常见原因是进程(或线程)当前已阻止了该特定信号。
如果特定信号,例如 SIGINT,处于未决状态,则不可能再次排队相同的信号。
是可能有多个不同类型的信号处于未决状态。例如,SIGINT 和 SIGTERM 信号可能是未决的(即尚未传递到目标进程)
如何屏蔽信号?
信号可以通过设置进程信号掩码或者在编写多线程程序时设置线程信号掩码来屏蔽(意味着它们将保持在未决状态)。
线程/子进程中的处理方式
创建新线程时会发生什么?
新线程继承了调用线程的掩码的副本
pthread_sigmask( ... ); // set my mask to block delivery of some signals pthread_create( ... ); // new thread will start with a copy of the same mask
分叉时会发生什么?
子进程继承了父进程的信号处理方式。换句话说,如果在分叉之前安装了 SIGINT 处理程序,那么子进程在传递 SIGINT 时也会调用处理程序。
请注意,分叉期间子进程的未决信号不会被继承。
执行期间会发生什么?
信号掩码和信号处理方式都会传递到 exec-ed 程序。www.gnu.org/software/libc/manual/html_node/Executing-a-File.html#Executing-a-File
未决信号也会被保留。信号处理程序会被重置,因为原始处理程序代码随着旧进程一起消失了。
分叉期间会发生什么?
子进程继承了父进程的信号处理方式和父进程的信号掩码的副本。
例如,如果在父进程中阻塞了SIGINT
,那么在子进程中也会被阻塞。例如,如果父进程为 SIG-INT 安装了处理程序(回调函数),那么子进程也会执行相同的行为。
但是未决信号不会被子进程继承。
如何在单线程程序中屏蔽信号?
使用sigprocmask
!使用 sigprocmask,您可以设置新的掩码,向进程掩码添加新的要屏蔽的信号,并解除当前被屏蔽的信号。您还可以通过传递非空值来确定现有掩码(并在以后使用)。
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);`
来自 sigprocmask 的 Linux 手册页,
SIG_BLOCK: The set of blocked signals is the union of the current set and the set argument. SIG_UNBLOCK: The signals in set are removed from the current set of blocked signals. It is permissible to attempt to unblock a signal which is not blocked. SIG_SETMASK: The set of blocked signals is set to the argument set.
sigset 类型的行为类似于位图,只是使用函数而不是使用&和|来显式设置和取消位。
在修改一个位之前忘记初始化信号集是一个常见的错误。例如,
sigset_t set, oldset; sigaddset(&set, SIGINT); // Ooops! sigprocmask(SIG_SETMASK, &set, &oldset)
正确的代码将集合初始化为全部打开或全部关闭。例如,
sigfillset(&set); // all signals sigprocmask(SIG_SETMASK, &set, NULL); // Block all the signals! // (Actually SIGKILL or SIGSTOP cannot be blocked...) sigemptyset(&set); // no signals sigprocmask(SIG_SETMASK, &set, NULL); // set the mask to be empty again
如何在多线程程序中阻止信号?
在多线程程序中阻止信号与单线程程序类似:
- 使用 pthread_sigmask 而不是 sigprocmask
- 阻止所有线程中的信号,以防止其异步传递
确保信号在所有线程中被阻止的最简单方法是在创建新线程之前在主线程中设置信号掩码
sigemptyset(&set); sigaddset(&set, SIGQUIT); sigaddset(&set, SIGINT); pthread_sigmask(SIG_BLOCK, &set, NULL); // this thread and the new thread will block SIGQUIT and SIGINT pthread_create(&thread_id, NULL, myfunc, funcparam);
就像我们在 sigprocmask 中看到的那样,pthread_sigmask 包括一个“how”参数,用于定义如何使用信号集:
pthread_sigmask(SIG_SETMASK, &set, NULL) - replace the thread's mask with given signal set pthread_sigmask(SIG_BLOCK, &set, NULL) - add the signal set to the thread's mask pthread_sigmask(SIG_UNBLOCK, &set, NULL) - remove the signal set from the thread's mask
在多线程程序中如何传递待处理的信号?
信号被传递到任何未阻止该信号的信号线程。
如果两个或更多线程可以接收信号,那么哪个线程将被中断是任意的!
信号,第三部分:触发信号
如何从 shell 发送信号给进程?
您已经知道发送SIG_INT
的一种方法,只需在 shell 中键入CTRL-C
。您还可以使用kill
(如果知道进程 ID)和killall
(如果知道进程名称)。
# First let's use ps and grep to find the process we want to send a signal to $ ps au | grep myprogram angrave 4409 0.0 0.0 2434892 512 s004 R+ 2:42PM 0:00.00 myprogram 1 2 3 #Send SIGINT signal to process 4409 (equivalent of `CTRL-C`) $ kill -SIGINT 4409 #Send SIGKILL (terminate the process) $ kill -SIGKILL 4409 $ kill -9 4409
killall
类似,只是它是根据程序名称匹配。下面的两个例子,发送SIGINT
然后SIGKILL
来终止正在运行myprogram
的进程。
# Send SIGINT (SIGINT can be ignored) $ killall -SIGINT myprogram # SIGKILL (-9) cannot be ignored! $ killall -9 myprogram
如何从正在运行的 C 程序发送信号给进程?
使用raise
或kill
int raise(int sig); // Send a signal to myself! int kill(pid_t pid, int sig); // Send a signal to another process
对于非根进程,信号只能发送给相同用户的进程,即你不能随便 SIGKILL 我的进程!参见 kill(2)即 man -s2 以获取更多详细信息。
如何向特定线程发送信号?
使用pthread_kill
int pthread_kill(pthread_t thread, int sig)
在下面的示例中,执行func
的新创建的线程将被SIGINT
中断。
pthread_create(&tid, NULL, func, args); pthread_kill(tid, SIGINT); pthread_kill(pthread_self(), SIGKILL); // send SIGKILL to myself
pthread_kill(threadid,SIGKILL)
会杀死进程还是线程?
它将杀死整个进程。尽管单个线程可以设置信号掩码,但信号处理(每个信号执行的处理程序/动作表)是每个进程而不是每个线程。这意味着sigaction
可以从任何线程调用,因为您将为进程中的所有线程设置信号处理程序。
如何捕获(处理)信号?
您可以选择异步或同步地处理挂起的信号。
安装信号处理程序以异步处理信号使用sigaction
(或者,对于简单的示例,signal
)。
同步捕获挂起信号使用sigwait
(它会阻塞,直到信号被传递)或signalfd
(它也会阻塞并提供一个文件描述符,可以使用read()
来检索挂起的信号)。
参见Signals, Part 4
以获取使用sigwait
的示例
信号,第四部分:Sigaction
我如何使用sigaction
?
您应该使用sigaction
而不是signal
,因为它具有更好定义的语义。不同操作系统上的signal
会执行不同的操作,这是不好的,sigaction
更具可移植性,如果需要,对于线程更好地定义。
要更改进程的“信号处理方式” - 即当信号传递到您的进程时会发生什么 - 使用sigaction
您可以使用系统调用sigaction
来设置信号的当前处理程序,或者读取特定信号的当前信号处理程序。
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
sigaction 结构包括两个回调函数(我们只会看’handler’版本),一个信号掩码和一个标志字段。
struct sigaction { void (*sa_handler)(int); void (*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask; int sa_flags; };
我如何将signal
调用转换为等效的sigaction
调用?
假设您为警报信号安装了信号处理程序,
signal(SIGALRM, myhandler);
等效的sigaction
代码是:
struct sigaction sa; sa.sa_handler = myhandler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sigaction(SIGALRM, &sa, NULL)
但是,我们通常也可以设置掩码和标志字段。掩码是在信号处理程序执行期间使用的临时信号掩码。SA_RESTART 标志将自动重新启动一些(但不是所有)否则会提前返回(带有 EINTR 错误)的系统调用。后者意味着我们可以在一定程度上简化其余代码,因为可能不再需要重启循环。
sigfillset(&sa.sa_mask); sa.sa_flags = SA_RESTART; /* Restart functions if interrupted by handler */
我如何使用 sigwait?
Sigwait 可以用来一次读取一个挂起的信号。sigwait
用于同步等待信号,而不是在回调中处理它们。多线程程序中典型的 sigwait 用法如下所示。请注意,线程信号掩码首先被设置(并将被新线程继承)。这可以防止信号被传递,因此它们将保持挂起状态,直到调用 sigwait。还要注意,相同的设置 sigset_t 变量被 sigwait 使用 - 除了设置被阻塞信号的集合之外,它被用作 sigwait 可以捕获和返回的信号集合。
编写自定义信号处理线程(如下面的示例)的一个优点是,现在您可以使用更多的 C 库和系统函数,否则不能安全地在信号处理程序中使用,因为它们不是异步信号安全的。
基于http://pubs.opengroup.org/onlinepubs/009695399/functions/pthread_sigmask.html
static sigset_t signal_mask; /* signals to block */ int main (int argc, char *argv[]) { pthread_t sig_thr_id; /* signal handler thread ID */ sigemptyset (&signal_mask); sigaddset (&signal_mask, SIGINT); sigaddset (&signal_mask, SIGTERM); pthread_sigmask (SIG_BLOCK, &signal_mask, NULL); /* New threads will inherit this thread's mask */ pthread_create (&sig_thr_id, NULL, signal_thread, NULL); /* APPLICATION CODE */ ... } void *signal_thread (void *arg) { int sig_caught; /* signal caught */ /* Use same mask as the set of signals that we'd like to know about! */ sigwait(&signal_mask, &sig_caught); switch (sig_caught) { case SIGINT: /* process SIGINT */ ... break; case SIGTERM: /* process SIGTERM */ ... break; default: /* should normally not happen */ fprintf (stderr, "\nUnexpected signal %d\n", sig_caught); break; } }
信号复习问题
主题
- 信号
- 信号处理程序安全
- 信号处理
- 信号状态
- 在 Forking/Exec 时的挂起信号
- 在 Forking/Exec 时的信号处理
- 在 C 中引发信号
- 在多线程程序中引发信号
问题
- 什么是信号?
- 在 UNIX 下如何处理信号?(奖励:Windows 呢?)
- 函数是什么意思信号处理程序安全
- 进程信号处理是什么?
- 我如何在单线程程序中改变信号处理?多线程呢?
- 为什么要使用 sigaction 而不是 signal?
- 我如何异步和同步地捕获信号?
- 在我 fork 后,挂起的信号会怎样?Exec?
- 我 fork 后我的信号处理怎么样?Exec?
考试练习问题
警告,这些是很好的练习,但不全面。CS241 期末考试假设你完全理解并能应用课程的所有主题。问题将主要但不完全集中在你在实验室和编程作业中使用过的主题上。
考试题目
期末考试可能包括多项选择题,测试你对以下内容的掌握程度。
CSP (critical section problems) HTTP SIGINT TCP TLB Virtual Memory arrays barrier c strings chmod client/server coffman conditions condition variables context switch deadlock dining philosophers epoll exit file I/O file system representation fork/exec/wait fprintf free heap allocator heap/stack inode vs name malloc mkfifo mmap mutexes network ports open/close operating system terms page fault page tables pipes pointer arithmetic pointers printing (printf) producer/consumer progress/mutex race conditions read/write reader/writer resource allocation graphs ring buffer scanf buffering scheduling select semaphores signals sizeof stat stderr/stdout symlinks thread control (_create, _join, _exit) variable initializers variable scope vm thrashing wait macros write/read with errno, EINTR and partial data
C 编程:复习问题
警告-问题编号可能会更改
内存和字符串
问题 1.1
在下面的示例中,哪些变量保证打印零值?
int a; static int b; void func() { static int c; int d; printf("%d %d %d %d\n",a,b,c,d); }
问题 1.2
在下面的示例中,哪些变量保证打印零值?
void func() { int* ptr1 = malloc( sizeof(int) ); int* ptr2 = realloc(NULL, sizeof(int) ); int* ptr3 = calloc( 1, sizeof(int) ); int* ptr4 = calloc( sizeof(int) , 1); printf("%d %d %d %d\n",*ptr1,*ptr2,*ptr3,*ptr4); }
问题 1.3
解释下面尝试复制字符串的错误。
char* copy(char*src) { char*result = malloc( strlen(src) ); strcpy(result, src); return result; }
问题 1.4
为什么下面尝试复制字符串的尝试有时成功有时失败?
char* copy(char*src) { char*result = malloc( strlen(src) +1 ); strcat(result, src); return result; }
问题 1.4
解释下面的代码中尝试复制字符串的两个错误。
char* copy(char*src) { char result[sizeof(src)]; strcpy(result, src); return result; }
问题 1.5
以下哪个是合法的?
char a[] = "Hello"; strcpy(a, "World"); char b[] = "Hello"; strcpy(b, "World12345", b); char* c = "Hello"; strcpy(c, "World");
问题 1.6
完成函数指针 typedef 以声明一个接受 void参数并返回 void的函数指针。将您的类型命名为’pthread_callback’
typedef ______________________;
问题 1.7
除了函数参数之外,线程的堆栈上还存储了什么?
问题 1.8
使用strcpy
strlen
和指针算术实现char* strcat(char*dest, const char*src)
的版本
char* mystrcat(char*dest, const char*src) { ? Use strcpy strlen here return dest; }
问题 1.9
使用循环和无函数调用实现size_t strlen(const char*)
的版本。
size_t mystrlen(const char*s) { }
问题 1.10
识别以下strcpy
实现中的三个错误。
char* strcpy(const char* dest, const char* src) { while(*src) { *dest++ = *src++; } return dest; }
打印
问题 2.1
找出两个错误!
fprintf("You scored 100%");
格式化和打印到文件
问题 3.1
完成以下代码以打印到文件。将名称、逗号和分数打印到文件’result.txt’
char* name = .....; int score = ...... FILE *f = fopen("result.txt",_____); if(f) { _____ } fclose(f);
打印到字符串
问题 4.1
如何将变量 a,mesg,val 和 ptr 的值打印到一个字符串?将 a 打印为整数,mesg 打印为 C 字符串,val 打印为双精度值,ptr 打印为十六进制指针。您可以假设 mesg 指向一个短的 C 字符串(<50 个字符)。奖励:如何使这段代码更健壮或能够应对?
char* toString(int a, char*mesg, double val, void* ptr) { char* result = malloc( strlen(mesg) + 50); _____ return result; }
输入解析
问题 5.1
为什么应该检查 sscanf 和 scanf 的返回值?
问题 5.2
为什么’gets’很危险?
问题 5.3
编写一个使用getline
的完整程序。确保您的程序没有内存泄漏。
堆内存
何时使用 calloc 而不是 malloc?何时 realloc 会有用?
(待办事项-将此问题移动到另一页)程序员在下面的代码中犯了什么错误?使用堆内存可以修复吗?使用全局(静态)内存可以修复吗?
static int id; char* next_ticket() { id ++; char result[20]; sprintf(result,"%d",id); return result; }
多线程编程:复习问题
警告 - 问题编号可能会更改
问题 1
以下代码是否线程安全?重新设计以下代码以使其线程安全。提示:如果消息内存对每次调用都是唯一的,则互斥锁是不必要的。
static char message[20]; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; void format(int v) { pthread_mutex_lock(&mutex); sprintf(message, ":%d:" ,v); pthread_mutex_unlock(&mutex); return message; }
问题 2
以下哪一个不会导致进程退出?
- 从最后一个运行的线程中返回 pthread 的起始函数。
- 原始线程从主函数返回。
- 任何导致分段错误的线程。
- 任何调用
exit
的线程。 - 在仍有其他线程运行时,在主线程中调用
pthread_exit
。
问题 3
为以下程序中将打印的"W"字符的数量写一个数学表达式。假设 a、b、c、d 都是小正整数。您的答案可以使用一个返回其最低值参数的’min’函数。
unsigned int a=...,b=...,c=...,d=...; void* func(void* ptr) { char m = * (char*)ptr; if(m == 'P') sem_post(s); if(m == 'W') sem_wait(s); putchar(m); return NULL; } int main(int argv, char** argc) { sem_init(s,0, a); while(b--) pthread_create(&tid, NULL, func, "W"); while(c--) pthread_create(&tid, NULL, func, "P"); while(d--) pthread_create(&tid, NULL, func, "W"); pthread_exit(NULL); /*Process will finish when all threads have exited */ }
问题 4
完成以下代码。以下代码应该交替打印A
和B
。它表示两个轮流执行的线程。添加条件变量调用到func
,以便等待的线程不需要不断检查turn
变量。问:pthread_cond_broadcast
是必要的还是pthread_cond_signal
足够?
pthread_cond_t cv = PTHREAD_COND_INITIALIZER; pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER; void* turn; void* func(void* mesg) { while(1) { // Add mutex lock and condition variable calls ... while(turn == mesg) { /* poll again ... Change me - This busy loop burns CPU time! */ } /* Do stuff on this thread */ puts( (char*) mesg); turn = mesg; } return 0; } int main(int argc, char** argv){ pthread_t tid1; pthread_create(&tid1, NULL, func, "A"); func("B"); // no need to create another thread - just use the main thread return 0; }
问题 5
在给定的代码中标识临界区。添加互斥锁以使代码线程安全。添加条件变量调用,使total
永远不会变成负数或超过 1000。相反,调用应该阻塞,直到可以安全地继续。解释为什么pthread_cond_broadcast
是必要的。
int total; void add(int value) { if(value < 1) return; total += value; } void sub(int value) { if(value < 1) return; total -= value; }
问题 6
一个非线程安全的数据结构有size()
enq
和 deq
方法。使用条件变量和互斥锁来完成线程安全的、阻塞版本。
void enqueue(void* data) { // should block if the size() would become greater than 256 enq(data); } void* dequeue() { // should block if size() is 0 return deq(); }
问题 7
您的创业公司提供使用最新交通信息的路径规划。您过度支付的实习生创建了一个非线程安全的数据结构,其中包含两个函数:shortest
(使用但不修改图)和set_edge
(修改图)。
graph_t* create_graph(char* filename); // called once // returns a new heap object that is the shortest path from vertex i to j path_t* shortest(graph_t* graph, int i, int j); // updates edge from vertex i to j void set_edge(graph_t* graph, int i, int j, double time);
为了性能,多个线程必须能够同时调用shortest
,但是当没有其他线程在shortest
或set_edge
内执行时,图只能被一个线程修改。
使用互斥锁和条件变量来实现读者-写者解决方案。下面显示了一个不完整的尝试。尽管这个尝试是线程安全的(因此足够用于演示日!),但它不允许多个线程同时计算shortest
路径,并且不具有足够的吞吐量。
path_t* shortest_safe(graph_t* graph, int i, int j) { pthread_mutex_lock(&m); path_t* path = shortest(graph, i, j); pthread_mutex_unlock(&m); return path; } void set_edge_safe(graph_t* graph, int i, int j, double dist) { pthread_mutex_lock(&m); set_edge(graph, i, j, dist); pthread_mutex_unlock(&m); }
同步概念:复习问题
注意,线程编程同步问题在另一页上。本页重点讨论概念性主题。问题编号可能会更改
Q1
每个 Coffman 条件的含义是什么?(例如,你能提供每个条件的定义吗)
- 持有和等待
- 循环等待
- 无抢占
- 互斥
Q2
逐个举例打破每个 Coffman 条件的真实生活例子。一个需要考虑的情况:画家、油漆和画笔。持有和等待 循环等待 无抢占 互斥
Q3
确定餐馆哲学家代码何时导致死锁(或者不导致)。例如,如果你看到以下代码片段,哪个 Coffman 条件没有满足?
// Get both locks or none. pthread_mutex_lock( a ); if( pthread_mutex_trylock( b ) ) { /*failed*/ pthread_mutex_unlock( a ); ... }
Q4
有多少进程被阻塞?
- P1 获取 R1
- P2 获取 R2
- P1 获取 R3
- P2 等待 R3
- P3 获取 R5
- P1 获取 R4
- P3 等待 R1
- P4 等待 R5
- P5 等待 R1
Q5
以下哪些陈述对于读者-写者问题是真实的?
- 可能有多个活跃的读者
- 可能有多个活跃的写者
- 当有一个活跃的写者时,活跃的读者数量必须为零
- 如果有一个活跃的读者,活跃的写者数量必须为零
- 一个写者必须等到当前活跃的读者完成
内存:复习问题
问题编号可能会改变
Q1
以下是什么,它们的目的是什么?
- 翻译旁路缓冲
- 物理地址
- 内存管理单元
- 脏位
Q2
你如何确定页偏移中使用了多少位?
Q3
上下文切换后 20 毫秒,TLB 包含你的数值代码使用的所有逻辑地址,该代码 100%的时间执行主内存访问。相对于单级页表,两级页表的开销(减速)是多少?
Q4
解释为什么在上下文切换发生时必须刷新 TLB(即 CPU 被分配到不同进程上工作)。
管道:复习问题
问题编号可能会有所变化
Q1
填写空白以使以下程序打印 123456789。如果cat
没有给出参数,它只是打印其输入直到 EOF。奖励:解释为什么下面的close
调用是必要的。
int main() { int i = 0; while(++i < 10) { pid_t pid = fork(); if(pid == 0) { /* child */ char buffer[16]; sprintf(buffer, ______,i); int fds[ ______]; pipe( fds); write( fds[1], ______,______ ); // Write the buffer into the pipe close( ______ ); dup2( fds[0], ______); execlp( "cat", "cat", ______ ); perror("exec"); exit(1); } waitpid(pid, NULL, 0); } return 0; }
Q2
使用 POSIX 调用fork
pipe
dup2
和close
来实现一个自动评分程序。将子进程的标准输出捕获到一个管道中。子进程应该使用exec
命令执行程序./test
,除了进程名称之外不带任何额外的参数。在父进程中从管道中读取:一旦捕获的输出包含!字符,就退出父进程。在退出父进程之前,向子进程发送 SIGKILL。如果输出包含!,则退出 0。否则,如果子进程退出导致管道写端关闭,则以值 1 退出。确保在父进程和子进程中关闭未使用的管道端。
Q3(高级)
这个高级挑战使用管道让“AI 玩家”自己玩游戏,直到游戏结束。程序tictactoe
接受一行输入 - 到目前为止所做的转动序列,打印相同的序列,然后再加上一个转动,然后退出。一个转动由两个字符指定。例如,“A1”和“C3”是两个对角位置。字符串B2A1A3
是一个 3 个转动/步骤的游戏。一个有效的响应是B2A1A3C1
(C1 响应阻止了对角线 B2 A3 的威胁)。输出行还可以包括后缀“-I win”、“-You win”、“-invalid”或“-draw”。使用管道来控制每个创建的子进程的输入和输出。当输出包含“-”时,打印最终输出行(整个游戏序列和结果)并退出。
文件系统:复习问题
问题编号可能会更改
问题 1
编写一个使用 fseek 和 ftell 的函数,将文件的中间字符替换为’X’
void xout(char* filename) { FILE *f = fopen(filename, ____ ); }
问题 2
在ext2
文件系统中,从磁盘读取多少个 inode 才能访问文件/dir1/subdirA/notes.txt
的第一个字节?假设根目录中的目录名称和 inode 编号(但不是 inode 本身)已经在内存中。
问题 3
在ext2
文件系统中,必须从磁盘读取多少个最小磁盘块才能访问文件/dir1/subdirA/notes.txt
的第一个字节?假设根目录中的目录名称和 inode 编号以及所有 inode 已经在内存中。
问题 4
在具有 32 位地址和 4KB 磁盘块的ext2
文件系统中,一个 inode 可以存储 10 个直接磁盘块编号。需要多大的文件大小才需要单一间接表?ii)双重间接表?
问题 5
修复下面的 shell 命令chmod
,以设置文件secret.txt
的权限,使所有者可以读取、写入和执行权限,组可以读取,其他人没有访问权限。
chmod 000 secret.txt
网络:复习问题
- 带交互式多选题的维基
- 见编码问题
- 见简答问题
- 见MP 可穿戴设备思考问题
简答问题
Q1
什么是套接字?
Q2
监听端口 1000 和端口 2000 有什么特别之处?
- 端口 2000 比端口 1000 慢两倍
- 端口 2000 比端口 1000 快两倍
- 端口 1000 需要 root 权限
- 无
Q3
IPv4 和 IPv6 之间的一个重要区别是什么?
Q4
何时以及为什么会使用 ntohs?
Q5
如果主机地址是 32 位,我最有可能使用哪种 IP 方案?128 位呢?
Q6
哪种常见的网络协议是基于数据包的,可能无法成功传递数据?
Q7
哪种常见的协议是基于流的,如果数据包丢失将重新发送数据?
Q8
什么是 SYN ACK ACK-SYN 握手?
Q9
以下哪项不是 TCP 的特性之一?
- 数据包重排序
- 流量控制
- 数据包重传
- 简单的错误检测
- 加密
Q10
什么协议使用序列号?它们的初始值是多少?为什么?
Q11
构建 TCP 服务器需要的最小网络调用是什么?它们的正确顺序是什么?
Q12
构建 TCP 客户端所需的最小网络调用是什么?它们的正确顺序是什么?
Q13
何时在 TCP 客户端上调用 bind?
Q14
套接字绑定监听接受的目的是什么?
Q15
上述哪个调用可以阻塞,等待新客户端连接?
Q16
DNS 是什么?它对你有什么作用?CS241 网络调用中的哪些会为你使用它?
Q17
对于 getaddrinfo,如何指定服务器套接字?
Q18
为什么 getaddrinfo 可能会生成网络数据包?
Q19
哪个网络调用指定了允许的积压大小?
Q20
哪个网络调用返回一个新的文件描述符?
Q21
何时使用被动套接字?
Q22
何时使用 epoll 比 select 更好?何时使用 select 比 epoll 更好?
Q23
write(fd, data, 5000)
总是发送 5000 字节的数据吗?它何时会失败?
Q24
网络地址转换(NAT)是如何工作的?
Q25
@MCQ 假设网络客户端和服务器之间的传输时间为 20ms,建立 TCP 连接需要多长时间?20ms 40ms 100ms 60ms @ANS 3 次握手 @EXP @END
Q26
HTTP 1.0 和 HTTP 1.1 之间有哪些区别?如果网络传输时间为 20ms,从服务器传输 3 个文件到客户端需要多少毫秒?HTTP 1.0 和 HTTP 1.1 之间的传输时间有何不同?
编码问题
Q 2.1
写入网络套接字可能不会发送所有字节,并且可能会因为信号中断。检查write
的返回值来实现write_all
,它将重复调用write
以发送任何剩余的数据。如果write
返回-1,那么除非errno
是EINTR
,否则立即返回-1 - 在这种情况下重复上次的write
尝试。您将需要使用指针算术。
// Returns -1 if write fails (unless EINTR in which case it recalls write // Repeated calls write until all of the buffer is written. ssize_t write_all(int fd, const char *buf, size_t nbyte) { ssize_t nb = write(fd, buf, nbyte); return nb; }
Q 2.2
实现一个多线程 TCP 服务器,监听端口 2000。每个线程应从客户端文件描述符中读取 128 字节,并将其回显给客户端,然后关闭连接并结束线程。
Q 2.3
实现一个 UDP 服务器,监听端口 2000。保留一个大小为 200 字节的缓冲区。监听到一个到达的数据包。有效数据包为 200 字节或更少,并以四个字节 0x65 0x66 0x67 0x68 开头。忽略无效的数据包。对于有效的数据包,将第五个字节的值作为无符号值添加到一个运行总数中,并打印到目前为止的总数。如果运行总数大于 255,则退出。
信号:复习问题
给出通常由内核生成的两个信号的名称
给出一个不能被信号捕获的信号的名称
为什么在信号处理程序中调用任何函数(不是信号处理程序安全的函数)是不安全的?
编码问题
编写简短的代码,使用 SIGACTION 和 SIGNALSET 来创建一个 SIGALRM 处理程序。
系统编程笑话
系统编程笑话
警告:作者对这些“笑话”造成的任何神经凋亡概不负责。-允许抱怨。
灯泡笑话
Q.需要多少系统程序员来换一只灯泡?
A.一个,但他们不断更改它,直到返回零。
A.没有,他们更喜欢一个空的插座。
A.好吧,你开始只有一个,但实际上它等待一个孩子来做所有的工作。
抱怨者
为什么婴儿系统程序员喜欢他们的新彩色毯子?它是多线程的。
为什么你的程序如此精致柔软?我只使用 400 线程或更高线程的程序。
当坏学生 shell 进程死去时,他们去哪里?地狱分叉。
为什么 C 程序员如此凌乱?他们把所有东西都存储在一个大堆中。
系统程序员(定义)
系统程序员是…
知道sleepsort
是一个坏主意,但仍然梦想找借口使用它的人。
从不让他们的代码死锁的人…但当它发生时,会比其他人加起来造成更多问题。
一个相信僵尸是真实的人。
一个不相信他们的进程在没有使用相同的数据、内核、编译器、RAM、文件系统大小、文件系统格式、磁盘品牌、核心数量、CPU 负载、天气、磁通量、方向、精灵尘、星座、墙壁颜色、墙壁光泽和反射、主板、振动、照明、备用电池、时间、温度、湿度、月球位置、太阳-月球共同位置的情况下正确运行的人…
系统程序(定义)
一个系统程序…
发展到可以发送电子邮件。
发展到有潜力创建、连接和终结其他程序,并在所有可能的设备上消耗所有可能的 CPU、内存、网络…资源,但选择不这样做。今天。