Chinaunix首页 | 论坛 | 博客
  • 博客访问: 106444
  • 博文数量: 40
  • 博客积分: 1650
  • 博客等级: 上尉
  • 技术积分: 420
  • 用 户 组: 普通用户
  • 注册时间: 2007-07-20 13:05
文章分类
文章存档

2011年(1)

2009年(1)

2008年(1)

2007年(37)

我的朋友

分类: LINUX

2007-07-26 09:11:18

2.10 有名管道(FIFO)能做什么?
=============================

2.10.1 什么是有名管道?
-----------------------
有名管道是一个能在互不相关进程之间传送数据的特殊文件。一个或多个进程向
内写入数据,在另一端由一个进程负责读出。有名管道是在文件系统中可见的,
也就是说ls可以直接看到。(有名管道又称FIFO,也就是先入先出。)

有名管道可以将无关的进程联系起来,而无名的普通管道一般只能将父子进程联
系起来——除非你很努力地去尝试——当然也能联系两个无关进程。有名管道是
严格单向的,尽管在一些系统中无名管道是双向的。

2.10.2 我如何建立一个有名管道?
-------------------------------
在shell下交互地建立一个有名管道,你可以用mknod或mkfifo命令。在有些系统
中,mknod产生的文件可能在/etc目录下,也就是说,可能不在你的目录下出现,
所以请查看你系统中的man手册。[译者注:在Linux下,可以看一下fifo(4)]

要在程序中建立一个有名管道:

   /* 明确设置umask,因为你不知道谁会读写管道 */
   umask(0);
   if (mkfifo("test_fifo", S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP))
   {
       perror("mkfifo");
       exit(1);
   }

也可以使用mknod。[译者注:在Linux下不推荐使用mknod,因为其中有许多臭虫
在NFS下工作更要小心,能使用mkfifo就不要用mknod,因为mkfifo()是POSIX.1
标准。]

   /* 明确设置umask,因为你不知道谁会读写管道 */
   umask(0);
   if (mknod("test_fifo",
              S_IFIFO | S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP,
              0))
   {
       perror("mknod");
       exit(1);
   }

2.10.3 我如何使用一个有名管道?
-------------------------------
使用有名管道十分简单:你如同使用一个普通文件一样打开它,用read()和
write()进行操作。但对管道使用open()时可能引起阻塞,下面一些常用规律可
以参考。

   * 如果你同时用读写方式(O_RDWR)方式打开,则不会引起阻塞。
   * 如果你用只读方式(O_RDONLY)方式打开,则open()会阻塞一直到有写方打
     开管道,除非你指定了O_NONBLOCK,来保证打开成功。
   * 同样以写方式(O_WRONLY)打开也会阻塞到有读方打开管道,不同的是如果
     O_NONBLOCK被指定open()会以失败告终。

当对有名管道进行读写的时,注意点和操作普通管道和套接字时一样:当写入方
关闭连接时read()返回EOF,如果没有听众write()会得到一个SIGPIPE的信号,
对此信号进行屏蔽或忽略会引发一个EPIPE错误退出。

2.10.4 能否在NFS上使用有名管道?
--------------------------------
不能,在NFS协议中没有相关功能。(你可能可以在一个NFS文件系统中用有名管
道联系两个同在客户端的进程。)

2.10.5 能否让多个进程同时向有名管道内写入数据?
-----------------------------------------------
如果每次写入的数据少于PIPE_BUF的大小,那么就不会出现数据交叉的情况。但
由于对写入的多少没有限制,而read()操作会读取尽可能多的数据,因此你不能
知道数据到底是谁写入的。

PIPE_BUF的大小根据POSIX标准不能小于512,一些系统里在中被定
义,[译者注:Linux中不是,其值是4096。]这可以通过pathconf()或fpathconf()
对单独管道进行咨询得到。

2.10.6 有名管道的应用
---------------------
    “我如何时间服务器和多个客户端的双向交流?”

一对多的形式经常出现,只要每次客户端向服务器发出的指令小于PIPE_BUF,它
们就可以通过一个有名管道向服务器发送数据。客户端可以很容易地知道服务器
传发数据的管道名。

但问题在于,服务器不能用一个管道来和所有客户打交道。如果不止一个客户在
读一个管道,是无法确保每个客户都得到自己对应的回复。

一个办法就是每个客户在向服务器发送信息前都建立自己的读入管道,或让服务
器在得到数据后建立管道。使用客户的进程号(pid)作为管道名是一种常用的方
法。客户可以先把自己的进程号告诉服务器,然后到那个以自己进程号命名的管
道中读取回复。


3. 终端输入/输出
****************

3.1 我怎样使我的程序不回射输入?
================================

     我怎样能使我的程序不回射输入,就象登录时询问我的口令时那样?

有一个简单方法,也有一个稍微复杂点的方法:

简单方法是使用‘getpass()’函数,它几乎能在所有Unix系统上找到。它以一个
给定的字符串参数作为提示符(prompt)。它读取输入直到读到一个‘EOF’或换
行符(译者注:‘EOF’用‘^d’输入,而换行符为‘^m’或回车)然后返回一个
指向位于静态内存区包含键入字符的字符串指针。(译者注:字符串不包含换行
符)

复杂一点的方法是使用‘tcgetattr()’函数和‘tcsetattr()’函数,两个函数都使用
一个‘struct termios’结构来操纵终端。下面这两段程序应当能设置回射状态和
不回射状态。

        #include
        #include

     static struct termios stored_settings;

     void echo_off(void)
     {
         struct termios new_settings;
         tcgetattr(0,&stored_settings);
         new_settings = stored_settings;
         new_settings.c_lflag &= (~ECHO);
         tcsetattr(0,TCSANOW,&new_settings);
         return;
     }

     void echo_on(void)
     {
         tcsetattr(0,TCSANOW,&stored_settings);
         return;
     }

两段程序使用到的都是在POSIX标准定义的。

3.2 我怎样从终端读取单个字符?
==============================

     我怎样从终端读取单个字符?我的程序总是要等着用户按回车。

终端通常在标准(canonical)模式,在此模式输入总是经编辑后以行读入。你可以
设置终端为非标准(non-canonical)模式,而在此模式下你可以设置在输入传递给
你的程序前读入多少字符。你也可以设定非标准模式的计时器为0,这个计时器
根据设定的时间间隔清空你的缓冲区。这样做使你可以使用‘getc()’函数立即
获得用户的按键输入。我们使用的‘tcgetattr()’函数和‘tcsetattr()’函数都
是在POSIX中定义用来操纵‘termios’结构的。

       #include
       #include

     static struct termios stored_settings;

     void set_keypress(void)
     {
         struct termios new_settings;

         tcgetattr(0,&stored_settings);

         new_settings = stored_settings;

         /* Disable canonical mode, and set buffer size to 1 byte */
         new_settings.c_lflag &= (~ICANON);
         new_settings.c_cc[VTIME] = 0;
         new_settings.c_cc[VMIN] = 1;

         tcsetattr(0,TCSANOW,&new_settings);
         return;
     }

     void reset_keypress(void)
     {
         tcsetattr(0,TCSANOW,&stored_settings);
         return;
     }

3.3 我怎样检查是否一个键被摁下?
================================

    我怎样检查是否一个键被摁下?在DOS上我用‘kbhit()’函数,但是在UNIX
    上看来没有相同作用的函数?

如果你设定了终端为单一字符模式(参见上一个问题解答),那么(在大多数系统)
上你可以使用‘select()’函数或‘poll()’函数测试输入是否可读。

3.4 我怎样将光标在屏幕里移动?
==============================

     我怎样将光标在屏幕里移动?我想不用curses而做全屏编辑。(译者注:curses
     是一个C/C++编程工具库,它提供编程者许多函数调用,在不用关心终端类型
     的情况下操纵终端的显示)。

不开玩笑,你也许不应该想去做这个。Curses工具库知道怎样控制不同终端类型
所表现出的奇特的东西(oddities);当然termcap/terminfo数据会告诉你任何终端类型
具有的这些奇特东西,但你可能会发现正确把握所有这些奇特组合是一件艰巨的
工作。(译者注:在Linux系统上,termcap数据位于/etc/termcap,而terminfo数据位于
/usr/share/terminfo下按不同终端类型首字母存放的不同文件,目前终端类型数已逾
两千种)

但是,你坚决要把你自己搞的手忙脚乱(getting your hands dirty),那么去研究一下
‘termcap’的函数集,特别是‘tputs()’,‘tparm()’和‘tgoto()’函数。

3.5 pttys是什么?
=================

Pseudo-teletypes(pttys, ptys,或其它不同的缩写)是具有两部份的伪设备(pseudo-devices):
一部份为“主人”一边,你可以认为是一个‘用户’,另一部份是“仆人”一边,
它象一个标准的tty设备一样工作。

它们之所以存在是为了提供在程序控制下的一种模拟串行终端行为的方法。比
如,‘telnet’在远端系统使用一个伪终端;服务器的远端登录shell程序只是从“仆
人”一边的tty设备期待着得到操作行为,而在“主人”一边的伪终端由一个守护程
序控制,同时守护程序将所有数据通过网络转发。pttys也被其它程序使用,比如
‘xterm’,‘expect’,‘script’,‘screen’,‘emacs’和其它很多程序。

3.6 怎样控制一个串行口和调制解调器?
====================================

Unix系统下对于串行口的控制很大程度上受串行终端传统的使用影响。以往,
需要不同ioctls函数调用的组合和其它黑客行为才能控制一个串行设备的正确操
作,不过值得庆幸的是,POSIX在这个方面的标准化作了一些努力。

如果你使用的系统不支持‘’头文件,‘tcsetattr()’和其它相关函数,
那么你只能到其它地方去找点资料(或将你的老古董系统升级一下)。

但是不同的系统仍然有显著的区别,主要是在设备名,硬件流控制的操作,和
调制解调器的信号方面。(只要可能,尽量让设备驱动程序去做握手(handshaking)
工作,而不要试图直接操纵握手信号。)

打开和初始华串行设备的基本步骤是:

    * 调用‘open()’函数打开设备;而且可能需要使用特定标志作为参数:

    `O_NONBLOCK'
          除非使用这个标志,否则打开一个供拨入(dial-in)或由调制解调器控制的设
          备会造成‘open()’调用被阻塞直到线路接通(carrier is present)。一个非
          阻塞的打开操作给你在需要时令调制解调器控制失效的机会。(参见下面的
          CLOCAL)

    `O_NOCTTY'
          在自4.4BSD演化的系统上这个标志是多余的,但在其它系统上它控制串行
          设备是否成为会话的控制终端。在大多数情况下你可能不想获得一个控制
          终端,所以就要设置这个标志,但是也有例外情况。

   * 调用‘tcgetattr()’函数获得当前设备模式。虽然有人会经常取消(ignore)得到的
      大多数或全部初始设定,但它仍然不失为一个初始化‘struct termios’结构的
      便利方法。

   * 设置termios 结构里‘c_iflag’,‘c_oflag’,‘c_flag’,‘c_lfag’和‘c_cc’
      为合适的值。(参见下面部分。)

   * 调用‘cfsetispeed()’和‘cfsetospeed()’设定设想的波特率。很少系统允许你
     设置不同的输入和输出速度,所以普通规律是你需要设成同一个设想的值。

   * 调用‘tcsetattr()’设定设备模式。

   * 如果你是用‘O_NONBLOCK’标志打开的端口,你可能希望调用‘fcntl()’
     函数将‘O_NONBLOCK’标志重新设置成关闭。因为不同系统看来对是否
     由非阻塞打开的端口对今后的‘read()’调用造成影响有不同的处理;所以
     最好显式地设置好。

一旦你打开并设置了端口,你可以正常地调用‘read()’函数和‘write()’函数。
注意到‘read()’函数的行为将受到你调用‘tcsetattr()’函数时的标志参数设定
的控制。

‘tcflush()’,‘tcdrain()’,‘tcsendbreak()’和‘tcflow()’是其它一些你应当
注意的函数。

当你使用完端口想要关闭时,要注意一些系统上特别恶心的小危险;如果有任何
输出数据等待被写到设备(比如输出流被硬件或软件握手而暂停),你的进程将因
为‘close()’函数调用而被挂起(hang)直到输出数据排空,而且这时的进程是*不
可杀的*(unkillably)。所以调用‘tcflush()’函数丢弃待发数据可能是个明智之举。

(在我的经验中,在tty设备上被阻塞的输出数据是造成不可杀进程最普通的原因。)

3.6.1 串行设备和类型
--------------------

不同系统用于串行端口设备的设备名大相径庭。以下是不同系统的一些例子:

   * ‘/dev/tty[0-9][a-z]’作为直接访问设备,而
     ‘/dev/tty[0-9][A-Z]’ 作为调制解调器控制设备(比如SCO Unix)

   * ‘/dev/cua[0-9]p[0-9]’作为直接访问设备,‘/dev/cul[0-9]p[0-9]’作为拨出设
      备,而‘/dev/ttyd[0-9]p[0-9]’作为拨入设备(比如HP-UX)

   * ‘/dev/cua[a-z][0-9]’作为拨出设备而‘/dev/tty[a-z][0-9]’作为拨入设备(比如
      FreeBSD)

是否正确地同所用设备名交互,并在任何硬件握手信号线上产生相应的效果是
与系统,配置和硬件有关的,但是差不多总是遵循下面这些规则(假设硬件是
RS-232 DTE):

   - 对于任何设备的一个成功打开操作应当设置DTR和RTS

   - 一个对于由调制解调器控制或供拨入的设备的阻塞打开操作将等待DCD(并且
     可能DSR和/或CTS也需要)被设置,通常在设置DTR/RTS之后。

   - 如果一个对于拨出设备的打开操作正巧赶上一个对于相应拨入设备的打开操作
     因为等待线路接通而阻塞,那么打开拨出的操作*也许*造成打开拨入的操作完
     成,但*也许也不*造成。一些系统为拨入和拨出端口实现一个简单的共享方案,
     当拨出端口在使用时,拨入端口被有效地设置成睡眠状态(“put to sleep”);其
     它系统不这样做,在这种系统上为避免竞争(contention)问题,需要外部的协助才
     能使拨入和拨出共享端口(比如UUCP锁定文件的使用)。

3.6.2 设置termios的标志位
-------------------------

这里是对你在使用你自己打开的串行设备时设置termios标志的一些提示(即与你使
用已存在的控制tty相反)

3.6.2.1 c_iflag
...............

你也许希望将*所有*‘c_iflag’的标志位设成0,除非你希望使用软件流控制(ick),
在这种情况下你设置‘IXON’和‘IXOFF’。(译者注:有三个标志控制流控制:
IXON,IXOFF ,和IXANY,如果IXON被设置,那么tty输入队列的软件流控制
被设置。当程序无法跟上输入队列的速度,tty传输一个STOP字符,而当输入队
列差不多空时发送START字符。如果IXON被设置,那么tty输出队列的软件流控
制被设置。当tty所连接的设备跟不上输出速度,tty将阻塞程序对tty的写操作。如果
IXANY被设置,那么一旦tty从设备收到任何字符,被暂定的输出将继续 - 译自SCO
Unix 网上文档,“TTY flow
control”章节,第五,六段)

3.6.2.2 c_oflag
...............

大部分‘c_oflag’的标志位是为了能使对于慢终端的输出可以正常工作所做的这
样或那样的黑客行为,由此,一些较新的系统认为几乎所有这些标志位已经过
时从而摈弃了它们(特别是所有血淋淋(gory)的输出排列对齐(output-padding)选项)。
如同‘c_iflag’,将它设置成全0对于大部分应用程序来说是合理的。

3.6.2.3 c_cflag
...............

当设置字符的大小时,记住首先使用‘CSIZE’屏蔽,比如设置8位字符,需要:

         attr.c_cflag &= ~CSIZE;
         attr.c_cflag |= CS8;

在‘c_cflag’里的其它你有可能需要设置为*真*的标志包括‘CREAD’和
‘HUPCL’。

如果你需要产生偶校验,那么设置‘PARENB’并清除‘PARODD’;如果你
需要产生奇校验,那么同时设置‘PARENB’和‘PARODD’。如果你根本不
想设置校验,那么确认清除‘PARENB’。

清除‘CSTOPB’ ,除非你真需要产生两个停止位。

设置硬件流控制的标志可能也能在‘c_cflag’中找到,但它们不是被标准化的(
这是一个遗憾)

3.6.2.4 c_lflag
...............

大部分应用程序可能需要关闭‘ICANON’(标准状态,即基于行的,并进行输
入处理),‘ECHO’和‘ISIG’。

‘IEXTEN’是个更复杂的问题。如果你不把它关闭,具体实现允许你作一些非
标准的事情(比如在‘c_cc’中定义增加的控制字符)从而可能导致不可预料的接
果,但是在一些系统上,你可能需要保持‘IEXTEN’标志为真以得到一些有用
的特征,比如硬件流控制。

3.6.2.5 c_cc
............

这是一个包括输入中带有特殊含义字符的数组。这些字符被命名为‘VINTR’,
‘VSTOP’等等;这些名字是这个数组的索引。

(这些字符中的两个其实根本不是字符,而是当‘ICANON’被关闭时对于
‘read()’函数行为的控制;它们是‘VMIN’和‘VTIME’。)

这些索引名字经常被提及的方式会让人以为它们是实在的变量,比如“设置
VMIN为1” 其实意味着“设置c_cc[VMIN]为1”。这种简写是有用的并且只是
偶尔引起误会。

‘c_cc’的很多变量位置只有当其它标志被设定时才会用到。

只有‘ICANON’被设置,才用到以下变量:
     ‘VEOF’,‘VEOL’,‘VERASE’,‘VKILL’(如果定义了而且
     ‘IEXTEN’被设定,那么‘VEOL2’,‘VSTATUS’和‘VWERASE’
        也用到)

只有‘ISIG’被设置,才用到以下变量:
     ‘VINTR’,‘VQUIT’,‘VSUSP’(如果定义了而且‘IEXTEN’被设定,
       那么‘VDSUSP’也用到)

只有‘IXON’或‘IXOFF’被设置,才用到以下变量:
     ‘VSTOP’,‘VSTART’

只有‘ICANON’被取消,才用到以下变量:
     ‘VMIN’,‘VTIME’

不同系统实现会定义增加的‘c_cc’变量。谨慎的做法是在设定你希望使用的值
以前,使用‘_POSIX_VDISABLE’初始化这些变量(常量‘NCCS’提供这个数
组的大小)

‘VMIN’和‘VTIME’(根据不同的实现方法,它们有可能和‘VEOF’和‘VEOL’
分享相同两个变量)具有以下含义。‘VTIME’的值(如果不为0)总是被解释为以十
分之一秒为单位的计时器)(译者注:VTIME变量是一个字节长,所以1表示0.1秒,
最大为255,表示25.5秒)

`c_cc[VMIN] > 0, c_cc[VTIME] > 0'
     只要输入已经有VMIN字节长,或者输入至少有一个字符而在读取最后一个字
     符之前VTIME已经过期,或者被信号中断,‘read()’将返回。

`c_cc[VMIN] > 0, c_cc[VTIME] == 0'
     只要输入已经有VMIN字节长,或者被信号中断,‘read()’将返回。否则,将
     无限等待下去。

`c_cc[VMIN] == 0, c_cc[VTIME] > 0'
     只要有输入‘read()’就返回;如果VTIME过期却没有数据,它会返回没有读
     到字符。(这和调制解调器挂断时的文件结束标志有一点冲突;使用1作为VMIN,
     调用‘alarm()’或‘select()’函数并给定超时参数可以避免这个问题。)

`c_cc[VMIN] == 0, c_cc[VTIME] == 0'
      ‘read()’总是立刻返回;如果没有数据则返回没有读到字符。(与上面的问题
        相同)

4. 系统信息
***********

4.1 怎样知道我的系统有多少存储器容量?
=====================================

这是另一个‘经常未回答的问题’。在多数情况下,你不该试图去找到答案.

如果你必需得到答案,问题的答案通常是有的,但非常依赖于不同的操作系统。
例如,在Solaris中,可以用 `sysconf(_SC_PHYS_PAGES)' 和 `sysconf(_SC_PAGESIZE)';
在FreeBSD中,可以用`sysctl()'; 在Linux中可以通过读取并处理`/proc/meminfo'得到
(使用该文件时需小心你的程序,它要接受历史上任何不同合法格式). 其它的操作
系统有各自的方式,我也没有意识到更多可移植的方法。

在HP-UX(9版和10版)中,可以使用如下的代码:

     struct pst_static pst;

     if (pstat_getstatic(&pst, sizeof(pst), (size_t) 1, 0) != -1)
     {
         printf(" Page Size: %lu\n", pst.page_size);
         printf("Phys Pages: %lu\n", pst.physical_memory);
     }

4.2 我怎样检查一个用户的口令?
=============================

4.2.1 我怎样得到一个用户的口令?
-------------------------------

在多数的UNIX系统中, 用户口令通常存放在`/etc/passwd'文件中.  该文件一般是
这种格式:

用户名:口令:用户号:用户组号:注释:用户目录:登录shell
(username:password:uid:gid:gecos field:home directory:login shell)

但是这些标准随着时间而不断改变, 现在的用户信息可能存放在其它机器上, 或
者说并不存放在 `/etc/passwd' 文件中。 当今系统实现也使用 `shadow' 文件保存用
户口令以及一些敏感信息. 该文件只允许有特定权限的用户读取.

为安全考虑,用户口令一般是加密的,而不是用明文表示的。

POSIX 定义了一组访问用户信息的函数. 取得一个用户信息的最快方式是使用`getpwnam()'
和`getpwuid()' 函数. 这两个函数都返回一个结构指针, 该结构包含了用户的所有信
息. `getpwnam()' 接收用户名字符串(username), `getpwuid()' 接收用户号(uid),
(`uid_t'类型在POSIX中有定义). 若调用失败则返回NULL.

但是, 正如前面所讲, 当今的操作系统都有一个shadow文件存放敏感信息,即用户口令。
有些系统当调用者用户号是超级用户时返回用户口令, 其它用户要求你使用其它方式存取
shadow文件. 这时你可以使用`getspnam()', 通过输入用户名得到一个有关用户信息的结
构. 再者, 为了能够成功的完成这些, 你需要有一定的权限. (在一些系统中, 如HP-UX和
SCO, 你可以用`getprpwnam()'代替。)

4.2.2 我怎样通过用户号得到阴影口令文件中的口令?
-----------------------------------------------

      我的系统使用一组getsp*函数获得重要用户信息的. 然而, 我没有`getspuid()',
      只有`getspnam()'. 我怎样做才能通过用户号获得用户信息呢?

变通方法是相对非常容易的。下面的函数可以直接放入你个人的应用函数库:

       #include
       #include

     struct spwd *getspuid(uid_t pw_uid)
     {
       struct spwd *shadow;
       struct passwd *ppasswd;

       if( ((ppasswd = getpwuid(pw_uid)) == NULL)
           || ((shadow = getspnam(ppasswd->pw_name)) == NULL))
         return NULL;

       return shadow;
     }

现在的问题是, 有些系统在阴影文件中并不保存用户号(uid)以及其它的信息。

4.2.3 我怎样核对一个用户的口令?
-------------------------------

一个基本的问题是, 存在各种各样的认证系统, 所以口令也就并不总是象它们看上去
那样。 在传统的系统中, 使用UNIX风格的加密算法,加密算法是不同的,有些系统使
用DES(译者注:DES:Data Encryption Standard,为NIST[National Institute of
Standard and Technology]确认的标准加密算法,最新消息表明,NIST将采用一种新
的加密标准Rijndael逐步取代DES)算法,其它的系统, 如FreeBSD国际版使用MD5(译者
注:MD5是当今最为广泛使用的单项散列算法,由Ron Rivest发明,详细资料参见RFC 1321
算法。

最常用的方法是使用一种单项加密算法(译者注:即单项散列[Hash]算法)。输入的
明文口令被加密,然后与文件中存放的加密口令比较。怎样加密的详细信息可以
查看`crypt()'的手册页, 这里有一个通常做法的版本:

     /*  输入明文口令和加密口令, 检查是否匹配,
      *  成功返回 1, 其它情况返回 0。
     */

     int check_pass(const char *plainpw, const char *cryptpw)
     {
         return strcmp(crypt(plainpw,cryptpw), cryptpw) == 0;
     }

这个函数之所以能工作是因为加密函数使用的添加(salt)字串存放在加密口令字串的前部。

*警告:* 在一些系统中, 口令加密是使用一种‘crypt()’的变体,即‘bigcrypt()’函数。


5. 编程杂技
***********

5.1 我怎样使用通配字符比较字符串?
==================================

对于它的回答依赖于你所谓‘通配字符’一词的确切含义。

有两种很不相同的概念被认定为‘通配字符’。它们是:

*文件名通配模式*(filename patterns)
     这是shell用来进行文件名匹配替换的(expansion)(或称为‘globbing’)

*正则表达式*
     这是供编辑器用的,比如‘grep’,等等。它是用来匹配正文,而它们正常
     情况下不应用于文件名。

5.1.1 我怎样使用文件名通配模式比较字符串?
------------------------------------------

除非你不走运,你的系统应该有函数‘fnmatch()’供你进行文件名匹配。它一
般只允许Bourne Shell风格的模式。它识别‘*’,‘[...]’和‘?’,但可能不
支持在Korn和Bourne-Again shell程序下才有的更神秘(arcane)的模式。

如果你没有这个函数,那么比闭门造车更好的方法是你可以从BSD或GNU原程
序那里去抄(snarfing)一个过来。

而且,在普通的匹配实际文件名情况下,查阅‘glob()’函数,它将搜索到匹配
一个给定模式的所有存在文件。

5.1.2 我怎样使用正则表达式比较字符串?
--------------------------------------

有很多稍有句法不同的正则表达式;大部分系统起码使用两种:一种是‘ed’
程序可以识别的,有时候被记作‘基本正则表达式’,另一种是‘egrep’程序
可以识别的,记作‘扩充正则表达式’。Perl(译者注:Perl: Practical Extract and
Report Language,实用析取与报表语言)语言拥有它自己稍有不同的风格,Emacs
也是。

为了支持这么多格式,相应的有很多实现。系统一般有正则表达式匹配函数(通
常为‘regcomp()’函数和‘regexec()’函数)提供,但是要小心使用;有些系统
有超过一种实现可用,附之以不同的接口。另外,还有很多可用的软件库的实
现(顺便说一下,一般都是将一个正则表达式编译成内部形式然后再使用,因为
总是假设你有很多字符串要比较同一正则表达式。)

一个可用的软件库是‘rx’软件库,从GNU的镜像站点可以得到。它看来是正在
开发中,基于你不同的观点这是一件又好又不好的事情 :-)

5.2 什么是在程序中发送电子邮件的最好方法?
==========================================

有好几种从Unix程序发电子邮件的方法。根据不同情况最好的选择有所不同,
所以我将提供两个方法。还有第三种方法,这里没有说道,是连接本地主机的SMTP
(译者注:SMTP:Simple Mail Transfer Protocol简单邮件传输协议)端口并直接使
用SMTP协议,参见RFC 821(译者注:RFC:Request For Comments)。

5.2.1 简单方法:/bin/mail
-------------------------

对于简单应用,执行‘mail’程序已经是足够了(通常是‘/bin/mail’,但一些系统
上有可能是‘/usr/bin/mail’)。

*警告:*UCB Mail程序的一些版本甚至在非交互模式下也会执行在消息体(message
body)中以‘~!’或‘~|’打头的行所表示的命令。这可能有安全上的风险。

象这样执行:‘mail -s '标题' 收件人地址 ...’,程序将把标准输入作为消息体,
并提供缺省得消息头(其中包括已设定的标题),然后传递整个消息给‘sendmail’
进行投递。

这个范例程序在本地主机上发送一封测试消息给‘root’:

     #include

     #define MAILPROG "/bin/mail"

     int main()
     {
         FILE *mail = popen(MAILPROG " -s 'Test Message' root", "w");
         if (!mail)
         {
             perror("popen");
             exit(1);
         }

         fprintf(mail, "This is a test.\n");

         if (pclose(mail))
         {
             fprintf(stderr, "mail failed!\n");
             exit(1);
         }
     }

如果要发送的正文已经保存在一个文件中,那么可以这样做:

         system(MAILPROG " -s 'file contents' root

这个方法可以扩展到更复杂的情况,但是得当心很多潜在的危险(pitfalls):

   * 如果使用system()或popen(),你必须非常当心将参数括起来从而保护它们不被
     错误的进行文件名匹配替换或单词分割。

   * 基于用户设置数据来构造命令行是缓冲区越界错误和其它安全漏洞的普遍原
     因。

   * 这种方法不允许设定CC:(译者注:CC:Carbon Copy 抄送)或 BCC:(译者注:
     BCC:Blind Carbon Copy:盲送,指投递地址不在消息中出现的抄送)的收件人。
     (一些/bin/mail程序的版本允许,其它则不允许)

5.2.2 直接启动邮件传输代理(译者注:MTA: mail transfer agent):/usr/bin/sendmail
-------------------------------------------------------------------------------

‘mail’程序是“邮件用户代理”(Mail User Agent)的一个例子,它旨在供用户
执行以收发电子邮件,但它并不负责实际的传输。一个用来传输邮件的程序被
称为“邮件传输代理”(MTA),而在Unix系统普遍能找到的邮件传输代理被称为
‘sendmail’。也有其它在使用的邮件传输代理,比如‘MMDF’,但这些程序
通常包括一个程序来模拟‘sendmail’的普通做法。

历史上,‘sendmail’通常在‘/usr/lib’里找到,但现在的趋势是将应用库程序从
‘/usr/lib’挪出,并挪入比如‘/usr/sbin’或‘/usr/libexec’等目录。结果是,一般
总是以绝对路径启动‘sendmail’程序,而路径是由系统决定的。

为了了解‘sendmail’程序怎样工作,通常需要了解一下“信封”(envelope)的概
念。这非常类似书面信件;信封上定义这个消息投递给谁,并注明由谁发出(
为了报告错误的目的)。在信封中包含的是“消息头”和“消息体”,之间由一个
空行隔开。消息头的格式主要在RFC 822中提供;并且参见MIME 的RFC文档。(
译者注:MIME的文档包括:RFC1521,RFC1652)

有两种主要的方法使用‘sendmail’程序以生成一个消息:要么信封的收件人能
被显式的提供,要么‘sendmail’程序可被指示从消息头中推理出它们。两种方
法都有优缺点。

5.2.2.1 显式提供信封内容
.........................

消息的信封内容可在命令行上简单的设定。它的缺点在于邮件地址可能包含的
字符会造成‘system()’和‘popen() ’程序可观的以外出错(grief),比如单引号,
被括起的字符串等等。传递这些指令给shell程序并成功解释可以预见潜在的危
险。(可以将命令中任何一个单引号替换成单引号、反斜杠、单引号、单引号的
顺序组合,然后再将整个地址括上单引号。可怕,呃?)

以上的一些不愉快可以通过避开使用‘system()’或‘popen()’函数并求助于‘
fork()’和‘exec()’函数而避免。这有时不管怎样也是需要的;比如,用户
自定义的对于SIGCHLD信号的处理函数通常会打断‘pclose()’函数从而影响到
或大或小的范围。

这里是一个范例程序:

     #include
     #include
     #include
     #include
     #include
     #include

     /* #include 如果你有的话 */

     #ifndef _PATH_SENDMAIL
     #define _PATH_SENDMAIL "/usr/lib/sendmail"
     #endif

     /* -oi 意味着 "不要视‘ .’为消息的终止符"
      * 删除这个选项 ,"--" 如果使用sendmail 8版以前的版本 (并希望没有人
      * 曾经使用过一个以减号开头的收件人地址)
      * 你也许希望加 -oem (report errors by mail,以邮件方式报告错误)
      */

     #define SENDMAIL_OPTS "-oi","--"

     /* 下面是一个返回数组中的成员数的宏 */

     #define countof(a) ((sizeof(a))/sizeof((a)[0]))

     /* 发送由FD所包含以读操作打开的文件之内容至设定的收件人;前提是这
      * 个文件中包含RFC822定义的消息头和消息体,收件人列表由NULL指针
      * 标志结束;如果发现错误则返回-1,否则返回sendmail的返回值(它使用
      * 中提供的有意义的返回代码)
      */

     int send_message(int fd, const char **recipients)
     {
         static const char *argv_init[] = { _PATH_SENDMAIL, SENDMAIL_OPTS
                                     };
         const char **argvec = NULL;
         int num_recip = 0;
         pid_t pid;
         int rc;
         int status;

         /* 计算收件人数目 */

         while (recipients[num_recip])
             ++num_recip;

         if (!num_recip)
             return 0;    /* 视无收件人为成功 */

         /* 分配空间给参数矢量 */

         argvec = malloc((sizeof char*) * (num_recip+countof(argv_init)+1));
         if (!argvec)
             return -1;

         /* 初始化参数矢量 */

         memcpy(argvec, argv_init, sizeof(argv_init));
         memcpy(argvec+countof(argv_init),
                recipients, num_recip*sizeof(char*));
         argvec[num_recip + countof(argv_init)] = NULL;

         /* 需要在此增加一些信号阻塞 */

         /* 产生子进程 */

         switch (pid = fork())
         {
         case 0:   /* 子进程 */

             /* 建立管道 */
             if (fd != STDIN_FILENO)
                 dup2(fd, STDIN_FILENO);

             /* 其它地方已定义 -- 关闭所有>=参数的文件描述符对应的参数 */
             closeall(3);

             /* 发送: */
             execv(_PATH_SENDMAIL, argvec);
             _exit(EX_OSFILE);

         default:  /* 父进程 */

             free(argvec);
             rc = waitpid(pid, &status, 0);
             if (rc < 0)
                 return -1;
             if (WIFEXITED(status))
                 return WEXITSTATUS(status);
             return -1;

         case -1:  /* 错误 */
             free(argvec);
             return -1;
         }
     }

5.2.2.2 允许sendmail程序推理出收件人
.....................................

‘sendmail’的‘-t’选项指令‘sendmail’程序处理消息的头信息,并使用所有
包含收件人(即:‘To:’,‘Cc:’和‘Bcc:’)的头信息建立收件人列表。它的优
点在于简化了‘sendmail’的命令行,但也使得设置在消息头信息中所列以外的
收件人成为不可能。(这通常不是一个问题)

作为一个范例,以下这个程序将标准输入作为一个文件以MIME附件方式发送给
设定的收件人。为简洁起见略去了一些错误检查。这个程序需要调用‘metamail’
分发程序包的‘mimecode’程序。

     #include
     #include
     #include

     /* #include 如果你有的话 */

     #ifndef _PATH_SENDMAIL
     #define _PATH_SENDMAIL "/usr/lib/sendmail"
     #endif

     #define SENDMAIL_OPTS "-oi"
     #define countof(a) ((sizeof(a))/sizeof((a)[0]))

     char tfilename[L_tmpnam];
     char command[128+L_tmpnam];

     void cleanup(void)
     {
         unlink(tfilename);
     }

     int main(int argc, char **argv)
     {
         FILE *msg;
         int i;

         if (argc < 2)
         {
             fprintf(stderr, "usage: %s recipients...\n", argv[0]);
             exit(2);
         }

         if (tmpnam(tfilename) == NULL
             || (msg = fopen(tfilename,"w")) == NULL)
             exit(2);

         atexit(cleanup);

         fclose(msg);
         msg = fopen(tfilename,"a");
         if (!msg)
             exit(2);

         /* 建立收件人列表 */

         fprintf(msg, "To: %s", argv[1]);
         for (i = 2; i < argc; i++)
             fprintf(msg, ",\n\t%s", argv[i]);
         fputc('\n',msg);

         /* 标题 */

         fprintf(msg, "Subject: file sent by mail\n");

         /* sendmail程序会自动添加 From:, Date:, Message-ID: 等消息头信息 */

         /* MIME的处理 */

         fprintf(msg, "MIME-Version: 1.0\n");
         fprintf(msg, "Content-Type: application/octet-stream\n");
         fprintf(msg, "Content-Transfer-Encoding: base64\n");

         /* 消息头结束,加一个空行 */

         fputc('\n',msg);
         fclose(msg);

         /* 执行编码程序 */

         sprintf(command, "mimencode -b >>%s", tfilename);
         if (system(command))
             exit(1);

         /* 执行信使程序 */

         sprintf(command, "%s %s -t <%s",
                 _PATH_SENDMAIL, SENDMAIL_OPTS, tfilename);
         if (system(command))
             exit(1);

         return 0;
     }

    转自

阅读(966) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~