UIUC CS241 讲义:众包系统编程书(3/3)

235 阅读1小时+

原文:angrave/SystemProgramming

译者:飞龙

协议:CC BY-NC-SA 4.0

八、网络连接

POSIX,第一部分:错误处理

什么是 POSIX 错误处理?

在其他语言中,你可能会看到异常处理的实现。尽管在 C 中你技术上可以使用它们(你保留一个非常 try/catch 块的堆栈,并使用setjmplongjmp分别进入这些块),但 C 中的错误处理通常是用 posix 错误处理来完成的,代码通常看起来像这样。

int ret = some_system_call()
if(ret == ERROR_CODE){
switch(errno){
// Do different stuff based on the errno number.
}
}

在内核中,使用goto来清理应用程序的不同部分是非常常见的。你不应该使用 goto,因为它会使代码更难阅读。内核中的 goto 是出于必要性而存在的,所以不要学习它。

errno是什么,何时设置它?

POSIX 定义了一个特殊的整数errno,当系统调用失败时会设置它。errno的初始值是零(即没有错误)。当系统调用失败时,它通常会返回-1 来指示错误并设置errno

多线程呢?

每个线程都有自己的errno副本。这非常有用;否则一个线程的错误会干扰另一个线程的错误状态。

errno何时重置为零?

除非你明确将它重置为零!当系统调用成功时,它们会重置errno的值。

这意味着你只应该依赖 errno 的值,如果你知道一个系统调用失败了(例如它返回了-1)。

使用errno的注意事项和最佳实践是什么?

当复杂的错误处理使用库调用或系统调用可能改变errno的值时要小心。实际上,将errno的值复制到一个 int 变量中更安全:

// Unsafe - the first fprintf may change the value of errno before we use it!
if (-1 == sem_wait(&s)) {
   fprintf(stderr, "An error occurred!");
   fprintf(stderr, "The error value is %d\n", errno);
}
// Better, copy the value before making more system and library calls
if (-1 == sem_wait(&s)) {
   int errno_saved = errno;
   fprintf(stderr, "An error occurred!");
   fprintf(stderr, "The error value is %d\n", errno_saved);
}

同样,如果你的信号处理程序进行了任何系统或库调用,那么最好的做法是保存 errno 的原始值,并在返回之前恢复该值:

void handler(int signal) {
   int errno_saved = errno;

   // make system calls that might change errno

   errno = errno_saved;
}

如何打印出与特定错误号相关联的字符串消息?

使用strerror来获取错误值的简短(英文)描述

char *mesg = strerror(errno);
fprintf(stderr, "An error occurred (errno=%d): %s", errno, mesg);

perror 和 strerror 有什么关系?

在之前的页面中,我们使用 perror 将错误打印到标准错误输出。使用strerror,我们现在可以编写一个简单的perror实现:

void perror(char *what) {
   fprintf(stderr, "%s: %s\n", what, strerror(errno));
}

使用 strerror 的注意事项是什么?

不幸的是,strerror不是线程安全的。换句话说,两个线程不能同时调用它!

有两种解决方法:首先,我们可以使用互斥锁来定义一个临界区和一个本地缓冲区。所有调用strerror的地方都应该使用相同的互斥锁。

pthread_mutex_lock(&m);
char *result = strerror(errno);
char *message = malloc(strlen(result) + 1);
strcpy(message, result);
pthread_mutex_unlock(&m);
fprintf(stderr, "An error occurred (errno=%d): %s", errno, message);
free(message);

或者使用不太便携但线程安全的strerror_r

EINTR 是什么?对 sem_wait、read、write 有什么影响?

当信号(例如 SIGCHLD、SIGPIPE 等)传递到进程时,一些系统调用可能会被中断。此时,系统调用可能会返回而不执行任何操作!例如,可能没有读/写字节,信号量等待可能没有等待。

这种中断可以通过检查返回值和errno是否为 EINTR 来检测。在这种情况下,应该重试系统调用。通常会看到以下类型的循环,它包装了一个系统调用(比如 sem_wait)。"

while ((-1 == systemcall(...)) && (errno == EINTR)) { /* repeat! */}

小心写成== EINTR,而不是= EINTR

或者,如果结果值需要稍后使用...

while ((-1 == (result = systemcall(...))) && (errno == EINTR)) { /* repeat! */}

在 Linux 上,调用readwrite到本地磁盘通常不会返回 EINTR(相反,函数会自动为您重新启动)。然而,对应于网络流的文件描述符上调用readwrite可能会返回 EINTR。

哪些系统调用可能会被中断并需要包装?

使用手册页!手册页包括系统调用可能设置的错误(即 errno 值)列表。一个经验法则是'慢'(阻塞)调用(例如写入套接字)可能会被中断,但快速的非阻塞调用(例如 pthread_mutex_lock)不会。

来自 Linux 信号 7 手册页。

"如果在系统调用或库函数调用被阻塞时调用了信号处理程序,那么:

  • 信号处理程序返回后,调用将自动重新启动;或者

  • 调用失败,并显示错误 EINTR。发生这两种行为取决于接口以及信号处理程序是否使用了 SA_RESTART 标志(请参阅 sigaction(2))。这些细节在 UNIX 系统中各不相同;以下是 Linux 的细节。

如果对以下接口之一的阻塞调用被信号处理程序中断,那么如果使用了 SA_RESTART 标志,则在信号处理程序返回后,调用将自动重新启动;否则,调用将失败,并显示错误 EINTR:

  • 对“慢”设备的 read(2),readv(2),write(2),writev(2)和 ioctl(2)调用。 “慢”设备是指 I/O 调用可能会无限期地阻塞的设备,例如终端,管道或套接字。(根据此定义,磁盘不是慢设备。)如果对慢设备的 I/O 调用在被信号处理程序中断时已经传输了一些数据,则调用将返回成功状态(通常是传输的字节数)。

请注意,很容易相信设置'SA_RESTART'标志就足以使整个问题消失。不幸的是,这并不是真的:仍然有可能有系统调用会提前返回并设置EINTR!有关详细信息,请参阅signal(7)

Errno 异常?

有一些 POSIX 实用程序有自己的 errno。其中一个是当您调用getaddrinfo函数来检查错误并将其转换为字符串时,可以使用gai_strerror。不要混淆它们!

网络,第一部分:介绍

注意:显而易见,本页不是IP、UDP 或 TCP 的完整描述!相反,这是一个简短的介绍,足以让我们在以后的讲座中建立在这些概念之上。

“IP4”“IP6”是什么?

以下是互联网协议(IP)的“30 秒”介绍-这是从一台机器向另一台机器发送信息包(“数据报”)的主要方法。

“IP4”,或更准确地说,“IPv4”是互联网协议的第 4 版,描述了如何在网络上从一台机器发送信息包到另一台机器。大约 95%的互联网数据包今天都是 IPv4 数据包。IPv4 的一个重要限制是源地址和目的地址被限制为 32 位(IPv4 是在当时认为 4 亿台设备连接到同一网络是不可想象的时候设计的,或者至少不值得增加数据包大小)

每个 IPv4 数据包包括一个非常小的头部-通常为 20 字节(更准确地说,“八位字节”),其中包括源地址和目的地址。

从概念上讲,源地址和目的地址可以分为两部分:网络号(高位)和低位表示该网络上特定主机号。

更新的数据包协议“IPv6”解决了 IPv4 的许多限制(例如,使路由表更简单和 128 位地址),但是不到 5%的网络流量是基于 IPv6 的。

一台机器可以有一个 IPv6 地址和一个 IPv4 地址。

“没有像 127.0.0.1 这样的地方”!

特殊的 IPv4 地址是127.0.0.1,也称为本地主机。发送到 127.0.0.1 的数据包永远不会离开机器;该地址被指定为同一台机器。

请注意,32 位地址被分成 4 个八位字节,即点表示法中的每个数字可以是 0-255。但是 IPv4 地址也可以写成整数。

...和...“没有像 0:0:0:0:0:0:0:1 这样的地方”?

IPv6 中的 128 位本地主机地址是0:0:0:0:0:0:0:1,可以用缩写形式::1来表示。

什么是端口?

要使用 IPv4(或 IPv6)向互联网上的主机发送数据报(数据包),您需要指定主机地址和端口。端口是一个无符号的 16 位数字(即最大端口号为 65535)。

一个进程可以监听特定端口上的传入数据包。但是只有具有超级用户(root)访问权限的进程才能监听端口<1024。任何进程都可以监听 1024 或更高的端口。

经常使用的端口是端口 80:端口 80 用于未加密的 http 请求(即网页)。例如,如果一个网络浏览器连接到www.bbc.com/,那么它将连接到端口 80。

UDP 是什么?它什么时候使用?

UDP 是建立在 IPv4 和 IPv6 之上的无连接协议。它非常简单易用:决定目的地址和端口,然后发送数据包!然而,网络不能保证数据包是否会到达。如果网络拥挤,数据包(也称为数据报)可能会丢失。数据包可能会重复或无序到达。

在两个远程数据中心之间,典型的数据包丢失率为 3%。

UDP 的典型用例是当接收最新数据比接收所有数据更重要时。例如,游戏可能会发送玩家位置的持续更新。流媒体视频信号可能使用 UDP 发送图片更新。

TCP 是什么?它什么时候使用?

TCP 是建立在 IPv4 和 IPv6 之上的基于连接的协议(因此可以被描述为“TCP/IP”或“TCP over IP”)。TCP 在两台机器之间创建了一个“管道”,并抽象了互联网的低级数据包特性:因此,在大多数情况下,从一台机器发送的字节最终会到达另一端,而不会重复或丢失数据。

TCP 将自动管理重发数据包,忽略重复数据包,重新排列无序数据包,并改变发送数据包的速率。

TCP 的三次握手被称为 SYN,SYN-ACK 和 ACK。本页面上的图表有助于理解 TCP 握手。TCP 握手

今天互联网上的大多数服务(例如 Web 服务)使用 TCP,因为它隐藏了互联网更低级别的数据包特性的复杂性。

网络,第二部分:使用 getaddrinfo

如何使用getaddrinfo将主机名转换为 IP 地址?

函数getaddrinfo可以将人类可读的域名(例如www.illinois.edu)转换为 IPv4 和 IPv6 地址。实际上,它将返回一个 addrinfo 结构的链表:

struct addrinfo {
    int              ai_flags;
    int              ai_family;
    int              ai_socktype;
    int              ai_protocol;
    socklen_t        ai_addrlen;
    struct sockaddr *ai_addr;
    char            *ai_canonname;
    struct addrinfo *ai_next;
};

使用起来非常简单。例如,假设你想找出www.bbc.com的网页服务器的数值 IPv4 地址。我们分两个阶段来做。首先使用 getaddrinfo 构建可能连接的链表。其次使用getnameinfo将二进制地址转换为可读形式。

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>

struct addrinfo hints, *infoptr; // So no need to use memset global variables

int main() {
  hints.ai_family = AF_INET; // AF_INET means IPv4 only addresses

  int result = getaddrinfo("www.bbc.com", NULL, &hints, &infoptr);
  if (result) {
    fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(result));
    exit(1);
  }

  struct addrinfo *p;
  char host[256],service[256];

  for(p = infoptr; p != NULL; p = p->ai_next) {

    getnameinfo(p->ai_addr, p->ai_addrlen, host, sizeof(host), service, sizeof(service), NI_NUMERICHOST);
    puts(host);
  }

  freeaddrinfo(infoptr);
  return 0;
}

典型输出:

212.58.244.70
212.58.244.71 

www.cs.illinois.edu如何转换为 IP 地址?

神奇!不是开玩笑,使用了一个名为“DNS”(域名服务)的系统。如果一台机器本地没有答案,那么它会向本地 DNS 服务器发送一个 UDP 数据包。这个服务器反过来可能会查询其他上游 DNS 服务器。

DNS 安全吗?

DNS 本身很快,但不安全。DNS 请求未加密,容易受到“中间人”攻击的影响。例如,咖啡店的互联网连接可以轻松篡改您的 DNS 请求,并为特定域返回不同的 IP 地址

如何连接到 TCP 服务器(例如网页服务器)?

TODO 有三个基本的系统调用,你需要连接到远程机器:

getaddrinfo -- Determine the remote addresses of a remote host
socket  -- Create a socket
connect  -- Connect to the remote host using the socket and address information 

如果getaddrinfo调用成功,它将创建一个addrinfo结构的链表,并将给定的指针设置为指向第一个。

套接字调用创建一个传出套接字并返回一个描述符(有时称为“文件描述符”),可以与readwrite等一起使用。在这个意义上,它是网络模拟open打开文件流的功能-只是我们还没有将套接字连接到任何地方!

最后,连接调用尝试连接到远程机器。我们传递原始套接字描述符,以及存储在 addrinfo 结构中的套接字地址信息。有不同类型的套接字地址结构(例如 IPv4 与 IPv6),可能需要更多的内存。因此,除了传递指针外,还传递了结构的大小:

// Pull out the socket address info from the addrinfo struct:
connect(sockfd, p->ai_addr, p->ai_addrlen)

如何释放为 addrinfo 结构的链表分配的内存?

在清理代码的一部分上调用freeaddrinfo,在最顶层的addrinfo结构上:

void freeaddrinfo(struct addrinfo *ai);

如果 getaddrinfo 失败,我可以使用strerror打印出错误吗?

不。使用getaddrinfo进行错误处理有点不同:

  • 返回值就是错误代码(即不要使用errno

  • 使用gai_strerror获取等效的简短英文错误文本:

int result = getaddrinfo(...);
if(result) { 
   const char *mesg = gai_strerror(result); 
   ...
}

我可以只请求 IPv4 或 IPv6 连接吗?仅限 TCP?

是的!使用传递给getaddrinfo的 addrinfo 结构来定义你想要的连接类型。

例如,要指定基于 IPv6 的基于流的协议:

struct addrinfo hints;
memset(hints, 0, sizeof(hints));

hints.ai_family = AF_INET6; // Only want IPv6 (use AF_INET for IPv4)
hints.ai_socktype = SOCK_STREAM; // Only want stream-based connection

关于使用gethostbyname的代码示例呢?

旧函数gethostbyname已被弃用;这是将主机名转换为 IP 地址的旧方法。端口地址仍然需要使用 htons 函数手动设置。使用更新的getaddrinfo更容易编写支持 IPv4 和 IPv6 的代码

是这么简单!?

是也不是。创建一个简单的 TCP 客户端很容易-但是网络通信提供了许多不同级别的抽象,以及可以在每个抽象级别设置的几个属性和选项(例如,我们还没有讨论可以操纵套接字选项的setsockopt)。有关更多信息,请参阅此指南

网络,第三部分:构建一个简单的 TCP 客户端

套接字

int socket(int domain, int type, int protocol);

Socket 使用域(通常为 IPv4 的 AF_INET),类型是使用 UDP 还是 TCP,协议是任何附加选项。这在内核中创建了一个套接字对象,可以与外部世界/网络通信。这将返回一个 fd,因此您可以像使用普通文件描述符一样使用它!请记住,您希望从 socketfd 读取或写入,因为它仅代表客户端的套接字对象,否则您希望遵守服务器的约定。

getaddressinfo

我们在上一节看到了这个!你们是这方面的专家。

连接

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

将 sockfd 传递给它,然后传递您要访问的地址及其长度,您将可以连接(只要检查错误)。请记住,网络调用极易失败。

读取/写入

一旦我们成功连接,我们可以像处理任何旧文件描述符一样读取或写入。请记住,如果您连接到一个网站,您希望遵守 HTTP 协议规范,以便获得任何有意义的结果。通常有库来做这个,通常你不会在套接字级别连接,因为周围有其他库或软件包

完整的简单 TCP 客户端示例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <unistd.h>

int main(int argc, char **argv)
{
    int s;
    int sock_fd = socket(AF_INET, SOCK_STREAM, 0);

    struct addrinfo hints, *result;
    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_family = AF_INET; /* IPv4 only */
    hints.ai_socktype = SOCK_STREAM; /* TCP */

    s = getaddrinfo("www.illinois.edu", "80", &hints, &result);
    if (s != 0) {
            fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(s));
            exit(1);
    }

    if(connect(sock_fd, result->ai_addr, result->ai_addrlen) == -1){
                perror("connect");
                exit(2);
        }

    char *buffer = "GET / HTTP/1.0\r\n\r\n";
    printf("SENDING: %s", buffer);
    printf("===\n");
    write(sock_fd, buffer, strlen(buffer));

    char resp[1000];
    int len = read(sock_fd, resp, 999);
    resp[len] = '\0';
    printf("%s\n", resp);

    return 0;
}

示例输出:

SENDING: GET / HTTP/1.0

===
HTTP/1.1 200 OK
Date: Mon, 27 Oct 2014 19:19:05 GMT
Server: Apache/2.2.15 (Red Hat) mod_ssl/2.2.15 OpenSSL/1.0.1e-fips mod_jk/1.2.32
Last-Modified: Fri, 03 Feb 2012 16:51:10 GMT
ETag: "401b0-49-4b8121ea69b80"
Accept-Ranges: bytes
Content-Length: 73
Connection: close
Content-Type: text/html

Provided by Web Services at Public Affairs at the University of Illinois 

对 HTTP 请求和响应的评论

上面的示例演示了使用超文本传输协议向服务器发出请求。使用以下请求请求网页(或其他资源):

GET / HTTP/1.0 

有四个部分(方法例如 GET,POST,...);资源(例如/ /index.html /image.png);协议“HTTP/1.0”和两个新行(\r\n\r\n)

服务器的第一行响应描述了所使用的 HTTP 版本以及请求是否成功,使用了一个 3 位数的响应代码:

HTTP/1.1 200 OK 

如果客户端请求了一个不存在的文件,例如GET /nosuchfile.html HTTP/1.0,那么第一行包括响应代码是著名的404响应代码:

HTTP/1.1 404 Not Found 

网络,第四部分:构建一个简单的 TCP 服务器

htons是什么,何时使用它?

整数可以以最低有效字节优先或最高有效字节优先表示。只要机器本身在内部一致,任何方法都是合理的。对于网络通信,我们需要在约定的格式上进行标准化。

htons(xyz)以网络字节顺序返回 16 位无符号整数“short”值 xyz。htonl(xyz)以网络字节顺序返回 32 位无符号整数“long”值 xyz。

这些函数被读作“主机到网络”;反向函数(ntohs、ntohl)将网络排序的字节值转换为主机排序。那么,主机排序是小端还是大端?答案是-这取决于您的机器!这取决于运行代码的主机的实际架构。如果架构恰好与网络排序相同,那么这些函数的结果就是参数。对于 x86 机器,主机和网络排序是不同的。

总结:无论何时读取或写入低级 C 网络结构(例如端口和地址信息),请记住使用上述函数确保正确转换为/从机器格式。否则,显示或指定的值可能是不正确的。

用于创建服务器的“大 4”网络调用是什么?

创建 TCP 服务器所需的四个系统调用是:socketbindlistenaccept。每个都有特定的目的,并且应按上述顺序调用。

端口信息(由 bind 使用)可以手动设置(许多旧的仅 IPv4 的 C 代码示例都这样做),也可以使用getaddrinfo创建

我们稍后也会看到 setsockopt 的示例。

调用socket的目的是什么?

为网络通信创建一个端点。一个新的套接字本身并不特别有用;虽然我们已经指定了基于数据包或基于流的连接,但它并没有绑定到特定的网络接口或端口。相反,套接字返回一个网络描述符,可以在以后调用 bind、listen 和 accept 时使用。

调用bind的目的是什么

bind调用将抽象套接字与实际网络接口和端口关联起来。可以在 TCP 客户端上调用 bind,但通常不需要指定出站端口。

调用listen的目的是什么

listen调用指定了等待处理的传入连接的队列大小,即尚未被accept分配网络描述符的连接。高性能服务器的典型值为 128 或更多。

为什么服务器套接字是被动的?

服务器套接字不会主动尝试连接到另一个主机;相反,它们等待传入的连接。此外,当对等方断开连接时,服务器套接字不会关闭。相反,当远程客户端连接时,它会立即被转移到未使用的端口号以进行未来通信。

调用accept的目的是什么

一旦服务器套接字被初始化,服务器调用accept等待新的连接。与socketbindlisten不同,这个调用将会阻塞。也就是说,如果没有新的连接,这个调用将会阻塞,只有当一个新的客户端连接时才会返回。

注意,accept调用返回一个新的文件描述符。这个文件描述符特定于特定的客户端。常见的编程错误是使用原始服务器套接字描述符进行服务器 I/O,然后惊讶地发现网络代码失败了。

创建 TCP 服务器的注意事项是什么?

  • 使用被动服务器套接字的套接字描述符(如上所述)

  • 未指定getaddrinfo的 SOCK_STREAM 要求

  • 无法重用现有端口。

  • 不初始化未使用的结构条目

  • 如果端口当前正在使用,bind调用将失败

注意,端口是每台机器的,而不是每个进程或每个用户的。换句话说,当另一个进程使用该端口时,您不能使用端口 1234。更糟糕的是,默认情况下,端口在进程结束后会被“占用”。

服务器代码示例

下面是一个工作的简单服务器示例。请注意,此示例不完整 - 例如,它既不关闭套接字描述符,也不释放getaddrinfo创建的内存。

#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <unistd.h>
#include <arpa/inet.h>

int main(int argc, char **argv)
{
    int s;
    int sock_fd = socket(AF_INET, SOCK_STREAM, 0);

    struct addrinfo hints, *result;
    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_flags = AI_PASSIVE;

    s = getaddrinfo(NULL, "1234", &hints, &result);
    if (s != 0) {
            fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(s));
            exit(1);
    }

    if (bind(sock_fd, result->ai_addr, result->ai_addrlen) != 0) {
        perror("bind()");
        exit(1);
    }

    if (listen(sock_fd, 10) != 0) {
        perror("listen()");
        exit(1);
    }

    struct sockaddr_in *result_addr = (struct sockaddr_in *) result->ai_addr;
    printf("Listening on file descriptor %d, port %d\n", sock_fd, ntohs(result_addr->sin_port));

    printf("Waiting for connection...\n");
    int client_fd = accept(sock_fd, NULL, NULL);
    printf("Connection made: client_fd=%d\n", client_fd);

    char buffer[1000];
    int len = read(client_fd, buffer, sizeof(buffer) - 1);
    buffer[len] = '\0';

    printf("Read %d chars\n", len);
    printf("===\n");
    printf("%s\n", buffer);

    return 0;
}

为什么我的服务器不能重用端口?

默认情况下,当套接字关闭时,端口不会立即释放。相反,端口会进入“TIMED-WAIT”状态。这可能会在开发过程中导致重大混乱,因为超时可能会使有效的网络代码看起来失败。

要能够立即重用端口,需要在绑定端口之前指定SO_REUSEPORT

int optval = 1;
setsockopt(sfd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval));

bind(....

这里是一个关于SO_REUSEPORT的扩展 stackoverflow 入门讨论

网络,第五部分:关闭端口,重用端口和其他技巧

关闭和关闭之间有什么区别?

当您不再需要从套接字读取更多数据,写入更多数据或完成两者时,请使用shutdown调用。当您关闭套接字以进行进一步写入(或读取)时,该信息也会发送到连接的另一端。例如,如果您在服务器端关闭套接字以进行进一步写入,那么稍后,阻塞的read调用可能返回 0,表示不再需要更多字节。

当您的进程不再需要套接字文件描述符时,请使用close

如果在创建套接字文件描述符后进行了fork,则所有进程都需要在套接字资源可以重新使用之前关闭套接字。如果您关闭套接字以进行进一步读取,那么所有进程都会受到影响,因为您已更改了套接字,而不仅仅是文件描述符。

良好编写的代码将在调用close之前shutdown套接字。

当我重新运行我的服务器代码时,它不起作用!为什么?

默认情况下,套接字关闭后,端口进入超时状态,在此期间不能重新使用(“绑定到新套接字”)。

通过在绑定到端口之前设置套接字选项 REUSEPORT 可以禁用此行为:

    int optval = 1;
    setsockopt(sock_fd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval));

    bind(sock_fd, ...);

TCP 客户端可以绑定到特定端口吗?

是的!实际上,出站 TCP 连接会自动绑定到客户端上未使用的端口。通常情况下,不需要在客户端上显式设置端口,因为系统会智能地在合理的接口上找到一个未使用的端口(例如,如果当前通过 WiFi 连接,则是无线网卡)。但是,如果您需要明确选择特定的以太网卡,或者防火墙仅允许从特定范围的端口值进行出站连接,则可能会有用。

要显式绑定到以太网接口和端口,请在connect之前调用bind

谁连接到我的服务器?

accept系统调用可以选择性地通过传递 sockaddr 结构提供有关远程客户端的信息。不同的协议具有不同的struct sockaddr变体,它们的大小也不同。使用最简单的结构是sockaddr_storage,它足够大以表示所有可能类型的 sockaddr。请注意,C 没有任何继承模型。因此,我们需要将我们的结构明确转换为“基本类型”结构 sockaddr。

    struct sockaddr_storage clientaddr;
    socklen_t clientaddrsize = sizeof(clientaddr);
    int client_id = accept(passive_socket,
            (struct sockaddr *) &clientaddr,
             &clientaddrsize);

我们已经看到getaddrinfo可以构建 addrinfo 条目的链表(每个条目都可以包含套接字配置数据)。如果我们想要将套接字数据转换为 IP 和端口地址怎么办?输入getnameinfo,它可以用于将本地或远程套接字信息转换为域名或数字 IP。类似地,端口号可以表示为服务名称(例如端口 80 的“http”)。在下面的示例中,我们请求客户端 IP 地址和客户端端口号的数字版本。

    socklen_t clientaddrsize = sizeof(clientaddr);
    int client_id = accept(sock_id, (struct sockaddr *) &clientaddr, &clientaddrsize);
    char host[256], port[256];
    getnameinfo((struct sockaddr *) &clientaddr,
          clientaddrsize, host, sizeof(host), port, sizeof(port),
          NI_NUMERICHOST | NI_NUMERICSERV);

待办事项:讨论 NI_MAXHOST 和 NI_MAXSERV,以及 NI_NUMERICHOST

getnameinfo 示例:我的 IP 地址是多少?

要获取当前计算机的 IP 地址的 IP 地址链表,请使用getifaddrs,它将返回 IPv4 和 IPv6 IP 地址的链接列表(可能还包括其他接口)。我们可以检查每个条目并使用getnameinfo打印主机的 IP 地址。ifaddrs 结构包括家族,但不包括结构的大小。因此,我们需要根据家族(IPv4 v IPv6)手动确定结构的大小。

 (family == AF_INET) ? sizeof(struct sockaddr_in) : sizeof(struct sockaddr_in6)

完整的代码如下所示。

    int required_family = AF_INET; // Change to AF_INET6 for IPv6
    struct ifaddrs *myaddrs, *ifa;
    getifaddrs(&myaddrs);
    char host[256], port[256];
    for (ifa = myaddrs; ifa != NULL; ifa = ifa->ifa_next) {
        int family = ifa->ifa_addr->sa_family;
        if (family == required_family && ifa->ifa_addr) {
            if (0 == getnameinfo(ifa->ifa_addr,
                                (family == AF_INET) ? sizeof(struct sockaddr_in) :
                                sizeof(struct sockaddr_in6),
                                host, sizeof(host), port, sizeof(port)
                                 , NI_NUMERICHOST | NI_NUMERICSERV  ))
                puts(host);
            }
        }

我的机器的 IP 地址是多少(shell 版本)

答案:使用ifconfig(或 Windows 的 ipconfig)。但是这个命令为每个接口生成大量输出,因此我们可以使用 grep 过滤输出。

ifconfig | grep inet

Example output:
    inet6 fe80::1%lo0 prefixlen 64 scopeid 0x1 
    inet 127.0.0.1 netmask 0xff000000 
    inet6 ::1 prefixlen 128 
    inet6 fe80::7256:81ff:fe9a:9141%en1 prefixlen 64 scopeid 0x5 
    inet 192.168.1.100 netmask 0xffffff00 broadcast 192.168.1.255 

网络,第六部分:创建 UDP 服务器

如何创建 UDP 服务器?

有各种可用的函数调用来发送 UDP 套接字。我们将使用较新的 getaddrinfo 来帮助设置套接字结构。

请记住,UDP 是一个简单的基于数据包的协议;两个主机之间没有建立连接。

首先,初始化 hints addrinfo 结构以请求一个 IPv6,被动数据报套接字。

memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_INET6; // INET for IPv4
hints.ai_socktype =  SOCK_DGRAM;
hints.ai_flags =  AI_PASSIVE;

接下来,使用 getaddrinfo 来指定端口号(我们不需要指定主机,因为我们正在创建一个服务器套接字,而不是向远程主机发送数据包)。

getaddrinfo(NULL, "300", &hints, &res);

sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
bind(sockfd, res->ai_addr, res->ai_addrlen);

端口号是<1024,所以程序将需要root权限。我们也可以指定一个服务名称,而不是一个数字端口值。

到目前为止,调用与 TCP 服务器类似。对于基于流的服务,我们将调用listenaccept。对于我们的 UDP 服务器,我们可以开始等待套接字上数据包的到达。

struct sockaddr_storage addr;
int addrlen = sizeof(addr);

// ssize_t recvfrom(int socket, void* buffer, size_t buflen, int flags, struct sockaddr *addr, socklen_t * address_len);

byte_count = recvfrom(sockfd, buf, sizeof(buf), 0, &addr, &addrlen);

addr 结构将保存有关到达数据包的发送者(源)信息。请注意,sockaddr_storage类型足够大,可以容纳所有可能类型的套接字地址(例如 IPv4、IPv6 和其他套接字类型)。

完整代码

#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <unistd.h>
#include <arpa/inet.h>

int main(int argc, char **argv)
{
    int s;

    struct addrinfo hints, *result;
    memset(&hints, 0, sizeof(hints));
    hints.ai_family = AF_INET6; // INET for IPv4
    hints.ai_socktype =  SOCK_DGRAM;
    hints.ai_flags =  AI_PASSIVE;

    getaddrinfo(NULL, "300", &hints, &res);

    int sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);

    if (bind(sockfd, res->ai_addr, res->ai_addrlen) != 0) {
        perror("bind()");
        exit(1);
    }
    struct sockaddr_storage addr;
    int addrlen = sizeof(addr);

    while(1){
        char buffer[1000];
        ssize_t byte_count = recvfrom(sockfd, buf, sizeof(buf), 0, &addr, &addrlen);
        buffer[byte_count] = '\0';
    }

    printf("Read %d chars\n", len);
    printf("===\n");
    printf("%s\n", buffer);

    return 0;
}

网络,第七部分:非阻塞 I/O,select()和 epoll

不要浪费时间等待

通常,当你调用read()时,如果数据尚不可用,它将等待数据准备就绪后再返回。当你从磁盘读取数据时,这种延迟可能不会很长,但当你从一个慢速网络连接中读取数据时,如果数据到达的话,可能需要很长时间。

POSIX 允许你在文件描述符上设置一个标志,以便对该文件描述符的任何read()调用都会立即返回,无论它是否已经完成。在这种模式下,你的read()调用将启动读取操作,而在它工作时,你可以做其他有用的工作。这被称为“非阻塞”模式,因为read()的调用不会阻塞。

要将文件描述符设置为非阻塞:

    // fd is my file descriptor
    int flags = fcntl(fd, F_GETFL, 0);
    fcntl(fd, F_SETFL, flags | O_NONBLOCK);

对于套接字,你可以通过将SOCK_NONBLOCK添加到socket()的第二个参数来以非阻塞模式创建它。

    fd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);

当文件处于非阻塞模式时,你调用read(),它将立即返回可用的字节。假设从套接字的另一端的服务器已经到达了 100 个字节,你调用read(fd, buf, 150)read将立即返回值 100,表示它读取了你要求的 150 个字节中的 100 个。假设你尝试通过调用read(fd, buf+100, 50)来读取剩余的数据,但是最后的 50 个字节还没有到达。read()将返回-1,并将全局错误变量errno设置为 EAGAIN 或 EWOULDBLOCK。这是系统告诉你数据还没有准备好的方式。

write()也可以在非阻塞模式下工作。假设你想使用套接字向远程服务器发送 40,000 字节。系统一次只能发送这么多字节。通常系统一次可以发送大约 23,000 字节。在非阻塞模式下,write(fd, buf, 40000)将返回它立即能够发送的字节数,大约为 23,000。如果你立即再次调用write(),它将返回-1,并将 errno 设置为 EAGAIN 或 EWOULDBLOCK。这是系统告诉你它仍在忙于发送最后一块数据,并且还没有准备好发送更多数据。

如何检查 I/O 何时完成?

有几种方法。让我们看看如何使用selectepoll来做。

select

    int select(int nfds, 
               fd_set *readfds, 
               fd_set *writefds,
               fd_set *exceptfds, 
               struct timeval *timeout);

给定三组文件描述符,select()将等待其中任何一个文件描述符变为“准备就绪”。

  • readfds - 在readfds中的文件描述符在有可读数据或已达到 EOF 时准备就绪。

  • writefds - 在writefds中的文件描述符在调用 write()时将会成功。

  • exceptfds - 系统特定,定义不清晰。只需将其传递为 NULL。

select()返回准备就绪的文件描述符的总数。如果它们在timeout定义的时间内没有准备好,它将返回 0。在select()返回后,调用者需要循环遍历 readfds 和/或 writefds 中的文件描述符,以查看哪些是准备好的。由于 readfds 和 writefds 充当输入和输出参数,当select()指示有准备好的文件描述符时,它会覆盖它们以反映只有准备好的文件描述符。除非调用者的意图是只调用一次select(),否则在调用它之前保存 readfds 和 writefds 的副本是个好主意。

    fd_set readfds, writefds;
    FD_ZERO(&readfds);
    FD_ZERO(&writefds);
    for (int i=0; i < read_fd_count; i++)
      FD_SET(my_read_fds[i], &readfds);
    for (int i=0; i < write_fd_count; i++)
      FD_SET(my_write_fds[i], &writefds);

    struct timeval timeout;
    timeout.tv_sec = 3;
    timeout.tv_usec = 0;

    int num_ready = select(FD_SETSIZE, &readfds, &writefds, NULL, &timeout);

    if (num_ready < 0) {
      perror("error in select()");
    } else if (num_ready == 0) {
      printf("timeout\n");
    } else {
      for (int i=0; i < read_fd_count; i++)
        if (FD_ISSET(my_read_fds[i], &readfds))
          printf("fd %d is ready for reading\n", my_read_fds[i]);
      for (int i=0; i < write_fd_count; i++)
        if (FD_ISSET(my_write_fds[i], &writefds))
          printf("fd %d is ready for writing\n", my_write_fds[i]);
    }

有关 select()的更多信息

epoll

epoll不是 POSIX 的一部分,但它受 Linux 支持。这是一种更有效的等待多个文件描述符的方式。它会告诉你哪些描述符准备好了。它甚至可以为每个描述符存储少量数据,比如数组索引或指针,使得更容易访问与该描述符相关的数据。

使用 epoll,首先您必须使用epoll_create()创建一个特殊的文件描述符。您不会读取或写入此文件描述符;您只需将其传递给其他 epoll_xxx 函数,并在最后调用 close()。

    epfd = epoll_create(1);

对于要使用 epoll 监视的每个文件描述符,您需要使用epoll_ctl()EPOLL_CTL_ADD选项将其添加到 epoll 数据结构中。您可以向其中添加任意数量的文件描述符。

    struct epoll_event event;
    event.events = EPOLLOUT;  // EPOLLIN==read, EPOLLOUT==write
    event.data.ptr = mypointer;
    epoll_ctl(epfd, EPOLL_CTL_ADD, mypointer->fd, &event)

要等待某些文件描述符准备就绪,请使用epoll_wait()。它填充的 epoll_event 结构将包含您在添加此文件描述符时提供的 event.data 中的数据。这使您可以轻松查找与此文件描述符关联的自己的数据。

    int num_ready = epoll_wait(epfd, &event, 1, timeout_milliseconds);
    if (num_ready > 0) {
      MyData *mypointer = (MyData*) event.data.ptr;
      printf("ready to write on %d\n", mypointer->fd);
    }

假设您正在等待向文件描述符写入数据,但现在您想要等待从中读取数据。只需使用epoll_ctl()EPOLL_CTL_MOD选项来更改您正在监视的操作类型。

    event.events = EPOLLOUT;
    event.data.ptr = mypointer;
    epoll_ctl(epfd, EPOLL_CTL_MOD, mypointer->fd, &event);

要取消订阅一个文件描述符,同时保持其他文件描述符处于活动状态,请使用epoll_ctl()EPOLL_CTL_DEL选项。

    epoll_ctl(epfd, EPOLL_CTL_DEL, mypointer->fd, NULL);

要关闭 epoll 实例,请关闭其文件描述符。

    close(epfd);

除了非阻塞的read()write()之外,对非阻塞套接字上的任何connect()调用也将是非阻塞的。要等待连接完成,请使用select()或 epoll 等待套接字可写。

有关 select 的边缘情况的有趣博文

idea.popcount.org/2017-01-06-select-is-fundamentally-broken/

RPC,第一部分:远程过程调用简介

什么是 RPC?

远程过程调用。RPC 是我们可以在不同的机器上执行一个过程(函数)的想法。实际上,该过程可能在同一台机器上执行,但可能在不同的上下文中执行-例如在不同的用户下以不同的权限和不同的生命周期。

什么是特权分离?

远程代码将在不同的用户和不同权限下执行。实际上,远程调用可能以比调用者更多或更少的权限执行。原则上,这可以用来提高系统的安全性(通过确保组件以最低权限运行)。不幸的是,安全问题需要仔细评估,以确保 RPC 机制不能被利用来执行不需要的操作。例如,RPC 实现可能会隐式信任任何连接的客户端执行任何操作,而不是在数据的子集上执行子集的操作。

什么是存根代码?什么是编组?

存根代码是隐藏执行远程过程调用复杂性所必需的代码。存根代码的作用之一是编组必要的数据成为可以作为字节流发送到远程服务器的格式。

// On the outside 'getHiscore' looks like a normal function call
// On the inside the stub code performs all of the work to send and receive the data to and from the remote machine.

int getHiscore(char* game) {
  // Marshall the request into a sequence of bytes:
  char* buffer;
  asprintf(&buffer,"getHiscore(%s)!", name);

  // Send down the wire (we do not send the zero byte; the '!' signifies the end of the message)
  write(fd, buffer, strlen(buffer) );

  // Wait for the server to send a response
  ssize_t bytesread = read(fd, buffer, sizeof(buffer));

  // Example: unmarshal the bytes received back from text into an int
  buffer[bytesread] = 0; // Turn the result into a C string

  int score= atoi(buffer);
  free(buffer);
  return score;
}

什么是服务器存根代码?什么是解组?

服务器存根代码将接收请求,将请求解组成有效的内存数据调用底层实现,并将结果发送回调用者。

如何发送 int?float?结构?链表?图?

要实现 RPC,您需要决定(并记录)将数据序列化为字节序列的约定。即使是一个简单的整数也有几种常见选择:

  • 有符号还是无符号?

  • ASCII

  • 固定字节数或根据大小而变化

  • 小端或大端的二进制格式?

要编组一个结构,决定哪些字段需要序列化。可能不需要发送所有数据项(例如,某些项可能与特定的 RPC 无关,或者可以由服务器从其他数据项重新计算)。

编组链表时,无需发送链接指针-只需流式传输值。作为解组的一部分,服务器可以从字节序列中重新创建链表结构。

通过从头节点/顶点开始,可以递归访问简单树以创建数据的序列化版本。循环图通常需要额外的内存来确保每个边和顶点都被处理一次。

什么是 IDL(接口设计语言)?

手动编写存根代码是痛苦的、乏味的、容易出错的、难以维护的,难以从实现的代码中逆向工程出线协议。更好的方法是指定数据对象、消息和服务,并自动生成客户端和服务器代码。

接口设计语言的现代示例是 Google 的 Protocol Buffer .proto 文件。

RPC 与本地调用的复杂性和挑战?

远程过程调用比本地调用慢得多(10 倍至 100 倍),并且比本地调用更复杂。RPC 必须将数据编组成兼容的格式。这可能需要通过数据结构进行多次传递,临时内存分配和数据表示的转换。

健壮的 RPC 存根代码必须智能地处理网络故障和版本控制。例如,服务器可能需要处理来自仍在运行早期版本存根代码的客户端的请求。

安全的 RPC 将需要实施额外的安全检查(包括身份验证和授权),验证数据并加密客户端和主机之间的通信。

传输大量结构化数据

让我们通过 3 种不同的格式-JSON、XML 和 Google Protocol Buffers 来检查使用 3 种不同格式传输数据的方法。JSON 和 XML 是基于文本的协议。以下是 JSON 和 XML 消息的示例。

<ticket><price currency='dollar'>10</price><vendor>travelocity</vendor></ticket>
{ 'currency':'dollar' , 'vendor':'travelocity', 'price':'10' }

谷歌协议缓冲区是一个开源的高效二进制协议,非常注重高吞吐量、低 CPU 开销和最小内存复制。已经为多种语言实现了协议缓冲区,包括 Go、Python、C++和 C。这意味着可以从.proto 规范文件生成多种语言的客户端和服务器存根代码,以便将数据编组到二进制流中并从中解组。

谷歌协议缓冲区通过忽略消息中存在的未知字段来减少版本问题。有关更多信息,请参阅协议缓冲区的介绍。

developers.google.com/protocol-buffers/docs/overview

网络复习问题

主题

  • IPv4 与 IPv6

  • TCP 与 UDP

  • 数据包丢失/基于连接

  • 获取地址信息

  • DNS

  • TCP 客户端调用

  • TCP 服务器调用

  • 关闭

  • recvfrom

  • epoll 与 select

  • RPC

问题

  • 什么是 IPv4?IPv6?它们之间有什么区别?

  • TCP 是什么?UDP 是什么?给我它们的优缺点。我什么时候会使用其中一个而不是另一个?

  • 哪种协议是无连接的,哪种是基于连接的?

  • 什么是 DNS?DNS 的路由是什么?

  • 套接字的作用是什么?

  • 建立 TCP 客户端的调用是什么?

  • 建立 TCP 服务器的调用是什么?

  • 套接字关闭和关闭之间有什么区别?

  • 何时可以使用readwriterecvfromsendto呢?

  • epoll相对于select有哪些优势?select相对于epoll有哪些优势?

  • 什么是远程过程调用?何时应该使用它?

  • 什么是编组/解组?为什么 HTTP 是 RPC?

九、文件系统

文件系统,第一部分:介绍

导航/术语

设计一个文件系统!你的设计目标是什么?

文件系统的设计是一个困难的问题,因为有许多我们想要满足的高级设计目标。一个不完整的理想目标清单包括:

  • 可靠和健壮(即使有硬件故障或由于断电而导致不完整的写入)

  • 访问(安全)控制

  • 会计和配额

  • 索引和搜索

  • 版本控制和备份功能

  • 加密

  • 自动压缩

  • 高性能(例如内存中的缓存)

  • 高效使用存储去重

并非所有文件系统都原生支持所有这些目标。例如,许多文件系统不会自动压缩很少使用的文件

......是什么?

在标准的 Unix 文件系统中:

  • .表示当前目录

  • ..表示父目录

  • ...不是任何目录的有效表示(这不是爷爷文件夹)。它可能是磁盘上的一个文件的名称。

绝对路径和相对路径是什么?

绝对路径是从您的目录树的'根节点'开始的路径。相对路径是从树中的当前位置开始的路径。

相对路径和绝对路径的一些例子是什么?

如果您从您的主目录开始(简称“~”),那么Desktop/cs241将是一个相对路径。它的绝对路径对应物可能是类似于/Users/[yourname]/Desktop/cs241的东西。

如何简化a/b/../c/./

记住..表示'父文件夹',.表示'当前文件夹'。

例如:a/b/../c/.

  • 步骤 1:cd a(在 a 中)

  • 步骤 2:cd b(在 a/b 中)

  • 步骤 3:cd ..(在 a 中,因为..表示'父文件夹')

  • 步骤 4:cd c(在 a/c 中)

  • 步骤 5:cd .(在 a/c 中,因为.表示'当前文件夹')

因此,这条路径可以简化为a/c

那么什么是文件系统?

文件系统是如何在磁盘上组织信息的。每当您想要访问一个文件时,文件系统规定了文件的读取方式。这是一个文件系统的示例图像。

哇,这太多了,让我们分解一下

  • 超级块:这个块包含关于文件系统的元数据,大小、最后修改时间、日志、索引节点数和第一个索引节点的起始位置、数据块数和第一个数据块的起始位置。

  • 索引节点:这是关键的抽象。索引节点是一个文件。

  • 磁盘块:这是数据存储的地方。文件的实际内容

索引节点如何存储文件内容?

来自Wikipedia

在类 Unix 风格的文件系统中,索引节点,非正式地称为 inode,是用来表示文件系统对象的数据结构,可以是各种东西,包括文件或目录。每个 inode 存储文件系统对象数据的属性和磁盘块位置。文件系统对象属性可能包括操作元数据(例如更改、访问、修改时间),以及所有者和权限数据(例如组 ID、用户 ID、权限)。

要读取文件的前几个字节,跟随第一个间接块指针到第一个间接块并读取前几个字节,写入是相同的过程。如果要读取整个文件,继续读取直接块,直到大小用完(我们稍后会讨论间接块)

“计算机科学中的所有问题都可以通过另一层间接性来解决。”- David Wheeler

为什么要使磁盘块的大小与内存页面相同?

支持虚拟内存,这样我们就可以将东西分页到内存中和从内存中分页出来。

我们想要为每个文件存储什么信息?

  • 文件名

  • 文件大小

  • 创建时间、最后修改时间、最后访问时间

  • 权限

  • 文件路径

  • 校验和

  • 文件数据(索引节点)

文件的传统权限是什么:用户-组-其他权限?

一些常见的文件权限包括:

  • 755:rwx r-x r-x

用户:rwx,组:r-x,其他人:r-x

用户可以读取、写入和执行。组和其他人只能读取和执行。

  • 644:rw- r-- r--

用户:rw-,组:r--,其他人:r--

用户可以读写。组和其他人只能读。

对于每个角色的常规文件,有 3 个权限位是什么?

  • 读(最高有效位)

  • 写(第二位)

  • 执行(最低有效位)

“644”“755”是什么意思?

这些是八进制格式(基数 8)的权限示例。每个八进制数字对应不同的角色(用户、组、全局)。

我们可以按照八进制格式读取权限如下:

  • 644 - 用户权限为 R/W,组权限为 R,全局权限为 R

  • 755 - 用户权限为 R/W/X,组权限为 R/X,全局权限为 R/X

每个间接表可以存储多少个指针?

举个例子,假设我们将磁盘分成 4KB 块,并且我们想要寻址多达 2^32 块。

最大磁盘大小为 4KB * 2^32 = 16TB(记住 2^10 = 1024)

一个磁盘块可以存储 4KB / 4B(每个指针需要 32 位)= 1024 个指针。每个指针指向一个 4KB 的磁盘块 - 因此您可以引用多达 1024 * 4KB = 4MB 的数据

对于相同的磁盘配置,双间接块存储 1024 个指针指向 1024 个间接表。因此,双间接块可以引用多达 1024 * 4MB = 4GB 的数据。

同样,三重间接块可以引用多达 4TB 的数据。

转到文件系统:第二部分

文件系统,第二部分:文件是索引节点(其他一切都只是数据...)

大意:忘记文件名:'索引节点'就是文件。

通常认为文件名是'实际'文件。不是!相反,将索引节点视为文件。索引节点包含元信息(最后访问、所有权、大小)并指向用于保存文件内容的磁盘块。

那么...我们如何实现一个目录?

目录只是名称到索引节点号的映射。POSIX 提供了一小组函数来读取每个条目的文件名和索引节点号(见下文)

让我们想想它在实际文件系统中是什么样子。理论上,目录就像实际文件一样。磁盘块将包含目录条目dirent。这意味着我们的磁盘块可以看起来像这样

索引节点号名称
2043567hi.txt

...

每个目录条目可以是固定大小,也可以是可变的 C 字符串。这取决于特定文件系统在较低级别实现的方式。

我如何找到文件的索引节点号?

从 shell 中,使用带有-i选项的ls

$ ls -i
12983989 dirlist.c      12984068 sandwich.c 

从 C 中调用 stat 函数之一(下面介绍)。

我如何找出文件(或目录)的元信息?

使用 stat 调用。例如,要找出我的'notes.txt'文件上次访问的时间 -

   struct stat s;
   stat("notes.txt", & s);
   printf("Last accessed %s", ctime(s.st_atime));

实际上有三个版本的stat

       int stat(const char *path, struct stat *buf);
       int fstat(int fd, struct stat *buf);
       int lstat(const char *path, struct stat *buf);

例如,您可以使用fstat来查找与该文件关联的文件描述符的文件的元信息

   FILE *file = fopen("notes.txt", "r");
   int fd = fileno(file); /* Just for fun - extract the file descriptor from a C FILE struct */
   struct stat s;
   fstat(fd, & s);
   printf("Last accessed %s", ctime(s.st_atime));

第三个调用'lstat'我们将在介绍符号链接时讨论。

除了访问、创建和修改时间之外,stat 结构还包括索引节点号、文件长度和所有者信息。

struct stat {
               dev_t     st_dev;     /* ID of device containing file */
               ino_t     st_ino;     /* inode number */
               mode_t    st_mode;    /* protection */
               nlink_t   st_nlink;   /* number of hard links */
               uid_t     st_uid;     /* user ID of owner */
               gid_t     st_gid;     /* group ID of owner */
               dev_t     st_rdev;    /* device ID (if special file) */
               off_t     st_size;    /* total size, in bytes */
               blksize_t st_blksize; /* blocksize for file system I/O */
               blkcnt_t  st_blocks;  /* number of 512B blocks allocated */
               time_t    st_atime;   /* time of last access */
               time_t    st_mtime;   /* time of last modification */
               time_t    st_ctime;   /* time of last status change */
           };

我如何列出目录的内容?

让我们编写我们自己的'version of 'ls'来列出目录的内容。

#include <stdio.h>
#include <dirent.h>
#include <stdlib.h>
int main(int argc, char **argv) {
    if(argc == 1) {
        printf("Usage: %s [directory]\n", *argv);
        exit(0);
    }
    struct dirent *dp;
    DIR *dirp = opendir(argv[1]);
    while ((dp = readdir(dirp)) != NULL) {
        puts(dp->d_name);
    }

    closedir(dirp);
    return 0;
}

我如何读取目录的内容?

答:使用 opendir readdir closedir 例如,这是一个非常简单的'ls'实现,用于列出目录的内容。

#include <stdio.h>
#include <dirent.h>
#include <stdlib.h>
int main(int argc, char **argv) {
    if(argc ==1) {
        printf("Usage: %s [directory]\n", *argv);
        exit(0);
    }
    struct dirent *dp;
    DIR *dirp = opendir(argv[1]);
    while ((dp = readdir(dirp)) != NULL) {
        printf("%s %lu\n", dp-> d_name, (unsigned long)dp-> d_ino );
    }

    closedir(dirp);
    return 0;
}

注意:在调用 fork()后,父进程或子进程可以使用 readdir()、rewinddir()或 seekdir()。如果父进程和子进程都使用上述方法,行为是未定义的。

我如何检查文件是否在当前目录中?

例如,要查看特定目录是否包含文件(或文件名)'名称',我们可以编写以下代码。(提示:你能发现错误吗?)

int exists(char *directory, char *name)  {
    struct dirent *dp;
    DIR *dirp = opendir(directory);
    while ((dp = readdir(dirp)) != NULL) {
        puts(dp->d_name);
        if (!strcmp(dp->d_name, name)) {
        return 1; /* Found */
        }
    }
    closedir(dirp);
    return 0; /* Not Found */
}

上面的代码有一个微妙的错误:它泄漏资源!如果找到匹配的文件名,那么'closedir'将不会作为早期返回的一部分调用。opendir 打开的任何文件描述符和分配的任何内存都不会被释放。这意味着最终进程将耗尽资源,并且openopendir调用将失败。

修复的方法是确保我们在每个可能的代码路径中释放资源。在上面的代码中,这意味着在return 1之前调用closedir。忘记释放资源是一个常见的 C 编程错误,因为 C 语言中没有支持确保所有代码路径都始终释放资源。

使用 readdir 的陷阱是什么?例如,递归搜索目录?

有两个主要的陷阱和一个考虑:readdir函数返回“.”(当前目录)和“..”(父目录)。如果要查找子目录,需要明确排除这些目录。

对于许多应用程序来说,首先检查当前目录,然后递归搜索子目录是合理的。这可以通过将结果存储在链接列表中来实现,或者重置目录结构以从头开始重新开始。

最后要注意的一点:readdir不是线程安全的!对于多线程搜索,请使用readdir_r,它要求调用者传递现有 dirent 结构的地址。

有关 readdir 的更多详细信息,请参阅 readdir 的 man 页面。

我如何确定目录条目是否是目录?

答:使用S_ISDIR来检查 stat 结构中存储的模式位

要检查文件是否为常规文件,请使用S_ISREG

   struct stat s;
   if (0 == stat(name, &s)) {
      printf("%s ", name);
      if (S_ISDIR( s.st_mode)) puts("is a directory");
      if (S_ISREG( s.st_mode)) puts("is a regular file");
   } else {
      perror("stat failed - are you sure I can read this file's meta data?");
   }

目录也有 inode 吗?

是的!虽然更好的想法是,一个目录(就像一个文件)一个 inode(带有一些数据-目录名称和 inode 内容)。它碰巧是一种特殊类型的 inode。

来自Wikipedia

Unix 目录是关联结构的列表,每个结构包含一个文件名和一个 inode 号。

请记住,inode 不包含文件名-只包含其他文件元数据。

如何让相同的文件出现在文件系统中的两个不同位置?

首先要记住,文件名!=文件。将 inode 视为'文件',目录只是一个名称列表,每个名称都映射到一个 inode 号。其中一些 inode 可能是常规文件 inode,其他可能是目录 inode。

如果我们已经在文件系统上有一个文件,我们可以使用'ln'命令创建到相同 inode 的另一个链接

$ ln file1.txt blip.txt 

然而,blip.txt 相同的文件;如果我编辑 blip,我正在编辑与'file1.txt!'相同的文件!我们可以通过显示两个文件名指向相同的 inode 来证明这一点:

$ ls -i file1.txt blip.txt
134235 file1.txt
134235 blip.txt 

这些链接(也称为目录条目)称为'硬链接'

等效的 C 调用是link

link(const char *path1, const char *path2);

link("file1.txt", "blip.txt");

为了简单起见,上面的例子在同一个目录中创建了硬链接,但是硬链接可以在同一个文件系统的任何地方创建。

当我rm(删除)一个文件时会发生什么?

当您删除文件(使用rmunlink)时,您正在从目录中删除一个 inode 引用。但是 inode 可能仍然被其他目录引用。为了确定文件的内容是否仍然需要,每个 inode 都保留一个引用计数,每当创建或销毁新链接时,该引用计数都会更新。

案例研究:最小化文件重复的备份软件

硬链接的一个示例用途是有效地在不同时间点创建文件系统的多个存档。一旦存档区域有特定文件的副本,未来的存档可以重用这些存档文件,而不是创建重复的文件。苹果的“Time Machine”软件就是这样做的。

我可以像常规文件一样创建目录的硬链接吗?

不。好吧是的。不是真的...实际上你并不真的想这样做,是吗?POSIX 标准说不,你不可以!ln命令只允许 root 执行此操作,只有在提供-d选项时才能执行此操作。但是,即使 root 也可能无法执行此操作,因为大多数文件系统会阻止它!

为什么?

文件系统的完整性假设目录结构(不包括我们稍后将讨论的软链接)是从根目录可达的非循环树。如果允许目录链接,强制执行或验证此约束将变得昂贵。打破这些假设可能导致文件完整性工具无法修复文件系统。递归搜索可能永远不会终止,目录可能有多个父目录,但“..”只能指向一个父目录。总的来说,这是一个坏主意。

文件系统,第三部分:权限

提醒我权限再次是什么意思?

每个文件和目录都有一组 9 个权限位和一个类型字段

  • r,读取文件的权限

  • w,写入文件的权限

  • x,执行文件的权限

chmod 777

chmod777
01111111111
drwxrwxrwx
1234
  1. 文件类型

  2. 所有者权限

  3. 组权限

  4. 其他人的权限

mknod更改第一个字段,文件的类型。chmod接受一个数字和一个文件,并更改权限位。

文件有一个所有者。如果您的进程具有与所有者相同的用户 ID(或 root),则第一个三元组中的权限适用于您。如果您与文件在同一组中(所有文件也属于一个组),则下一组权限位适用于您。如果以上都不适用,则最后一个三元组适用于您。

如何更改文件的权限?

使用chmod(简称“更改文件模式位”)

有一个系统调用,int chmod(const char *path, mode_t mode);但我们将集中在 shell 命令上。使用chmod的两种常见方法是使用八进制值或使用符号字符串:

$ chmod 644 file1
$ chmod 755 file2
$ chmod 700 file3
$ chmod ugo-w file4
$ chmod o-rx file4 

基于 8('八进制')位数字描述了每个角色的权限:拥有文件的用户,组和其他人。八进制数是给三种权限的三个值的总和:读取(4),写入(2),执行(1)

示例:chmod 755 myfile

  • r + w + x = 数字

  • 用户具有 4+2+1,完全权限

  • 组具有 4+0+1,读取和执行权限

  • 所有用户都有 4+0+1,读取和执行权限

如何从 ls 中读取权限字符串?

使用`ls -l'。请注意,权限将以'drwxrwxrwx'格式输出。第一个字符表示文件类型。第一个字符的可能值:

  • (-)常规文件

  • (d)目录

  • (c)字符设备文件\

  • (l)符号链接

  • (p)管道

  • (b)块设备

  • (s)套接字

什么是 sudo?

使用sudo成为机器上的管理员。例如通常(除非在'/etc/fstab'文件中明确指定,您需要 root 访问权限才能挂载文件系统)。sudo可用于临时以 root 身份运行命令(前提是用户具有 sudo 权限)

$ sudo mount /dev/sda2 /stuff/mydisk
$ sudo adduser fred 

如何更改文件的所有权?

使用chown 用户名文件名

如何从代码中设置权限?

chmod(const char *path, mode_t mode);

为什么有些文件是'setuid'?这是什么意思?

在运行文件时,设置用户 ID 的位会更改与进程关联的用户。这通常用于需要以 root 身份运行但由非 root 用户执行的命令。一个例子是sudo

在执行时设置组 ID 会更改进程所在的组。

它们为什么有用?

最常见的用例是用户可以在程序运行期间具有 root(管理员)访问权限。

sudo 以什么权限运行?

$ ls -l /usr/bin/sudo
-r-s--x--x  1 root  wheel  327920 Oct 24 09:04 /usr/bin/sudo 

's'位表示执行和设置 uid;进程的有效用户 ID 将与父进程不同。在这个例子中,它将是 root

getuid()和 geteuid()之间有什么区别?

  • getuid返回真实用户 ID(如果以 root 身份登录,则为零)

  • geteuid返回有效用户 ID(如果作为 root 运行,例如由于程序上设置了 setuid 标志,则为零)

如何确保只有特权用户可以运行我的代码?

  • 通过调用geteuid()来检查用户的有效权限。返回值为零表示程序有效地作为 root 运行。

转到文件系统:第四部分

文件系统,第四部分:使用目录

如何找出文件(inode)是常规文件还是目录?

使用S_ISDIR宏来检查 stat 结构中的模式位:

struct stat s;
stat("/tmp", &s);
if (S_ISDIR(s.st_mode)) { ... 

请注意,稍后我们将编写健壮的代码来验证 stat 调用是否成功(返回 0);如果“stat”调用失败,我们应该假设 stat 结构内容是任意的。

我如何递归进入子目录?

首先是一个谜题-在以下代码中你能找到多少个错误?

void dirlist(char *path) {

  struct dirent *dp;
  DIR *dirp = opendir(path);
  while ((dp = readdir(dirp)) != NULL) {
     char newpath[strlen(path) + strlen(dp->d_name) + 1];
     sprintf(newpath,"%s/%s", newpath, dp->d_name);
     printf("%s\n", dp->d_name);
     dirlist(newpath);
  }
}

int main(int argc, char **argv) { dirlist(argv[1]); return 0; }

你找到了所有 5 个错误吗?

// Check opendir result (perhaps user gave us a path that can not be opened as a directory
if (!dirp) { perror("Could not open directory"); return; }
// +2 as we need space for the / and the terminating 0
char newpath[strlen(path) + strlen(dp->d_name) + 2]; 
// Correct parameter
sprintf(newpath,"%s/%s", path, dp->d_name); 
// Perform stat test (and verify) before recursing
if (0 == stat(newpath,&s) && S_ISDIR(s.st_mode)) dirlist(newpath)
// Resource leak: the directory file handle is not closed after the while loop
closedir(dirp);

什么是符号链接?它们是如何工作的?我怎么做一个?

symlink(const char *target, const char *symlink);

要在 shell 中创建符号链接,请使用ln -s

要将链接的内容读取为文件,请使用“readlink”

$ readlink myfile.txt
../../dir1/notes.txt 

要读取符号链接的元(stat)信息,请使用“lstat”而不是“stat”

struct stat s1, s2;
stat("myfile.txt", &s1); // stat info about  the notes.txt file
lstat("myfile.txt", &s2); // stat info about the symbolic link

符号链接的优点

  • 可以引用尚不存在的文件

  • 与硬链接不同,可以引用目录以及常规文件

  • 可以引用存在于当前文件系统之外的文件(和目录)

主要缺点:比常规文件和目录慢。当读取链接的内容时,它们必须被解释为目标文件的新路径。

“/dev/null”是什么,何时使用?

文件“/dev/null”是存储您永远不需要读取的位的好地方!发送到“/dev/null/”的字节永远不会被存储-它们只是被丢弃。 “/dev/null”的常见用途是丢弃标准输出。例如,

$ ls . >/dev/null 

为什么我想设置目录的粘性位?

当目录的粘性位被设置时,只有文件的所有者、目录的所有者和 root 用户才能重命名(或删除)该文件。当多个用户对共享目录具有写访问权限时,这是有用的。

粘性位的常见用途是用于共享和可写的“/tmp”目录。

为什么 shell 和脚本程序以“#!/usr/bin/env python”开头?

答:为了可移植性!虽然可能会将完全合格的路径写入 python 或 perl 解释器,但这种方法不是可移植的,因为您可能已将 python 安装在不同的目录中。

要克服这一点,使用“env”实用程序来查找并执行用户路径上的程序。env 实用程序本身通常存储在“/usr/bin”中-必须使用绝对路径指定。

如何制作“隐藏”文件,即不被“ls”列出?我如何列出它们?

简单!创建以“.”开头的文件(或目录)-然后(默认情况下)它们不会被标准工具和实用程序显示。

这通常用于将配置文件隐藏在用户的主目录中。例如,“ssh”将其首选项存储在一个名为“.sshd”的目录中。

要列出所有文件,包括通常隐藏的条目,请使用带有“-a”选项的“ls”

$ ls -a
.           a.c         myls
..          a.out           other.txt
.secret 

如果我关闭目录上的执行位会发生什么?

目录的执行位用于控制目录内容是否可列出。

$ chmod ugo-x dir1
$ ls -l
drw-r--r--   3 angrave  staff   102 Nov 10 11:22 dir1 

但是,当尝试列出目录的内容时,

$ ls dir1
ls: dir1: Permission denied 

换句话说,目录本身是可发现的,但其内容无法列出。

什么是文件通配(由谁执行)?

在执行程序之前,shell 将参数扩展为匹配的文件名。例如,如果当前目录有三个以 my 开头的文件名(my1.txt mytext.txt myomy),那么

$ echo my* 

扩展到

$ echo my1.txt mytext.txt myomy 

这被称为文件通配,并在执行命令之前进行处理。即命令的参数与手动输入每个匹配的文件名相同。

创建安全目录

假设您在/tmp 中创建了自己的目录,然后设置了权限,以便只有您可以使用该目录(见下文)。这安全吗?

$ mkdir /tmp/mystuff
$ chmod 700 /tmp/mystuff 

在目录创建和权限更改之间存在一个机会窗口。这导致了几个基于竞争条件的漏洞(攻击者在权限被移除之前以某种方式修改目录)。一些例子包括:

另一个用户用一个硬链接替换mystuff,指向第二个用户拥有的现有文件或目录,然后他们就能读取和控制mystuff目录的内容。哦不 - 我们的秘密不再是秘密了!

然而,在这个特定的例子中,/tmp目录设置了粘滞位,因此其他用户可能无法删除mystuff目录,上述简单的攻击场景是不可能的。这并不意味着创建目录,然后稍后将目录设为私有是安全的!更好的版本是从一开始就原子性地创建具有正确权限的目录 -

$ mkdir -m 700 /tmp/mystuff 

如何自动创建父目录?

$ mkdir -p d1/d2/d3 

如果它们不存在,将自动创建 d1 和 d2。

我的默认 umask 是 022;这是什么意思?

umask 减去(减少)权限位从 777,并且在使用 open、mkdir 等创建新文件和新目录时使用。因此,022(八进制)表示组和其他权限不包括可写位。每个进程(包括 shell)都有一个当前的 umask 值。在分叉时,子进程继承父进程的 umask 值。

例如,通过在 shell 中将 umask 设置为 077,可以确保将来创建的文件和目录只能被当前用户访问,

$ umask 077
$ mkdir secretdir 

作为一个代码示例,假设使用open()创建一个新文件,并且模式位是666(用户、组和其他的写入和读取位):

open("myfile", O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);

如果 umask 是八进制 022,那么创建的文件的权限将是 0666 和~022,即。

           S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH

我怎样才能从一个文件复制字节到另一个文件?

使用多功能的dd命令。例如,以下命令将从文件/dev/urandom复制 1MB 的数据到文件/dev/null。数据被复制为 1024 个块,每个块大小为 1024 字节。

$ dd if=/dev/urandom of=/dev/null bs=1k count=1024 

上面示例中的输入和输出文件都是虚拟的 - 它们不存在于磁盘上。这意味着传输速度不受硬件功率的影响。相反,它们是内核提供的虚拟文件系统的一部分。虚拟文件/dev/urandom提供无限的随机字节流,而虚拟文件/dev/null会忽略写入它的所有字节。/dev/null的常见用途是丢弃命令的输出,

$ myverboseexecutable > /dev/null 

另一个常用的/dev 虚拟文件是/dev/zero,它提供无限的零字节流。例如,我们可以对读取内核中的流零字节到进程内存并将字节写回内核而不进行任何磁盘 I/O 的操作系统性能进行基准测试。请注意,吞吐量(约 20GB/s)强烈依赖于块大小。对于小块大小,额外的readwrite系统调用的开销将占主导地位。

$ dd if=/dev/zero of=/dev/null bs=1M count=1024
1024+0 records in
1024+0 records out
1073741824 bytes (1.1 GB) copied, 0.0539153 s, 19.9 GB/s 

当我触摸一个文件时会发生什么?

touch可执行文件如果文件不存在则创建文件,并且还会更新文件的最后修改时间为当前时间。例如,我们可以用当前时间创建一个新的私有文件:

$ umask 077       # all future new files will maskout all r,w,x bits for group and other access
$ touch file123   # create a file if it does not exist, and update its modified time
$ stat file123
  File: `file123'
  Size: 0           Blocks: 0          IO Block: 65536  regular empty file
Device: 21h/33d Inode: 226148      Links: 1
Access: (0600/-rw-------)  Uid: (395606/ angrave)   Gid: (61019/     ews)
Access: 2014-11-12 13:42:06.000000000 -0600
Modify: 2014-11-12 13:42:06.001787000 -0600
Change: 2014-11-12 13:42:06.001787000 -0600 

touch的一个示例用途是在修改 makefile 中的编译器选项后,强制 make 重新编译未更改的文件。记住,make 是“懒惰的” - 它将比较源文件的修改时间和相应输出文件的修改时间,以确定是否需要重新编译文件。

$ touch myprogram.c   # force my source file to be recompiled
$ make 

转到文件系统:第五部分

文件系统,第五部分:虚拟文件系统

虚拟文件系统

POSIX 系统,如 Linux 和基于 BSD 的 Mac OSX,包括几个作为文件系统的一部分挂载(可用)的虚拟文件系统。这些虚拟文件系统中的文件不存在于磁盘上;当进程请求目录列表时,它们由内核动态生成。Linux 提供了 3 个主要的虚拟文件系统

/dev  - A list of physical and virtual devices (for example network card, cdrom, random number generator)
/proc - A list of resources used by each process and (by tradition) set of system information
/sys - An organized list of internal kernel entities 

例如,如果我想要一个连续的 0 流,我可以cat /dev/zero

如何找出当前有哪些文件系统可用(已挂载)?

使用mount,不带任何选项地使用 mount 会生成一个列表(每行一个文件系统)已挂载的文件系统,包括网络、虚拟和本地(旋转磁盘/基于 SSD 的)文件系统。以下是 mount 的典型输出

$ mount
/dev/mapper/cs241--server_sys-root on / type ext4 (rw)
proc on /proc type proc (rw)
sysfs on /sys type sysfs (rw)
devpts on /dev/pts type devpts (rw,gid=5,mode=620)
tmpfs on /dev/shm type tmpfs (rw,rootcontext="system_u:object_r:tmpfs_t:s0")
/dev/sda1 on /boot type ext3 (rw)
/dev/mapper/cs241--server_sys-srv on /srv type ext4 (rw)
/dev/mapper/cs241--server_sys-tmp on /tmp type ext4 (rw)
/dev/mapper/cs241--server_sys-var on /var type ext4 (rw)rw,bind)
/srv/software/Mathematica-8.0 on /software/Mathematica-8.0 type none (rw,bind)
engr-ews-homes.engr.illinois.edu:/fs1-homes/angrave/linux on /home/angrave type nfs (rw,soft,intr,tcp,noacl,acregmin=30,vers=3,sec=sys,sloppy,addr=128.174.252.102) 

请注意,每行都包括文件系统类型、文件系统源和挂载点。为了减少这种输出,我们可以将其导入到grep中,只看到与正则表达式匹配的行。

>mount | grep proc  # only see lines that contain 'proc'
proc on /proc type proc (rw)
none on /proc/sys/fs/binfmt_misc type binfmt_misc (rw) 

random 和 urandom 之间的区别?

/dev/random 是一个包含数字生成器的文件,其中熵是从环境噪声中确定的。随机将阻塞/等待,直到从环境中收集到足够的熵。

/dev/urandom 就像 random 一样,但不同之处在于它允许重复(熵阈值较低),因此不会阻塞。

其他文件系统

$ cat /proc/sys/kernel/random/entropy_avail
$ hexdump /dev/random
$ hexdump /dev/urandom

$ cat /proc/meminfo
$ cat /proc/cpuinfo
$ cat /proc/cpuinfo | grep bogomips

$ cat /proc/meminfo | grep Swap

$ cd /proc/self
$ echo $$; cd /proc/12345; cat maps 

挂载文件系统

假设我有一个挂接在/dev/cdrom上的文件系统,我想要从中读取。我必须在进行任何操作之前将其挂载到一个目录上。

$ sudo mount /dev/cdrom /media/cdrom
$ mount
$ mount | grep proc 

如何挂载磁盘映像?

假设你下载了一个可引导的 Linux 磁盘映像...

wget http://cosmos.cites.illinois.edu/pub/archlinux/iso/2015.04.01/archlinux-2015.04.01-dual.iso 

在将文件系统放入 CD 之前,我们可以将文件作为文件系统挂载并浏览其内容。请注意,挂载需要 root 访问权限,因此让我们使用 sudo 来运行它

$ mkdir arch
$ sudo mount -o loop archlinux-2015.04.01-dual.iso ./arch
$ cd arch 

在挂载命令之前,arch 目录是新的,显然是空的。挂载后,arch/的内容将从存储在archlinux-2014.11.01-dual.iso文件中的文件和目录中提取出来。需要loop选项,因为我们想要挂载一个常规文件而不是物理磁盘这样的块设备。

loop 选项将原始文件包装为块设备-在这个例子中,我们将在下面找到文件系统是在/dev/loop0下提供的:我们可以通过运行不带任何参数的 mount 命令来检查文件系统类型和挂载选项。我们将将输出导入到grep中,以便只看到包含'arch'的相关输出行(s)

$ mount | grep arch
/home/demo/archlinux-2014.11.01-dual.iso on /home/demo/arch type iso9660 (rw,loop=/dev/loop0) 

iso9660 文件系统是最初为光学存储介质(即 CDRom)设计的只读文件系统。尝试更改文件系统的内容将失败

$ touch arch/nocando
touch: cannot touch `/home/demo/arch/nocando': Read-only file system 

转到文件系统:第六部分

文件系统,第六部分:内存映射文件和共享内存

操作系统如何将我的进程和库加载到内存中?

通过将文件的内容映射到进程的地址空间。如果许多程序只需要对同一个文件进行读取访问(例如/bin/bash,C 库),那么相同的物理内存可以在多个进程之间共享。

相同的机制可以被程序用来直接将文件映射到内存

如何将文件映射到内存?

下面显示了一个将文件映射到内存的简单程序。需要注意的关键点是:

  • mmap 需要一个文件描述符,所以我们需要先打开文件

  • 我们寻找我们想要的大小并写入一个字节,以确保文件足够长

  • 完成后调用 munmap 将文件从内存中取消映射。

这个例子还显示了预处理器常量“LINE”和“FILE”,它们保存了当前正在编译的文件的行号和文件名。

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>

int fail(char *filename, int linenumber) { 
  fprintf(stderr, "%s:%d %s\n", filename, linenumber, strerror(errno)); 
  exit(1);
  return 0; /*Make compiler happy */
}
#define QUIT fail(__FILE__, __LINE__ )

int main() {
  // We want a file big enough to hold 10 integers 
  int size = sizeof(int) * 10;

  int fd = open("data", O_RDWR | O_CREAT | O_TRUNC, 0600); //6 = read+write for me!

  lseek(fd, size, SEEK_SET);
  write(fd, "A", 1);

  void *addr = mmap(0, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
  printf("Mapped at %p\n", addr);
  if (addr == (void*) -1 ) QUIT;

  int *array = addr;
  array[0] = 0x12345678;
  array[1] = 0xdeadc0de;

  munmap(addr,size);
  return 0;

}

我们的二进制文件的内容可以使用 hexdump 列出

$ hexdump data
0000000 78 56 34 12 de c0 ad de 00 00 00 00 00 00 00 00
0000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000020 00 00 00 00 00 00 00 00 41 

细心的读者可能会注意到我们的整数是以最低有效字节格式写入的(因为这是 CPU 的字节序),而且我们分配了一个多出一个字节的文件!

PROT_READ | PROT_WRITE选项指定了虚拟内存保护。选项PROT_EXEC(这里没有使用)可以设置为允许 CPU 在内存中执行指令(例如,如果您映射了一个可执行文件或库,这将非常有用)。

内存映射文件的优势是什么

对于许多应用程序,主要优势是:

简化编码-文件数据立即可用。无需解析传入数据并将其存储在新的内存结构中。

文件共享-内存映射文件在多个进程之间共享相同数据时特别高效。

对于简单的顺序处理,内存映射文件不一定比标准的“基于流”的read / fscanf 等方法更快。

如何在父进程和子进程之间共享内存?

简单-使用mmap而不是文件-只需指定 MAP_ANONYMOUS 和 MAP_SHARED 选项!

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h> /* mmap() is defined in this header */
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>

int main() {

  int size = 100 * sizeof(int);  
  void *addr = mmap(0, size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
  printf("Mapped at %p\n", addr);

  int *shared = addr;
  pid_t mychild = fork();
  if (mychild > 0) {
    shared[0] = 10;
    shared[1] = 20;
  } else {
    sleep(1); // We will talk about synchronization later
    printf("%d\n", shared[1] + shared[0]);
  }

  munmap(addr,size);
  return 0;
}

我可以使用共享内存进行 IPC 吗?

是的!作为一个简单的例子,你可以只保留几个字节,并在想要子进程退出时更改共享内存中的值。共享内存是一种非常高效的进程间通信形式,因为没有复制开销-这两个进程实际上共享相同的物理内存帧。

转到文件系统:第七部分

文件系统,第七部分:可扩展和可靠的文件系统

可靠的单磁盘文件系统

内核如何以及为什么缓存文件系统?

大多数文件系统在物理内存中缓存大量磁盘数据。在这方面,Linux 特别极端:所有未使用的内存都被用作巨大的磁盘缓存。

磁盘缓存可能会对整个系统性能产生重大影响,因为磁盘 I/O 速度很慢。这对于旋转磁盘上的随机访问请求尤其如此,其中磁盘读写延迟由移动读写磁盘头到正确位置所需的寻道时间主导。

为了提高效率,内核会缓存最近使用的磁盘块。对于写入,我们必须在性能和可靠性之间进行权衡:磁盘写入也可以被缓存(“写回缓存”),其中修改后的磁盘块存储在内存中直到被驱逐。或者可以采用“写穿缓存”策略,其中磁盘写入立即发送到磁盘。后者比写回缓存更安全(因为文件系统修改会快速存储到持久介质),但比写回缓存慢;如果写入被缓存,那么它们可以被延迟,并且可以根据每个磁盘块的物理位置进行高效调度。

请注意,这是一个简化的描述,因为固态硬盘(SSD)可以用作辅助写回缓存。

无论是固态硬盘(SSD)还是旋转硬盘,在读取或写入顺序数据时都具有改进的性能。因此,操作系统通常可以使用预读策略来分摊读取请求成本(例如旋转硬盘的时间成本),并请求每个请求的几个连续磁盘块。通过在用户应用程序需要下一个磁盘块之前发出下一个磁盘块的 I/O 请求,可以减少表面磁盘 I/O 延迟。

我的数据很重要!我可以强制磁盘写入保存到物理介质并等待完成吗?

是的(几乎)。调用sync请求将文件系统更改写入(刷新)到磁盘。但并非所有操作系统都会遵守此请求,即使数据已从内核缓冲区中驱逐,磁盘固件也会使用内部磁盘缓存,或者可能尚未完成更改物理介质。

注意,您还可以使用fsync(int fd)请求将与特定文件描述符相关的所有更改刷新到磁盘。

如果我的磁盘在重要操作中失败怎么办?

别担心,大多数现代文件系统都有一种称为日志的东西来解决这个问题。文件系统在完成潜在昂贵的操作之前,会将其要做的事情写在日志中。在崩溃或故障的情况下,可以逐步查看日志并查看哪些文件损坏并修复它们。这是一种在关键数据存在且没有明显备份的情况下挽救硬盘的方法。

磁盘故障的可能性有多大?

磁盘故障是用“平均故障时间”来衡量的。对于大型数组,平均故障时间可能会非常短。例如,如果 MTTF(单个磁盘)= 30,000 小时,则 MTTF(100 个磁盘)= 30000/100 = 300 小时,即约 12 天!

冗余

如何保护我的数据免受磁盘故障?

很简单!数据存储两次!这是“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-namepm 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 个数据块。

inode 磁盘块用于数据

(来源: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 个返回值,其余的位是信息性的。

位移

请注意,没有必要记住这一点,这只是对状态变量内部存储信息的高级概述。

Android 源代码

/如果 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 程序发送信号给进程?

使用raisekill

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

完成以下代码。以下代码应该交替打印AB。它表示两个轮流执行的线程。添加条件变量调用到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() enqdeq 方法。使用条件变量和互斥锁来完成线程安全的、阻塞版本。

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,但是当没有其他线程在shortestset_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 dup2close来实现一个自动评分程序。将子进程的标准输出捕获到一个管道中。子进程应该使用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 

网络:复习问题

简答问题

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,那么除非errnoEINTR,否则立即返回-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、内存、网络...资源,但选择不这样做。今天。