vim如何感知编辑文件及终端窗口大小被外部修改的实现比较

intro

vim中编辑一个文件时,如果文件在vim外被修改,那么vim会提示:

W11: Warning: File "tsecer.md" has changed since editing started
See ":help W11" for more info.
[O]K, (L)oad File, Load File (a)nd Options:

这个提示通常相当及时:只要被修改就能看到提示。

但是vim编辑的时候是把整个文件都读取到内存的,这个读取到内存的文件内容在vim中被叫做buffer。也就是说,当vim打开文件之后,vim就已经不再依赖buffer对应的磁盘文件内容了。

此时vim应该如何才能及时的感受到磁盘文件被修改了呢?

inotify

Linux提供了监控文件inode变化的inotify机制,其中就包含了监控文件被修改的功能。

       IN_MODIFY (+)
             File was modified (e.g., write(2), truncate(2)).

直观上讲,这种机制看起来更“先进”一些。但是vim的设计是在尽可能多的平台上运行,而很多/大部分非linux平台并没有这个功能,所以最好不要依赖这个功能。

单独线程定时监测

如果单单为了这个功能就创建一个线程,看起来依然是有些过度设计。并且监测线程如何把监测结果通知到主线程并在屏幕下方显示出提示也是一个问题。

主循环监测

vim的整体处理逻辑和很多系统本质上都是相同的模型:在while循环中不断收集请求并完成相应服务。例如UE的主框架,Ngix等。

vim同样有自己的主循环,并且通常会在normal_cmd函数中通过safe_vgetc来读取用户输入。

在这个循环中,可以看到通过check_timestamps调用来检测是否有buffer对应的磁盘文件被修改。

这意味着:在用户进行一次有效输入之后,必定能检测到可能的文件修改。

/*
 * Main loop: Execute Normal mode commands until exiting Vim.
 * Also used to handle commands in the command-line window, until the window
 * is closed.
 * Also used to handle ":visual" command after ":global": execute Normal mode
 * commands, return when entering Ex mode.  "noexmode" is TRUE then.
 */
    void
main_loop(
    int		cmdwin,	    // TRUE when working in the command-line window
    int		noexmode)   // TRUE when return on entering Ex mode
{
///...
    while (!cmdwin || cmdwin_result == 0)
    {
	if (stuff_empty())
	{
	    did_check_timestamps = FALSE;
	    if (need_check_timestamps)
		check_timestamps(FALSE);
///...
		normal_cmd(&oa, TRUE);
///...
    }
}

切UI focus

但是使用过vim的都知道:及时在没有任何用户输入的情况下,vim同样能检测到文件的修改。那么这个时机又是在哪里呢?

因为相关代码比较简单,所以简单浏览下就可以看到,vim中触发检测的时机还有一个:就是在ui_focus_change函数。触发的时机是特定的终端序列。也就是,当用户把操作界面(从其它应用)切换到终端时,有些终端会向对方发送特定的焦点切入/焦点切出(KE_FOCUSGAINED/KE_FOCUSLOST)控制序列,这个序列并不会作为用户输入(因为这的确不是用户输入),但是可以在这个时机来触发一次(及时的)检测。

从注释上看,这个机制是伴随着多窗口的GUI出现的(Handle FocusIn/FocusOut event sequences reported by XTerm.)。

/*
 * Check if typebuf.tb_buf[] contains a terminal key code.
 * Check from typebuf.tb_buf[typebuf.tb_off] to typebuf.tb_buf[typebuf.tb_off
 * + "max_offset"].
 * Return 0 for no match, -1 for partial match, > 0 for full match.
 * Return KEYLEN_REMOVED when a key code was deleted.
 * With a match, the match is removed, the replacement code is inserted in
 * typebuf.tb_buf[] and the number of characters in typebuf.tb_buf[] is
 * returned.
 * When "buf" is not NULL, buf[bufsize] is used instead of typebuf.tb_buf[].
 * "buflen" is then the length of the string in buf[] and is updated for
 * inserts and deletes.
 */
    int
check_termcode(
    int		max_offset,
    char_u	*buf,
    int		bufsize,
    int		*buflen)
{
    ///...
#if defined(UNIX) || defined(VMS)
	/*
	 * Handle FocusIn/FocusOut event sequences reported by XTerm.
	 * (CSI I/CSI O)
	 */
	if (key_name[0] == KS_EXTRA
# ifdef FEAT_GUI
		&& !gui.in_use
# endif
	    )
	{
	    if (key_name[1] == KE_FOCUSGAINED)
	    {
		if (!focus_state)
		{
		    ui_focus_change(TRUE);
		    did_cursorhold = TRUE;
		    focus_state = TRUE;
		}
		key_name[1] = (int)KE_IGNORE;
	    }
	    else if (key_name[1] == KE_FOCUSLOST)
	    {
		if (focus_state)
		{
		    ui_focus_change(FALSE);
		    did_cursorhold = TRUE;
		    focus_state = FALSE;
		}
		key_name[1] = (int)KE_IGNORE;
	    }
	}
#endif
///...
}

窗口变化如何感知

经常使用vim(或者bash)也可以感受到当终端大小变化之后,vim的行显示也会相应变化。这意味着本地终端大小变化之后,远端vim也可以感知到变化。那么这个变化是不是也是通过和焦点类似的通过特殊的控制序列实现的呢?

vim的处理

测试vim的代码就会发现并非如此。vim的流程是在SIGWINCH信号的处理函数中设置了窗口大小变化的标志为:


/*
 * We need correct prototypes for a signal function, otherwise mean compilers
 * will barf when the second argument to signal() is ``wrong''.
 * Let me try it with a few tricky defines from my own osdef.h	(jw).
 */
#if defined(SIGWINCH)
    static void
sig_winch SIGDEFARG(sigarg)
{
    // this is not required on all systems, but it doesn't hurt anybody
    mch_signal(SIGWINCH, sig_winch);
    do_resize = TRUE;
}
#endif

    static void
set_signals(void)
{
#if defined(SIGWINCH)
    /*
     * WINDOW CHANGE signal is handled with sig_winch().
     */
    mch_signal(SIGWINCH, sig_winch);
#endif

然后通过ioctl从tty终端中读取更新后窗口大小:

/*
 * Try to get the current window size:
 * 1. with an ioctl(), most accurate method
 * 2. from the environment variables LINES and COLUMNS
 * 3. from the termcap
 * 4. keep using the old values
 * Return OK when size could be determined, FAIL otherwise.
 */
    int
mch_get_shellsize(void)
{
    long	rows = 0;
    long	columns = 0;
    char_u	*p;

    /*
     * 1. try using an ioctl. It is the most accurate method.
     *
     * Try using TIOCGWINSZ first, some systems that have it also define
     * TIOCGSIZE but don't have a struct ttysize.
     */
# ifdef TIOCGWINSZ
    {
	struct winsize	ws;
	int fd = 1;

	// When stdout is not a tty, use stdin for the ioctl().
	if (!isatty(fd) && isatty(read_cmd_fd))
	    fd = read_cmd_fd;
	if (ioctl(fd, TIOCGWINSZ, &ws) == 0)
	{
	    columns = ws.ws_col;
	    rows = ws.ws_row;
#  ifdef FEAT_EVAL
	    ch_log(NULL, "Got size with TIOCGWINSZ: %ld x %ld", columns, rows);
#  endif
	}
    }

内核的处理

那么这个SIGWINCH信号从那里来的呢?查看内核的代码可以看到,在tty窗口大小变化的时候,内核会向tty的读取进程发送这个信号:


/**
 * tty_do_resize - resize event
 * @tty: tty being resized
 * @ws: new dimensions
 *
 * Update the termios variables and send the necessary signals to peform a
 * terminal resize correctly.
 */
int tty_do_resize(struct tty_struct *tty, struct winsize *ws)
{
	struct pid *pgrp;

	/* Lock the tty */
	mutex_lock(&tty->winsize_mutex);
	if (!memcmp(ws, &tty->winsize, sizeof(*ws)))
		goto done;

	/* Signal the foreground process group */
	pgrp = tty_get_pgrp(tty);
	if (pgrp)
		kill_pgrp(pgrp, SIGWINCH, 1);
	put_pid(pgrp);

	tty->winsize = *ws;
done:
	mutex_unlock(&tty->winsize_mutex);
	return 0;
}

/**
 *	pty_resize		-	resize event
 *	@tty: tty being resized
 *	@ws: window size being set.
 *
 *	Update the termios variables and send the necessary signals to
 *	peform a terminal resize correctly
 */

static int pty_resize(struct tty_struct *tty,  struct winsize *ws)
{
	struct pid *pgrp, *rpgrp;
	struct tty_struct *pty = tty->link;

	/* For a PTY we need to lock the tty side */
	mutex_lock(&tty->winsize_mutex);
	if (!memcmp(ws, &tty->winsize, sizeof(*ws)))
		goto done;

	/* Signal the foreground process group of both ptys */
	pgrp = tty_get_pgrp(tty);
	rpgrp = tty_get_pgrp(pty);

	if (pgrp)
		kill_pgrp(pgrp, SIGWINCH, 1);
	if (rpgrp != pgrp && rpgrp)
		kill_pgrp(rpgrp, SIGWINCH, 1);

	put_pid(pgrp);
	put_pid(rpgrp);

	tty->winsize = *ws;
	pty->winsize = *ws;	/* Never used so will go away soon */
done:
	mutex_unlock(&tty->winsize_mutex);
	return 0;
}

sshd的处理

在远程终端场景中,应该是通过ssh之类的协议告诉远端sshd进程终端大小变化,然后sshd调用tty的设置窗口大小接口来修改tty的大小,同时触发内核向vim发送SIGWINCH信号。

例如,在openssh的代码中可以看到对窗口变化协议的处理,并且在处理函数中

static int
  session_window_change_req(struct ssh *ssh, Session *s)
  {
          int r;
  
          if ((r = sshpkt_get_u32(ssh, &s->col)) != 0 || 
              (r = sshpkt_get_u32(ssh, &s->row)) != 0 || 
              (r = sshpkt_get_u32(ssh, &s->xpixel)) != 0 || 
              (r = sshpkt_get_u32(ssh, &s->ypixel)) != 0 || 
              (r = sshpkt_get_end(ssh)) != 0)
                  sshpkt_fatal(ssh, r, "%s: parse packet", __func__);
          pty_change_window_size(s->ptyfd, s->row, s->col, s->xpixel, s->ypixel);
          return 1;
  }
 /* Changes the window size associated with the pty. */
  
  void
  pty_change_window_size(int ptyfd, u_int row, u_int col,
          u_int xpixel, u_int ypixel)
  {
          struct winsize w;
  
          /* may truncate u_int -> u_short */
          w.ws_row = row;
          w.ws_col = col;
          w.ws_xpixel = xpixel;
          w.ws_ypixel = ypixel;
          (void) ioctl(ptyfd, TIOCSWINSZ, &w);
  }

而对TIOCSWINSZ的处理则执行到内核的resize并触发SIGWINCH信号。

outro

从这个终端焦点切入/切出的控制序列可以看到:为了实现特定的功能,软件设计“悄悄的”引入了很多巧妙的机制。它们在兼容之前机制的同时,提供了更友好的操作界面。

之前内核或者ssh的代码看起来平平无奇,但是当它们协作完成一些有用的功能时,是不是代码立刻鲜活的有生命起来了?😃

posted on 2025-12-02 22:41  tsecer  阅读(0)  评论(0)    收藏  举报

导航