Linux: 进程标准输入输出设备描述符

Linux: 进程标准输入输出设备描述符

  • 说明
  • 标准输入输出文件
  • C库中标准输出设备
  • init进程创建shell并打开标准输入输出文件
  • 执行可执行程序时标准输入输出继承关系
  • telnet/ssh以及其他终端标准输入输出
  • dup/dup2补充

说明

C库:musl 1.2.4
busybox:1.36.0
Linux内核:4.0
参考链接:Linux: 进程标准输入、输出的建立过程简析

标准输入输出文件

百度百科如下介绍:
  执行一个 Shell 命令行时通常会自动打开三个标准文件,即标准输入文件(stdin),通常对应终端的键盘;标准输出文件(stdout)和标准错误输出文件(stderr),这两个文件都对应终端的屏幕。进程将从标准输入文件中得到输入数据,将正常输出数据输出到标准输出文件,而将错误信息送到标准错误文件中。

本文就根据嵌入式常用的busybox工具源码来分析,busybox的标准输入输出是如何建立的。

C库中标准输出设备

int printf(const char *restrict fmt, ...)
{
	int ret;
	va_list ap;
	va_start(ap, fmt);
	ret = vfprintf(stdout, fmt, ap);
	va_end(ap);
	return ret;
}

  根据C库代码,可以看到vsprintf直接使用了stdout这个文件句柄(C标准库的fopen打开的FILE句柄。

static unsigned char buf[BUFSIZ+UNGET];
hidden FILE __stdout_FILE = {
	.buf = buf+UNGET,
	.buf_size = sizeof buf-UNGET,
	.fd = 1,
	.flags = F_PERM | F_NORD,
	.lbf = '\n',
	.write = __stdout_write,
	.seek = __stdio_seek,
	.close = __stdio_close,
	.lock = -1,
};
FILE *const stdout = &__stdout_FILE;

  代码中直接指定了fd=1。但是遍历C库代码,也没有看到打开设备的地方。

init进程创建shell并打开标准输入输出文件

  init进程在初始化的过程中,会创建shell。在init进程初始化时,会去对console进行初始化(这里分析的是busyboxinit进程代码)

init/init.c->init_main()->console_init

static void console_init(void)
{
#ifdef VT_OPENQRY
	int vtno;
#endif
	char *s;

	s = getenv("CONSOLE");																	(1)
	if (!s)
		s = getenv("console");																(2)
#if defined(__FreeBSD__) || defined(__FreeBSD_kernel__)
	/* BSD people say their kernels do not open fd 0,1,2; they need this: */
	if (!s)
		s = (char*)"/dev/console";															(3)
#endif
	if (s) {
		int fd = open(s, O_RDWR | O_NONBLOCK | O_NOCTTY);
		if (fd >= 0) {
			dup2(fd, STDIN_FILENO);															(4)
			dup2(fd, STDOUT_FILENO);														(5)
			xmove_fd(fd, STDERR_FILENO);													(6)
		}
		dbg_message(L_LOG, "console='%s'", s);
	} else {
		/* Make sure fd 0,1,2 are not closed
		 * (so that they won't be used by future opens) */
		bb_sanitize_stdio();
// Users report problems
//		/* Make sure init can't be blocked by writing to stderr */
//		fcntl(STDERR_FILENO, F_SETFL, fcntl(STDERR_FILENO, F_GETFL) | O_NONBLOCK);
	}

......

1)查找CONSOLE环境变量
2)如果没找到CONSOLE环境变量,则转为查找console环境变量
3)如果没有上述环境变量,则直接打开/dev/console
4)复制fd到标准输入
5)复制fd到标准输出
6)将fd移动到标准错误

  至此,init进程完成了标准输入、输出、错误的文件打开。除了busybox外,其他的init进程应该也都是遵循这套标准的。

/ # ls -l /proc/1/fd
total 0
lrwx------    1 0        0               64 Jun 10 02:57 0 -> /dev/console
lrwx------    1 0        0               64 Jun 10 02:57 1 -> /dev/console
lrwx------    1 0        0               64 Jun 10 02:57 2 -> /dev/console

执行可执行程序时标准输入输出继承关系

  shell程序通过 fork() + exec*() 调用序列,来启动 目标可执行程序。在 fork() 过程中,子进程将继承 shell 程序打开的文件描述符表,所以shell 打开的 标准输入(STDIN_FILENO)、标准输出(STDOUT_FILENO)、标准错误输出(STDERR_FILENO) 文件描述符,将会被 目标可执行程序继承。
内核调用关系如下:

sys_fork()
	do_fork()
		_do_fork()
			copy_process()
				copy_files()
static int copy_files(unsigned long clone_flags, struct task_struct *tsk)
{
	struct files_struct *oldf, *newf;
	int error = 0;

	/*
	 * A background process may not have any files ...
	 */
	oldf = current->files;
	if (!oldf)
		goto out;

	if (clone_flags & CLONE_FILES) {
		atomic_inc(&oldf->count);
		goto out;
	}

	/* 复制父进程 shell 打开的文件描述符到子进程*/
	newf = dup_fd(oldf, &error);
	if (!newf)
		goto out;

	tsk->files = newf;
	error = 0;
out:
	return error;
}

  以上就是从 shell 启动程序的 标准输入(STDIN_FILENO)、标准输出(STDOUT_FILENO)、标准错误输出(STDERR_FILENO) 建立的秘密。

telnet/ssh以及其他终端标准输入输出

  众所周知,我们在终端中cat文件,执行程序中打印printf等,都输出到当前终端上。所以当我们使用telnet/ssh。虽然是开了一个子进程,但是终端势必是需要重定向的。

  这里剖析一下busyboxtelnetd

networking/telnetd.c->telnetd_main()->make_new_session()

static struct tsession *
make_new_session(
		IF_FEATURE_TELNETD_STANDALONE(int sock)
		IF_NOT_FEATURE_TELNETD_STANDALONE(void)
) {
#if !ENABLE_FEATURE_TELNETD_STANDALONE
	enum { sock = 0 };
#endif
	const char *login_argv[2];
	struct termios termbuf;
	int fd, pid;
	char tty_name[GETPTY_BUFSIZE];
	struct tsession *ts = xzalloc(sizeof(struct tsession) + BUFSIZE * 2);

	/*ts->buf1 = (char *)(ts + 1);*/
	/*ts->buf2 = ts->buf1 + BUFSIZE;*/

	/* Got a new connection, set up a tty */
	fd = xgetpty(tty_name);									(1)
......

1)通过xgetpty获取tty设备名称。
networking/telnetd.c->telnetd_main()->make_new_session()-> getpty.c->xgetpty()

int FAST_FUNC xgetpty(char *line)
{
	int p;

#if ENABLE_FEATURE_DEVPTS
	p = open("/dev/ptmx", O_RDWR);												(1)
	if (p >= 0) {
		grantpt(p); /* chmod+chown corresponding slave pty */
		unlockpt(p); /* (what does this do?) */
# ifndef HAVE_PTSNAME_R
		{
			const char *name;
			name = ptsname(p); /* find out the name of slave pty */				(2)
			if (!name) {
				bb_simple_perror_msg_and_die("ptsname error (is /dev/pts mounted?)");
			}
			safe_strncpy(line, name, GETPTY_BUFSIZE);							(3)
		}
# else
		/* find out the name of slave pty */
		if (ptsname_r(p, line, GETPTY_BUFSIZE-1) != 0) {
			bb_simple_perror_msg_and_die("ptsname error (is /dev/pts mounted?)");
		}
		line[GETPTY_BUFSIZE-1] = '\0';
# endif
		return p;
	}

1)打开/dev/ptmx设备
2)通过打开ptmx设备的fd,来获取pty设备名称。
3)拷贝名称

然后继续来看make_new_session函数

networking/telnetd.c->telnetd_main()->make_new_session()

......
	/* Open the child's side of the tty */
	/* NB: setsid() disconnects from any previous ctty's. Therefore
	 * we must open child's side of the tty AFTER setsid! */
	close(0);
	xopen(tty_name, O_RDWR); /* becomes our ctty */
	xdup2(0, 1);
	xdup2(0, 2);
......

因为telnetd进程本身是从原来的终端继承过来的,所以要先关闭0设备节点,然后打开获取到名字的tty设备。随后通过xdup2拷贝0设备到1和2。

  至此,新的telnetd终端完成了自己的标准输入输出文件打开。通过该telnetd打开的进程,都将会继承当前的标准输入输出。

(由于手上没有telnet的设备,这里放一个ubuntussh的终端显示以作参考)

root@ubuntu:/home/# ps
   PID TTY          TIME CMD
  3421 pts/9    00:00:00 bash
  3500 pts/9    00:00:00 ps
root@ubuntu:/home/# 
root@ubuntu:/home/# ls -l /proc/3421/fd
total 0
lrwx------ 1 root root 64 Jun 10 11:06 0 -> /dev/pts/9
lrwx------ 1 root root 64 Jun 10 11:06 1 -> /dev/pts/9
lrwx------ 1 root root 64 Jun 10 11:06 2 -> /dev/pts/9
lrwx------ 1 root root 64 Jun 10 11:51 255 -> /dev/pts/9

dup/dup2补充

xdup2busybox自己定义的

void FAST_FUNC xdup2(int from, int to)
{
	if (dup2(from, to) != to)
		bb_simple_perror_msg_and_die("can't duplicate file descriptor");
		//		" %d to %d", from, to);
}

dupdup2函数:

int dup(int filedes); int dup2(int filedes,int filedes2);

返回:若成功为新的文件描述符,若出错为-1

作用:用来复制一个文件描述符,经常用来重定向进程的stdin,stdout,stderr

dup返回的新文件描述符一定是当前可用文件描述符中最小数值,该新的描述符是传递给它的描述符的拷贝,这意味着这两个描述符共享同一个数据结构。用dup2则可以用filedes2参数指定新描述符的数值,如果filedes2已经打开,则先将其关闭,如果filedes等于filedes2,则dup2返回filedes2,而不关闭它。

你可能感兴趣的:(Linux,linux,运维,服务器)