您的位置:首页 > 运维架构 > Linux

Linux script and scriptreplay(三)

2016-05-04 08:02 513 查看

前言

在上面两篇博客中,介绍了script和scriptreplay的使用以及原理,这节中就来分析一下script的原理。

在分析script之前需要了解终端的概念

终端是一种字符设备,它有多种类型,通常使用tty来简称各种类型的终端设备。 设备名放在特殊目录/dev/下,终端特殊设备文件一般有以下几种:

1.串行端口终端(/dev/ttySn)

串行端口终端是使用计算机串行端口连接的终端设备。计算机把每个串行端口看做是一个字符设备,有段时间这些串行端口设备通常称为终端设备,那是它们的最大用途就是用来连接终端。

2.伪终端

伪终端(Pseudo Terminal)是成对的逻辑终端设备,即伪终端是由Master和Slave共同组成的,通常用于实现网络登陆功能,如ssh,telnet等。伪终端一般分为两类:

(1)BSD 伪终端

BSD伪终端中,定义/dev/pty[p-za-e][0-9a-f] 是Master; /dev/tty[p-za-e][0-9a-f] 是Slave,他们都是配对好的,即/dev/ptyp1对应/dev/ttyp1,共同伪终端。这种看似简单的定义使得编程实现很困难,因为我们需要遍历/dev/目录,一个一个的尝试才能找到一对合适的终端

(2)Unix 98伪终端

与BSD伪终端不同,始终以/dev/ptmx作为Master的复制设备,每次打开/dev/ptmx才能得到一个master设备的fd,同时在/dev/pts目录下得到一个Slave设备,这种方式下,编程就变得简单了许多。

3.控制终端

如果当前进程有控制终端的话,那么/dev/tty就是当前进程的控制终端的特殊文件。可以使用
ps -ef
来查看进程与哪个控制终端相连。对于你登陆的shell,/dev/tty就是你使用的终端。你可以使用
tty
命令查看它具体对应哪个实际终端设备

4.控制台终端

在linux系统中,计算机显示器通常被称为控制台终端。它仿真了类型为linux的一种终端(TERM=Linux),并且有一些设备特殊文件与之相关联:tty[1-6]使用Alt+[F1—F6]组合键时,我们就可以切换到tty2、tty3等上面去。tty1–tty6等称为虚拟终端,而tty0则是当前所使用虚拟终端的一个别名,系统所产生的信息会发送到该终端上。因此不管当前正在使用哪个虚拟终端,系统信息都会发送到控制台终端上。你可以登录到不同的虚拟终端上去,因而可以让系统同时有几个不同的会话期存在。

script关键代码分析

getmaster

void
getmaster() {
//此种方式为Unix98伪终端,直接调用openpty函数获取Master和Slave
#if HAVE_LIBUTIL && HAVE_PTY_H
tcgetattr(STDIN_FILENO, &tt);
ioctl(STDIN_FILENO, TIOCGWINSZ, (char *)&win);
if (openpty(&master, &slave, NULL, &tt, &win) < 0) {
warn(_("openpty failed"));
fail();
}
#else
//BSD伪终端,需要便利/dev目录,找到一对可用的Master和Slave
char *pty, *bank, *cp;
struct stat stb;

pty = &line[strlen("/dev/ptyp")];
for (bank = "pqrs"; *bank; bank++) {
line[strlen("/dev/pty")] = *bank;
*pty = '0';
if (stat(line, &stb) < 0)
break;
for (cp = "0123456789abcdef"; *cp; cp++) {
*pty = *cp;
master = open(line, O_RDWR);
if (master >= 0) {
char *tp = &line[strlen("/dev/")];
int ok;

/* verify slave side is usable */
*tp = 't';
ok = access(line, R_OK|W_OK) == 0;
*tp = 'p';
if (ok) {
tcgetattr(STDIN_FILENO, &tt);
ioctl(STDIN_FILENO, TIOCGWINSZ,
(char *)&win);
return;
}
close(master);
master = -1;
}
}
}
master = -1;
warn(_("out of pty's"));
fail();
#endif /* not HAVE_LIBUTIL */
}


dooutput

void
dooutput(FILE *timingfd) {
ssize_t cc;
time_t tvec;
char obuf[BUFSIZ];
struct timeval tv;
double oldtime=time(NULL), newtime;
int flgs = 0;
ssize_t wrt;
ssize_t fwrt;

close(STDIN_FILENO);
#ifdef HAVE_LIBUTIL
close(slave);
#endif
tvec = time((time_t *)NULL);
my_strftime(obuf, sizeof obuf, "%c\n", localtime(&tvec));
//输出开始信息到typescript文件
fprintf(fscript, _("Script started on %s"), obuf);

do {
if (die && flgs == 0) {
/* ..child is dead, but it doesn't mean that there is
* nothing in buffers.
*/
flgs = fcntl(master, F_GETFL, 0);
if (fcntl(master, F_SETFL, (flgs | O_NONBLOCK)) == -1)
break;
}
if (tflg)
gettimeofday(&tv, NULL);

errno = 0;
//从master中读取数据
cc = read(master, obuf, sizeof (obuf));

if (die && errno == EINTR && cc <= 0)
/* read() has been interrupted by SIGCHLD, try it again
* with O_NONBLOCK
*/
continue;
if (cc <= 0)
break;
//计算上次输出到本次输出的时间间隔
if (tflg) {
newtime = tv.tv_sec + (double) tv.tv_usec / 1000000;
fprintf(timingfd, "%f %zd\n", newtime - oldtime, cc);
oldtime = newtime;
}
//将从master中读到的数据输出到stdout
wrt = write(STDOUT_FILENO, obuf, cc);
if (wrt < 0) {
warn (_("write failed"));
fail();
}
//将数据写入到typescript文件中
fwrt = fwrite(obuf, 1, cc, fscript);
if (fwrt < cc) {
warn (_("cannot write script file
10291
"));
fail();
}
if (fflg)
fflush(fscript);
} while(1);

if (flgs)
fcntl(master, F_SETFL, flgs);
done();
}


doinput

void
doinput() {
ssize_t cc;
char ibuf[BUFSIZ];

fclose(fscript);
//从stdin中读取输入,写入到master中
while (die == 0) {
if ((cc = read(STDIN_FILENO, ibuf, BUFSIZ)) > 0) {
ssize_t wrt = write(master, ibuf, cc);
if (wrt < 0) {
warn (_("write failed"));
fail();
}
}
else if (cc < 0 && errno == EINTR && resized)
resized = 0;
else
break;
}

done();
}


script程序流程梳理

Created with Raphaël 2.1.0开始读取参数获取主设备fork子进程是否是子进程?fork子进程是否是子进程dooutput(记录数据以及输出)结束doshell(execl启动一个shell)doinput(循环读取输入)yesnoyesno

golang实现一个Unix98伪终端的script程序

package main

import (
"bufio"
"flag"
"fmt"
"os"
"os/exec"
"syscall"
"time"
"unsafe"
)

// #include <termios.h>
import "C"

const (
PtyMaster = "/dev/ptmx"
)

var (
TimingFile = flag.String("t", "timing", "timing file")
)

func main() {
flag.Parse()

typeScript := "typescript"

fmt.Println(os.Args, len(os.Args), *TimingFile)
args := flag.Args()
if len(args) != 1 {
fmt.Printf("[usage]\n\n%s -t timingfile typescript\n", os.Args[0])
os.Exit(1)
}

typeScript = args[0]
script, err := os.OpenFile(typeScript, os.O_RDWR|os.O_CREATE, 0664)
if err != nil {
fmt.Printf("Open Typescript %s Error:%v", typeScript, err)
os.Exit(1)
}

timing, err := os.OpenFile(*TimingFile, os.O_RDWR|os.O_CREATE, 0664)
if err != nil {
fmt.Printf("Open Timing %s Error:%v", TimingFile, err)
os.Exit(1)
}

cmd := exec.Command("/bin/bash")
p, err := start(cmd)
if err != nil {
fmt.Printf("start error:%v", err)
os.Exit(1)
}

err = fixtty()
if err != nil {
fmt.Printf("fixtty error:%v", err)
os.Exit(1)
}

// 循环从master中读取输出
go func(p, timing, script *os.File) {
fmt.Fprintf(script, "Script started on %s\n", time.Now())
oldTime := time.Now()
for {
// fmt.Println("2")
buf := make([]byte, 1024)
n, err := p.Read(buf)

if err != nil {
break
}
newTime := time.Now()
delay := newTime.Sub(oldTime)
oldTime = newTime
//print data to stdout
fmt.Print(string(buf[:n]))
//save data to file
fmt.Fprintf(timing, "%f %d\n", float64(delay)/1e9, n)
fmt.Fprint(script, string(buf[:n]))
}
done(script)
os.Exit(0)
}(p, timing, script)

//循环从标准输入中读取输入
for {

buf := make([]byte, 1024)
rd := bufio.NewReader(os.Stdin)
n, err := rd.Read(buf)
if err != nil {
fmt.Printf("outside:%v\n", err)
// close master
p.Close()
break
}
p.Write(buf[:n])
}

}

func done(f *os.File) {
fmt.Fprintf(f, "Script done on %s\n", time.Now())
fmt.Printf("Script done, file is %s", f.Name())
}

// fixtty  将stdin对应的终端设备设置为不回显,非规范方式
func fixtty() error {
var term Termios
e := tcget(os.Stdin.Fd(), &term)
if e != 0 {
return e
}
term.Lflag &^= (syscall.ECHO | syscall.ICANON)
e = tcset(os.Stdin.Fd(), &term)
if e != 0 {
return e
}
return nil
}

//start 启动子进程
func start(cmd *exec.Cmd) (pty *os.File, err error) {
m, s, e := getPty()
if e != nil {
return nil, e
}
defer s.Close()
cmd.Stdin = s
cmd.Stdout = s
cmd.Stderr = s
cmd.SysProcAttr = &syscall.SysProcAttr{Setctty: true, Setsid: true}
err = cmd.Start()
if err != nil {
m.Close()
return nil, err
}
return m, err

}

//getPty 获取一对Pty设备
func getPty() (master, slave *os.File, err error) {
m, e := os.OpenFile(PtyMaster, os.O_RDWR, 0)
if e != nil {
return nil, nil, e
}

sname, e := ptsName(m)
if e != nil {
return nil, nil, e
}

e = unlockpt(m)

if e != nil {
return nil, nil, e
}

s, e := os.OpenFile(sname, os.O_RDWR, 0)
if e != nil {
return nil, nil, e
}
return m, s, nil
}

//ioctl 模拟c中的ioctl函数
func ioctl(fd, cmd, ptr uintptr) error {
_, _, e := syscall.Syscall(syscall.SYS_IOCTL, fd, cmd, ptr)
if e != 0 {
return e
}
return nil
}

//ptsName 获取master对应的slave
func ptsName(f *os.File) (string, error) {
var n int
if err := ioctl(f.Fd(), syscall.TIOCGPTN, uintptr(unsafe.Pointer(&n))); err != nil {
return "", err
}
return fmt.Sprintf("/dev/pts/%d", n), nil
}

//unlockpt 释放pty锁
func unlockpt(f *os.File) error {
var n int
return ioctl(f.Fd(), syscall.TIOCSPTLCK, uintptr(unsafe.Pointer(&n)))
}

// Termios is the Unix API for terminal I/O.
// It is passthrough for syscall.Termios in order to make it portable with
// other platforms where it is not available or handled differently.
type Termios syscall.Termios

// func cfmakeraw()
func tcget(fd uintptr, p *Termios) syscall.Errno {
ret, err := C.tcgetattr(C.int(fd), (*C.struct_termios)(unsafe.Pointer(p)))
if ret != 0 {
return err.(syscall.Errno)
}
return 0
}

func tcset(fd uintptr, p *Termios) syscall.Errno {
ret, err := C.tcsetattr(C.int(fd), C.TCSANOW, (*C.struct_termios)(unsafe.Pointer(p)))
if ret != 0 {
return err.(syscall.Errno)
}
return 0
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  linux 终端 golang