- 论坛徽章:
- 0
|
② 单进程方法
这种方法只需要一个额外的进程就可以完成输入输出两项功能,更加有效,但是比较
难于理解。下面举例的是,一个进程同时在两个终端上运行的情况。
原理主要是利用同步I/O多路转换函数select来实现对两个终端以及管道的检查, 判
断哪个端口有数据可读就读数据,这里选择timeout时间值为0,表示无限制地堵塞,直到
有数据可读。然后在用FD_ISSET来判断具体是哪个端口上有数据可读。
有的程序在运行时调用ttyname(0)来显示程序的标准输入定向在哪个终端上,为了正
确显示终端名,可以将输入输出直接重定向在fd1和fd2上,比如dup2(fd1,0), 但这样程
序的输入输出就无法截取,所以在其中要通过管道来实现记录。
一般程序在开始运行时总是先显示一些内容(因为此时程序的标准输出是p2[1],显示
实际上是将这些内容通过p2[1]写入管道p2中),然后用户才敲击键盘输入数据。所以一开
始总是p2[0]最新有数据,于是就被父进程读出,再写到终端文件fd2和fd4中, 这样这些
内容就同时显示在tty09和tty10上。管道中的内容一经读出就不再存在了,这是管道的特
点。如果不从fd3中读数据,则tty10不参与操作,只能实现“转播”的效果。运行方法也
是 ttp1 &
/* 程序:ttp1.c */ │ ioctl(fd1,TCSETA,&t_attr);
#include <stdio.h> │ ioctl(fd3,TCSETA,&t_attr);
#include <stdlib.h> │ FD_ZERO(&rrset);
#include <unistd.h> │ FD_SET(p2[0],&rrset);
#include <fcntl.h> │ FD_SET(fd1,&rrset);
#include <signal.h> │ FD_SET(fd3,&rrset);
#include <termio.h> │ while(1){
int exitprog(); │ rset=rrset;
int exitprog(){ │ if(select(FD_SETSIZE,&rset,0,0,0)==-1)
exit(0); │ continue;
} │ if(FD_ISSET(p2[0],&rset)){
main(){ │ n=read(p2[0],buf,sizeof(buf));
int fdw,fd1,fd2,fd3,fd4,n; │ buf[n]='\0';
int p1[2],p2[2];char buf[2001]; │ if(n>0) write(fd2,buf,strlen(buf));
struct termio t_attr; │ if(n>0) write(fd4,buf,strlen(buf));
fd_set rset,rrset; │ if(n>0){ /* 记录屏幕输出 */
setpgrp(); │ write(fdw,"o=",2);
signal(SIGCLD,exitprog); │ write(fdw,buf,n);
pipe(p1); │ write(fdw,"\n",2);
pipe(p2); │ }
fd1=open("/dev/tty09",O_RDONLY); │ }
fd3=open("/dev/tty10",O_RDONLY); │ if(FD_ISSET(fd1,&rset)){
fd2=open("/dev/tty09",O_WRONLY); │ n=read(fd1,buf,sizeof(buf));
fd4=open("/dev/tty10",O_WRONLY); │ buf[n]='\0';
switch(fork()){ │ if(n>0) write(p1[1],buf,strlen(buf));
case -1:exit(1); │ if(n!=0){ /* 记录键盘输入 */
case 0 : │ write(fdw,"i=",2);
setsid(); │ write(fdw,buf,n);
dup2(p1[0],0); │ write(fdw,"\n",2);
dup2(p2[1],1); │ }
//dup2(p2[1],2); │ }
close(p1[0]);close(p1[1]); │ if(FD_ISSET(fd3,&rset)){
close(p2[0]);close(p2[1]); │ n=read(fd3,buf,sizeof(buf));
execl("/usr/tese/bin/tese", │ buf[n]='\0';
"/usr/tese/bin/tese",0); │ if(n>0) write(p1[1],buf,strlen(buf));
} │ if(n!=0){ /* 记录键盘输入 */
close(p1[0]); │ write(fdw,"i3=",3);
close(p2[1]); │ write(fdw,buf,n);
fdw=open("./abcerr", │ write(fdw,"\n",2);
O_CREAT|O_RDWR|O_TRUNC,0660); │ }
ioctl(fd1,TCGETA,&t_attr); │ }
t_attr.c_cc[VMIN]=1; │ }
t_attr.c_cc[VTIME]=0; │}
如果业务进程在运行后也生成子进程也将是同样有效的,因为生成的子进程也会继承
父进程的标准输入和输出。如在银行综合业务系统中,在启动终端后先运行tese进程,
这个进程又生成进程tese_nase,这个新进程还会再生成一个子进程tese_nase,实验证明
在这种情况下,上述程序一样有效,但在SCO OpenServer 5.05的ksh界面下运行时要去掉
dup2(p2[1],2)这条语句,否则会造成当前终端不正常。上面的双进程方法在ksh界面下无
法正常运行tese程序,但在普通的sh界面下可以。
当然用一个进程来同时记录在不同终端上的不同业务进程也是可能的,无非是多创建
一些子进程和管道。
3 在伪终端上记录进程的输入输出
创建伪终端时并不使用管道技术,而是使用dup2函数,将生成的子进程的标准输入输
出以及标准错误输出都转移到终端文件句柄中。
直接运行 vtp,不必在后台运行。比如在tty03上运行后,用tty命令就可发现现在
的终端已经是/dev/ttyp2,其中的字母“p”,即表示伪终端(pseude),用exit 命令或用
ctrl+d退出时,终端恢复正常。因为vtp通过调用函数ttyname(0)获得当前终端名字, 如
果vtp在后台运行,这个函数就返回空值,使得程序无法正常运行。 因为要生成的子进程
是/bin/sh解释命令,所以同样要复制标准错误输出(stderr)到终端文件中。
因为伪终端实际上并不接收用户输入,我们实际上是借用正常终端的输入,再将这些
输入的字符写到伪终端设备上,sh在伪终端上运行,由父进程将在伪终端上显示的内容写
到正常终端上。伪终端是一个以成对方式操作的软设备: 送往某对的一个成员的输出被送
给该对另一个成员的输入。 比如/dev/ptyp2和/dev/ttyp2,其中的ptyp2用来连接到真实
终端tty03,父进程打开当前终端(ttyname(0))和ptyp2,然后在这两个设备上等待读取数
据。父进程读取用户在终端tty03上输入的字符并写入设备/dev/ptyp2,而ptyp2的输出就
是ttyp2的输入,这样ttyp2就接收到用户输入的字符。 反之ttyp2的输出又是ptyp2 的输
入,这样父进程再读取ptyp2的内容,这这些内容写到真实终端上,这样ttyp2的输出就会
显示出来。由于子进程运行的是sh进程,该进程一开始显示提示字符$或#,所以父进程先
读取ptyp2的内容,这样提示字符就显示出来了。从中可以看出ptyp2起到类似双向管道的
作用,利用这个软设备沟通真实终端和伪终端。ptyp2和ttyp2之间的操作由操作系统自动
完成。在伪终端上运行的其他任何进程的输入输出也会全部被记录下来。
下面程序不但实现记录进程输入输出的功能,还能实现类似DOS下的F3键功能, 可以
通过shift+F1来重复命令。注意其中的'^['和'^M'是一个字符,UNIX下F3键是"^[[O"。
首先将当前终端的属性记录下来,用来设置ttyp2的属性, 同时对当前终端作必要的
设置,让它成为一个原始终端。 再退出时再用保存的终端属性恢复。 其中有一点需要注
意,原来sh的提示符是通过stderr输出的,所以子进程的stderr也要重定向到ttyp2上。
⑴┌─────┐ ┌─────┐ ┌─────┐
用户←→┤/dev/tty03├──→┤/dev/ptyp2├──→┤/dev/ttyp2│
│父进程vtp ├←──┤ (管道) ├←──┤ /bin/sh │
└─────┘ ⑵└─────┘ └─────┘
真实终端 伪终端
图2 伪终端的运行过程图
在另一个终端上查看进程运行情况,可以发现有一个sh进程在伪终端/dev/ttyp2上运
行,这个进程是由在tty03上运行的vtp进程生成的。我们还可以使用stty -a或stty -g来
查看终端的属性。
# ps -t03 │ # ps -tp2
PID TTY TIME COMMAND │ PID TTY TIME COMMAND
379 03 0:01 sh │ 575 p2 0:00 sh
574 03 0:00 vtp │
/* 程序: vtp.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <tinfo.h>
#include <termio.h>
#include <sys/stream.h>
#define BUFSIZE 4096
int console_fd,pty_fd;
struct termio t_attr;
int exitproc();
char pttyname[]="/dev/ptyp2";
exitproc(){
ioctl(console_fd,TCSETAF,&t_attr);
exit(0);
}
static void set_console_raw(int fd){
struct termios buf;
if(tcgetattr(fd,&buf)<0) exit(-1);
buf.c_lflag&=~(ECHO|ICANON|IEXTEN|ISIG);
buf.c_iflag&=~(BRKINT|ICRNL|INPCK|ISTRIP|IXON);
buf.c_cflag&=~(CSIZE|PARENB);
buf.c_cflag|=CS8;
buf.c_oflag&=~(OPOST);
buf.c_cc[VMIN]=1;
buf.c_cc[VTIME]=0;
if(tcsetattr(fd,TCSAFLUSH,&buf)<0) exit(-1);
}
main(){
int fdw,i,nread,slave_fd;
fd_set rset,rrset;
static unsigned char buf[BUFSIZE],cmd[255],buf1[255];
signal(SIGTERM,exitproc);/* kill -15 vtp_pid */
signal(SIGKILL,exitproc);/* kill -9 vtp_pid 无效,不能被捕捉或忽略 */
signal(SIGCLD,exitproc); /* kill -9 pty's sh_pid */
/* SIGCLD,在子进程(伪终端)退出后,exitproc会处理好返回到正常的终端 */
console_fd=open(ttyname(0),O_RDWR);
ioctl(console_fd,TCGETA,&t_attr);
set_console_raw(console_fd);
pty_fd=open(pttyname,O_RDWR);
fdw=open("./abcerr",O_CREAT|O_RDWR|O_TRUNC,0660);
switch(fork()){
case -1: exit(1);
case 0 :
setsid(); /* execlp进程在ttyp2上运行 */
slave_fd=open("/dev/ttyp2",O_RDWR);
if(slave_fd==-1){
printf("打开%s出错,可能与它的执行权限有关!\n",pttyname);
exit(-1);
}
ioctl(slave_fd,TCSETAF,&t_attr);
dup2(slave_fd,0);
dup2(slave_fd,1);
dup2(slave_fd,2); /* 少了这一行,不会显示$或#提示符 */
close(slave_fd);
execlp("/bin/sh","/bin/sh",NULL);
}
FD_ZERO(&rrset);
FD_SET(console_fd,&rrset);
FD_SET(pty_fd,&rrset);
i=0;
while(1){
rset=rrset;
if(select(FD_SETSIZE,&rset,0,0,0)==-1) continue;
if(FD_ISSET(console_fd,&rset)){
nread=read(console_fd,buf,BUFSIZE);
buf[nread]='\0';
if(strcmp(buf,"^[[Y")==0){ /* shift+F1 */
strcpy(buf,cmd);
i=strlen(cmd);
if(nread>0) write(pty_fd,buf,strlen(buf));
continue;
}
if(nread>0) write(pty_fd,buf,strlen(buf));
/* 记录输入 */
if(nread>0){
write(fdw,"i=",2);
write(fdw,buf,nread);
write(fdw,"\n",2);
if(buf[0]=='^M'){ /* 回车键 */
cmd='\0';
i=0;
}else
cmd[i++]=buf[0];
}
}
if(FD_ISSET(pty_fd,&rset)){
nread=read(pty_fd,buf,BUFSIZE);
buf[nread]='\0';
if(nread>0) write(console_fd,buf,strlen(buf)); /* #,$也在此输出 */
/* 记录输出 */
if(nread>0){
write(fdw,"o=",2);
write(fdw,buf,nread);
write(fdw,"\n",2);
}
}
}
}
4 防止进程被跟踪的方法
可以在业务程序和终端激活程序之间建立一种联系,最简单的方法是采用启动密码的
方法,比如如下程序。
/* 程序:upws.c */ │ fp=fopen(tmpfile,"w"));
#include <stdio.h> │ sprintf(command,"TERM=%s",argv[3]);
main (argc,argv) │ putenv(command);/* 设置终端类型 */
int argc;char *argv[]; │ /* 取当前进程号并存放在临时文件中 */
{ │ fprintf(fp,"%d",getpid());
FILE *fp; │ fclose(fp);
char tmpfile[128],command[128]; │ /* 执行argv[4]指定的程序,这个程序 */
/* 生成以终端名命名的临时文件 */ │ /* 将替换当前进程 */
sprintf(tmpfile,"%s/%s",argv[1], │ execl(argv[4],argv[4],argv[2],"123",0);
argv[2]); │}
其中的“123”为启动密码,在业务程序trctrl开始运行时就要检查这个密码, 如果
不符则退出。这个密码不能出现在upws的命令行中。 |
|