2017年12月

有时可能会需要在重启时或者每次系统启动时运行某些命令或者脚本。我们要怎样做呢?本文中我们就对此进行讨论。 我们会用两种方法来描述如何在 CentOS/RHEL 以及 Ubuntu 系统上做到重启或者系统启动时执行命令和脚本。 两种方法都通过了测试。

方法 1 – 使用 rc.local

这种方法会利用 /etc/ 中的 rc.local 文件来在启动时执行脚本与命令。我们在文件中加上一行来执行脚本,这样每次启动系统时,都会执行该脚本。

不过我们首先需要为 /etc/rc.local 添加执行权限,

$ sudo chmod +x /etc/rc.local

然后将要执行的脚本加入其中:

$ sudo vi /etc/rc.local

在文件最后加上:

sh /root/script.sh &

然后保存文件并退出。使用 rc.local 文件来执行命令也是一样的,但是一定要记得填写命令的完整路径。 想知道命令的完整路径可以运行:

$ which command

比如:

$ which shutter
/usr/bin/shutter

如果是 CentOS,我们修改的是文件 /etc/rc.d/rc.local 而不是 /etc/rc.local。 不过我们也需要先为该文件添加可执行权限。

注意:- 启动时执行的脚本,请一定保证是以 exit 0 结尾的。

方法 2 – 使用 Crontab

该方法最简单了。我们创建一个 cron 任务,这个任务在系统启动后等待 90 秒,然后执行命令和脚本。

要创建 cron 任务,打开终端并执行

$ crontab -e

然后输入下行内容,

@reboot ( sleep 90 ; sh \location\script.sh )

这里 \location\script.sh 就是待执行脚本的地址。

我们的文章至此就完了。如有疑问,欢迎留言。


via: http://linuxtechlab.com/executing-commands-scripts-at-reboot/

作者:Shusain 译者:lujun9972 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

groff 是大多数 Unix 系统上所提供的流行的文本格式化工具 nroff/troff 的 GNU 版本。它一般用于编写手册页,即命令、编程接口等的在线文档。在本文中,我们将给你展示如何使用 groff 编写你自己的 man 手册页。

在 Unix 系统上最初有两个文本处理系统:troff 和 nroff,它们是由贝尔实验室为初始的 Unix 所开发的(事实上,开发 Unix 系统的部分原因就是为了支持这样的一个文本处理系统)。这个文本处理器的第一个版本被称作 roff(意为 “runoff”——径流);稍后出现了 troff,在那时用于为特定的 排字机 Typesetter 生成输出。nroff 是更晚一些的版本,它成为了各种 Unix 系统的标准文本处理器。groff 是 nroff 和 troff 的 GNU 实现,用在 Linux 系统上。它包括了几个扩展功能和一些打印设备的驱动程序。

groff 能够生成文档、文章和书籍,很多时候它就像是其它的文本格式化系统(如 TeX)的血管一样。然而,groff(以及原来的 nroff)有一个固有的功能是 TeX 及其变体所缺乏的:生成普通 ASCII 输出。其它的系统在生成打印的文档方面做得很好,而 groff 却能够生成可以在线浏览的普通 ASCII(甚至可以在最简单的打印机上直接以普通文本打印)。如果要生成在线浏览的文档以及打印的表单,groff 也许是你所需要的(虽然也有替代品,如 Texinfo、Lametex 等等)。

groff 还有一个好处是它比 TeX 小很多;它所需要的支持文件和可执行程序甚至比最小化的 TeX 版本都少。

groff 一个特定的用途是用于格式化 Unix 的 man 手册页。如果你是一个 Unix 程序员,你肯定需要编写和生成各种 man 手册页。在本文中,我们将通过编写一个简短的 man 手册页来介绍 groff 的使用。

像 TeX 一样,groff 使用特定的文本格式化语言来描述如何处理文本。这种语言比 TeX 之类的系统更加神秘一些,但是更加简洁。此外,groff 在基本的格式化器之上提供了几个宏软件包;这些宏软件包是为一些特定类型的文档所定制的。举个例子, mgs 宏对于写作文章或论文很适合,而 man 宏可用于 man 手册页。

编写 man 手册页

groff 编写 man 手册页十分简单。要让你的 man 手册页看起来和其它的一样,你需要从源头上遵循几个惯例,如下所示。在这个例子中,我们将为一个虚构的命令 coffee 编写 man 手册页,它用于以各种方式控制你的联网咖啡机。

使用任意文本编辑器,输入如下代码,并保存为 coffee.man。不要输入每行的行号,它们仅用于本文中的说明。

.TH COFFEE 1 "23 March 94"
.SH NAME
coffee \- Control remote coffee machine
.SH SYNOPSIS
\fBcoffee\fP [ -h | -b ] [ -t \fItype\fP ]
\fIamount\fP
.SH DESCRIPTION
\fBcoffee\fP queues a request to the remote
coffee machine at the device \fB/dev/cf0\fR.
The required \fIamount\fP argument specifies
the number of cups, generally between 0 and
12 on ISO standard coffee machines.
.SS Options
.TP
\fB-h\fP
Brew hot coffee. Cold is the default.
.TP
\fB-b\fP
Burn coffee. Especially useful when executing
\fBcoffee\fP on behalf of your boss.
.TP
\fB-t \fItype\fR
Specify the type of coffee to brew, where
\fItype\fP is one of \fBcolumbian\fP,
\fBregular\fP, or \fBdecaf\fP.
.SH FILES
.TP
\fC/dev/cf0\fR
The remote coffee machine device
.SH "SEE ALSO"
milk(5), sugar(5)
.SH BUGS
May require human intervention if coffee
supply is exhausted.

清单 1:示例 man 手册页源文件

不要让这些晦涩的代码吓坏了你。字符串序列 \fB\fI\fR 分别用来改变字体为粗体、斜体和正体(罗马字体)。\fP 设置字体为前一个选择的字体。

其它的 groff 请求 request 以点(.)开头出现在行首。第 1 行中,我们看到的 .TH 请求用于设置该 man 手册页的标题为 COFFEE、man 的部分为 1、以及该 man 手册页的最新版本的日期。(说明,man 手册的第 1 部分用于用户命令、第 2 部分用于系统调用等等。使用 man man 命令了解各个部分)。

在第 2 行,.SH 请求用于标记一个 section 的开始,并给该节名称为 NAME。注意,大部分的 Unix man 手册页依次使用 NAMESYNOPSISDESCRIPTIONFILESSEE ALSONOTESAUTHORBUGS 等节,个别情况下也需要一些额外的可选节。这只是编写 man 手册页的惯例,并不强制所有软件都如此。

第 3 行给出命令的名称,并在一个横线(-)后给出简短描述。在 NAME 节使用这个格式以便你的 man 手册页可以加到 whatis 数据库中——它可以用于 man -kapropos 命令。

第 4-6 行我们给出了 coffee 命令格式的大纲。注意,斜体 \fI...\fP 用于表示命令行的参数,可选参数用方括号扩起来。

第 7-12 行给出了该命令的摘要介绍。粗体通常用于表示程序或文件的名称。

在 13 行,使用 .SS 开始了一个名为 Options 的子节。

接着第 14-25 行是选项列表,会使用参数列表样式表示。参数列表中的每一项以 .TP 请求来标记;.TP 后的行是参数,再之后是该项的文本。例如,第 14-16 行:

.TP
\fB-h\P
Brew hot coffee. Cold is the default.

将会显示如下:

-h     Brew hot coffee. Cold is the default.

第 26-29 行创建该 man 手册页的 FILES 节,它用于描述该命令可能使用的文件。可以使用 .TP 请求来表示文件列表。

第 30-31 行,给出了 SEE ALSO 节,它提供了其它可以参考的 man 手册页。注意,第 30 行的 .SH 请求中 "SEE ALSO" 使用括号扩起来,这是因为 .SH 使用第一个空格来分隔该节的标题。任何超过一个单词的标题都需要使用引号扩起来成为一个单一参数。

最后,第 32-34 行,是 BUGS 节。

格式化和安装 man 手册页

为了在你的屏幕上查看这个手册页格式化的样式,你可以使用如下命令:

$ groff -Tascii -man coffee.man | more

-Tascii 选项告诉 groff 生成普通 ASCII 输出;-man 告诉 groff 使用 man 手册页宏集合。如果一切正常,这个 man 手册页显示应该如下。

COFFEE(1)                                               COFFEE(1)
NAME
       coffee - Control remote coffee machine
SYNOPSIS
       coffee [ -h | -b ] [ -t type ] amount
DESCRIPTION
       coffee  queues  a  request to the remote coffee machine at
       the device /dev/cf0\. The required amount  argument  speci-
       fies the number of cups, generally between 0 and 12 on ISO
       standard coffee machines.
   Options
       -h     Brew hot coffee. Cold is the default.
       -b     Burn coffee. Especially useful when executing  cof-
              fee on behalf of your boss.
       -t type
              Specify  the  type of coffee to brew, where type is
              one of columbian, regular, or decaf.
FILES
       /dev/cf0
              The remote coffee machine device
SEE ALSO
       milk(5), sugar(5)
BUGS
       May  require  human  intervention  if  coffee  supply   is
       exhausted.

格式化的 man 手册页

如之前提到过的,groff 能够生成其它类型的输出。使用 -Tps 选项替代 -Tascii 将会生成 PostScript 输出,你可以将其保存为文件,用 GhostView 查看,或用一个 PostScript 打印机打印出来。-Tdvi 会生成设备无关的 .dvi 输出,类似于 TeX 的输出。

如果你希望让别人在你的系统上也可以查看这个 man 手册页,你需要安装这个 groff 源文件到其它用户的 %MANPATH 目录里面。标准的 man 手册页放在 /usr/man。第一部分的 man 手册页应该放在 /usr/man/man1 下,因此,使用命令:

$ cp coffee.man /usr/man/man1/coffee.1

这将安装该 man 手册页到 /usr/man 中供所有人使用(注意使用 .1 扩展名而不是 .man)。当接下来执行 man coffee 命令时,该 man 手册页会被自动重新格式化,并且可查看的文本会被保存到 /usr/man/cat1/coffee.1.Z 中。

如果你不能直接复制 man 手册页的源文件到 /usr/man(比如说你不是系统管理员),你可创建你自己的 man 手册页目录树,并将其加入到你的 %MANPATH%MANPATH 环境变量的格式同 %PATH 一样,举个例子,要添加目录 /home/mdw/man%MANPATH ,只需要:

$ export MANPATH=/home/mdw/man:$MANPATH

groff 和 man 手册页宏还有许多其它的选项和格式化命令。找到它们的最好办法是查看 /usr/lib/groff 中的文件; tmac 目录包含了宏文件,自身通常会包含其所提供的命令的文档。要让 groff 使用特定的宏集合,只需要使用 -m macro (或 -macro) 选项。例如,要使用 mgs 宏,使用命令:

groff -Tascii -mgs files...

groff 的 man 手册页对这个选项描述了更多细节。

不幸的是,随同 groff 提供的宏集合没有完善的文档。第 7 部分的 man 手册页提供了一些,例如,man 7 groff_mm 会给你 mm 宏集合的信息。然而,该文档通常只覆盖了在 groff 实现中不同和新功能,而假设你已经了解过原来的 nroff/troff 宏集合(称作 DWB:the Documentor's Work Bench)。最佳的信息来源或许是一本覆盖了那些经典宏集合细节的书。要了解更多的编写 man 手册页的信息,你可以看看 man 手册页源文件(/usr/man 中),并通过它们来比较源文件的输出。

这篇文章是《Running Linux》 中的一章,由 Matt Welsh 和 Lar Kaufman 著,奥莱理出版(ISBN 1-56592-100-3)。在本书中,还包括了 Linux 下使用的各种文本格式化系统的教程。这期的《Linux Journal》中的内容及《Running Linux》应该可以给你提供在 Linux 上使用各种文本工具的良好开端。

祝好,撰写快乐!

Matt Welsh ([email protected])是康奈尔大学的一名学生和系统程序员,在机器人和视觉实验室从事于时时机器视觉研究。

(题图:wikimedia.org)


via: http://www.linuxjournal.com/article/1158

作者:Matt Welsh 译者:wxy 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

这是并发服务器系列的第三节。第一节 介绍了阻塞式编程,第二节:线程 探讨了多线程,将其作为一种可行的方法来实现服务器并发编程。

另一种常见的实现并发的方法叫做 事件驱动编程,也可以叫做 异步 编程 注1 。这种方法变化万千,因此我们会从最基本的开始,使用一些基本的 API 而非从封装好的高级方法开始。本系列以后的文章会讲高层次抽象,还有各种混合的方法。

本系列的所有文章:

阻塞式 vs. 非阻塞式 I/O

作为本篇的介绍,我们先讲讲阻塞和非阻塞 I/O 的区别。阻塞式 I/O 更好理解,因为这是我们使用 I/O 相关 API 时的“标准”方式。从套接字接收数据的时候,调用 recv 函数会发生 阻塞,直到它从端口上接收到了来自另一端套接字的数据。这恰恰是第一部分讲到的顺序服务器的问题。

因此阻塞式 I/O 存在着固有的性能问题。第二节里我们讲过一种解决方法,就是用多线程。哪怕一个线程的 I/O 阻塞了,别的线程仍然可以使用 CPU 资源。实际上,阻塞 I/O 通常在利用资源方面非常高效,因为线程就等待着 —— 操作系统将线程变成休眠状态,只有满足了线程需要的条件才会被唤醒。

非阻塞式 I/O 是另一种思路。把套接字设成非阻塞模式时,调用 recv 时(还有 send,但是我们现在只考虑接收),函数返回的会很快,哪怕没有接收到数据。这时,就会返回一个特殊的错误状态 注2 来通知调用者,此时没有数据传进来。调用者可以去做其他的事情,或者尝试再次调用 recv 函数。

示范阻塞式和非阻塞式的 recv 区别的最好方式就是贴一段示例代码。这里有个监听套接字的小程序,一直在 recv 这里阻塞着;当 recv 返回了数据,程序就报告接收到了多少个字节 注3

int main(int argc, const char** argv) {
  setvbuf(stdout, NULL, _IONBF, 0);

  int portnum = 9988;
  if (argc >= 2) {
    portnum = atoi(argv[1]);
  }
  printf("Listening on port %d\n", portnum);

  int sockfd = listen_inet_socket(portnum);
  struct sockaddr_in peer_addr;
  socklen_t peer_addr_len = sizeof(peer_addr);

  int newsockfd = accept(sockfd, (struct sockaddr*)&peer_addr, &peer_addr_len);
  if (newsockfd < 0) {
    perror_die("ERROR on accept");
  }
  report_peer_connected(&peer_addr, peer_addr_len);

  while (1) {
    uint8_t buf[1024];
    printf("Calling recv...\n");
    int len = recv(newsockfd, buf, sizeof buf, 0);
    if (len < 0) {
      perror_die("recv");
    } else if (len == 0) {
      printf("Peer disconnected; I'm done.\n");
      break;
    }
    printf("recv returned %d bytes\n", len);
  }

  close(newsockfd);
  close(sockfd);

  return 0;
}

主循环重复调用 recv 并且报告它返回的字节数(记住 recv 返回 0 时,就是客户端断开连接了)。试着运行它,我们会在一个终端里运行这个程序,然后在另一个终端里用 nc 进行连接,发送一些字符,每次发送之间间隔几秒钟:

$ nc localhost 9988
hello                                   # wait for 2 seconds after typing this
socket world
^D                                      # to end the connection>

监听程序会输出以下内容:

$ ./blocking-listener 9988
Listening on port 9988
peer (localhost, 37284) connected
Calling recv...
recv returned 6 bytes
Calling recv...
recv returned 13 bytes
Calling recv...
Peer disconnected; I'm done.

现在试试非阻塞的监听程序的版本。这是代码:

int main(int argc, const char** argv) {
  setvbuf(stdout, NULL, _IONBF, 0);

  int portnum = 9988;
  if (argc >= 2) {
    portnum = atoi(argv[1]);
  }
  printf("Listening on port %d\n", portnum);

  int sockfd = listen_inet_socket(portnum);
  struct sockaddr_in peer_addr;
  socklen_t peer_addr_len = sizeof(peer_addr);

  int newsockfd = accept(sockfd, (struct sockaddr*)&peer_addr, &peer_addr_len);
  if (newsockfd < 0) {
    perror_die("ERROR on accept");
  }
  report_peer_connected(&peer_addr, peer_addr_len);

  // 把套接字设成非阻塞模式
  int flags = fcntl(newsockfd, F_GETFL, 0);
  if (flags == -1) {
    perror_die("fcntl F_GETFL");
  }

  if (fcntl(newsockfd, F_SETFL, flags | O_NONBLOCK) == -1) {
    perror_die("fcntl F_SETFL O_NONBLOCK");
  }

  while (1) {
    uint8_t buf[1024];
    printf("Calling recv...\n");
    int len = recv(newsockfd, buf, sizeof buf, 0);
    if (len < 0) {
      if (errno == EAGAIN || errno == EWOULDBLOCK) {
        usleep(200 * 1000);
        continue;
      }
      perror_die("recv");
    } else if (len == 0) {
      printf("Peer disconnected; I'm done.\n");
      break;
    }
    printf("recv returned %d bytes\n", len);
  }

  close(newsockfd);
  close(sockfd);

  return 0;
}

这里与阻塞版本有些差异,值得注意:

  1. accept 函数返回的 newsocktfd 套接字因调用了 fcntl, 被设置成非阻塞的模式。
  2. 检查 recv 的返回状态时,我们对 errno 进行了检查,判断它是否被设置成表示没有可供接收的数据的状态。这时,我们仅仅是休眠了 200 毫秒然后进入到下一轮循环。

同样用 nc 进行测试,以下是非阻塞监听器的输出:

$ ./nonblocking-listener 9988
Listening on port 9988
peer (localhost, 37288) connected
Calling recv...
Calling recv...
Calling recv...
Calling recv...
Calling recv...
Calling recv...
Calling recv...
Calling recv...
Calling recv...
recv returned 6 bytes
Calling recv...
Calling recv...
Calling recv...
Calling recv...
Calling recv...
Calling recv...
Calling recv...
Calling recv...
Calling recv...
Calling recv...
Calling recv...
recv returned 13 bytes
Calling recv...
Calling recv...
Calling recv...
Peer disconnected; I'm done.

作为练习,给输出添加一个时间戳,确认调用 recv 得到结果之间花费的时间是比输入到 nc 中所用的多还是少(每一轮是 200 ms)。

这里就实现了使用非阻塞的 recv 让监听者检查套接字变为可能,并且在没有数据的时候重新获得控制权。换句话说,用编程的语言说这就是 轮询 polling —— 主程序周期性的查询套接字以便读取数据。

对于顺序响应的问题,这似乎是个可行的方法。非阻塞的 recv 让同时与多个套接字通信变成可能,轮询这些套接字,仅当有新数据到来时才处理。就是这样,这种方式 可以 用来写并发服务器;但实际上一般不这么做,因为轮询的方式很难扩展。

首先,我在代码中引入的 200ms 延迟对于演示非常好(监听器在我输入 nc 之间只打印几行 “Calling recv...”,但实际上应该有上千行)。但它也增加了多达 200ms 的服务器响应时间,这无意是不必要的。实际的程序中,延迟会低得多,休眠时间越短,进程占用的 CPU 资源就越多。有些时钟周期只是浪费在等待,这并不好,尤其是在移动设备上,这些设备的电量往往有限。

但是当我们实际这样来使用多个套接字的时候,更严重的问题出现了。想像下监听器正在同时处理 1000 个客户端。这意味着每一个循环迭代里面,它都得为 这 1000 个套接字中的每一个 执行一遍非阻塞的 recv,找到其中准备好了数据的那一个。这非常低效,并且极大的限制了服务器能够并发处理的客户端数。这里有个准则:每次轮询之间等待的间隔越久,服务器响应性越差;而等待的时间越少,CPU 在无用的轮询上耗费的资源越多。

讲真,所有的轮询都像是无用功。当然操作系统应该是知道哪个套接字是准备好了数据的,因此没必要逐个扫描。事实上,就是这样,接下来就会讲一些 API,让我们可以更优雅地处理多个客户端。

select

select 的系统调用是可移植的(POSIX),是标准 Unix API 中常有的部分。它是为上一节最后一部分描述的问题而设计的 —— 允许一个线程可以监视许多文件描述符 注4 的变化,而不用在轮询中执行不必要的代码。我并不打算在这里引入一个关于 select 的全面教程,有很多网站和书籍讲这个,但是在涉及到问题的相关内容时,我会介绍一下它的 API,然后再展示一个非常复杂的例子。

select 允许 多路 I/O,监视多个文件描述符,查看其中任何一个的 I/O 是否可用。

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

readfds 指向文件描述符的缓冲区,这个缓冲区被监视是否有读取事件;fd_set 是一个特殊的数据结构,用户使用 FD_* 宏进行操作。writefds 是针对写事件的。nfds 是监视的缓冲中最大的文件描述符数字(文件描述符就是整数)。timeout 可以让用户指定 select 应该阻塞多久,直到某个文件描述符准备好了(timeout == NULL 就是说一直阻塞着)。现在先跳过 exceptfds

select 的调用过程如下:

  1. 在调用之前,用户先要为所有不同种类的要监视的文件描述符创建 fd_set 实例。如果想要同时监视读取和写入事件,readfdswritefds 都要被创建并且引用。
  2. 用户可以使用 FD_SET 来设置集合中想要监视的特殊描述符。例如,如果想要监视描述符 2、7 和 10 的读取事件,在 readfds 这里调用三次 FD_SET,分别设置 2、7 和 10。
  3. select 被调用。
  4. select 返回时(现在先不管超时),就是说集合中有多少个文件描述符已经就绪了。它也修改 readfdswritefds 集合,来标记这些准备好的描述符。其它所有的描述符都会被清空。
  5. 这时用户需要遍历 readfdswritefds,找到哪个描述符就绪了(使用 FD_ISSET)。

作为完整的例子,我在并发的服务器程序上使用 select,重新实现了我们之前的协议。完整的代码在这里;接下来的是代码中的重点部分及注释。警告:示例代码非常复杂,因此第一次看的时候,如果没有足够的时间,快速浏览也没有关系。

使用 select 的并发服务器

使用 I/O 的多发 API 诸如 select 会给我们服务器的设计带来一些限制;这不会马上显现出来,但这值得探讨,因为它们是理解事件驱动编程到底是什么的关键。

最重要的是,要记住这种方法本质上是单线程的 注5 。服务器实际上在 同一时刻只能做一件事。因为我们想要同时处理多个客户端请求,我们需要换一种方式重构代码。

首先,让我们谈谈主循环。它看起来是什么样的呢?先让我们想象一下服务器有一堆任务,它应该监视哪些东西呢?两种类型的套接字活动:

  1. 新客户端尝试连接。这些客户端应该被 accept
  2. 已连接的客户端发送数据。这个数据要用 第一节 中所讲到的协议进行传输,有可能会有一些数据要被回送给客户端。

尽管这两种活动在本质上有所区别,我们还是要把它们放在一个循环里,因为只能有一个主循环。循环会包含 select 的调用。这个 select 的调用会监视上述的两种活动。

这里是部分代码,设置了文件描述符集合,并在主循环里转到被调用的 select 部分。

// “master” 集合存活在该循环中,跟踪我们想要监视的读取事件或写入事件的文件描述符(FD)。
fd_set readfds_master;
FD_ZERO(&readfds_master);
fd_set writefds_master;
FD_ZERO(&writefds_master);

// 监听的套接字一直被监视,用于读取数据,并监测到来的新的端点连接。
FD_SET(listener_sockfd, &readfds_master);

// 要想更加高效,fdset_max 追踪当前已知最大的 FD;这使得每次调用时对 FD_SETSIZE 的迭代选择不是那么重要了。
int fdset_max = listener_sockfd;

while (1) {
  // select() 会修改传递给它的 fd_sets,因此进行拷贝一下再传值。
  fd_set readfds = readfds_master;
  fd_set writefds = writefds_master;

  int nready = select(fdset_max + 1, &readfds, &writefds, NULL, NULL);
  if (nready < 0) {
    perror_die("select");
  }
  ...

这里的一些要点:

  1. 由于每次调用 select 都会重写传递给函数的集合,调用器就得维护一个 “master” 集合,在循环迭代中,保持对所监视的所有活跃的套接字的追踪。
  2. 注意我们所关心的,最开始的唯一那个套接字是怎么变成 listener_sockfd 的,这就是最开始的套接字,服务器借此来接收新客户端的连接。
  3. select 的返回值,是在作为参数传递的集合中,那些已经就绪的描述符的个数。select 修改这个集合,用来标记就绪的描述符。下一步是在这些描述符中进行迭代。
...
for (int fd = 0; fd <= fdset_max && nready > 0; fd++) {
  // 检查 fd 是否变成可读的
  if (FD_ISSET(fd, &readfds)) {
    nready--;

    if (fd == listener_sockfd) {
      // 监听的套接字就绪了;这意味着有个新的客户端连接正在联系
      ...
    } else {
      fd_status_t status = on_peer_ready_recv(fd);
      if (status.want_read) {
        FD_SET(fd, &readfds_master);
      } else {
        FD_CLR(fd, &readfds_master);
      }
      if (status.want_write) {
        FD_SET(fd, &writefds_master);
      } else {
        FD_CLR(fd, &writefds_master);
      }
      if (!status.want_read && !status.want_write) {
        printf("socket %d closing\n", fd);
        close(fd);
      }
    }

这部分循环检查 可读的 描述符。让我们跳过监听器套接字(要浏览所有内容,看这个代码) 然后看看当其中一个客户端准备好了之后会发生什么。出现了这种情况后,我们调用一个叫做 on_peer_ready_recv回调 函数,传入相应的文件描述符。这个调用意味着客户端连接到套接字上,发送某些数据,并且对套接字上 recv 的调用不会被阻塞 注6 。这个回调函数返回结构体 fd_status_t

typedef struct {
  bool want_read;
  bool want_write;
} fd_status_t;

这个结构体告诉主循环,是否应该监视套接字的读取事件、写入事件,或者两者都监视。上述代码展示了 FD_SETFD_CLR 是怎么在合适的描述符集合中被调用的。对于主循环中某个准备好了写入数据的描述符,代码是类似的,除了它所调用的回调函数,这个回调函数叫做 on_peer_ready_send

现在来花点时间看看这个回调:

typedef enum { INITIAL_ACK, WAIT_FOR_MSG, IN_MSG } ProcessingState;

#define SENDBUF_SIZE 1024

typedef struct {
  ProcessingState state;

  // sendbuf 包含了服务器要返回给客户端的数据。on_peer_ready_recv 句柄填充这个缓冲,
  // on_peer_read_send 进行消耗。sendbuf_end 指向缓冲区的最后一个有效字节,
  // sendptr 指向下个字节
  uint8_t sendbuf[SENDBUF_SIZE];
  int sendbuf_end;
  int sendptr;
} peer_state_t;

// 每一端都是通过它连接的文件描述符(fd)进行区分。只要客户端连接上了,fd 就是唯一的。
// 当客户端断开连接,另一个客户端连接上就会获得相同的 fd。on_peer_connected 应该
// 进行初始化,以便移除旧客户端在同一个 fd 上留下的东西。
peer_state_t global_state[MAXFDS];

fd_status_t on_peer_ready_recv(int sockfd) {
  assert(sockfd < MAXFDs);
  peer_state_t* peerstate = &global_state[sockfd];

  if (peerstate->state == INITIAL_ACK ||
      peerstate->sendptr < peerstate->sendbuf_end) {
    // 在初始的 ACK 被送到了客户端,就没有什么要接收的了。
    // 等所有待发送的数据都被发送之后接收更多的数据。
    return fd_status_W;
  }

  uint8_t buf[1024];
  int nbytes = recv(sockfd, buf, sizeof buf, 0);
  if (nbytes == 0) {
    // 客户端断开连接
    return fd_status_NORW;
  } else if (nbytes < 0) {
    if (errno == EAGAIN || errno == EWOULDBLOCK) {
      // 套接字 *实际* 并没有准备好接收,等到它就绪。
      return fd_status_R;
    } else {
      perror_die("recv");
    }
  }
  bool ready_to_send = false;
  for (int i = 0; i < nbytes; ++i) {
    switch (peerstate->state) {
    case INITIAL_ACK:
      assert(0 && "can't reach here");
      break;
    case WAIT_FOR_MSG:
      if (buf[i] == '^') {
        peerstate->state = IN_MSG;
      }
      break;
    case IN_MSG:
      if (buf[i] == '$') {
        peerstate->state = WAIT_FOR_MSG;
      } else {
        assert(peerstate->sendbuf_end < SENDBUF_SIZE);
        peerstate->sendbuf[peerstate->sendbuf_end++] = buf[i] + 1;
        ready_to_send = true;
      }
      break;
    }
  }
  // 如果没有数据要发送给客户端,报告读取状态作为最后接收的结果。
  return (fd_status_t){.want_read = !ready_to_send,
                       .want_write = ready_to_send};
}

peer_state_t 是全状态对象,用来表示在主循环中两次回调函数调用之间的客户端的连接。因为回调函数在客户端发送的某些数据时被调用,不能假设它能够不停地与客户端通信,并且它得运行得很快,不能被阻塞。因为套接字被设置成非阻塞模式,recv 会快速的返回。除了调用 recv, 这个句柄做的是处理状态,没有其它的调用,从而不会发生阻塞。

举个例子,你知道为什么这个代码需要一个额外的状态吗?这个系列中,我们的服务器目前只用到了两个状态,但是这个服务器程序需要三个状态。

来看看 “套接字准备好发送” 的回调函数:

fd_status_t on_peer_ready_send(int sockfd) {
  assert(sockfd < MAXFDs);
  peer_state_t* peerstate = &global_state[sockfd];

  if (peerstate->sendptr >= peerstate->sendbuf_end) {
    // 没有要发送的。
    return fd_status_RW;
  }
  int sendlen = peerstate->sendbuf_end - peerstate->sendptr;
  int nsent = send(sockfd, peerstate->sendbuf, sendlen, 0);
  if (nsent == -1) {
    if (errno == EAGAIN || errno == EWOULDBLOCK) {
      return fd_status_W;
    } else {
      perror_die("send");
    }
  }
  if (nsent < sendlen) {
    peerstate->sendptr += nsent;
    return fd_status_W;
  } else {
    // 所有东西都成功发送;重置发送队列。
    peerstate->sendptr = 0;
    peerstate->sendbuf_end = 0;

    // 如果我们现在是处于特殊的 INITIAL_ACK 状态,就转变到其他状态。
    if (peerstate->state == INITIAL_ACK) {
      peerstate->state = WAIT_FOR_MSG;
    }

    return fd_status_R;
  }
}

这里也一样,回调函数调用了一个非阻塞的 send,演示了状态管理。在异步代码中,回调函数执行的很快是受争议的,任何延迟都会阻塞主循环进行处理,因此也阻塞了整个服务器程序去处理其他的客户端。

用脚步再来运行这个服务器,同时连接 3 个客户端。在一个终端中我们运行下面的命令:

$ ./select-server

在另一个终端中:

$ python3.6 simple-client.py  -n 3 localhost 9090
INFO:2017-09-26 05:29:15,864:conn1 connected...
INFO:2017-09-26 05:29:15,864:conn2 connected...
INFO:2017-09-26 05:29:15,864:conn0 connected...
INFO:2017-09-26 05:29:15,865:conn1 sending b'^abc$de^abte$f'
INFO:2017-09-26 05:29:15,865:conn2 sending b'^abc$de^abte$f'
INFO:2017-09-26 05:29:15,865:conn0 sending b'^abc$de^abte$f'
INFO:2017-09-26 05:29:15,865:conn1 received b'bcdbcuf'
INFO:2017-09-26 05:29:15,865:conn2 received b'bcdbcuf'
INFO:2017-09-26 05:29:15,865:conn0 received b'bcdbcuf'
INFO:2017-09-26 05:29:16,866:conn1 sending b'xyz^123'
INFO:2017-09-26 05:29:16,867:conn0 sending b'xyz^123'
INFO:2017-09-26 05:29:16,867:conn2 sending b'xyz^123'
INFO:2017-09-26 05:29:16,867:conn1 received b'234'
INFO:2017-09-26 05:29:16,868:conn0 received b'234'
INFO:2017-09-26 05:29:16,868:conn2 received b'234'
INFO:2017-09-26 05:29:17,868:conn1 sending b'25$^ab0000$abab'
INFO:2017-09-26 05:29:17,869:conn1 received b'36bc1111'
INFO:2017-09-26 05:29:17,869:conn0 sending b'25$^ab0000$abab'
INFO:2017-09-26 05:29:17,870:conn0 received b'36bc1111'
INFO:2017-09-26 05:29:17,870:conn2 sending b'25$^ab0000$abab'
INFO:2017-09-26 05:29:17,870:conn2 received b'36bc1111'
INFO:2017-09-26 05:29:18,069:conn1 disconnecting
INFO:2017-09-26 05:29:18,070:conn0 disconnecting
INFO:2017-09-26 05:29:18,070:conn2 disconnecting

和线程的情况相似,客户端之间没有延迟,它们被同时处理。而且在 select-server 也没有用线程!主循环 多路 处理所有的客户端,通过高效使用 select 轮询多个套接字。回想下 第二节中 顺序的 vs 多线程的客户端处理过程的图片。对于我们的 select-server,三个客户端的处理流程像这样:

多客户端处理流程

所有的客户端在同一个线程中同时被处理,通过乘积,做一点这个客户端的任务,然后切换到另一个,再切换到下一个,最后切换回到最开始的那个客户端。注意,这里没有什么循环调度,客户端在它们发送数据的时候被客户端处理,这实际上是受客户端左右的。

同步、异步、事件驱动、回调

select-server 示例代码为讨论什么是异步编程、它和事件驱动及基于回调的编程有何联系,提供了一个良好的背景。因为这些词汇在并发服务器的(非常矛盾的)讨论中很常见。

让我们从一段 select 的手册页面中引用的一句话开始:

select,pselect,FD\_CLR,FD\_ISSET,FD\_SET,FD\_ZERO - 同步 I/O 处理

因此 select同步 处理。但我刚刚演示了大量代码的例子,使用 select 作为 异步 处理服务器的例子。有哪些东西?

答案是:这取决于你的观察角度。同步常用作阻塞处理,并且对 select 的调用实际上是阻塞的。和第 1、2 节中讲到的顺序的、多线程的服务器中对 sendrecv 是一样的。因此说 select同步的 API 是有道理的。可是,服务器的设计却可以是 异步的,或是 基于回调的,或是 事件驱动的,尽管其中有对 select 的使用。注意这里的 on_peer_* 函数是回调函数;它们永远不会阻塞,并且只有网络事件触发的时候才会被调用。它们可以获得部分数据,并能够在调用过程中保持稳定的状态。

如果你曾经做过一些 GUI 编程,这些东西对你来说应该很亲切。有个 “事件循环”,常常完全隐藏在框架里,应用的 “业务逻辑” 建立在回调上,这些回调会在各种事件触发后被调用,用户点击鼠标、选择菜单、定时器触发、数据到达套接字等等。曾经最常见的编程模型是客户端的 JavaScript,这里面有一堆回调函数,它们在浏览网页时用户的行为被触发。

select 的局限

使用 select 作为第一个异步服务器的例子对于说明这个概念很有用,而且由于 select 是很常见、可移植的 API。但是它也有一些严重的缺陷,在监视的文件描述符非常大的时候就会出现。

  1. 有限的文件描述符的集合大小。
  2. 糟糕的性能。

从文件描述符的大小开始。FD_SETSIZE 是一个编译期常数,在如今的操作系统中,它的值通常是 1024。它被硬编码在 glibc 的头文件里,并且不容易修改。它把 select 能够监视的文件描述符的数量限制在 1024 以内。曾有些人想要写出能够处理上万个并发访问的客户端请求的服务器,所以这个问题很有现实意义。有一些方法,但是不可移植,也很难用。

糟糕的性能问题就好解决的多,但是依然非常严重。注意当 select 返回的时候,它向调用者提供的信息是 “就绪的” 描述符的个数,还有被修改过的描述符集合。描述符集映射着描述符“就绪/未就绪”,但是并没有提供什么有效的方法去遍历所有就绪的描述符。如果只有一个描述符是就绪的,最坏的情况是调用者需要遍历 整个集合 来找到那个描述符。这在监视的描述符数量比较少的时候还行,但是如果数量变的很大的时候,这种方法弊端就凸显出了 注7

由于这些原因,为了写出高性能的并发服务器, select 已经不怎么用了。每一个流行的操作系统有独特的不可移植的 API,允许用户写出非常高效的事件循环;像框架这样的高级结构还有高级语言通常在一个可移植的接口中包含这些 API。

epoll

举个例子,来看看 epoll,Linux 上的关于高容量 I/O 事件通知问题的解决方案。epoll 高效的关键之处在于它与内核更好的协作。不是使用文件描述符,epoll_wait 用当前准备好的事件填满一个缓冲区。只有准备好的事件添加到了缓冲区,因此没有必要遍历客户端中当前 所有 监视的文件描述符。这简化了查找就绪的描述符的过程,把空间复杂度从 select 中的 O(N) 变为了 O(1)。

关于 epoll API 的完整展示不是这里的目的,网上有很多相关资源。虽然你可能猜到了,我还要写一个不同的并发服务器,这次是用 epool 而不是 select。完整的示例代码 在这里。实际上,由于大部分代码和 用 select 的服务器相同,所以我只会讲要点,在主循环里使用 epoll

struct epoll_event accept_event;
accept_event.data.fd = listener_sockfd;
accept_event.events = EPOLLIN;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listener_sockfd, &accept_event) < 0) {
  perror_die("epoll_ctl EPOLL_CTL_ADD");
}

struct epoll_event* events = calloc(MAXFDS, sizeof(struct epoll_event));
if (events == NULL) {
  die("Unable to allocate memory for epoll_events");
}

while (1) {
  int nready = epoll_wait(epollfd, events, MAXFDS, -1);
  for (int i = 0; i < nready; i++) {
    if (events[i].events & EPOLLERR) {
      perror_die("epoll_wait returned EPOLLERR");
    }

    if (events[i].data.fd == listener_sockfd) {
      // 监听的套接字就绪了;意味着新客户端正在连接。
      ...
    } else {
      // A peer socket is ready.
      if (events[i].events & EPOLLIN) {
        // 准备好了读取
        ...
      } else if (events[i].events & EPOLLOUT) {
        // 准备好了写入
        ...
      }
    }
  }
}

通过调用 epoll_ctl 来配置 epoll。这时,配置监听的套接字数量,也就是 epoll 监听的描述符的数量。然后分配一个缓冲区,把就绪的事件传给 epoll 以供修改。在主循环里对 epoll_wait 的调用是魅力所在。它阻塞着,直到某个描述符就绪了(或者超时),返回就绪的描述符数量。但这时,不要盲目地迭代所有监视的集合,我们知道 epoll_write 会修改传给它的 events 缓冲区,缓冲区中有就绪的事件,从 0 到 nready-1,因此我们只需迭代必要的次数。

要在 select 里面重新遍历,有明显的差异:如果在监视着 1000 个描述符,只有两个就绪, epoll_waits 返回的是 nready=2,然后修改 events 缓冲区最前面的两个元素,因此我们只需要“遍历”两个描述符。用 select 我们就需要遍历 1000 个描述符,找出哪个是就绪的。因此,在繁忙的服务器上,有许多活跃的套接字时 epollselect 更加容易扩展。

剩下的代码很直观,因为我们已经很熟悉 “select 服务器” 了。实际上,“epoll 服务器” 中的所有“业务逻辑”和 “select 服务器” 是一样的,回调构成相同的代码。

这种相似是通过将事件循环抽象分离到一个库/框架中。我将会详述这些内容,因为很多优秀的程序员曾经也是这样做的。相反,下一篇文章里我们会了解 libuv,一个最近出现的更加受欢迎的时间循环抽象层。像 libuv 这样的库让我们能够写出并发的异步服务器,并且不用考虑系统调用下繁琐的细节。


  • 注1:我试着在做网络浏览和阅读这两件事的实际差别中突显自己,但经常做得头疼。有很多不同的选项,从“它们是一样的东西”到“一个是另一个的子集”,再到“它们是完全不同的东西”。在面临这样主观的观点时,最好是完全放弃这个问题,专注特殊的例子和用例。
  • 注2:POSIX 表示这可以是 EAGAIN,也可以是 EWOULDBLOCK,可移植应用应该对这两个都进行检查。
  • 注3:和这个系列所有的 C 示例类似,代码中用到了某些助手工具来设置监听套接字。这些工具的完整代码在这个 仓库utils 模块里。
  • 注4:select 不是网络/套接字专用的函数,它可以监视任意的文件描述符,有可能是硬盘文件、管道、终端、套接字或者 Unix 系统中用到的任何文件描述符。这篇文章里,我们主要关注它在套接字方面的应用。
  • 注5:有多种方式用多线程来实现事件驱动,我会把它放在稍后的文章中进行讨论。
  • 注6:由于各种非实验因素,它 仍然 可以阻塞,即使是在 select 说它就绪了之后。因此服务器上打开的所有套接字都被设置成非阻塞模式,如果对 recvsend 的调用返回了 EAGAIN 或者 EWOULDBLOCK,回调函数就装作没有事件发生。阅读示例代码的注释可以了解更多细节。
  • 注7:注意这比该文章前面所讲的异步轮询的例子要稍好一点。轮询需要 一直 发生,而 select 实际上会阻塞到有一个或多个套接字准备好读取/写入;select 会比一直询问浪费少得多的 CPU 时间。

via: https://eli.thegreenplace.net/2017/concurrent-servers-part-3-event-driven/

作者:Eli Bendersky 译者:GitFuture 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

说到游戏,人们一般都会推荐使用 Windows 系统。Windows 能提供更好的显卡支持和硬件兼容性,所以对于游戏爱好者来说的确是个更好的选择。但你是否想过在 Linux 系统上玩游戏?这的确是可以的,也许你以前还曾经考虑过。但在几年之前, Steam for Linux 上可玩的游戏并不是很吸引人。

但现在情况完全不一样了。Steam 商店里现在有许多支持 Linux 平台的游戏(包括很多主流大作)。我们在本文中将介绍 Steam 上最好的一些 Linux 游戏。

在进入正题之前,先介绍一个省钱小窍门。如果你是个狂热的游戏爱好者,在游戏上花费很多时间和金钱的话,我建议你订阅 Humble 每月包 Humble Monthly 。这是个每月收费的订阅服务,每月只用 12 美元就能获得价值 100 美元的游戏。

这个游戏包中可能有些游戏不支持 Linux,但除了 Steam 游戏之外,它还会让 Humble Bundle 网站上所有的游戏和书籍都打九折,所以这依然是个不错的优惠。

更棒的是,你在 Humble Bundle 上所有的消费都会捐出一部分给慈善机构。所以你在享受游戏的同时还在帮助改变世界。

Steam 上最好的 Linux 游戏

以下排名无先后顺序。

额外提示:虽然在 Steam 上有很多支持 Linux 的游戏,但你在 Linux 上玩游戏时依然可能会遇到各种问题。你可以阅读我们之前的文章:每个 Linux 游戏玩家都会遇到的烦人问题

Steam 上最佳 Linux 动作类游戏

1、 《 反恐精英:全球攻势 Counter-Strike: Global Offensive 》(多人)

《CS:GO》毫无疑问是 Steam 上支持 Linux 的最好的 FPS 游戏之一。我觉得这款游戏无需介绍,但如果你没有听说过它,我要告诉你这将会是你玩过的最好玩的多人 FPS 游戏之一。《CS:GO》还是电子竞技中的一个主流项目。想要提升等级的话,你需要在天梯上和其他玩家同台竞技。但你也可以选择更加轻松的休闲模式。

我本想写《彩虹六号:围攻行动》,但它目前还不支持 Linux 或 Steam OS。

2、 《 求生之路 2 Left 4 Dead 2 》(多人/单机)

这是最受欢迎的僵尸主题多人 FPS 游戏之一。在 Steam 优惠时,价格可以低至 1.3 美元。这是个有趣的游戏,能让你体会到你在僵尸游戏中期待的战栗和刺激。游戏中的环境包括了沼泽、城市、墓地等等,让游戏既有趣又吓人。游戏中的枪械并不是非常先进,但作为一个老游戏来说,它已经提供了足够真实的体验。

3、 《 无主之地 2 Borderlands 2 》(单机/协作)

《无主之地 2》是个很有意思的 FPS 游戏。它和你以前玩过的游戏完全不同。画风看上去有些诡异和卡通化,如果你正在寻找一个第一视角的射击游戏,我可以保证,游戏体验可一点也不逊色!

如果你在寻找一个好玩而且有很多 DLC 的 Linux 游戏,《无主之地 2》绝对是个不错的选择。

4、 《 叛乱 Insurgency 》(多人)

《叛乱》是 Steam 上又一款支持 Linux 的优秀的 FPS 游戏。它剑走偏锋,从屏幕上去掉了 HUD 和弹药数量指示。如同许多评论者所说,这是款注重武器和团队战术的纯粹的射击游戏。这也许不是最好的 FPS 游戏,但如果你想玩和《三角洲部队》类似的多人游戏的话,这绝对是最好的游戏之一。

5、 《 生化奇兵:无限 Bioshock: Infinite 》(单机)

《生化奇兵:无限》毫无疑问将会作为 PC 平台最好的单机 FPS 游戏之一而载入史册。你可以利用很多强大的能力来杀死你的敌人。同时你的敌人也各个身怀绝技。游戏的剧情也非常丰富。你不容错过!

6、 《 杀手(年度版) HITMAN - Game of the Year Edition 》(单机)

《杀手》系列无疑是 PC 游戏爱好者们的最爱之一。本系列的最新作开始按章节发布,让很多玩家觉得不满。但现在 Square Enix 撤出了开发,而最新的年度版带着新的内容重返舞台。在游戏中发挥你的想象力暗杀你的目标吧,杀手47!

7、 《 传送门 2 Portal 2

《传送门 2》完美地结合了动作与冒险。这是款解谜类游戏,你可以与其他玩家协作,并开发有趣的谜题。协作模式提供了和单机模式截然不同的游戏内容。

8、 《 杀出重围:人类分裂 Deux Ex: Mankind Divided

如果你在寻找隐蔽类的射击游戏,《杀出重围》是个填充你的 Steam 游戏库的完美选择。这是个非常华丽的游戏,有着最先进的武器和超乎寻常的战斗机制。

9、 《 地铁 2033 重置版 Metro 2033 Redux 》 / 《 地铁:最后曙光 重置版 Metro Last Light Redux

《地铁 2033 重置版》和《地铁:最后曙光 重置版》是经典的《地铁 2033》和《地铁:最后曙光》的最终版本。故事发生在世界末日之后。你需要消灭所有的变种人来保证人类的生存。剩下的就交给你自己去探索了!

10、 《 坦能堡 Tannenberg 》(多人)

《坦能堡》是个全新的游戏 - 在本文发表一个月前刚刚发售。游戏背景是第一次世界大战的东线战场(1914-1918)。这款游戏只有多人模式。如果你想要在游戏中体验第一次世界大战,不要错过这款游戏!

Steam 上最佳 Linux 角色扮演类游戏

11、 《 中土世界:暗影魔多 Shadow of Mordor

《中土世界:暗影魔多》 是 Steam 上支持 Linux 的最好的开放式角色扮演类游戏之一。你将扮演一个游侠(塔里昂),和光明领主(凯勒布理鹏)并肩作战击败索隆的军队(并最终和他直接交手)。战斗机制非常出色。这是款不得不玩的游戏!

12、 《 神界:原罪加强版 Divinity: Original Sin – Enhanced Edition

《神界:原罪》是一款极其优秀的角色扮演类独立游戏。它非常独特而又引人入胜。这或许是评分最高的带有冒险和策略元素的角色扮演游戏。加强版添加了新的游戏模式,并且完全重做了配音、手柄支持、协作任务等等。

13、 《 废土 2:导演剪辑版 Wasteland 2: Director’s Cut

《废土 2》是一款出色的 CRPG 游戏。如果《辐射 4》被移植成 CRPG 游戏,大概就是这种感觉。导演剪辑版完全重做了画面,并且增加了一百多名新人物。

14、 《 阴暗森林 Darkwood

一个充满恐怖的俯视角角色扮演类游戏。你将探索世界、搜集材料、制作武器来生存下去。

最佳赛车 / 运动 / 模拟类游戏

15、 《 火箭联盟 Rocket League

《火箭联盟》是一款充满刺激的足球游戏。游戏中你将驾驶用火箭助推的战斗赛车。你不仅是要驾车把球带进对方球门,你甚至还可以让你的对手化为灰烬!

这是款超棒的体育动作类游戏,每个游戏爱好者都值得拥有!

16、 《 公路救赎 Road Redemption

想念《暴力摩托》了?作为它精神上的续作,《公路救赎》可以缓解你的饥渴。当然,这并不是真正的《暴力摩托 2》,但它一样有趣。如果你喜欢《暴力摩托》,你也会喜欢这款游戏。

17、 《 尘埃拉力赛 Dirt Rally

《尘埃拉力赛》是为想要体验公路和越野赛车的玩家准备的。画面非常有魄力,驾驶手感也近乎完美。

18、 《F1 2017》

《F1 2017》是另一款令人印象深刻的赛车游戏。由《尘埃拉力赛》的开发者 Codemasters & Feral Interactive 制作。游戏中包含了所有标志性的 F1 赛车,值得你去体验。

19、 《 超级房车赛:汽车运动 GRID Autosport

《超级房车赛》是最被低估的赛车游戏之一。《超级房车赛:汽车运动》是《超级房车赛》的续作。这款游戏的可玩性令人惊艳。游戏中的赛车也比前作更好。推荐所有的 PC 游戏玩家尝试这款赛车游戏。游戏还支持多人模式,你可以和你的朋友组队参赛。

最好的冒险游戏

20、 《 方舟:生存进化 ARK: Survival Evolved

《方舟:生存进化》是一款不错的生存游戏,里面有着激动人心的冒险。你发现自己身处一个未知孤岛(方舟岛),为了生存下去并逃离这个孤岛,你必须去驯服恐龙、与其他玩家合作、猎杀其他人来抢夺资源、以及制作物品。

21、 《 这是我的战争 This War of Mine

一款独特的战争游戏。你不是扮演士兵,而是要作为一个平民来面对战争带来的艰难。你需要在身经百战的敌人手下逃生,并帮助其他的幸存者。

22、 《 疯狂的麦克斯 Mad Max

生存和暴力概括了《疯狂的麦克斯》的全部内容。游戏中有性能强大的汽车,开放性的世界,各种武器,以及徒手肉搏。你要不断地探索世界,并注意升级你的汽车来防患于未然。在做决定之前,你要仔细思考并设计好策略。

最佳独立游戏

23、 《 泰拉瑞亚 Terraria

这是款在 Steam 上广受好评的 2D 游戏。你在旅途中需要去挖掘、战斗、探索、建造。游戏地图是自动生成的,而不是固定不变的。也许你刚刚遇到的东西,你的朋友过一会儿才会遇到。你还将体验到富有新意的 2D 动作场景。

24、 《 王国与城堡 Kingdoms and Castles

在《王国与城堡》中,你将建造你自己的王国。在管理你的王国的过程中,你需要收税、保护森林、规划城市,并且发展国防来防止别人入侵你的王国。

这是款比较新的游戏,但在独立游戏中已经相对获得了比较高的人气。

Steam 上最佳 Linux 策略类游戏

25、 《 文明 5 Sid Meier’s Civilization V

《文明 5》是 PC 上评价最高的策略游戏之一。如果你想的话,你可以去玩《文明 6》。但是依然有许多玩家喜欢《文明 5》,觉得它更有独创性,游戏细节也更富有创造力。

26、 《 全面战争:战锤 Total War: Warhammer

《全面战争:战锤》是 PC 平台上一款非常出色的回合制策略游戏。可惜的是,新作《战锤 2》依然不支持 Linux。但如果你喜欢使用飞龙和魔法来建造与毁灭帝国的话,2016 年的《战锤》依然是个不错的选择。

27、 《 轰炸小队 Bomber Crew

想要一款充满乐趣的策略游戏?《轰炸小队》就是为你准备的。你需要选择合适的队员并且让你的队伍稳定运转来取得最终的胜利。

28、 《 奇迹时代 3 Age of Wonders III

非常流行的策略游戏,包含帝国建造、角色扮演、以及战争元素。这是款精致的回合制策略游戏,请一定要试试!

29、 《 城市:天际线 Cities: Skylines

一款非常简洁的策略游戏。你要从零开始建造一座城市,并且管理它的全部运作。你将体验建造和管理城市带来的愉悦与困难。我不觉得每个玩家都会喜欢这款游戏——它的用户群体非常明确。

30、 《 幽浮 2 XCOM 2

《幽浮 2》是 PC 上最好的回合制策略游戏之一。我在想如果《幽浮 2》能够被制作成 FPS 游戏的话该有多棒。不过它现在已经是一款好评如潮的杰作了。如果你有多余的预算能花在这款游戏上,建议你购买“ 天选之战 War of the Chosen “ DLC。

总结

我们从所有支持 Linux 的游戏中挑选了大部分的主流大作以及一些评价很高的新作。

你觉得我们遗漏了你最喜欢的支持 Linux 的 Steam 游戏么?另外,你还希望哪些 Steam 游戏开始支持 Linux 平台?

请在下面的回复中告诉我们你的想法。


via: https://itsfoss.com/best-linux-games-steam/

作者:Ankush Das 译者:yixunx 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

最近我们采访了 Mark McIntyre,谈了他是如何使用 Fedora 系统的。这也是 Fedora 杂志上系列文章的一部分。该系列简要介绍了 Fedora 用户,以及他们是如何用 Fedora 把事情做好的。如果你想成为采访对象,请通过反馈表与我们联系。

Mark McIntyre 是谁?

Mark McIntyre 为极客而生,以 Linux 为乐趣。他说:“我在 13 岁开始编程,当时自学 BASIC 语言,我体会到其中的乐趣,并在乐趣的引导下,一步步成为专业的码农。” Mark 和他的侄女都是披萨饼的死忠粉。“去年秋天,我和我的侄女开始了一个任务,去尝试诺克斯维尔的许多披萨饼连锁店。点击这里可以了解我们的进展情况。”Mark 也是一名业余的摄影爱好者,并且在 Flickr 上 发布自己的作品

作为一名开发者,Mark 有着丰富的工作背景。他用过 Visual Basic 编写应用程序,用过 LotusScript、 PL/SQL(Oracle)、 Tcl/TK 编写代码,也用过基于 Python 的 Django 框架。他的强项是 Python。这也是目前他作为系统工程师的工作语言。“我经常使用 Python。由于我的工作变得更像是自动化工程师, Python 用得就更频繁了。”

McIntyre 自称是个书呆子,喜欢科幻电影,但他最喜欢的一部电影却不是科幻片。“尽管我是个书呆子,喜欢看《 星际迷航 Star Trek 》、《 星球大战 Star Wars 》之类的影片,但《 光荣战役 Glory 》或许才是我最喜欢的电影。”他还提到,电影《 冲出宁静号 Serenity 》是一个著名电视剧的精彩后续(指《萤火虫》)。

Mark 比较看重他人的谦逊、知识与和气。他欣赏能够设身处地为他人着想的人。“如果你决定为另一个人服务,那么你会选择自己愿意亲近的人,而不是让自己备受折磨的人。”

McIntyre 目前在 Scripps Networks Interactive 工作,这家公司是 HGTV、Food Network、Travel Channel、DIY、GAC 以及其他几个有线电视频道的母公司。“我现在是一名系统工程师,负责非线性视频内容,这是所有媒体要开展线上消费所需要的。”他为一些开发团队提供支持,他们编写应用程序,将线性视频从有线电视发布到线上平台,比如亚马逊、葫芦。这些系统既包含预置系统,也包含云系统。Mark 还开发了一些自动化工具,将这些应用程序主要部署到云基础结构中。

Fedora 社区

Mark 形容 Fedora 社区是一个富有活力的社区,充满着像 Fedora 用户一样热爱生活的人。“从设计师到封包人,这个团体依然非常活跃,生机勃勃。” 他继续说道:“这使我对该操作系统抱有一种信心。”

2002 年左右,Mark 开始经常使用 IRC 上的 #fedora 频道:“那时候,Wi-Fi 在启用适配器和配置模块功能时,有许多还是靠手工实现的。”为了让他的 Wi-Fi 能够工作,他不得不重新去编译 Fedora 内核。

McIntyre 鼓励他人参与 Fedora 社区。“这里有许多来自不同领域的机会。前端设计、测试部署、开发、应用程序打包以及新技术实现。”他建议选择一个感兴趣的领域,然后向那个团体提出疑问。“这里有许多机会去奉献自己。”

对于帮助他起步的社区成员,Mark 赞道:“Ben Williams 非常乐于助人。在我第一次接触 Fedora 时,他帮我搞定了一些 #fedora 支持频道中的安装补丁。” Ben 也鼓励 Mark 去做 Fedora 大使

什么样的硬件和软件?

McIntyre 将 Fedora Linux 系统用在他的笔记本和台式机上。在服务器上他选择了 CentOS,因为它有更长的生命周期支持。他现在的台式机是自己组装的,配有 Intel 酷睿 i5 处理器,32GB 的内存和2TB 的硬盘。“我装了个 4K 的显示屏,有足够大的地方来同时查看所有的应用。”他目前工作用的笔记本是戴尔灵越二合一,配备 13 英寸的屏,16 GB 的内存和 525 GB 的 m.2 固态硬盘。

Mark 现在将 Fedora 26 运行在他过去几个月装配的所有机器中。当一个新版本正式发布的时候,他倾向于避开这个高峰期。“除非在它即将发行的时候,我的工作站中有个正在运行下一代测试版本,通常情况下,一旦它发展成熟,我都会试着去获取最新的版本。”他经常采取就地更新:“这种就地更新方法利用 dnf 系统升级插件,目前表现得非常好。”

为了搞摄影,McIntyre 用上了 GIMPDarktable,以及其他一些照片查看包和快速编辑包。当不用 Web 电子邮件时,Mark 会使用 Geary,还有GNOME Calendar。Mark 选用 HexChat 作为 IRC 客户端,HexChat 与在 Fedora 服务器实例上运行的 ZNC bouncer 联机。他的部门通过 Slave 进行沟通交流。

“我从来都不是 IDE 粉,所以大多数的编辑任务都是在 vim 上完成的。”Mark 偶尔也会打开一个简单的文本编辑器,如 gedit,或者 xed。他用 GPaste 做复制和粘贴工作。“对于终端的选择,我已经变成 Tilix 的忠粉。”McIntyre 通过 Rhythmbox 来管理他喜欢的播客,并用 Epiphany 实现快速网络查询。


via: https://fedoramagazine.org/mark-mcintyre-fedora/

作者:Charles Profitt 译者:zrszrszrs 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

目标:使用 GPG 加密文件

发行版:适用于任何发行版

要求:安装了 GPG 的 Linux 或者拥有 root 权限来安装它。

难度:简单

约定:

  • # - 需要使用 root 权限来执行指定命令,可以直接使用 root 用户来执行,也可以使用 sudo 命令
  • $ - 可以使用普通用户来执行指定命令

介绍

加密非常重要。它对于保护敏感信息来说是必不可少的。你的私人文件应该要被加密,而 GPG 提供了很好的解决方案。

安装 GPG

GPG 的使用非常广泛。你在几乎每个发行版的仓库中都能找到它。如果你还没有安装它,那现在就来安装一下吧。

Debian/Ubuntu

$ sudo apt install gnupg

Fedora

# dnf install gnupg2

Arch

# pacman -S gnupg

Gentoo

# emerge --ask app-crypt/gnupg

创建密钥

你需要一个密钥对来加解密文件。如果你为 SSH 已经生成过了密钥对,那么你可以直接使用它。如果没有,GPG 包含工具来生成密钥对。

$ gpg --full-generate-key

GPG 有一个命令行程序可以帮你一步一步的生成密钥。它还有一个简单得多的工具,但是这个工具不能让你设置密钥类型,密钥的长度以及过期时间,因此不推荐使用这个工具。

GPG 首先会询问你密钥的类型。没什么特别的话选择默认值就好。

下一步需要设置密钥长度。4096 是一个不错的选择。

之后,可以设置过期的日期。 如果希望密钥永不过期则设置为 0

然后,输入你的名称。

最后,输入电子邮件地址。

如果你需要的话,还能添加一个注释。

所有这些都完成后,GPG 会让你校验一下这些信息。

GPG 还会问你是否需要为密钥设置密码。这一步是可选的, 但是会增加保护的程度。若需要设置密码,则 GPG 会收集你的操作信息来增加密钥的健壮性。 所有这些都完成后, GPG 会显示密钥相关的信息。

加密的基本方法

现在你拥有了自己的密钥,加密文件非常简单。 使用下面的命令在 /tmp 目录中创建一个空白文本文件。

$ touch /tmp/test.txt

然后用 GPG 来加密它。这里 -e 标志告诉 GPG 你想要加密文件, -r 标志指定接收者。

$ gpg -e -r "Your Name" /tmp/test.txt

GPG 需要知道这个文件的接收者和发送者。由于这个文件给是你的,因此无需指定发送者,而接收者就是你自己。

解密的基本方法

你收到加密文件后,就需要对它进行解密。 你无需指定解密用的密钥。 这个信息被编码在文件中。 GPG 会尝试用其中的密钥进行解密。

$ gpg -d /tmp/test.txt.gpg

发送文件

假设你需要发送文件给别人。你需要有接收者的公钥。 具体怎么获得密钥由你自己决定。 你可以让他们直接把公钥发送给你, 也可以通过密钥服务器来获取。

收到对方公钥后,导入公钥到 GPG 中。

$ gpg --import yourfriends.key

这些公钥与你自己创建的密钥一样,自带了名称和电子邮件地址的信息。 记住,为了让别人能解密你的文件,别人也需要你的公钥。 因此导出公钥并将之发送出去。

gpg --export -a "Your Name" > your.key

现在可以开始加密要发送的文件了。它跟之前的步骤差不多, 只是需要指定你自己为发送人。

$ gpg -e -u "Your Name" -r "Their Name" /tmp/test.txt

结语

就这样了。GPG 还有一些高级选项, 不过你在 99% 的时间内都不会用到这些高级选项。 GPG 就是这么易于使用。你也可以使用创建的密钥对来发送和接受加密邮件,其步骤跟上面演示的差不多, 不过大多数的电子邮件客户端在拥有密钥的情况下会自动帮你做这个动作。


via: https://linuxconfig.org/how-to-encrypt-and-decrypt-individual-files-with-gpg

作者:Nick Congleton 译者:lujun9972 校对:wxy

本文由 LCTT 原创编译,Linux 中国 荣誉推出