通用网关协议 (CGI) 进化史 (中篇)

CGI 由于前面提到的性能问题, 越来越无法满足大多数网站的要求. 于是, FastCGI 和 Simple CGI 出现了.

Simple CGI (SCGI)

Simple CGI 简称 SCGI, 和 FastCGI 一起, 是为了解决原始 CGI 的性能问题而出现的. 它们的解决方式和 “将脚本程序解释器嵌入 webserver (如 mod_php, mod_python” 不同, 它们的解决方式是创建一个 long-running 的后台进程, 以处理 webserver 的 forward 过来的请求.

当然, webserver 上仍然需要实现 FastCGI 或者 SCGI 协议的, apache 有 mod_fastcgi/mod_fcgid, lighttpd 也有 mod_fastcgi 和 mod_scgi.

SCGI 和 FastCGI 基本是一样的, 除了 SCGI 比 FastCGI 更容易实现 — 正如其名字所暗示的那样.

下面看一下在各个 webserver 对 SCGI 的支持情况与配置方式.

Apache

最初, Apache 的模块 mod_scgi 负责实现 scgi 协议, 这个模块不是 Apache 自己开发的, 似乎是 python 用户组开发的, 因为官网就是 python 站上 (参考1). 这个模块在 Apache 2.0+ 上可用, 是非常稳定的模块. 可是现在其开发似乎停滞了, 可能是因为 Apache 上 SCGI 用的少, 而且 mod_proxy_scgi 模块出来的原因吧.

mod_proxy_scgi 模块是相对较新的模块, 是被包含在 Apache 源代码里的, 内建的模块.

mod_scgi 模块的配置大概如下 (详见 参考5):

# (This actually better set up permanently with the command line
# "a2enmod scgi" but shown here for completeness)
LoadModule scgi_module /usr/lib/apache2/modules/mod_scgi.so

# Set up a location to be served by an SCGI server process
SCGIMount /dynamic/ 127.0.0.1:4000
The deprecated way of delegating requests to an SCGI server is as follows:

<Location "/dynamic">
    # Enable SCGI delegation
    SCGIHandler On
    # Delegate requests in the "/dynamic" path to daemon on local
    # server, port 4000
    SCGIServer 127.0.0.1:4000
</Location>

mod_proxy_scgi 模块的配置大概如下 (详见 参考6):

ProxyPass /scgi-bin/ scgi://localhost:4000/
<Proxy balancer://somecluster>
    BalancerMember scgi://localhost:4000
    BalancerMember scgi://localhost:4001
</Proxy>

前面说了, SCGI 的解决方式是创建一个 long-running 的进程, 或者监听某个 TCP/IP 端口, 或者监听一个 unix 套接字, 以便于和 webserver 通信. 那么现在 webserver 相当于 scgi 客户端, 我们现在还缺少一个 scgi 服务器端.

从上面的 mod_scgi 和 mod_proxy_scgi 的配置也可以看出, SCGI 协议里是还需要一个 scgi 服务器端的.

SCGI 的服务器端是和语言相关的 (与 FastCGI 一样), 不同的语言有不同的实现, 参考8 介绍了这一点.

注: 下面的内容对 FastCGI 也是适用的

如最古老的 CGI 协议一样, SCGI 服务器会将请求递交给它的子进程, 子进程会去执行实际的任务. 不同的地方是, 子进程完成任务后不会退出, 而是 sleep, 等待下一个请求的到来.

SCGI 的另一个好处是, 它不必非得和 webserver 处在同一个机器上.

Lighttpd

Lighttpd 自带着 mod_scgi 模块, 其配置方式与 Lighttpd 的 FastCGI 配置方式一样, 可参考 Lighttpd 的 FastCGI 部分.

Nginx

Nginx 也有自己的 ngx_http_scgi_module 模块以支持 scgi.

参考

  1. SCGI 的官方网站, 不明白为何是放在 python 官网上的: http://python.ca/scgi/
  2. 维基页, 讲得很少: http://en.wikipedia.org/wiki/Simple_Common_Gateway_Interface
  3. 强烈推荐: https://docs.python.org/2/howto/webservers.html
  4. 讲了 Apache 中的两种 scgi 的模块, 推荐: http://alesteska.blogspot.com/2012/07/scgi-in-apache-http-server-and-in.html
  5. 讲了 Apache 中的 mod_scgi 模块的配置: http://quixote.python.ca/scgi.dev/doc/guide.html
  6. Apache 官网上关于 mod_proxy_scgi 的介绍: http://httpd.apache.org/docs/trunk/mod/mod_proxy_scgi.html
  7. http://woof.sourceforge.net/woof-ug/_woof/docs/ug/apache_scgi
  8. 吐血推荐, 不过这文章有四页, 只看前两页即可: http://www.linuxjournal.com/article/9310

通用网关协议 (CGI) 进化史 (上篇)

早期的网站基本都是静态的, 那时候的 web server 几乎所有工作就是给访问者提供静态资源, 网站与访问者之间缺乏交互. 后来随着 WWW 的发展网站变得交互性强了起来, 交互性强了意味着 web server 端的业务逻辑复杂了起来, 不再是简单地解析 url, 定位并返回用户请求的资源, 而是要处理很多用户请求的动态资源以及许多复杂的业务, 这些工作都交给 web server 来做是不现实的, 为何不现实呢说起来就罗嗦了, 以后再补充吧 (可以从网站的开发速度与难度, 可扩展性等方面考虑一下).

后来 CGI 出现了, 它使得 web server 可以把复杂的业务逻辑交给 cgi 脚本程序来做, CGI 协议定义了 web server 与 cgi 程序之间通信的 context, web server 一收到动态资源的请求就 fork 一个子进程调用 cgi 程序处理这个请求, 同时将和此请求相关的 context 传给 cgi 程序, 像是 path_info, script path, request method, remote ip 等等…

显然每次来个请求 web server 就去 fork 子进程是很低效的, 所以后来 fastcgi 出来了, 它定义了一种通信规范使得 cgi 程序和 web server 之间能通过 socket 通信, 这样一来 cgi 这边也需要有一个专门的 daemon 进程来和 web server 保持连接, php-fpm 就是这么一个东西了.

对了, 你还说了个 apache 的 php5_module, 这是 apache 自己搞的一个插件架构, apache 自己有一套类似于 cgi 协议的东西, 好象是叫做 sapi, php 的处理程序是直接静态或者动态编译进 apache 的, 可以直接被 apache 通过函数调用来调用, 这个并不能算是所有的事都交给了 apache 去干, 这得益于 apache 的插件架构, 而这个插件架构背后的思想和 cgi/fastcgi 实际上已经没有本质区别了

挂载 (mount) 深入理解

首先引用一句 wiki 上的定义来开篇:

Mounting takes place before a computer can use any kind of storage device (such as a hard drive, CD-ROM, or network share). The user or their operating system must make it accessible through the computer’s file system. A user can only access files on mounted media.

意思是说, “挂载” 发生在计算机想要使用任何类型的存储设备 (如硬盘, CD-ROM, 网络设备) 之前. 操作系统必须将这个设备纳入自己的文件系统中去.

要注意的是, 这里的存储设备不一定必须是外部的存储设备, 也可以是你安装系统的硬盘上的分区.

例子先

光看上面说的还不够, 先看个例子吧, 这个例子摘自 man mount, 在 man 手册中这个例子下的一句话非常好的解释了 mount 到底是什么.

mount -t type device dir

在这个例子下面有这么一句话:

This tells the kernel to attch the filesystem fount on device (which is of type type) at the directory dir.

这句话非常重要, 我们一定要明白, 挂载操作, 实际上是把设备 device 中的文件系统附加到 dir 上, 然后我们就可以通过访问 dir 来访问这个设备.

明白了这一点, 我们就能明白 “挂载” 的本质了, 挂载的本质就是针对某一设备, 分析出其文件系统结构, 并根据其文件系统类型调用 linux 中相应的驱动, 处理其的元数据, 将这些信息附加到 linux 的目录树上呈现出来.

明白这一点之后, 后面的 bind mount, loop mount 以及 remount 的区别就能够很清楚了.

挂载点

什么是挂载点呢? 还是先借用 Wiki 上的一句话:

A mount point is a physical location in the partition used as a root filesystem.

不幸的是, Wiki 上的这句话并不准确, 这句话的意思也就是说 “挂载点就是 root 分区中的一个位置”, 这句话错在 “root 分区” 上.

我们知道在安装 Linux 系统时可能会为磁盘分多个区, 最普遍的情况就是很多用户会给 /home 目录单独分一个区. 而且有一部分用户还会在 /home/username 目录下建立一个专门用来挂载各种设备的目录 (如 /home/username/mnt-point) 而不使用系统的 /mnt 目录. 那么这时候, 难道说 /home/username/mnt-point 这个目录就不是挂载点了吗? 显然它也是挂载点, 但它确并不是位于 root 分区 (即 / 分区).

国外有一篇文章, 用毫不装逼的方式说出了”挂载点”的本质:

In simple words a mount point is a directory to access your data (files and folders) which is stored in your disks.

所以说白了, 挂载点就是一个目录. 所以下文中当我应该说”挂载到某一挂载点”的时候我都直接说”挂载到某一目录”.

假设备挂载 (loop mount)

loop device

明白 loop mount 之前, 最好先清除什么是 loop device, 有耐心的话可以参见维基百科中的条目, 比较长, 没耐心的话可以直接看我下面的描述, 简洁些.

简单来说, loop device 能够提供将一个档案挂载到某一目录的功能. 这和 bind mount (下文会介绍) 有些类似, 但并不相同. 原始的 mount 只是为了将正常的设备挂载, bind mount 使得可以挂载目录, 而 loop device 使得可以挂载档案.

在 linux 中, loop device 就是指 /dev/loop0, /dev/loop1, /dev/loop2 … 这些设备, 它们是虚假的设备(pseudo device), 不像 /dev/sda 在你的主机里物理存在. loop device 需要你在编译内核的时候将其静态编译或者编译为动态模块, 然后需要使用 modprobe 加载其模块(这个模块包含了 loop device 的驱动程序以及 losetup 这种提供给用户来操作 loop device 的程序), 这时其驱动程序就回创建 /dev/loop0, /dev/loop1 … 这几个设备文件.

档案

注意, 我在说档案的时候, 指的是英文中的 archive, 它和文件 file 是不同的东西, 档案 archive 是一个打包的文件集, 里面一般包含许多文件, 比如 tar, jar, iso 就是常见的档案格式.

用过 dd 的人应该知道, 这个强大的命令可以将整个磁盘或者磁盘分区克隆下来, 放到一个文件里, 一般, 这样的文件我们都以 .img 后缀为其命名并称这样的文件为镜像文件. 我所说的档案也包含这类情况.

loop mount

ok, 明白了什么是 loop device, 也明白了档案是什么, 那么到底如何把一个档案挂载到某个目录下呢?

实际上 loop mount 采取了一个瞒天过海的方式, 它先将这个档案映射到某个 loop device 上, 像这样:

# losetup /dev/loop0 xxxx.iso

通过这种方式来欺骗 mount 命令, 让 mount 命令以为 /dev/loop0 上面真的有设备. 这时运行 mount 就行了:

# mount -t iso9660 /dev/loop0 /path/to/mount/point

这么看起来, 当你想挂载某一个档案的时候(比如某个 iso), 你首先得把这个档案和某一个 loop device 关联起来, 使用 losetup 命令. 然后使用 mount 命令将这个 loop device 设备挂载到某个目录上. 实际上不必这样, mount 命令自身其实就有一个能把这两步合并的功能, 那就是这样:

# mount -t iso9660 -o loop /dev/loop0 /path/to/mount/point

最后我们再来想一想, 是不是所有的档案都可以用这种方式挂载? 显然不是的, 根据 mount 命令有个 -t 参数来看, 在挂载的时候是需要指定文件系统的类型的(不指定的话 mount 命令会自动识别), 还记得上面说的挂载的本质吗?

"挂载操作, 实际上是把设备 _device_ 中的**文件系统**附加到 _dir_ 上,".

不被识别的文件系统是不能被挂载的, 如果你没有加载 ReiserFS 模块, 那么挂载具有 ReiserFS 文件系统的设备时就会报 “unknown file system” 错误. 像上面说的 tar, jar, zip 这样的档案, 它们只是一种打包/压缩格式, 本身就不是一种文件系统格式, 当然是不能被 linux 识别的. 它们虽然可以映射到某一个 loop device, 但并不能被挂载.

但是像 .iso 文件, 它一般包含 iso 9660 文件系统, 都知道这是一种 CD 上采用的文件系统. 还有就是你可以使用 dd, mkfs 命令来创建一个 ext2, ext3 等文件系统的档案. 这样的档案才是可以被挂载的.

loop mount 一直以来是 Unix-like 系统下很有用的特性, 能帮助你当你拿到一个 iso 文件后, 不必将其刻录到 CD/DVD 里就能查看里面的内容. windows 下直到 windows 7 才支持这一特性, 在此之前都需要借助第三方软件如 Daemon Tools 来实现虚拟光驱的功能.

绑定式挂载 (bind mount)

上面所说的 “挂载” 都是指让你将某个设备挂载到某一目录, 不管这个设备是真实的物理设备, 还是假的 loop 设备, 它都是设备. 而 “绑定式挂载” 能够允许你将已经的存在目录挂载到另一目录. 比如:

 #  mount --bind / /home/username/mnt-point

这样, 你的 mnt-point 目录下也会有 etc, opt, usr 等目录, 这一过程我们称作 “将根目录绑定到 /home/username/mnt-point 上”, 所以, 你在一处改变目录下的内容的话, 在另一处也能够看到改变.

需要注意的一点是如果根目录树下有某个目录是挂载到另一个磁盘分区的话, 那么它可能不会被绑定到新的目录下. 比如说如果 /usr 和 / 处于不同的磁盘分区(/ 在 sda1, /usr 在 sda2), 那么你可能会发现 /home/username/mnt-point/usr 是空的, 那么这时可以额外挂载一次来使得 /usr 也出现在 /home/username/mnt-point/usr:

 #  mount --bind /usr /home/username/mnt-point/usr

不过你也可以在一开始就执行:

 #  mount --rbind / /home/username/mnt-point

关于绑定式挂载, man 2 mount 中的描述是 “使一个文件, 或者一个目录树在另一个目录上可见”. 这地方不太理解, 就我所知, 只能将目录绑定到目录, 不能将文件绑定到目录的. 我尝试过将一个普通的文件绑定到目录, 但报错了. 不知道 man 手册里这个说法是什么意思. 我只能这么理解: 目录也是文件, 所以这种说法没错吧….

重新挂载 (remount)

借助于绑定式挂载, 可以实现有趣的效果, 比如说, 你可以将 / 绑定到 /, 将 /tmp/test/ 绑定到 /tmp/test/ (运行 mount 命令就能看到效果). 不过… 这么干有个鸟用啊!! 谁这么无聊会去这么干啊!!

这就是 remount 存在的原因, 我们虽然可以通过绑定式挂载耍点小聪明, 将自己绑定到自己上, 但这与没绑定没有任何区别啊; 然而借助 remount, 我们就可以在重新挂载的时候修改挂载的参数.

remount 最常用的情况就是将一个文件系统由只读重新挂载为读写, 或者相反. 比如:

# mount -o remount,rw /

关于 remount 的详情, 可以看一下 man 手册, 这里就不多介绍了.

supermount

“超级挂载”, 这个项目的目的是让你能够免去手动 mount/umount 的过程, 达到 “插上 U 盘就开始拷文件” 以及 “拷完文件就拔掉 U 盘” 的效果.

Update:

经 Ubuntu 中文论坛 @astolia 大侠指正, 关于 “绑定式挂载”, 虽然不能将一个文件挂载到目录, 但确是可以将一个文件挂载到另一个文件的! 比如说这样:

# touch /tmp/test
# touch /tmp/test2
# echo "hello mount" > /tmp/test
# mount --bind /tmp/test /tmp/test2

然后你再去查看 /tmp/test2, 会发现它也有 “hello mount” 这个内容. 所以这样一开始说的 “挂载点就是目录” 这个说法也不太对了….. 再次感谢 @astolia ~~~

非常好的一篇文章

这篇文章也是 参考1 中提到的文章, 由于写的太好, 为了防止其丢失, 我把它转到这里了, 下面那篇英文就是. 我之所以敢转这篇文章是因为这篇文章的版权声明停留在 2006 年, 它的主人大概是忘了更新. 我从这篇文章里摘取了几个比较重要的点作为笔记.

  • 在 “挂载” 的概念中, “设备” 可以是一个分区 (如 /dev/sda9), 可以是另一块磁盘, 可以是 CDROM, 软盘, USB, 磁带等等.
  • “挂载点” 是一个目录, 而且往往是一个空目录, 但这不是必须的. 如果这个目录不是空的, 那么挂载之后, 这个目录中以前的内容会被 “隐藏” 起来变得不可访问.

Mounting Definition

Mounting is the attaching of an additional filesystem to the currently accessible filesystem of a computer.

A filesystem is a hierarchy of directories (also referred to as a directory tree) that is used to organize files on a computer or storage media (e.g., a CDROM or floppy disk). On computers running Linux or other Unix-like operating systems, the directories start with the root directory, which is the directory that contains all other directories and files on the system and which is designated by a forward slash ( / ). The currently accessible filesystem is the filesystem that can be accessed on a computer at a given time.

In order to gain access to files on a storage device, the user must first inform the operating system where in the directory tree to mount the device. A device in a mounting context can be a partition (i.e., a logically independent section) on a hard disk drive (HDD), a CDROM, a floppy disk, a USB (universal serial bus) key drive, a tape drive, or any other external media. For example, to access the files on a CDROM, the user must inform the system to make the filesystem on the CDROM appear in some directory, typically /mnt/cdrom (which exists for this very purpose).

The mount point is the directory (usually an empty one) in the currently accessible filesystem to which a additional filesystem is mounted. It becomes the root directory of the added directory tree, and that tree becomes accessible from the directory to which it is mounted (i.e., its mount point). Any original contents of a directory that is used as a mount point become invisible and inaccessible while the filesystem is still mounted.

The /mnt directory exists by default on all Unix-like systems. It, or usually its subdirectories (such as /mnt/floppy and /mnt/usb), are intended specifically for use as mount points for removable media such as CDROMs, USB key drives and floppy disks.

On some operating systems, everything is mounted automatically by default so that users are never even aware that there is any such thing as mounting. Linux and other Unix-like systems can likewise be configured so that everything is mounted by default, as a major feature of such systems is that they are highly configurable. However, they are not usually set up this way, for both safety and security reasons. Moreover, only the root user (i.e., administrative user) is generally permitted by default to mount devices and filesystems on such systems, likewise as safety and security measures.

In the simplest case, such as on some personal computers, the entire filesystem on a computer running a Unix-like operating system resides on just a single partition, as is typical for Microsoft Windows systems. More commonly, it is spread across several partitions, possibly on different physical disks or even across a network. Thus, for example, the system may have one partition for the root directory, a second for the /usr directory, a third for the /home directory and a fourth for use as swap space. (Swap space is a part of HDD that is used for virtual memory, which is the simulation of additional main memory).

The only partition that can be accessed immediately after a computer boots (i.e., starts up) is the root partition, which contains the root directory, and usually at least a few other directories as well. The other partitions must be attached to this root filesystem in order for an entire, multiple-partition filesystem to be accessible. Thus, about midway through the boot process, the operating system makes these non-root partitions accessible by mounting them on to specified directories in the root partition.

Systems can be set up so that external storage devices can be mounted automatically upon insertion. This is convenient and is usually satisfactory for home computers. However, it can cause security problems, and thus it is usually not (or, at least, should not be) permitted for networked computers in businesses and other organizations. Rather, such devices must be mounted manually after insertion, and such manual mounting can only be performed by the root account.

Mounting can often be performed manually by the root user by merely using the mount command followed by the name of the device to be mounted and its mounting destination (but in some cases it is also necessary to specify the type of filesystem). For example, to mount the eighth partition on the first HDD, which is designated by /dev/hda8, using a directory named /dir8 as the mount point, the following could be used:

  mount /dev/hda8 /dir8
  

Removing the connection between the mounted device and the rest of the filesystem is referred to as unmounting. It is performed by running the umount (with no letter n after the first u) command, likewise followed by the name of the device to be unmounted and its mount point. For example, to unmount the eighth partition from the root filesystem, the following would be used:

  umount /dev/hda8 /dir8
  

A list of the devices that are currently mounted can be seen by viewing the /etc/fstab file. This plain text configuration file also shows the mount points and other information about the devices, and it is employed during the boot process to tell the system which partitions to automatically mount. It can be safely viewed by using the cat command, i.e.,

  cat /etc/fstab
  

参考

  1. 非常好的解释了 “挂载” 的概念, 强烈推荐这篇文章: http://www.linfo.org/mounting.html
  2. 维基, 也很好的解释了 “挂载” 的概念, 唯有一点不足的是对 “挂载点” 解释的不够好: http://en.wikipedia.org/wiki/Mount_(computing)
  3. 其中有一句话不装逼的说出了 “挂载点” 是什么: http://www.linuxnix.com/2013/09/what-is-a-mount-point-in-linuxunix.html
  4. 解释了 bind mount 的概念: http://www.funtoo.org/Funtoo_Filesystem_Guide,_Part_3#Bind_Mounts

在 Linux C 程序中如何正确的判断一个文件/目录文件是否存在

其实不光是在 Linux 下编程, 在其他平台下我们都会有这样的需求: 我们要为应用程序创建自己的数据或者日志目录, 应用程序在每次启动时会检查文件系统中是否已经有了自己的目录, 没有的话就创建它, 有了的话就跳过这一步. 那么如何去判断文件系统中是否已经存在了要创建的目录呢?

Linux 或者 GNU C 都没有提供一个像 file_exists() 这样直观的系统调用给我们, 所以我们得通过其它的调用来达成这个目标.

实际上当我第一次要解决这个问题时, 我先 google 了一下, 这个问题在 stackoverflow 上有人问过而且非常受欢迎, 很多人对这个问题又点赞又收藏的, 自然, 这个问题也收到了不少好的答案, 这篇文章算是对这些好答案的总结和延伸.

我们先来看一个大家都应该知道的方式, 第一种方式:

fopen()

fopen() 方法是流阶级的方法, 这个方法接收用户提供的文件名, 以及访问方式, 然后尝试着打开文件, 打开成功则返回 handle, 失败则返回 NULL. 因此有人提出了使用这个方法来判断指定的文件是否存在的方案:

{% codeblock lang:c %}
#include <stdio.h>

...

FILE *fp = NULL;
fp = fopen("/tmp/test/somefile", "r");

if(fp) {
  // exists
} else {
  // not exists
}

fclose(fp);

...

{% endcodeblock %}

这也是 stackoverflow 上唯一一个得负分的答案, 这个方案的问题在于它没有考虑到文件权限的问题, 而 fopen() 这个函数又是如此的简单 — 不管因为什么原因打开文件失败了, 它只是返回 NULL 给你, 不会提供更多的错误信息.

如果文件存在, 而只是你没有对这个文件的读权限, 那么你同样会得到 NULL 返回值, 而你又不能获得其它导致失败的原因, 于是你想当然的认为这个文件不存在, 于是错误就发生了. 下面的两种情况都能够导致你打开失败:

  1. 你对 test 目录没有 x 权限
  2. 你对 somefile 没有 r 权限

这两种情况下, 显然文件是存在的, 但是我们却得到了 NULL 返回值.

open()

一个改进的放案是使用 open() 系统调用, 这是处于 fopen() 底层的调用, 它提供了丰富的出错信息, 以便于你能够检查出错的原因. 这个方案如下:

{% codeblock lang:c %}
#include <fcntl.h>
#include <errno.h>

...

fd = open(pathname, O_RDONLY);
if(fd < 0) {
    switch (errno) {
        case EACCES:  // you don't have permission
        break;
        case ENOENT:  // the file doesn't exists
        break;
        default:
        break;
    }
} else {
    // use the file
}

close(fd);

{% endcodeblock %}

使用 open() 调用能够帮你完成很多其它的额外的功能, 比如说在文件不存在的时候创建它, 等等.

看起来 open() 的解决方案已经足够了, 但是, 说到底 open() 是需要打开一个文件的, 可能你只是想检查文件是否存在, 而并不想读取它的内容, 这样打开操作就带来了不必要的工作. 如果仅仅是想检查文件是否存在, 或者是否对文件有读, 写, 执行权限的话, 我们还有另一种更好地选择:

access()

access() 调用以一种更明朗的方式专门检查文件是否存在, 文件是否可读, 可写, 可执行. 不过需要注意的是, access() 在检查文件是否存在以及程序对文件是否具有读写执行权限时, 使用的是程序的实际用户 ID, 而不是有效用户 ID. 实际上这个特点对于 “setuid 化” 的程序是很有用的, 因为 “setuid 化” 的程序可能常常会检查实际用户对某一文件是否具有响应的权限.

使用 access() 来检查文件是否存在的代码如下:

{% codeblock lang:c %}
if( access( fname, F_OK ) != -1 ) {
// file exists
} else {
switch(errno) {
case EACCES:
break;
case ENOENT:
break;
}
}
{% endcodeblock %}

access() 在失败时也会通过 errno 提供错误信息, 当你对要检查的文件的父目录没有 x 权限时, 会产生 EACCES; 当要访问的文件不存在时, 会产生 ENOENT.

stat()

以上几种方法, 都只是根据我们指定的文件名来判断这个文件是否存在, 而不管它是一般文件还是目录文件, 如果我们不仅要确认一个文件存在, 还要确认它是目录文件, 那上面几种方法就不能满足了, 这时候我们可以用 stat() 调用:

{% codeblock lang:c %}
struct stat st_stat = {0};
int ret = stat(DBDIR, &st_stat);
///
// 如果 stat 调用失败不是由于文件不存在导致的, 那么直接返回
//
if(ret && errno != ENOENT) {
fprintf(stderr, “Check directory error: %s\n”, strerror(errno));
return 1;
}

///
// 如果 stat() 调用失败是由于目录不存在, 就创建目录
// 如果 stat() 调用没有失败, 但是已经存在的那个文件不是目录文件, 也创建它
//
if((ret && errno == ENOENT) || (! ret && ! S_ISDIR(st_stat.st_mode))) {
  ///
  // 创建目录并赋予其 rwxr-xr-x 权限
  //
  if(mkdir(DBDIR, S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH)) {
    fprintf(stderr, "Crate directory error: %s\n", strerror(errno));

    return 1;
  }
}

{% endcodeblock %}

总结

  1. 需要注意的是, 以上四个函数调用都会收到一个共同的限制: 如果要检查的文件/目录文件的父目录没有 x 权限, 那么都会产生 EACCES 错误或者返回 NULL(fopen).

stackoverflow 上的链接

http://stackoverflow.com/questions/230062/whats-the-best-way-to-check-if-a-file-exists-in-c-cross-platform

RHEL 上搞定 HP MicroServer 的 Smart Array Controller B120i 磁盘阵列

MicroServer Gen8 是 HP 服务器里较新的一个系列, 其所配备的磁盘阵列卡 — Smart Array Controller B120i, 也是比较新的一种阵列卡, 目前 HP 仅提供了 RHEL, OpenSUSE, Microsoft 的驱动程序.

我们就是要在 MicroServer Gen8 上安装 RHEL6.

MicroServer Gen8 的主板上的 ROM 上搭在了一个小型的配置系统, 叫做 Intelligence Provisioning, 在这里你可以对磁盘阵列进行分区(正如 hardware raid 都会带有一个控制系统来管理自己的说法一样), 还可以配置你要安装的操作系统(不过在通过这个配置你可以安装的系统有限, 仅限于 HP 提供了阵列卡驱动的那些系统), 还带了一些系统健康状态监控的功能.

对于上述的几个可以在 Intelligence Provisoning 中配置的操作系统, MicroServer Gen8 似乎都提供了他们的安装程序, 这点比较方便, 因为在 Intelligence Provisioning 中配置好我们想安装的操作系统之后, 重启机器就回进入这个操作系统的安装界面, 然后你只需要提供操作系统的镜像就可以继续你的安装. 但是 Gen8 预置的 RHEL 操作系统安装程序却是有一个严重的不如人意的地方, 稍候我会说明这一点.

众所周知, 安装软件时, 一般来说这个软件会提供一个安装程序, 我记得 windows 下以前最火的制作安装程序的软件叫 InstallShield 不知现在还是不是最火的, 使用这个软件就可以制作出那种傻瓜化的一路下一步的软件安装程序, 而 linux 下的安装程序, 应该就得算各种包管理系统或者是./configure, make, make install 三步曲了吧. 那么安装操作系统的话, 也是需要一个操作系统安装程序的, 操作系统安装程序可谓多种多样, 甚至有些已经超越了”操作系统安装程序”这个界限, 自己直接提供了一个操作系统, 比如通过制作 Ubuntu Live CD/USB 安装盘, 你都可以直接使用在安装盘上的系统而不用把它安装到你的硬盘上 — 当然, 这似乎已经不属于”操作系统安装程序”了. 这么分吧, 一般来说, 操作系统安装程序分为两种, 带图形界面的和不带图形界面的. 不管带不带图形界面, 操作系统安装程序都会包含一些安装过程中必备的硬件驱动程序, 比如硬盘的驱动程序, 这样安装系统的时候, 操作系统安装程序才能识别出你的硬盘然后让你选择将系统安装到哪个硬盘上; 可能还有网卡驱动程序, 这样你在安装的过程中就能联网获取更新.

一般来说, 操作系统安装程序中所带的驱动已经够了, 但是现在不行, 我们想把系统安装在 raid 上, 也就是 Smart Array Controller B120i 上, 这是一种 Hardware RAID, 这就需要我们的 RHEL6 系统安装程序具备识别 Smart Array Controller B120i 的驱动, 但是不幸的是 Redhat 尚未提供这个驱动, 用在 Redhat 官网下载的系统引导镜像(系统安装程序镜像)制作成的系统安装盘里, 并不具备 Smart Array B120i 的驱动, 无论哪个版本. 幸运的是, HP 提供了适用于 RHEL6.x 各个版本的驱动程序. 这样我们就可以在安装过程中加载这个驱动程序.

对了, 我们安装的 RHEL 版本是 RHEL6.2

Ok, 下面我们就开始正式的安装过程.

1. 制作操作系统安装盘

这个参考了 redhat 官方文档, 我们制作了一个 BIOS-based 的 USB 启动盘, 注意不要用 UEFI, MicroServer Gen8 还不支持 UEFI, 我试验过, 制作了 UEFI-based 的启动盘, 不被识别, BIOS 中没有选项可以启用 UEFI, HP 官网一篇文章也说了 MicroServer Gen8 不支持 UEFI, 链接我暂时找不到了.

需要的镜像 rhel-server-6.2-x86_64-boot.iso 可以在网上下载到. 制作时需要一个 U 盘.

上面我们也说了 Gen8 预置了各种系统的安装程序, 为什么我们这里还要制作系统安装盘呢, 因为 Gen8 预置的安装程序不能进入 boot prompt 界面, 而我们后面要进入这个界面手动加载 HP 提供的 Smart Array B120i 驱动. 这就是 Gen8 预置系统安装程序不尽人意的地方.

注意, 如果安装的是 rhel6.2, 就老老实实用 rhel-server-6.2-x86_64-boot.iso 这个镜像制作安装盘, 不要用 rhel-server-6.3-x86_64-boot.iso, 我一开始因为找不到 6.2 的boot 镜像, 但是找到了 6.3 的就用了 6.3 的 boot 来安装 rhel6.2, 但是在加载 Smart Array B120i 驱动那里折腾了好久 — hpvsa-1.2.8-140.rhel6u2.x86_64.dd 无论如何都加载不上.

要用 6.3 的 boot 就只能加载 hpvsa-1.2.8-140.rhel6ur3.x86_64.dd 这个驱动, 但这是没道理的, 为何要用 6.3 的 boot 加载 6.3 的 Smart Array B120i 驱动然后安装 6.2 的 rhel? 这完全是找折腾受.

2. 系统镜像盘制作

系统镜像 rhel-server-6.2-x86_64-dvd.iso 网上可以下到, 下载之后要严格按照 redhat 官方的文档 制作系统镜像盘, 这里我们需要另一个 U 盘.

3. 下载 HP 提供的 Smart Array B120i 驱动

HP 提供的驱动在 HP 官网下载, 要下载与自己系统对应的版本, 我们这里是 hpvsa-1.2.8-140.rhel6u2.x86_64.dd, 下载之后这个驱动程序放在另一个 U 盘, 这个 U 盘不必是空的, 总之目前为止我们需要第三个 U 盘.

4. 开始安装

  1. 插入第一个 U 盘, 启动 MicroServer Gen8, 首先进入 BIOS Setup 界面, 选择 Embedded SATA Configuration -> Enabel Dynamic HP Smart Array B120i RAID Support, 然后重启
  2. 选择临时启动介质为 USB Key 启动
  3. 进入 rhel server 的安装界面, 这个界面让你选择安装方式, 让光标停留在 “Install or Upgrade exsiting system” 这一项上, 然后按下 TAB 键直接在当前界面编辑引导参数, 或者是按下 ESC 键进入 boot prompt 界面, 插入第二和第三个 U 盘(包含 HP Smart Array B120i 驱动的那个)
  4. 无论你在哪个界面, 输入 “linux dd blacklist=ahci”, linux dd 命令使得你可以附加额外的驱动给系统安装程序, blacklist=ahci 这句是为了防止安装程序加载了普通的硬盘驱动将磁盘阵列识别为多个硬盘而不是一个阵列, 个人觉得可能不加 blacklist=ahci 也能成功但最好加上
  5. 安装程序会询问你是否有包含驱动的磁盘, 选 Yes, 然后找到你刚才插入的 U 盘(不知道是那个的话就挨个打开看看), 找到里面的 hpvsa-1.2.8-140.rhel6u2.x86_64.dd, 按 Enter, 等安装完了之后, 安装程序会问你还有没有别的驱动要安装, 我们没有别的驱动要装了, 选 No.
  6. 安装程序会正式进入安装界面, 让你选则安装语言等, 在选择安装介质的时候, 选择第二个 U 盘, 继续, 会出现一个界面, 里面会显示安装程序检测到的所有连接到机器的上的存储介质, 不出意外的话, 你就会看到你的 RAID 阵列也在里面.
  7. 接下来就是常规的系统安装操作, 调整分区, 下一步, 下一步….

至此, 安装安成.

GNU Screen Tutorial

概念

  • 会话, screen 有自己的会话的概念, 当你运行 screen 命令后, 你就启动了一个 “会话”. 下文中的”会话”都是指 screen 会话.
  • 窗口, 每一个会话可以包含一至多个窗口, 在每一个窗口中, 你可以做单独的工作. 比如说在窗口1 中开着 vim 写作, 在窗口2 中运行 netstat 监控网络, 而第三个窗口保持 bash shell 不做其他的事

默认情况下, 当你运行 screen 之后, screen 将为你建立一个会话, 并且为你建立一个默认的窗口, 并且, 为你在这个窗口中运行 bash shell.

优点

使用 screen 的优点是很多的, 尤其是你需要 ssh 登录到远程主机进行工作的时候, screen 可以帮助你使你即使从远程主机退出了, 你的工作也能够继续, 这是其一, 另一点就是它可以帮你”节省终端” – 当你需要登录远程做多个工作的时候, 你不必开多个终端分别 ssh 登录远程, 然后分别做各自的工作. 这在 多个窗口 这一节会分析.

运行 screen

只需要简单的执行 screen 命令:

$ screen

一般来说, 你会看到一屏关于 screen 程序的介绍和版权信息, 根据按空格或回车就能跳过. 此时, 你便处于 screen 的”会话中”了, 之所以要加引号是因为你还可以处于会话之外, 稍候会提到.

会话之中

当你处于会话之中的时候, 你需要一种特殊的指令格式来给 screen 会话下达指令, 这种特殊的格式就是每当你想下达指令时, 你要先按一下 “Command Key”, “Command Key” 在 screen 中被默认设定为 ^A(Ctrl-A), 也就是说, 每一步操作你都要先按一下 ^A. 比如想新建一个窗口, 那么就需要按下 Ctrl-a, c.

处于会话中时, 你可以对 screen 会话下达的指令很多, 你可以通过 Ctrl-A, ? 来列出所有可以执行的指令.

多个窗口

默认情况下, 当你开启一个 screen 会话后, 会话中是只有一个窗口的, 拥有多个窗口可以为我们带来极大的方便, 尤其是在用 ssh 登录到远程主机的时候. 比如说我们需要登录到远程主机同时开着 iostat 和 netstat 来实时监控远程主机的运行状态. 我们有如下两种方式:

  • 如果不使用 screen, 我们就需要开两个终端, 运行两次 ssh 登录到远程, 然后一个终端运行 iostat, 另一个终端运行 netstat;

    这种方式无疑要麻烦许多, 你要登录两次远程, 还浪费了你本地的终端…

  • 如果我们使用 screen 但是不使用多窗口, 我们固然可以只开一个终端 ssh 到远程, 然后开一个 screen 会话, 运行 iostat, 然后 detach 这个会话(后面介绍), 再开一个 screen 会话, 运行 netstat.

    这种方式比上面那种好不了多少, 甚至更差, 因为你没法同时检测磁盘运行和网络运行状况, 你要查看磁盘运行, 就要 detach 掉运行 netstat 的那个 screen 会话, retach 到运行 iostat 的那个会话, 反之亦然.

然而当你在一个会话中使用多窗口后, 一切都变得那么优雅, 而创建新的窗口又是那么的简单, 下面我们来看一下与窗口相关的几个指令:

  • Ctrl-A, c, 创建一个新的窗口, 以这种方式创建的窗口会默认的运行 bash shell
  • Ctrl-A, w, 显示会话中的所有窗口, 会显示窗口的标号, 当前的窗口会以星号标识
  • Ctrl-A, 0/Ctrl-A, 1…, 切换到标号为 0/1 的窗口
  • Ctrl-A, ", 显示会话中的所有窗口, 并且可以通过方向键和回车键选择切换窗口
  • Ctrl-A, p/Ctrl-A, n, 切换到前一个/后一个窗口

如此一来, 我们就可以在一个窗口中运行 iostat, 然后 Ctrl-A, c 创建另一个窗口运行 netstat, 不过慢着, 以这种方式创建出来的新窗口, 其显示区域是会覆盖之前的窗口的, 这样一来, 岂不是也不能同时查看 iostatnetstat 的监控情况吗? 别急, screen 还有另一个功能.

多显示区域

我觉得 screen 最方便的地方就是支持将会话分成多个显示区域, 并且每个显示区域都可以显示任意的一个窗口.

我们可以在会话中创建两个窗口, 分别运行 iostat 和 netstat, 然后将会话分成左右两个显示区域, 每个区域显示一个窗口的内容, perfect right?

  • Ctrl-A, S, 水平分割会话中的显示区域
  • Ctrl-A, |, 垂直分割
  • Ctrl-A, TAB, 切换到下一个显示区域

分割出来的新的显示区域默认是什么都不运行也什么都不显示的, 我们可以在切换到新的显示区域后, 结合 Ctrl-A, n/Ctrl-A, p/Ctrl-A, 0/Ctrl-A, " 来让这个新的区域显示已有的窗口, 通过 Ctrl-A, c 等来在这个区域新建一个窗口(并运行 bash shell).

会话之外

脱离(detach)会话

当我们按下 Ctrl-A, d, 我们就能够从会话中脱离, 这也就使我们回到了正常的 shell 环境中.

列出存在的 screen 会话

命令 $ screen -ls 能够列出当前系统中存在的会话, 这会显示出会话的名字.

依附到(retach)会话

命令 $ screen -r [session_name] 可以是我们依附到某个存在的会话中. 我们可以先使用 screen -ls 获得当前的会话列表, 然后依附到某个会话.

退出 screen

在会话中的 shell 提示符下输入 exit 命令就可以退出会话, 不过, 如果你开了多个窗口, 那么每执行一次 exit, 只是会退出一个窗口, 直到最后一个窗口退出, 会话才会退出.

PPTP VPN 基础与搭建

我们知道 VPN 全称是虚拟专用网(Virtual Private Network), 就像很多计算机名词一样, 这个词也已经被过分使用, 而导致很多人知道它的全称却不知道它具体代表什么意思了. 在这里我们先好好复习一下 VPN 的定义.

我不喜欢肢解名字来解释一件东西, 但这里还是挺适用的, 首先从名字可以看出来, VPN 有两个特点:

专用 (Private)

之所以称为专用网, 是因为这种网络只是用于本机构的主机和本机构的主机之间进行通信. 而不是用于本机构内的主机和外界网络通信.

虚拟 (Virtual)

而之所以称之为虚拟, 则是因为实际上并没有使用专用的物理的线路来连接分散于各地的本机构的网络. 而是使用了现有的互联网的线路.

我们发现, 实际上虚拟专用网和我们接触最多的局域网很像:

  • 首先, 它们使用的都是专用地址
    • 10.0.0.0 — 10.255.255.255
    • 172.16.0.0 — 172.31.255.255
    • 192.168.0.0 — 192.168.255.255
  • 其次, 它们都能划分不同的子网

实际上, 我们平常所说的局域网也可以说成是专用网, 因为严格来讲, 局域网中的主机是不能直接和外界通信的, 只能直接和自己网络的主机进行通信. 要和外界通信, 首先要有一个公网的地址, 这要借助 NAT, 那就是间接和外界通信了.

我们来看一下不同的地方, 不同的是, 在局域网中, 不管划分多少个子网, 子网中的主机物理位置都不会里的太远, 而在虚拟专用网中就不同了, 主机的距离离的非但远, 还不是一般的远, 可能跨国跨海.

在局域网中, 不同的主机通过物理专线连接路由器然后主机之间进行交流, 这在虚拟专用网中就不太实际了, 总不能我在太平洋东岸放个路由器连几台主机, 然后拉个海底电缆到西岸连几台主机吧, 虽然理论上是可行的. 于是, 借助现有的因特网线路资源, 就造就了虚拟专用网的”虚拟”特性.

数据安全

但是, 使用了因特网的线路, 数据也就暴露给了整个因特网, 一般机构内部的通信是不希望泄漏给外界的, 所以一般 VPN 网络都有加密的要求.

GRE 协议

待续…

搭建 VPN (PPTP)

  1. 安装 PPTPD
    • RHEL/CentOS
      rpm -i http://poptop.sourceforge.net/yum/stable/rhel6/pptp-release-current.noarch.rpm
      yum -y install pptpd
      

      实际上, 我也不知道为什么 CentOS 自己的源里没有 pptp 这个软件包.

    • Debian/Ubuntu

      # apt-get install pptpd
      
  2. 稍加配置
    • /etc/pptpd.conf

      添加如下两行:

      localip 192.168.80.1
      remoteip 192.168.80.224-238,192.168.80.245
      

      这里 localip 是 pptp server 的 ip 地址, remoteip 是每一个连接到 pptp server 的客户端能够获得的地址. 这里我故意使用了两种语法来指定 remoteip, 目的是向你展示一下.

    • /etc/ppp/chap-secrets

      添加几个用户吧

      # Secrets fro authentication using CHAP
      # client    server  secret      IP address
        username1 pptpd   passwd1      *
      

      这里 client 是用户名, server 是服务类型 — 我们这里是 pptpd, secret 是密码, IP address 允许连接到此 pptp server 的 IP, * 在这里表示任何.

    • /etc/ppp/options.pptpd

      添加如下两行:

      ms-dns 8.8.8.8
      ms-dns 8.8.4.4
      

      顺便解释一下, /etc/ppp/options.pptpd 这个文件是会被 /etc/pptpd.conf 包含的, /etc/ppp/options.pptpd 里有两个比较重要的两个参数:

      # Require the peer to authenticate itself using MS-CHAPv2 [Microsoft
      # Challenge Handshake Authentication Protocol, Version 2] authentication.
      require-mschap-v2
      # Require MPPE 128-bit encryption
      # (note that MPPE requires the use of MSCHAP-V2 during authentication)
      require-mppe-128
      # }}}
      

      启用了这两个参数后, 客户端也要启用这两个参数. 其中 mppe 需要响应的内核模块的支持. 如果你发现连接失败的话, 你可能需要安装并载入响应的模块.

Okay, 现在可以这样了: /etc/init.d/pptpd restart

  1. 还没完呢

    还要让系统允许 IP forward, 编辑 /etc/sysctl.conf 这个文件, 确保有这一行:

    net.ipv4.ip_forward = 1
    

    然后, 运行 sysctl -p.

  2. 路由配置

    这是最重要的一步, 因为这一步把我绊住过. 路由配置其实也很简单, 运行下面的命令就可以:

    # iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE 
    # iptables-save
    

    但不幸的是, 很多 openvz 因为缺少内核模块, 导致第一条命令无法使用, MASQUERADE 这个 taget 不被支持(相关的解释可以看我的有关 OpenVZ 的那片日志). 运行第一条命令就返回 no target/match 之类的错误. 因此我们采取的是另一种效果基本一样的写法:

    # iptables -t nat -A POSTROUTING -s 192.168.80.0/24 -j SNAT --to-source _PUBLIC IP_
    

    这里, 192.168.80.0/24 是你为 vpn 分配的内网段, PUBLIC IP 就是你的 vpn server 的公网地址.

    另外, 你很可能还要加上这一句:

    # iptables -A FORWARD -p tcp --syn -s 192.168.80.0/24 -j TCPMSS --set-mss 1356
    

    这一句用来调整 TCP 报文的大小, 当你发现 vpn 无法正常工作时不妨加上这句试试.

客户端配置

  1. 安装 pptp 软件

    yum -y install pptp
    
  2. 加载 ppp_mppe 模块(没有的话则要先安装)

    这一步是因为服务器端启用了 mppe 加密, 如果上面配置服务器端时没有启用 mppe 加密, 可以不用加载这个模块.

    modprobe ppp_mppe
    
  3. 创建一个 /etc/ppp/peers/younameit 文件, 加入 pptp 连接参数:
    pty "pptp 198.211.104.17 --nolaunchpppd"
    name username1
    password passwd1
    remotename PPTP
    require-mppe-128
    

    其中, 198.211.104.17 为你的 pptp server 的公网 IP, name 为你在 pptp server 上配置的用户名, password 是该用户的密码, 其它参数不必修改.

  4. 拨号

    # pon younameit
    

    连接成功后, 运行 ifconfig 你就能看到一个新增的 ppp0 接口, 地址处于你在服务器上所配置的地址段(这里是 192.168.80.0/24)

  5. 修改默认路由

    # route del default
    # route add default gw 192.168.80.1
    

后记

由于数据是 128 bit 加密的, 所以与 OpenVPN 比起来 PPTP 更省 CPU, 而且你仍然可以通过额外的加密手段来使通信更安全.

Troubleshooting

  1. “Protocol not available” 的可能原因:
    • 客户端或者你的路由器没有开放 1723 端口权限.
    • GFW
  2. 依然上不了 twiiter, facebook 等网站

    这个可能是 dns 的问题.

    这个问题一般如果你在 linux 下使用 pon 连解 pptp 时会出现, pon 这样的工具不会自动设置我们提供的 dns (虽然上面我们在 pptp server 配置文件里提供的 dns), 而是仍然使用你的原始连接的运营商的 dns, 这些 dns 自然一般是不会解析 twitter 这些网站的

参考

  1. DigitalOcean 的 VPN 搭建教程: https://www.digitalocean.com/community/articles/how-to-setup-your-own-vpn-with-pptp
  2. 很经典的教程: http://www.putdispenserhere.com/pptp-vpn-setup-guide-for-a-debian-openvz-vps/
  3. Arch Linux Wiki 的教程, 我没有参考, 但是它的配置方式有些特别: https://wiki.archlinux.org/index.php/PPTP_Server
  4. 早期的 redhat 下的 pptp 教程, 具有历史意义: http://poptop.sourceforge.net/dox/redhat-howto.phtml
  5. 阐述了 Protocol not available 错误的可能原因: http://poptop.sourceforge.net/dox/gre-protocol-unavailable.phtml