util-linux分页程序more实现详解
从linux应用程序角度看,主要分析以下实现:
- 对入参,环境变量的处理
- 对标准输入,标准输出,标准错误输出的使用
- 对信号的处理,例如Ctrl+C产生的SIGINT,Ctrl+Z产生的SIGSTSP,重新恢复执行SIGCONT,窗口大小变化SIGWINCH,进程退出SIGQUIT。
对入参,环境变量的处理
-
getenv获取环境变量字符串,然后使用strtok_r将字符串分割,最后调用argscan将结果保存到ctl。由于环境变量字符串长度不确定,故使用了xreallocarray进行扩容。ctl.exit_on_eof = getenv("POSIXLY_CORRECT") ? 0 : 1; if ((s = getenv("MORE")) != NULL) env_argscan(&ctl, s); const char delim[] = { ' ', '\n', '\t', '\0' }; char *str = xstrdup(s); char *key = NULL, *tok; env_argv = xreallocarray(NULL, size, sizeof(char *)); env_argv[0] = _("MORE environment variable"); /* program name */ for (tok = strtok_r(str, delim, &key); tok; tok = strtok_r(NULL, delim, &key)) { if (size == env_argc) { size *= 2; env_argv = xreallocarray(env_argv, size, sizeof(char *)); } env_argv[env_argc++] = tok; } argscan(ctl, env_argc, env_argv); -
在
argscan中,先处理非标准opt形式的特殊选项,再使用getopt_long处理。-
optind是一个由getopt和getopt_long函数使用的全局变量,它用于记录下一个要处理的命令行参数的索引。 -
optarg是一个全局变量,用于存储getopt或getopt_long函数解析到的选项参数。
/* Take care of number option and +args. * -<number> same as --lines * +<number> display file beginning from line number * +/<pattern> display file beginning from pattern match */ for (opt = 0; opt < as_argc; opt++) { int move = 0; if (as_argv[opt][0] == '-' && isdigit_string(as_argv[opt] + 1)) { ctl->lines_per_screen = strtos16_or_err(as_argv[opt], _("failed to parse number")); ctl->lines_per_screen = abs(ctl->lines_per_screen); move = 1; } else if (as_argv[opt][0] == '+') { if (isdigit_string(as_argv[opt] + 1)) { ctl->next_jump = strtos32_or_err(as_argv[opt], _("failed to parse number")) - 1; move = 1; } else if (as_argv[opt][1] == '/') { free(ctl->next_search); ctl->next_search = xstrdup(as_argv[opt] + 2); ctl->search_at_start = 1; move = 1; } } if (move) { as_argc = ul_remove_entry(as_argv, opt, as_argc); opt--; } } /* Reset optind, command line parsing needs this. */ optind = 0;while ((c = getopt_long(as_argc, as_argv, "dflcpsun:eVh", longopts, NULL)) != -1) { switch (c) { /* ... */ case 'n': ctl->lines_per_screen = strtou16_or_err(optarg, _("argument error")); break; case 'h': usage(); default: errtryhelp(EXIT_FAILURE); break; } } ctl->num_files = as_argc - optind; ctl->file_names = as_argv + optind; -
对标准输入,标准输出,标准错误输出的使用
-
在ctrl中有
no_tty_in,no_tty_out,no_tty_err三个变量,表示标准输入,标准输出,标准错误输出是tty设备还是管道等。no_tty_in, /* is input in interactive mode */ no_tty_out, /* is output in interactive mode */ no_tty_err, /* is stderr terminal */ if (ctl.no_tty_err) /* exit when we cannot read user's input */ ctl.exit_on_eof = 1; /* tcgetattr 获取tty属性, 非tty设备返回-1 */ #ifndef NON_INTERACTIVE_MORE ctl->no_tty_out = tcgetattr(STDOUT_FILENO, &ctl->output_tty); #endif ctl->no_tty_in = tcgetattr(STDIN_FILENO, &ctl->output_tty); ctl->no_tty_err = tcgetattr(STDERR_FILENO, &ctl->output_tty); ctl->original_tty = ctl->output_tty; -
标准输入,标准输出的使用(读取文件内容,输出到屏幕或其他设备)
more程序的输入输出均可被重定向,如对于标准输入:
cat error.log|more(more的标准输入被重定向为管道的读端),more error.log(more的标准输入未被重定向,仍为tty设备);对于标准输出,cat config.log |more|cat(more的标准输出被重定向到管道写端)。管道仅支持读写,tty设备拥有多种属性,故需分类处理。/* 如果标准输入未被重定向且argv中未指定要显示的文件,则报错 */ if (!ctl.no_tty_in && ctl.num_files == 0) { /* input content either by redirection of stdin */ warnx(_("bad usage")); /* or by agrv, or where is the input? */ errtryhelp(EXIT_FAILURE); } if (ctl.no_tty_in) { if (ctl.no_tty_out) copy_file(stdin); /* 如果标准输入和标准输出均被重定向,则直接拷贝标准输入到标准输出 */ else { ctl.current_file = stdin; /* 仅标准输入被重定向,当前输入则为stdin */ display_file(&ctl, left); } ctl.no_tty_in = 0; ctl.print_banner = 1; ctl.first_file = 0; } /* 标准输入未被重定向,当前输入为argv中的文件 */ while (ctl.argv_position < ctl.num_files) { checkf(&ctl, ctl.file_names[ctl.argv_position]); display_file(&ctl, left); ctl.first_file = 0; ctl.argv_position++; } -
标准错误输出的使用(程序运行时读取用户交互输入)
当标准输出为tty设备时,more程序运行需要读取键盘输入进行翻页,下一行等操作。
static cc_t read_user_input(struct more_control *ctl) { cc_t c; errno = 0; /* * Key commands can be read() from either stderr or stdin. If they * are read from stdin such as 'cat file.txt | more' then the pipe * input is understood as series key commands - and that is not * wanted. Keep the read() reading from stderr. */ if (read(STDERR_FILENO, &c, 1) <= 0) { if (errno != EINTR) more_exit(ctl); else c = ctl->output_tty.c_cc[VKILL]; } return c; }从stderr读取键盘输入是可行的,原因:
Before redirection stdin, stdout, and stderr are as expected connected to the same device.
#ctrl-alt-delor:~$ #↳ ll /dev/std* lrwxrwxrwx 1 root root 15 Jun 3 20:58 /dev/stderr -> /proc/self/fd/2 lrwxrwxrwx 1 root root 15 Jun 3 20:58 /dev/stdin -> /proc/self/fd/0 lrwxrwxrwx 1 root root 15 Jun 3 20:58 /dev/stdout -> /proc/self/fd/1 #ctrl-alt-delor:~$ #↳ ll /proc/self/fd/* lrwx------ 1 richard richard 64 Jun 30 19:14 /proc/self/fd/0 -> /dev/pts/12 lrwx------ 1 richard richard 64 Jun 30 19:14 /proc/self/fd/1 -> /dev/pts/12 lrwx------ 1 richard richard 64 Jun 30 19:14 /proc/self/fd/2 -> /dev/pts/12Therefore after most re-directions (that is if stderr) is not redirected. stderr is still connected to the terminal. Therefore it can be read, to get keyboard input.
对信号的处理
-
信号处理框架:使用
signalfd + poll处理信号- 统一事件处理:通过将信号转换为文件描述符,程序可以使用标准的 I/O 多路复用机制(如
poll)来处理信号和其他 I/O 事件,避免了传统信号处理函数(如signal或sigaction)在多线程或异步编程中可能遇到的问题。 - 避免竞态条件:使用
signalfd可以避免信号处理函数和主程序之间的竞态条件,因为信号的处理可以在主程序的控制流中进行,而不是在单独的信号处理函数中。
/* clear any inherited settings */ signal(SIGCHLD, SIG_DFL); sigemptyset(&ctl.sigset); sigaddset(&ctl.sigset, SIGINT); sigaddset(&ctl.sigset, SIGQUIT); sigaddset(&ctl.sigset, SIGTSTP); sigaddset(&ctl.sigset, SIGCONT); sigaddset(&ctl.sigset, SIGWINCH); sigprocmask(SIG_BLOCK, &ctl.sigset, NULL); ctl.sigfd = signalfd(-1, &ctl.sigset, SFD_CLOEXEC); struct pollfd pfd[] = { [POLLFD_SIGNAL] = { .fd = ctl->sigfd, .events = POLLIN | POLLERR | POLLHUP }, [POLLFD_STDIN] = { .fd = STDIN_FILENO, .events = POLLIN | POLLERR | POLLHUP }, [POLLFD_STDERR] = { .fd = STDERR_FILENO, .events = POLLIN | POLLERR | POLLHUP } }; rc = poll(pfd, ARRAY_SIZE(pfd), timeout); /* event on signal FD */ if (pfd[POLLFD_SIGNAL].revents) { struct signalfd_siginfo info; ssize_t sz; sz = read(pfd[POLLFD_SIGNAL].fd, &info, sizeof(info)); assert(sz == sizeof(info)); switch (info.ssi_signo) { case SIGINT: more_exit(ctl); break; case SIGQUIT: sigquit_handler(ctl); break; case SIGTSTP: sigtstp_handler(ctl); break; case SIGCONT: sigcont_handler(ctl); break; case SIGWINCH: sigwinch_handler(ctl); break; default: abort(); } } - 统一事件处理:通过将信号转换为文件描述符,程序可以使用标准的 I/O 多路复用机制(如
-
SIGTSTP, SIGCONT处理方式
-
SIGTSTP 先是下刷掉用户态缓存,然后恢复终端状态到起始状态,最后向自己发送SIGSTOP停止进程
/* Come here when we get a suspend signal from the terminal */ static void sigtstp_handler(struct more_control *ctl) { reset_tty(ctl); fflush(NULL); kill(getpid(), SIGSTOP); } /* ICANON 规范输入(canonical input)也被称作行缓冲输入: * 在规范模式下,输入以行为单位进行处理,用户输入的字符会被缓存,直到按下回车键才会将整行数据发送给程 序;在非规范模式下,输入不以行为单位,程序可以立即处理每个输入的字符。 * ECHO:输入是否回显到终端 */ static void reset_tty(struct more_control *ctl) { if (ctl->no_tty_out) return; fflush(NULL); ctl->output_tty.c_lflag |= ICANON | ECHO; ctl->output_tty.c_cc[VMIN] = ctl->original_tty.c_cc[VMIN]; ctl->output_tty.c_cc[VTIME] = ctl->original_tty.c_cc[VTIME]; tcsetattr(STDERR_FILENO, TCSANOW, &ctl->original_tty); } -
SIGCONT 重新设置终端状态
/* Come here when we get a continue signal from the terminal */ static void sigcont_handler(struct more_control *ctl) { set_tty(ctl); } static void set_tty(struct more_control *ctl) { ctl->output_tty.c_lflag &= ~(ICANON | ECHO); ctl->output_tty.c_cc[VMIN] = 1; /* read at least 1 char */ ctl->output_tty.c_cc[VTIME] = 0; /* no timeout */ tcsetattr(STDERR_FILENO, TCSANOW, &ctl->output_tty); }
-
-
SIGWINCH 重新获取
lines_per_page,num_columns等参数,重新设置line buffer。/* Come here if a signal for a window size change is received */ static void sigwinch_handler(struct more_control *ctl) { struct winsize win; if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &win) != -1) { if (win.ws_row != 0) { ctl->lines_per_page = win.ws_row; ctl->d_scroll_len = ctl->lines_per_page / 2 - 1; if (ctl->d_scroll_len < 1) ctl->d_scroll_len = 1; ctl->lines_per_screen = ctl->lines_per_page - 1; } if (win.ws_col != 0) ctl->num_columns = win.ws_col; } prepare_line_buffer(ctl); }
从分页程序角度看,主要分析以下实现:
- 如何使用控制消息控制终端
- 如何实现分页、分行输出,回滚查看等
- 如何在其他程序中调用分页程序实现分页,有哪些注意项
如何使用控制消息控制终端
-
通过ncurse库实现终端控制,先获取控制消息字符,然后调用
putp即可。#define TERM_CLEAR "clear" #define TERM_CLEAR_TO_LINE_END "el" ctl->erase_line = tigetstr(TERM_CLEAR_TO_LINE_END); ctl->clear = tigetstr(TERM_CLEAR); if (ctl->clear_line_ends) putp(ctl->erase_line); if (ctl->is_eof && ctl->exit_on_eof) { if (ctl->clear_line_ends) putp(ctl->clear_rest); return; }
如何实现分页、分行输出,回滚查看等
-
分页、分行输出,回滚查看
fgetc,get_line,fwrite(ctl->line_buf, length, 1, stdout),screen,display_file分级实现从stdin读取并输出到stdout,应用缓存大小和终端一样为行缓存。more_poll,read_command,more_key_command从stderr获取用户命令并返回需要显示的行数, 如果是显示上一页等回滚操作的话,需要先fseeko到文件头,然后再根据文件行数调用skip_lines进行跳转到上一页起始位置,再返回要显示的行数。#0 more_poll (ctl=0xd68, timeout=0, stderr_active=0x7ffff7f816a0 <_IO_2_1_stdout_>) at text-utils/more.c:1357 #1 0x000055555555c38d in more_key_command (ctl=0x7fffffffdfb0, filename=0x0) at text-utils/more.c:1678 #2 0x000055555555cf1c in screen (ctl=0x7fffffffdfb0, num_lines=0) at text-utils/more.c:1915 #3 0x000055555555d3b8 in display_file (ctl=0x7fffffffdfb0, left=61) at text-utils/more.c:1989 #4 0x000055555555dd9f in main (argc=2, argv=0x7fffffffe338) at text-utils/more.c:2178 /* Skip n lines in the file f */ static void skip_lines(struct more_control *ctl) { int c; while (ctl->next_jump > 0) { while ((c = more_getc(ctl)) != '\n') if (c == EOF) return; ctl->next_jump--; ctl->current_line++; } } static int skip_backwards(struct more_control *ctl, int nlines) { if (nlines == 0) nlines++; erase_to_col(ctl, 0); printf(P_("...back %d page", "...back %d pages", nlines), nlines); putchar('\n'); ctl->next_jump = ctl->current_line - (ctl->lines_per_screen * (nlines + 1)) - 1; if (ctl->next_jump < 0) ctl->next_jump = 0; more_fseek(ctl, 0); ctl->current_line = 0; skip_lines(ctl); return ctl->lines_per_screen; } /* 当getline获取到EOF时显示结束 */ nchars = get_line(ctl, &length); ctl->is_eof = nchars == EOF; if (ctl->is_eof && ctl->exit_on_eof) { if (ctl->clear_line_ends) putp(ctl->clear_rest); return; } /* 如果stdin未被重定向的话,键盘输入命令时stdin和stderr都会产生事件 */ /* event on stdin */ if (pfd[POLLFD_STDIN].revents) { /* Check for POLLERR and POLLHUP in stdin revents */ if ((pfd[POLLFD_STDIN].revents & POLLERR) && (pfd[POLLFD_STDIN].revents & POLLHUP)) more_exit(ctl); /* poll() return POLLHUP event after pipe close() and POLLNVAL * means that fd is already closed. */ if ((pfd[POLLFD_STDIN].revents & POLLHUP) || (pfd[POLLFD_STDIN].revents & POLLNVAL)) ctl->ignore_stdin = 1; else has_data++; }
如何在其他程序中调用分页程序实现分页,有哪些注意项
-
如果父进程数据写完成,如何通知子进程more?
关闭父进程管道写端即可,子进程读取的时候会产生EOF -
父进程一次往管道写多少数据合适?
管道大小默认为1M。cat /proc/sys/fs/pipe-max-size -
fork后子进程会继承父进程的哪些东西?
信号掩码和信号处理函数,需要调用sigaction和sigprocmask进行清理
控制终端,需要调用tcsetattr进行清理
文件描述符,包括:- 标准输入(
stdin)、标准输出(stdout)、标准错误(stderr)如果之前有对其属性进行设置,需要fcntl进行还原。 - 其他打开的文件、管道、套接字等。
- 标准输入(
-
子进程正常/异常退出时,父进程如何感知?
父进程在子进程退出时会受到SIGCHLD信号,可添加信号处理函数,使用waitpid获取退出信息。 -
父进程正常/异常退出时,子进程如何感知?
prctl(PR_SET_PDEATHSIG, SIGTERM);
When the parent process of the process running this code terminates, the kernel will send aSIGTERMsignal to the child process. -
管道和文件有哪些差别?
管道 是顺序访问设备,不支持随机访问, 因为管道的数据是流式的,无法回退或跳转
文件 是随机访问设备,支持通过fseek()移动文件指针到任意位置 -
创建子进程调用分页程序实例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include <sys/prctl.h>
#include <sys/wait.h>
#include <signal.h>
#include <sys/signalfd.h>
#include <errno.h>
#define BUF_SIZE 8192
#define MAX_EVENTS 10
#ifndef F_SETPIPE_SZ
#define F_SETPIPE_SZ 1031
#endif
void handle_sigterm(int sig) {
printf("Parent process exited. Received SIGTERM. Child Exiting...\n");
exit(0);
}
int main(int argc, char *argv[]) {
if (argc != 3) {
fprintf(stderr, "Usage: %s <filename> <recordfilename>\n", argv[0]);
return 1;
}
// 打开文件
FILE *file = fopen(argv[1], "r");
if (!file) {
perror("fopen");
return 1;
}
// 打开文件(记录每次读文件以及写管道的字节数)
FILE *file_record = fopen(argv[2], "w");
if (!file_record) {
perror("fopen");
return 1;
}
// 创建管道
int pipefd[2];
if (pipe(pipefd) == -1) {
perror("pipe");
fclose(file_record);
fclose(file);
return 1;
}
// 设置管道写端为非阻塞
fcntl(pipefd[1], F_SETFL, O_NONBLOCK);
// 设置管道大小为 4 KB
long new_size = 4 * 1024; // 4 KB
if (fcntl(pipefd[1], F_SETPIPE_SZ, new_size) == -1) {
perror("fcntl F_SETPIPE_SZ");
fclose(file_record);
fclose(file);
close(pipefd[0]);
close(pipefd[1]);
return 1;
}
// 创建子进程
pid_t pid = fork();
if (pid == -1) {
perror("fork");
fclose(file_record);
fclose(file);
close(pipefd[0]);
close(pipefd[1]);
return 1;
} else if (pid == 0) {
// 子进程
close(pipefd[1]); // 关闭写端
signal(SIGTERM, handle_sigterm);
// 设置父进程终止时接收 SIGTERM
if (prctl(PR_SET_PDEATHSIG, SIGTERM) == -1) {
perror("prctl");
exit(1);
}
// 重定向标准输入到管道读端
dup2(pipefd[0], STDIN_FILENO);
close(pipefd[0]);
// 执行 more 命令
execlp("more", "more", NULL);
perror("execlp"); // 如果 execlp 失败
exit(1);
} else {
// 父进程
close(pipefd[0]); // 关闭读端
// 设置信号掩码
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGCHLD);
sigprocmask(SIG_BLOCK, &mask, NULL);
// 创建 signalfd
int sfd = signalfd(-1, &mask, 0);
if (sfd == -1) {
perror("signalfd");
fclose(file_record);
fclose(file);
close(pipefd[1]);
return 1;
}
// 创建 epoll 实例
int epoll_fd = epoll_create1(0);
if (epoll_fd == -1) {
perror("epoll_create1");
close(sfd);
fclose(file_record);
fclose(file);
close(pipefd[1]);
return 1;
}
// 添加管道写端到 epoll
struct epoll_event ev;
ev.events = EPOLLOUT; // 监控写事件
ev.data.fd = pipefd[1];
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, pipefd[1], &ev) == -1) {
perror("epoll_ctl pipefd");
close(epoll_fd);
close(sfd);
fclose(file_record);
fclose(file);
close(pipefd[1]);
return 1;
}
// 添加 signalfd 到 epoll
ev.events = EPOLLIN; // 监控读事件
ev.data.fd = sfd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sfd, &ev) == -1) {
perror("epoll_ctl signalfd");
close(epoll_fd);
close(sfd);
fclose(file_record);
fclose(file);
close(pipefd[1]);
return 1;
}
// 读取文件并写入管道
char buf[BUF_SIZE];
struct epoll_event events[MAX_EVENTS];
int running = 1; // 控制循环的标志
while (running) {
int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
if (nfds == -1) {
perror("epoll_wait");
break;
}
for (int i = 0; i < nfds; i++) {
if (events[i].data.fd == pipefd[1]) {
size_t bytes_read = 0;
size_t bytes_written = 0;
size_t bytes_backwards = 0;
while (bytes_written == bytes_read) {
bytes_read = fread(buf, 1, BUF_SIZE, file);
if (bytes_read > 0) {
// 写入管道
bytes_written = write(pipefd[1], buf, bytes_read);
if (bytes_written == -1) {
if (errno != EAGAIN) {
perror("write");
}
bytes_written = 0;
}
fprintf(file_record, "bytes_read=%ld, bytes_write=%ld\n", bytes_read, bytes_written);
} else {
// 文件读取完毕,关闭管道写端
(void)epoll_ctl(epoll_fd, EPOLL_CTL_DEL, pipefd[1], NULL);
close(pipefd[1]);
pipefd[1] = -1;
fclose(file_record);
fclose(file);
}
}
if (bytes_read > 0) {
bytes_backwards = bytes_read - bytes_written;
fseek(file, -bytes_backwards, SEEK_CUR);
}
} else if (events[i].data.fd == sfd) {
// 处理信号
struct signalfd_siginfo fdsi;
ssize_t s = read(sfd, &fdsi, sizeof(fdsi));
if (s != sizeof(fdsi)) {
perror("read signalfd");
running = 0; // 退出循环
break;
}
if (fdsi.ssi_signo == SIGCHLD) {
printf("Child process exited. Parent exiting...\n");
running = 0; // 退出循环
break;
}
}
}
}
// 清理资源
close(epoll_fd);
close(sfd);
if (pipefd[1] != -1) {
close(pipefd[1]);
fclose(file_record);
fclose(file);
}
// 使用 waitpid 等待子进程
int status;
pid_t child_pid = waitpid(pid, &status, 0);
if (child_pid == -1) {
perror("waitpid");
return 1;
}
if (WIFEXITED(status)) {
printf("Child process exited with status: %d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("Child process killed by signal: %d\n", WTERMSIG(status));
}
}
return 0;
}

浙公网安备 33010602011771号