C库:musl 1.2.4
busybox:1.36.0
Linux内核:4.0
参考链接:Linux: 进程标准输入、输出的建立过程简析
百度百科如下介绍:
执行一个 Shell 命令行时通常会自动打开三个标准文件,即标准输入文件(stdin
),通常对应终端的键盘;标准输出文件(stdout
)和标准错误输出文件(stderr
),这两个文件都对应终端的屏幕。进程将从标准输入文件中得到输入数据,将正常输出数据输出到标准输出文件,而将错误信息送到标准错误文件中。
本文就根据嵌入式常用的busybox
工具源码来分析,busybox
的标准输入输出是如何建立的。
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
进程初始化时,会去对console
进行初始化(这里分析的是busybox
的init
进程代码)
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
) 建立的秘密。
众所周知,我们在终端中cat
文件,执行程序中打印printf
等,都输出到当前终端上。所以当我们使用telnet/ssh
。虽然是开了一个子进程,但是终端势必是需要重定向的。
这里剖析一下busybox
的telnetd
。
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
的设备,这里放一个ubuntu
的ssh
的终端显示以作参考)
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
xdup2
是busybox
自己定义的
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);
}
dup
和dup2
函数:
int dup(int filedes); int dup2(int filedes,int filedes2);
返回:若成功为新的文件描述符,若出错为-1
作用:用来复制一个文件描述符,经常用来重定向进程的stdin,stdout,stderr
。
由dup
返回的新文件描述符一定是当前可用文件描述符中最小数值,该新的描述符是传递给它的描述符的拷贝,这意味着这两个描述符共享同一个数据结构。用dup2
则可以用filedes2
参数指定新描述符的数值,如果filedes2
已经打开,则先将其关闭,如果filedes
等于filedes2,则dup2
返回filedes2
,而不关闭它。