util-linux分页程序more实现详解

从linux应用程序角度看,主要分析以下实现:

  1. 对入参,环境变量的处理
  2. 对标准输入,标准输出,标准错误输出的使用
  3. 对信号的处理,例如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 是一个由 getoptgetopt_long 函数使用的全局变量,它用于记录下一个要处理的命令行参数的索引。

    • optarg 是一个全局变量,用于存储 getoptgetopt_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/12
    

    Therefore 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 事件,避免了传统信号处理函数(如 signalsigaction)在多线程或异步编程中可能遇到的问题。
    • 避免竞态条件:使用 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();
        }
    }
    
  • 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);
    }
    

从分页程序角度看,主要分析以下实现:

  1. 如何使用控制消息控制终端
  2. 如何实现分页、分行输出,回滚查看等
  3. 如何在其他程序中调用分页程序实现分页,有哪些注意项

如何使用控制消息控制终端

  • 通过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_linefwrite(ctl->line_buf, length, 1, stdout)screendisplay_file分级实现从stdin读取并输出到stdout,应用缓存大小和终端一样为行缓存。

    more_poll, read_command, more_key_commandstderr获取用户命令并返回需要显示的行数, 如果是显示上一页等回滚操作的话,需要先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后子进程会继承父进程的哪些东西?
    信号掩码和信号处理函数,需要调用sigactionsigprocmask进行清理
    控制终端,需要调用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 a SIGTERM signal 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;
}
posted @ 2025-03-06 10:05  3yearleft  阅读(13)  评论(0)    收藏  举报