第十八章 调制解调器拨号器
时间:2005-08-31 来源:mantou
18.1 引言
与调制解调器(modem)相关的程序要处理如此种类繁多的调制解调器是很困
难的。在大多数Unix系统中总有两个程序来处理调制解调器。第一个是远程登录程
序,它允许我们拨通另外的计算机、登录和使用远程系统。在系统V中这个程序叫
做cu,而BSD则称它为tip。它们完成类似的工作,而且都可以处理很多不同类型的
调制解调器。另一个使用调制解调器的程序是uucico,它是UUCP包的一部分。问题
是不同种类调制解调器的具体特性一般都包含在这些程序的内部,于是如果想写其
他使用调制解调器的程序,我们就不得不做这些程序类似的工作。同样,如果我们
想要改变这些程序,使其不通过调制解调器,而利用其它介质通信(例如网络连接
),那么也要做很大的改动。
在这一章中,我们开发了一个程序来处理调制解调器所有需要处理的细节。我
们把所有这些的细节都集中到这一个程序中,而不是分散在多个程序里。(这个程
序的构思来自于Presotto 和Ritchie [1990]所描述的连接服务器)。为了使用这
一个程序,我们必须能如15.3节所说明的那样调用它,并使它传回文件描述符。然
后,我们用这个程序来开发远程登录程序(类似cu和tip )。
18.2 历史
cu(1) 命令(意思是call unix)是在Version 7中出现的。但它只能处理一种
特殊的自动拨号单元(ACU)。伯克利的Bill Shannon修改了cu,并把它实现在4.
2BSD的tip(1)中。这之间的最大改变是使用了一个文本文件/etc/remote来存放所
有的系统信息(电话号码、优先的拨号器、波特率、奇偶校验、流控制等)。这个
版本的tip支持六种不同的拨号单元和调制解调器,但要支持其他种类的调制解调
器则要修改源码。
与cu和tip一样,UUCP系统也可以使用调制解调器和自动拨号单元。UUCP对不
同的调制解调器进行加锁,因此多个UUCP的实例可以同时运行。这样,tip和cu程
序就不得不遵循UUCP协议,避免与UUCP冲突。在BSD系统中,UUCP使用了它自己的
拨号函数。这些函数被连接到UUCP的可执行程序中,这样增加新的调制解调器也需
要修改源码。
SVR2提供了一个dial(3)函数来将调制解调器拨号的一致特性归纳到一个库函
数中。这个函数由cu使用,但UUCP不使用。这是一个标准的C库函数,所以可以被
一般程序使用。
Honey DanBer UUCP系统,是将调制解调器命令从C源程序中抽取出来,将它们
放在一个Dialers文件中。这就允许不修改源码就加入新类型的调制解调器。但是
cu和uucp所使用的访问Dialers文件的函数不是很通用。这说明cu和UUCP可以不重
新开发代码去处理Dialers文件中的拨号信息,但除了cu和UUCP以外的程序并不能
使用这个文件。
在所有的这些版本的cu、tip和UUCP中,加锁保证了在同一时间只有一个程序使用
某一设备。因为这些程序工作在不同系统中,早期的版本不提供记录锁,而使用一
个早期形式的文件加锁。这会导致当一个程序崩溃时,该锁文件仍旧保留,所以又
开发特殊的技术来处理这种情况。(对特殊设备文件不能使用记录锁,所以记录锁
也不能完全解决问题)。
18.3 程序设计
我们来分析一下调制解调器拨号器(dialer)所应该具有的特性。
1.它必须在不改动源码的情况下支持新增加的调制解调器类型。
为了达到这个目标,我们使用了Honey DanBer的Dialers文件。我们将所有使
用这个文件来拨号调制解调器的代码都放到一个精灵服务进程中,这样任何程序都
可以使用15.5节中的客户机-服务器函数来访问它。
2.一定要使用一些特定形式的锁,以保证当那些持有锁的程序在非正常结束
时能自动释放它的锁。以前那些专门的技术,如那些在大多数cu和UUCP版本中仍然
使用的技术,都不应再使用。
我们用一个服务器精灵进程来处理所有设备加锁。因为在15.5节中的客户机-
服务器函数会在客户机终止时自动通知服务器,所以这个精灵进程能释放进程所持
有的任何加锁。
3.新的程序一定要能够使用我们所开发的所有特性。开发一个新的处理调制
解调器的程序不应当什么都要自己实现,它拨任何类型的调制解调器应该就象函数
调用一样简单方便。为此,我们让中央服务器精灵进程处理所有与拨号有关的操作
,并返回一个文件描述符。
4.客户机程序,例如cu和tip,不应当需要特别权限。这些程序不应当是设置
用户ID(set_user_ID)程序。但是我们要给予服务器精灵进程特殊权限,允许客
户机程序运行时无需特权。
显然我们不能改动已有的cu、tip和UUCP程序,但应该让其它程序在我们工作
的基础上实现起来更加简单。当然,我们也一定要充分吸取已有的Unix 拨号程序
的优点。
图18. 1描述了客户机-服务器工作模式的结构。
图18. 1 客户机-服务器工作模式的示意图
建立与远程系统的通信过程如下:
0.启动服务器端进程
1. 客户端程序启动,使用cli_conn函数(15.5节)建立与服务器端程序的连接。
客户端程序向服务端发出一个请求,请求拨号远程系统。
2. 服务端程序读取Systems、Devices和Dilaers配置文件来决定如何拨号远程系
统(我们在下一部分讲述这些文件)。如果正使用一个调制解调器,在对应的Dia
lers配置文件中就包含了这个特定调制解调器的所有命令。
3. 服务端程序打开该调制解调器设备并拨号该调制解调器。这需要一些时间(一
般15-30秒)。服务端程序处理所有对该调制解调器的加锁,以避免各用户间的冲
突。
4. 如果拨号成功,服务端程序返回一个该调制解调器设备的文件描述符给客户端
。在15.3节中的函数可以发送和接受这个描述符。
5. 客户端直接与远程系统通信。服务器端不再参与这个过程。客户端读写上一步
返回的文件描述符就可以了。
客户端与服务端的通信过程(步骤1-4)是通过一个流管道进行的。当客户端
完成与远程系统的通信时,客户端关闭该流管道。服务端发现该管道关闭,释放对
调制解调器设备的加锁。
18.4 数据文件
在这一部分,我们描述Honey DanBer UUCP系统所使用的三个文件:Systems、
Devices和Dialers。在这些文件中有很多UUCP所使用的域。我们这里不详细讲述
这些域和UUCP系统本身。参考Redman [1989]可得到更详细的信息。
18.2 分栏列出了在Systems 文件中的六个域。
name time type class phone login
host1
host1
host1
modem
laser Any
Any
Any
Any
Any ACU
ACU
ACU
modem
laser 19200
9600
2400
19200
19200 5551234
5552345
5556789
-
- (not used)
(not used)
(not used)
(not used)
(not used)
图18.2 Systems 文件
name 域是远程系统的名字。例如,我们可以使用cu host1这种形式的命令。
这里要注意的是,我们可以对同一远程系统建立多个项。系统按顺序尝试拨号这些
项。在18.2中名为modem和laser的项对应于与调制解调器和激光打印机的直接连接
。我们并不需要拨号来连接这些设备,但我们对它们仍需要打开合适的终端连线,
并处理好加锁问题。
Time域指定了拨号的星期和时间。这是一个UUCP的域。Type域指定了对这个特
定的name使用Devices文件中的哪一项。Class域是指线路速率(波特率)。Phone
域指定那些type为ACU项的电话号码,而其他项的phone域是一个连字符。最后一个
域login,是一个字符串。它是在UUCP中远程登录时所使用的,我们不使用这个域
。
type line line2 class dialer
ACU
ACU
ACU
ACU
modem
laser cua0
cua0
cua0
cua0
ttya
ttyb -
-
-
-
-
- 19200
9600
2400
1200
19200
19200 tbfast
tb9600
tb2400
tb1200
direct
direct
图18.3 Devices 文件
Devices文件包含了调制解调器和那些直接连接的主机的信息。图18.3列出了
这个文件中的五个域。Type 域与Systems文件中type域对应。Class域也一定要与
systems文件中对应的class域一致,它通常指定了线路速率。
设备的实际名称是对line字段加前缀/dev/。在这个例子中,实际设备是/dev
/cua0,/dev/ttya和/dev/ttyb。另外一个域line2,没有被使用。
最后一个域dialer,与Dialers文件中对应项一致。对于直接相连的项则为di
rect。
图18.4显示了Dialers文件的格式。这个文件包含了所有调制解调器的拨号命令。
dialer sub handshake
tb9600 =w- "" dApApApTQ0S2=255S12=255s50=6s58=2s68=255 c
OK EATDTT c CONNECTs9600 c ""
tbfast =w- "" dApApApTQ0S2=255S12=255s50=255s58=2s68=255s110=1s111=
30 c
OK EATDTT c CONNECTsFAST
图18.4 Dialers文件
这里只有两项,我们没有列出Devices中的tb1200和tb2400项。
Handshake域本应该写在同一行,因为版面的限制,我们把它放在两行上。
Dialer域是与Devices文件中的行相对应。Sub域则指定了在电话号码中等于号
和减号的替代字符。在图18.4中,这个域表明了用w代替等于号,逗号代替减号。
这样就允许Systems文件中的电话号码中含有等于号(意思是等待拨号音)和减号
(意思是暂停)。在不同的调制解调器上,这两个字符的含义不同,将它们代换成
何种字符,需在Dialers文件中指定。
最后一个域handshake,包含了实际的拨号指令。它是一连串以空格分开的字
符串,称为期望-发送串。我们期望(一直读取,直到得到匹配字符串)得到第一
个字符串,然后发送(写入)第二个字符串。作为一个例子,让我们来查看tbfas
t项。这个项是用于PEP(packetized ensemble protocol) 模式的Telebit Trailb
lazer 调制解调器。
1.第一个期望字符串是空,意思是"期望空"。这总是成功的。
2.发送第二个字符串,这个字符串以d开头,d表示暂停两秒。然后发送A。再暂
停半秒(p),发送另外一个A,暂停,再发送一个A,再暂停。接着,我们发送余
下的以T开头的字符串。这些都是设置调制解调器的命令。 发送一个回车,c表
明在发送字符串结尾不要开始新行。
3.从调制解调器读取,直到得到字符串OK 。
4.下一个发送串以E开头,这允许进行回应检查:每次我们发送给调制解调器一
个字符,我们就一直读取直到有回应。然后我们发送第四个字符ATDT。下一个特殊
字符T,是指使用替代的电话号码。然后是一个回车符,然后是c是指在发送字符
串后不要开始新行。
5.最后的期望字符串是等待调制解调器返回CONNECT FAST。(s意思是单个
空格)
当收到最后的期望字符串后,拨号就完成了。(当然,在这handshake字符串
中可能出现其它更多的特殊字符序列,我们就不详细说明了。)
现在来总结一下,我们对这三个文件的操作。
1. 使用远程系统的名称,在Systems文件中找到相同name的第一项。
2. 在Devices文件中找到对应的项,其type域和class域与Systems文件中项的相
应域匹配。
3. 在Dilaler文件中找到与Devices文件dialer域对应的项。
4. 拨号。
这个过程如果失败,有两个原因:(1)对应于devices文件中line域的设备已
经被其他人所使用。(2)拨号不成功。(例如,远程系统电话占线,或者远程系
统关机不响应电话等)。第二种情况我们一般可以通过对调制解调器读写超时来确
定。(可参考练习18.10)。不管出现哪一种情况,我们都要回到拨号的第一步,
然后选择Systems文件中同一远程系统的下一项。如同我们在图18.2中看到的,一
个特定的主机可以有多个项,每个主机可以有多个电话号码(同一个电话号码也可
以对应多个设备)。
在Honey DanBer 系统中还有其他我们没有用到的文件。如Dialcodes指定了S
ystems文件中电话号码的缩写,而Sysfiles文件允许指定Systems、Devices、Dia
lers文件的替代文件。
18.5 服务器端程序设计
现在我们开始描述一下服务器端的软件。有两个因素影响服务器端软件的设计
。
1. 拨号过程可能会延续一段时间(15-30秒),所以服务器端软件一定要创建一
个子进程来处理实际的拨号。
2. 服务器端的精灵进程(父进程)一定要管理所有的加锁。
图18.5 表示了这个过程的情况。
图18.5 调制解调器拨号器的工作过程
服务器的工作过程如下:
1. 父进程在它的服务端口,接收从客户端发来的请求。如我们在15.5节中所述,
这在客户机-服务器之间生成了一个流管道。父进程就象15.6节中的open server一
样,要同时处理多个客户机。
2. 基于 客户端要联系的远程系统的名字,父进程查询Systems文件和Devices文
件找到匹配的项。父进程同时也维护一个加锁表,记录哪些设备在被使用,这样它
就不查询那些被使用的项了。
3. 如果发现匹配项,创建出子进程来进行实际的拨号。(父进程这时可以处理其
他客户端请求)。如果成功,子进程就在客户端指定的流管道上将调制解调器的文
件描述符传给客户端。(这个管道在fork时也被复制了),并调用exit(0)。如果
发生了错误(例如,电话线占线、没有响应等),子进程调用exit(1)。
4. 在子 进程结束时,会发送信号SIGCHLD通知父进程。父进程就得到子进程的结
束状态(waitpid)。
如果子进程成功,父进程就不用再做其它事情。在客户端结束使用调制解调器
之前,必须一直对调制解调器加锁。客户端指定的客户端-父进程之间的流管道就
一直打开着。这样,当客户端终止时,父进程得到通知,然后释放对设备的加锁。
如果子进程不成功,父进程就从Systems文件中尝试找下一个匹配项。如果找
到了对远程系统的另一项,父进程返回上一步,创建一个新的子进程来拨号。如果
没有找到新的匹配项,父进程调用send_err(见程序15.4)后关闭与客户端的流管
道。
与每一个客户端有一个连接使子进程在必要时能将调试输出发回给客户端。发
生问题时,客户端常常想要看到整个实际拨号过程。
18.6 服务器端软件源代码
我们的服务器端软件共有17个文件。图18.6 详细说明了父进程和子进程所使用的
文件,以及这些文件中所包含的函数。图18.7描述了不同函数的调用过程。
Source file Parent Child Functions
childdial.c
cliargs.c
client.c
ctlstr.c
debug.c
devfile.c
dialfile.c
expectstr.c
lock.c
loop.c
main.c
request.c
sendstr.c
sigchld.c
sysfile.c
ttydial.c
ttyopen.c
P
P
P
P
P
P
P
P
P
C
C
C
C
C
C
C
C child_dial
cli_args
client_allc, client_add, client_del, client_sigchld
ctl_str
DEBUG, DEBUG_NONL
dev_next, dev_rew, dev_find
dial_next, dial_rew, dev_find
expect_str, exp_read, sig_alrm
find_line, lock_set, lock_rel, is_locked
loop, cli_done, child_done
main
request
send_str
sig_chld
sys_next, sys_rew, sys_posn
tty_dial
tty_open
图18.6 服务器端源程序代码
图18.7 服务器端的函数调用过程
程序18.1是call.h 头文件,它被包含在所有这些源程序文件中。call.h 包含
几个系统头文件,定义了一些基本的常量,声明了全局变量。
_______________________________________________________________________
_______
#include <sys/types.h>
#include <errno.h>
#include <signal.h>
#include "ourhdr.h"
#define CS_CALL "/home/stevens/calld" /* well-known name */
#define CL_CALL "call"
#define MAXSYSNAME 256
#define MAXSPEEDSTR 256
#define NALLOC 10 /* #structs to alloc/realloc for
*/
/* Client structs (client.c), Lock structs (lock.c) */
#define WHITE " " /* for separating tokens
*/
#define SYSTEMS "./Systems" /* my own copies for now
*/
#define DEVICES "./Devices"
#define DIALERS "./Dialers"
/* declare global variables */
extern int clifd;
extern int debug; /* nonzero if interactive (not daemon) *
extern int Debug; /* nonzero for dialing debug output */
extern char errmsg[]; /* error message string to return to cli
nt */
extern char *speed; /* speed (actually "class") to use */
extern char *sysname; /* name of system to call */
extern uid_t uid; /* client's uid */
extern volatile sig_atomic_t chld_flag; /* when SIGCHLD occurs *
extern enum parity { NONE, EVEN, ODD } parity; /* specified by client *
/
typedef struct { /* one Client struct per connected client */
int fd; /* fd, or -1 if available */
pid_t pid; /* child pid while dialing */
uid_t uid; /* client's user ID */
int childdone; /* nonzero when SIGCHLD from dialing child recvd:
1 means exit(0), 2 means exit(1) */
long sysftell; /* next line to read in Systems file */
long foundone; /* true if we find a matching sysfile entry */
int Debug; /* option from client */
enum parity parity; /* option from client */
char speed[MAXSPEEDSTR]; /* option from client */
char sysname[MAXSYSNAME];/* option from client */
} Client;
extern Client *client; /* ptr to malloc'ed array of Client structs */
extern int client_size;/* # entries in client[] array */
/* (both manipulated by client_XXX() fun
tions) */
typedef struct { /* everything for one entry in Systems file */
char *name; /* system name */
char *time; /* (e.g., "Any") time to call (ignored) */
char *type; /* (e.g., "ACU") or system name if direct connect */
char *class; /* (e.g., "9600") speed */
char *phone; /* phone number or "-" if direct connect */
char *login; /* uucp login chat (ignored) */
} Systems;
typedef struct { /* everything for one entry in Devices file */
char *type; /* (e.g., "ACU") matched by type in Systems */
char *line; /* (e.g., "cua0") without preceding "/dev/" */
char *line2; /* (ignored) */
char *class; /* matched by class in Systems */
char *dialer; /* name of dialer in Dialers */
} Devices;
typedef struct { /* everything for one entry in Dialers file */
char *dialer; /* matched by dialer in Devices */
char *sub; /* phone number substitution string (ignored) */
char *expsend; /* expect/send chat */
} Dialers;
extern Systems systems; /* filled in by sys_next() */
extern Devices devices; /* filled in by dev_next() */
extern Dialers dialers; /* filled in by dial_next() */
/* our function prototypes */
void child_dial(Client *); /* childdial.c *
int cli_args(int, char **); /* cliar
s.c */
int client_add(int, uid_t); /* clien
.c */
void client_del(int);
void client_sigchld(pid_t, int);
void loop(void); /* loop.
*/
char *ctl_str(char); /* ctlst
.c */
int dev_find(Devices *, const Systems *); /* devfile.c */
int dev_next(Devices *);
void dev_rew(void);
int dial_find(Dialers *, const Devices *); /* dialfile.c */
int dial_next(Dialers *);
void dial_rew(void);
int expect_str(int, char *); /* expec
str.c */
int request(Client *);
* request.c */
int send_str(int, char *, char *, int); /* sendstr.c */
void sig_chld(int); /* sigch
d.c */
long sys_next(Systems *); /* sysfile.c */
void sys_posn(long);
void sys_rew(void);
int tty_open(char *, char *, enum parity, int); /* ttyopen.c */
int tty_dial(int, char *, char *, char *, char *); /* ttydial.c */
pid_t is_locked(char *); /* lock.
*/
void lock_set(char *, pid_t);
void lock_rel(pid_t);
void DEBUG(char *, ...); /* debug.c */
void DEBUG_NONL(char *, ...);
_______________________________________________________________________
_______
程序18.1 call.h 头文件
我们定义了一个Client结构,它包含了每一客户的所有信息。这是一个对程序
15.26中类似结构的扩展。在创建一个子进程为客户端拨号和子进程终止之间,我
们可以处理任意多的其他客户。这个结构同时包含了我们所需要的其他信息,如尝
试找到Systems文件中的其他项,重新拨号等。
我们同样为Systems、Devices、Dialers文件中每一项定义了一个结构。
程序18.2 是这个服务器端程序的main函数。因为这个程序一般是作为精灵进
程运行,我们提供了一个 -d 的命令行选项,允许交互式运行。
_______________________________________________________________________
_______
#include "calld.h"
#include <syslog.h>
/* define global variables */
int clifd;
int debug; /* daemon's command line flag */
int Debug; /* Debug controlled by client, not cmd line */
char errmsg[MAXLINE];
char *speed;
char *sysname;
uid_t uid;
Client *client = NULL;
int client_size;
Systems systems;
Devices devices;
Dialers dialers;
volatile sig_atomic_t chld_flag;
enum parity parity = NONE;
int
main(int argc, char *argv[])
{
int c;
log_open("calld", LOG_PID, LOG_USER);
opterr = 0; /* don't want getopt() writing to stderr */
while ( (c = getopt(argc, argv, "d")) != EOF) {
switch (c) {
case 'd': /* debug */
debug = 1;
break;
case '?':
log_quit("unrecognized option: -%c", optopt);
}
}
if (debug == 0)
daemon_init();
loop(); /* never returns */
}
_______________________________________________________________________
_______
程序18.2 main 函数
当使用了 -d 选项后,所有对log_XXX函数的调用(见附录B)都送到标准错误。
否则它们就会被syslog记录下来。
函数loop是服务器程序的主循环(程序18.3)。它使用了select 函数来复用
不同的描述符。
_______________________________________________________________________
_______
#include "calld.h"
#include <sys/time.h>
#include <errno.h>
static void cli_done(int);
static void child_done(int);
static fd_set allset; /* one bit per client conn, plus one for listenfd
*/
/* modified by loop() and cli_do
e() */
void
loop(void)
{
int i, n, maxfd, maxi, listenfd, nread;
char buf[MAXLINE];
Client *cliptr;
uid_t uid;
fd_set rset;
if (signal_intr(SIGCHLD, sig_chld) == SIG_ERR)
log_sys("signal error");
/* obtain descriptor to listen for client reques
s on */
if ( (listenfd = serv_listen(CS_CALL)) < 0)
log_sys("serv_listen error");
FD_ZERO(&allset);
FD_SET(listenfd, &allset);
maxfd = listenfd;
maxi = -1;
for ( ; ; ) {
if (chld_flag)
child_done(maxi);
rset = allset; /* rset gets modified each time around *
if ( (n = select(maxfd + 1, &rset, NULL, NULL, NULL)) < 0) {
if (errno == EINTR) {
/* caught SIGCHLD, find entry with child
one set */
child_done(maxi);
continue; /* issue the select agai
*/
} else
log_sys("select error");
}
if (FD_ISSET(listenfd, &rset)) {
/* accept new client request */
if ( (clifd = serv_accept(listenfd, &uid)) < 0)
log_sys("serv_accept error: %d", clifd);
i = client_add(clifd, uid);
FD_SET(clifd, &allset);
if (clifd > maxfd)
maxfd = clifd; /* max fd for select() */
if (i > maxi)
maxi = i; /* max index in client[]
array */
log_msg("new connection: uid %d, fd %d", uid, clifd);
continue;
}
/* Go through client[] array.
Read any client data that has arrived. */
for (cliptr = &client[0]; cliptr <= &client[maxi]; cliptr++) {
if ( (clifd = cliptr->fd) < 0)
continue;
if (FD_ISSET(clifd, &rset)) {
/* read argument buffer from cli
nt */
if ( (nread = read(clifd, buf, MAXLINE)) < 0)
log_sys("read error on fd %d", clifd);
else if (nread == 0) {
/* The client has terminated or closed t
e stream
pipe. Now we can release its device
ock. */
log_msg("closed: uid %d, fd %d",
liptr->uid, clifd);
lock_rel(cliptr->pid);
cli_done(clifd);
continue;
}
/* Data has arrived from the client. Process th
client's request. */
if (buf[nread-1] != 0) {
log_quit("request from uid %d not null t
rminated:"
" %*.*s", uid, nread, n
ead, buf);
cli_done(clifd);
continue;
}
log_msg("starting: %s, from uid %d", buf, uid);
/* Parse the arguments, set opti
ns. Since
we may need to try calling ag
in for this
client, save options in clien
[] array. */
if (buf_args(buf, cli_args) < 0)
log_quit("command line error: %s", buf);
cliptr->Debug = Debug;
cliptr->parity = parity;
strcpy(cliptr->sysname, sysname);
strcpy(cliptr->speed, (speed == NULL) ? "" : spe
d);
cliptr->childdone = 0;
cliptr->sysftell = 0;
cliptr->foundone = 0;
if (request(cliptr) < 0) {
/* system not found, or unable t
connect */
if (send_err(cliptr->fd, -1, errmsg) < 0
log_sys("send_err error");
cli_done(clifd);
continue;
}
/* At this point request() has forked a child th
t is
trying to dial the remote system. We'll find
out the child's status when it terminates. */
}
}
&n