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

简介: UIUC CS241 讲义:众包系统编程书(7)

UIUC CS241 讲义:众包系统编程书(6)https://developer.aliyun.com/article/1427164

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

默认情况下,当套接字关闭时,端口不会立即释放。相反,端口会进入“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。这意味着我们的磁盘块可以看起来像这样

索引节点号 名称
2043567 hi.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

chmod 7 7 7
01 111 111 111
d rwx rwx rwx
1 2 3 4
  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 天!

冗余

UIUC CS241 讲义:众包系统编程书(8)https://developer.aliyun.com/article/1427166

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
4月前
|
存储 缓存 安全
UIUC CS241 讲义:众包系统编程书(4)
UIUC CS241 讲义:众包系统编程书(4)
198 0
|
4月前
|
存储 安全 网络协议
UIUC CS241 讲义:众包系统编程书(8)
UIUC CS241 讲义:众包系统编程书(8)
184 0
|
4月前
|
存储 NoSQL 编译器
UIUC CS241 讲义:众包系统编程书(1)
UIUC CS241 讲义:众包系统编程书(1)
64 0
|
4月前
|
存储 安全 NoSQL
UIUC CS241 讲义:众包系统编程书(2)
UIUC CS241 讲义:众包系统编程书(2)
113 0
|
4月前
|
网络协议 算法 安全
UIUC CS241 讲义:众包系统编程书(6)
UIUC CS241 讲义:众包系统编程书(6)
102 0
|
4月前
|
存储 缓存 算法
UIUC CS241 讲义:众包系统编程书(5)
UIUC CS241 讲义:众包系统编程书(5)
180 0
|
15天前
|
算法 Java 程序员
普林斯顿算法讲义(一)(2)
普林斯顿算法讲义(一)
53 0
|
15天前
|
机器学习/深度学习 存储 算法
普林斯顿算法讲义(三)(3)
普林斯顿算法讲义(三)
52 1
|
15天前
|
缓存 算法 搜索推荐
普林斯顿算法讲义(三)(1)
普林斯顿算法讲义(三)
34 0
|
4月前
|
存储 安全 Shell
UIUC CS241 讲义:众包系统编程书(3)
UIUC CS241 讲义:众包系统编程书(3)
205 0