当前位置: 首页 > article >正文

PostgreSQL的学习心得和知识总结(一百六十六)|深入理解PostgreSQL数据库之\watch元命令的实现原理


注:提前言明 本文借鉴了以下博主、书籍或网站的内容,其列表如下:

1、参考书籍:《PostgreSQL数据库内核分析》
2、参考书籍:《数据库事务处理的艺术:事务管理与并发控制》
3、PostgreSQL数据库仓库链接,点击前往
4、日本著名PostgreSQL数据库专家 铃木启修 网站主页,点击前往
5、参考书籍:《PostgreSQL中文手册》
6、参考书籍:《PostgreSQL指南:内幕探索》,点击前往


1、本文内容全部来源于开源社区 GitHub和以上博主的贡献,本文也免费开源(可能会存在问题,评论区等待大佬们的指正)
2、本文目的:开源共享 抛砖引玉 一起学习
3、本文不提供任何资源 不存在任何交易 与任何组织和机构无关
4、大家可以根据需要自行 复制粘贴以及作为其他个人用途,但是不允许转载 不允许商用 (写作不易,还请见谅 💖)
5、本文内容基于PostgreSQL master源码开发而成


深入理解PostgreSQL数据库之\watch元命令的实现原理

  • 文章快速说明索引
  • 功能使用背景说明
  • 功能实现源码解析



文章快速说明索引

学习目标:

做数据库内核开发久了就会有一种 少年得志,年少轻狂 的错觉,然鹅细细一品觉得自己其实不算特别优秀 远远没有达到自己想要的。也许光鲜的表面掩盖了空洞的内在,每每想到于此,皆有夜半临渊如履薄冰之感。为了睡上几个踏实觉,即日起 暂缓其他基于PostgreSQL数据库的兼容功能开发,近段时间 将着重于学习分享Postgres的基础知识和实践内幕。


学习内容:(详见目录)

1、\watch元命令的实现原理


学习时间:

2024年11月03日 15:17:37


学习产出:

1、PostgreSQL数据库基础知识回顾 1个
2、CSDN 技术博客 1篇
3、PostgreSQL数据库内核深入学习


注:下面我们所有的学习环境是Centos8+PostgreSQL master+Oracle19C+MySQL8.0

postgres=# select version();
                                     version                                     
---------------------------------------------------------------------------------
 PostgreSQL 18devel on x86_64-pc-linux-gnu, compiled by gcc (GCC) 13.1.0, 64-bit
(1 row)

postgres=#

#-----------------------------------------------------------------------------#

SQL> select * from v$version;          

BANNER        Oracle Database 19c EE Extreme Perf Release 19.0.0.0.0 - Production	
BANNER_FULL	  Oracle Database 19c EE Extreme Perf Release 19.0.0.0.0 - Production Version 19.17.0.0.0	
BANNER_LEGACY Oracle Database 19c EE Extreme Perf Release 19.0.0.0.0 - Production	
CON_ID 0


#-----------------------------------------------------------------------------#

mysql> select version();
+-----------+
| version() |
+-----------+
| 8.0.27    |
+-----------+
1 row in set (0.06 sec)

mysql>

功能使用背景说明

Linux的watch可以将命令的输出结果输出到标准输出设备,多用于周期性执行命令/定时执行命令补充说明。

[postgres@localhost:~/postgres → master]$ which watch
/usr/bin/watch
[postgres@localhost:~/postgres → master]$ 
[postgres@localhost:~/postgres → master]$ watch --help

Usage:
 watch [options] command

Options:
  -b, --beep             beep if command has a non-zero exit
  -c, --color            interpret ANSI color and style sequences
  -d, --differences[=<permanent>]
                         highlight changes between updates
  -e, --errexit          exit if command has a non-zero exit
  -g, --chgexit          exit when output from command changes
  -n, --interval <secs>  seconds to wait between updates
  -p, --precise          attempt run command in precise intervals
  -t, --no-title         turn off header
  -x, --exec             pass command to exec instead of "sh -c"

 -h, --help     display this help and exit
 -v, --version  output version information and exit

For more details see watch(1).
[postgres@localhost:~/postgres → master]$ date
2024年 11月 03日 星期日 18:05:04 PST
[postgres@localhost:~/postgres → master]$

该命令以周期性的方式执行给定的指令,指令输出以全屏方式显示。

[postgres@localhost:~/postgres → master]$ watch -d -n 10 date
[postgres@localhost:~/postgres → master]$

如上命令,打印结果如下所示:

在这里插入图片描述


psql中使用元命令\watch反复查看语句执行结果,如下:

postgres=# select version();
                                     version                                     
---------------------------------------------------------------------------------
 PostgreSQL 18devel on x86_64-pc-linux-gnu, compiled by gcc (GCC) 13.1.0, 64-bit
(1 row)

postgres=# select current_timestamp; \watch 10
       current_timestamp       
-------------------------------
 2024-11-03 18:16:56.346983-08
(1 row)

20241103日 星期日 181656(every 10s)

       current_timestamp       
-------------------------------
 2024-11-03 18:16:56.347301-08
(1 row)

20241103日 星期日 181706(every 10s)

      current_timestamp       
------------------------------
 2024-11-03 18:17:06.34787-08
(1 row)

20241103日 星期日 181716(every 10s)

       current_timestamp       
-------------------------------
 2024-11-03 18:17:16.347796-08
(1 row)

^C
postgres=#

如果觉得上面打印太多,可以使用手动快捷键 Ctrl + L 调用 clear 命令

-- 平时可以使用这个进行清屏

postgres=# \! clear

或者直接使用watch,如下:

[postgres@localhost:~/test/bin]$ watch -n 10 -d './psql -c "select current_timestamp;"'
[postgres@localhost:~/test/bin]$

在这里插入图片描述


功能实现源码解析

// src/bin/psql/help.c

	HELP0("  \\watch [[i=]SEC] [c=N] [m=MIN]\n"
		  "                         execute query every SEC seconds, up to N times,\n"
		  "                         stop if less than MIN rows are returned\n");
// src/bin/psql/command.c

/*
 * \watch -- execute a query every N seconds.
 * Optionally, stop after M iterations.
 * 
 * \watch — 每 N 秒执行一次查询。可选地,在 M 次迭代后停止。
 */
static backslashResult
exec_command_watch(PsqlScanState scan_state, bool active_branch,
				   PQExpBuffer query_buf, PQExpBuffer previous_buf)
{
	...
		/* If we parsed arguments successfully, do the command */
		if (success)
		{
			/* If query_buf is empty, recall and execute previous query */
			(void) copy_previous_query(query_buf, previous_buf);

			success = do_watch(query_buf, sleep, iter, min_rows);
		}

	...
}

在这里插入图片描述

需要注意的是,上面(左侧循环部分)是在解析参数。我们允许使用未标记的间隔或name=value,其中 name 来自集合 ('i', 'interval', 'c', 'count', 'm', 'min_rows')


我们今天学习的重点在于如上图高亮部分的do_watch函数,其他的不再赘述!

// 

/*
 * do_watch -- handler for \watch
 *
 * We break this out of exec_command to avoid having to plaster "volatile"
 * onto a bunch of exec_command's variables to silence stupider compilers.
 * 我们将其从 exec_command 中分离出来,
 * 以避免必须将“volatile”粘贴到 exec_command 的一堆变量上来使更愚蠢的编译器保持沉默。
 *
 * "sleep" is the amount of time to sleep during each loop, measured in
 * seconds.  The internals of this function should use "sleep_ms" for
 * precise sleep time calculations.
 * “sleep” 是每次循环期间休眠的时间,以秒为单位。
 * 此函数的内部应使用“sleep_ms”来精确计算休眠时间。
 */
static bool
do_watch(PQExpBuffer query_buf, double sleep, int iter, int min_rows);

do_watch函数在Windows和其他系统上面的处理和实现略有不同,我们这里优先按照Linux系统来看其具体的实现,如下:

在这里插入图片描述


第一步:信号和定时器初始化

	sigemptyset(&sigalrm_sigchld_sigint);
	sigaddset(&sigalrm_sigchld_sigint, SIGCHLD);
	sigaddset(&sigalrm_sigchld_sigint, SIGALRM);
	sigaddset(&sigalrm_sigchld_sigint, SIGINT);

	sigemptyset(&sigalrm_sigchld);
	sigaddset(&sigalrm_sigchld, SIGCHLD);
	sigaddset(&sigalrm_sigchld, SIGALRM);

	sigemptyset(&sigint);
	sigaddset(&sigint, SIGINT);

	/*
	 * Block SIGALRM and SIGCHLD before we start the timer and the pager (if
	 * configured), to avoid races.  sigwait() will receive them.
	 * 
	 * 在我们启动计时器和分页器(如果已配置)之前阻止 SIGALRM 和 SIGCHLD,以避免竞争。
	 * sigwait()将接收它们。
	 */
	sigprocmask(SIG_BLOCK, &sigalrm_sigchld, NULL);

定义信号集,用于捕获以下信号:

  • SIGCHLD:子进程结束信号
  • SIGALRM:定时器超时信号
  • SIGINT:用户中断信号 (Ctrl+C)

然后设置定时器,使用 setitimer 函数设置定时器的初始值和间隔值。如果定时器设置失败,记录错误并标记 done = true

	/*
	 * Set a timer to interrupt sigwait() so we can run the query at the
	 * requested intervals.
	 * 设置一个计时器来中断 sigwait(),以便我们可以按照要求的间隔运行查询。
	 */
	interval.it_value.tv_sec = sleep_ms / 1000;
	interval.it_value.tv_usec = (sleep_ms % 1000) * 1000;
	interval.it_interval = interval.it_value;
	if (setitimer(ITIMER_REAL, &interval, NULL) < 0)
	{
		pg_log_error("could not set timer: %m");
		done = true;
	}

setitimer 函数是一个用于设置定时器的系统调用,它允许程序在指定的时间间隔后执行信号操作。它是 UNIX-like 系统(如 Linux)中的标准函数之一。通过它程序可以指定一个定时器,并通过该定时器触发一个信号(通常是 SIGALRM)。

函数原型

#include <sys/time.h>

int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);

参数说明

  • which
    这是一个整数,表示定时器的种类。常用的值有:

    • ITIMER_REAL:该定时器会根据实际时间(系统时钟)进行计时。当定时器到期时,会发送一个 SIGALRM 信号给进程。
    • ITIMER_VIRTUAL:该定时器根据进程的虚拟时间(即进程实际执行的时间)进行计时。它不会受系统空闲时间的影响,只有在进程运行时计时。当定时器到期时,发送 SIGVTALRM 信号。
    • ITIMER_PROF:该定时器根据进程的实际时间和系统时间(包括用户时间和内核时间)进行计时。当定时器到期时,发送 SIGPROF 信号。
  • new_value
    这是一个指向 itimerval 结构体的指针,表示定时器的新值(即设置的时间间隔)。结构体定义如下:

  struct itimerval {
      struct timeval it_interval;  // 定时器间隔
      struct timeval it_value;     // 定时器初始时间
  };
  • it_value:定时器的初始延迟时间。当定时器开始时,等待 it_value 指定的时间(通常是秒和微秒),然后发送信号。如果 it_value 为 0,定时器立即启动。

  • it_interval:定时器的周期。如果 it_interval 为非零值,则定时器会不断地重复;如果为 0,则定时器只会触发一次。

  • old_value
    这是一个指向 itimerval 结构体的指针,用于保存当前定时器的值(如果存在)。如果不需要保存当前值,可以将其设置为 NULL

返回值

  • 成功时,setitimer 返回 0。
  • 失败时,返回 -1,并且 errno 被设置为相应的错误代码。常见的错误包括:
    • EINVAL:参数无效(如传递了无效的 which 值)。
    • ENOMEM:内存不足,无法分配定时器。

示例

以下是一个简单的示例,演示如何使用 setitimer 来设置一个定时器,每隔 1 秒钟触发一次 SIGALRM 信号:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/time.h>
#include <unistd.h>

void handler(int signum) {
    printf("Timer triggered!\n");
}

int main() {
    struct itimerval timer;

    // 设置定时器:1秒后触发,之后每隔1秒触发一次
    timer.it_value.tv_sec = 1;        // 初始延迟1秒
    timer.it_value.tv_usec = 0;       // 无微秒
    timer.it_interval.tv_sec = 1;     // 每隔1秒触发一次
    timer.it_interval.tv_usec = 0;    // 无微秒

    // 安装信号处理器
    signal(SIGALRM, handler);

    // 设置定时器
    if (setitimer(ITIMER_REAL, &timer, NULL) == -1) {
        perror("setitimer");
        exit(EXIT_FAILURE);
    }

    // 无限循环,等待信号触发
    while (1) {
        pause();  // 等待信号
    }

    return 0;
}

代码说明

  1. 信号处理函数:handler 用于处理定时器触发时发送的 SIGALRM 信号。当信号触发时,它会打印 “Timer triggered!”。
  2. 定时器设置:通过设置 itimerval 结构体的 it_valueit_interval 字段来指定定时器的行为。在本例中,定时器将在 1 秒后首次触发,然后每隔 1 秒重复触发。
  3. setitimer 调用:通过 setitimer 函数设置定时器,并指定 ITIMER_REAL,表示基于实际时间触发定时器。
  4. 等待信号:使用 pause 函数来挂起进程,直到接收到信号。

使用场景

  • 周期性任务:当你需要周期性地执行某个操作时,可以使用 setitimer 来设置定时器。例如,定时检查系统资源、定时刷新缓存等。
  • 延时操作:你可以设置定时器在特定的时间延迟后触发某个操作,比如等待 10 秒钟后发送一个信号来唤醒某个进程。
  • 性能分析:ITIMER_VIRTUALITIMER_PROF 可以用来监控程序在 CPU 上的执行时间,对于性能分析和调试非常有用。

注意事项

  • 定时器是基于进程的时间模型,可能会受到系统负载、CPU 调度等因素的影响。
  • 如果一个定时器在等待的时间期间发生了信号处理,可能会导致定时器行为的不确定性,特别是在多线程或多进程环境下。
  • setitimer 设置的是单个定时器,而不是定时任务队列。如果需要更复杂的定时任务调度机制,可能需要结合其他机制,如 timer_createalarm 或者基于事件循环的库。

第二步:对于分页器的分析,我后面会新开文档详细分析。今天的学习默认忽略掉pager,时间戳格式的选择 如下:

	/*
	 * Choose format for timestamps.  We might eventually make this a \pset
	 * option.  In the meantime, using a variable for the format suppresses
	 * overly-anal-retentive gcc warnings about %c being Y2K sensitive.
	 *  
	 * 选择时间戳格式。我们最终可能会将其设为 \pset 选项。
	 * 与此同时,使用变量来设置格式可以抑制 gcc 过于挑剔的警告,即 %c 是 Y2K 敏感的。
	 */
	strftime_fmt = "%c";

第三步:标题设置

	/*
	 * If there's a title in the user configuration, make sure we have room
	 * for it in the title buffer.  Allow 128 bytes for the timestamp plus 128
	 * bytes for the rest.
	 * 如果用户配置中有标题,请确保标题缓冲区有足够的空间。
	 * 时间戳留出 128 字节,其余部分留出 128 字节。
	 */
	user_title = myopt.title;
	title_len = (user_title ? strlen(user_title) : 0) + 256;
	title = pg_malloc(title_len);

第四步:循环执行查询,如下:

	/* Loop to run query and then sleep awhile */
	// 循环运行查询然后休眠一会儿
	while (!done)
	{
		time_t		timer;
		char		timebuf[128];

		/*
		 * Prepare title for output.  Note that we intentionally include a
		 * newline at the end of the title; this is somewhat historical but it
		 * makes for reasonably nicely formatted output in simple cases.
		 * 
		 * 准备输出标题。
		 * 请注意,我们故意在标题末尾添加一个换行符;这有点历史原因,但在简单情况下,它可以提供相当好的格式输出。
		 */
		timer = time(NULL);
		strftime(timebuf, sizeof(timebuf), strftime_fmt, localtime(&timer));

		if (user_title)
			snprintf(title, title_len, _("%s\t%s (every %gs)\n"),
					 user_title, timebuf, sleep_ms / 1000.0);
		else
			snprintf(title, title_len, _("%s (every %gs)\n"),
					 timebuf, sleep_ms / 1000.0);
		myopt.title = title;

		/* Run the query and print out the result */
		res = PSQLexecWatch(query_buf->data, &myopt, pagerpipe, min_rows);

		/*
		 * PSQLexecWatch handles the case where we can no longer repeat the
		 * query, and returns 0 or -1.
		 *  
		 * PSQLexecWatch 处理我们不能再重复查询的情况,并返回 0 或 -1。
		 */
		if (res <= 0)
			break;

		/* If we have iteration count, check that it's not exceeded yet */
		// 如果我们有迭代次数,请检查它是否尚未超出
		if (iter && (--iter <= 0))
			break;

		/* Quit if error on pager pipe (probably pager has quit) */
		if (pagerpipe && ferror(pagerpipe))
			break;

		/* Tight loop, no wait needed */
		// 紧密循环,无需等待
		if (sleep_ms == 0)
			continue;

#ifdef WIN32

		/*
		 * Wait a while before running the query again.  Break the sleep into
		 * short intervals (at most 1s); that's probably unnecessary since
		 * pg_usleep is interruptible on Windows, but it's cheap insurance.
		 * 
		 * 等待一段时间再运行查询。
		 * 将睡眠分成几个短间隔(最多 1 秒);这可能没有必要,因为 pg_usleep 在 Windows 上是可中断的,但这是一种廉价的保险。
		 */
		for (long i = sleep_ms; i > 0;)
		{
			long		s = Min(i, 1000L);

			pg_usleep(s * 1000L);
			if (cancel_pressed)
			{
				done = true;
				break;
			}
			i -= s;
		}
#else
		/* sigwait() will handle SIGINT. */
		sigprocmask(SIG_BLOCK, &sigint, NULL);
		if (cancel_pressed)
			done = true;

		/* Wait for SIGINT, SIGCHLD or SIGALRM. */
		while (!done)
		{
			int			signal_received;

			errno = sigwait(&sigalrm_sigchld_sigint, &signal_received);
			if (errno != 0)
			{
				/* Some other signal arrived? */
				if (errno == EINTR)
					continue;
				else
				{
					pg_log_error("could not wait for signals: %m");
					done = true;
					break;
				}
			}
			/* On ^C or pager exit, it's time to stop running the query. */
			// 在 ^C 或分页器退出时,就该停止运行查询了
			if (signal_received == SIGINT || signal_received == SIGCHLD)
				done = true;
			/* Otherwise, we must have SIGALRM.  Time to run the query again. */
			// 否则,我们肯定有 SIGALRM。是时候再次运行查询了。
			break;
		}

		/* Unblock SIGINT so that slow queries can be interrupted. */
		// 解除对 SIGINT 的阻止,以便可以中断慢速查询。
		sigprocmask(SIG_UNBLOCK, &sigint, NULL);
#endif
	}

解释一下上面逻辑,循环的主要逻辑包括:

  • 生成标题信息:包括用户标题、当前时间戳以及查询间隔。
  • 执行查询:调用 PSQLexecWatch 执行查询并输出结果。
  • 检查停止条件:
    • 查询失败或达到迭代上限。
    • 用户中断或取消操作 (SIGINT)。
  • 等待下一轮查询:
    • Windows 下用 pg_usleep 模拟定时器。
    • 非 Windows 系统通过 sigwait 等待定时信号或用户中断。

在上面的 do_watch 函数的实现中,涉及了多个信号处理相关的函数,尤其是 sigemptysetsigaddsetsigprocmasksigwait。这些函数用于管理和处理信号,确保在特定条件下能正确地处理中断和定时操作。下面是对每个函数的详细解释:

sigemptyset

sigemptyset(&sigalrm_sigchld_sigint);
  • 功能sigemptyset 用来初始化一个信号集,将其清空(即信号集中不包含任何信号)。
  • 参数sigset_t *set,指向要清空的信号集。
  • 用途:该函数清除信号集中的所有信号。通常用来初始化一个信号集,之后可以用 sigaddsetsigdelset 向其中添加或删除信号。

sigaddset

sigaddset(&sigalrm_sigchld_sigint, SIGCHLD);
sigaddset(&sigalrm_sigchld_sigint, SIGALRM);
sigaddset(&sigalrm_sigchld_sigint, SIGINT);
  • 功能sigaddset 将一个特定的信号添加到给定的信号集中。
  • 参数
    • sigset_t *set:要操作的信号集。
    • int signo:要添加到信号集的信号(例如 SIGALRMSIGINT 等)。
  • 用途:通过这个函数,我们可以将一个或多个信号添加到信号集中。例如,在 do_watch 函数中,使用 sigaddsetSIGALRM(定时器超时信号)、SIGCHLD(子进程结束信号)和 SIGINT(中断信号)添加到信号集 sigalrm_sigchld_sigint 中,这样可以在后续的信号处理过程中做相应处理。

sigprocmask 函数是用于操作信号掩码(signal mask)的系统调用,它可以阻塞或解除阻塞指定的信号集。信号掩码决定了哪些信号可以被传递给进程,以及哪些信号会被阻塞,直到它们被显式解除阻塞。

函数原型:

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

参数说明:

  1. how

    • 控制信号掩码的操作方式。它有三种取值:
      • SIG_BLOCK:将信号集 set 中的信号添加到当前的信号掩码中,阻塞这些信号。被阻塞的信号会被暂时“保存”,直到它们被解除阻塞。
      • SIG_UNBLOCK:将信号集 set 中的信号从当前的信号掩码中移除,解除阻塞。解除阻塞后,信号会立即交给信号处理程序或默认行为。
      • SIG_SETMASK:将信号掩码完全替换为 set 中的信号集。这意味着所有未在 set 中列出的信号都会被阻塞,set 中的信号会解除阻塞。
  2. set

    • 一个 sigset_t 类型的信号集,指定了要操作的信号集合。该集合包含一个或多个信号,可以使用 sigemptysetsigaddset 等函数来初始化和操作信号集。
  3. oldset

    • 一个 sigset_t 类型的指针,用于存储之前的信号掩码(在调用 sigprocmask 之前的掩码)。如果不需要保存原来的掩码,可以传入 NULL
    • SIG_BLOCKSIG_SETMASK 情况下,oldset 将保存旧的信号掩码。
    • 如果不关心先前的信号掩码,可以传入 NULL,不保存任何信息。

返回值:

  • 返回 0:表示成功。
  • 返回 -1:表示失败,并且设置 errno 来标明错误原因。常见的错误包括无效的信号集或无效的 how 值。

工作机制:

  • sigprocmask 允许我们 阻塞解除阻塞 信号,从而控制哪些信号可以影响进程的行为。阻塞信号意味着它们会在信号到达时被暂时“保存”,直到解除阻塞后才会进行处理。解除阻塞信号后,系统会处理这些信号(如果信号已经到达的话)。
  • sigprocmask 并不会中断正在处理的信号;它仅影响信号的传递和处理。
  • 阻塞信号集时,进程会“忽略”这些信号,直到显式解除阻塞。

常见用法:

  1. 阻塞信号
    如果你想阻止某些信号在进程中处理,可以使用 SIG_BLOCK

    sigset_t sigset;
    sigemptyset(&sigset);
    sigaddset(&sigset, SIGINT);  // 假设我们要阻塞 SIGINT
    
    sigprocmask(SIG_BLOCK, &sigset, NULL);  // 阻塞 SIGINT
    
  2. 解除阻塞信号
    如果你想解除对某些信号的阻塞,可以使用 SIG_UNBLOCK

    sigset_t sigset;
    sigemptyset(&sigset);
    sigaddset(&sigset, SIGINT);  // 假设我们要解除阻塞 SIGINT
    
    sigprocmask(SIG_UNBLOCK, &sigset, NULL);  // 解除阻塞 SIGINT
    
  3. 设置新的信号掩码
    如果你想完全替换当前的信号掩码,可以使用 SIG_SETMASK

    sigset_t sigset;
    sigemptyset(&sigset);
    sigaddset(&sigset, SIGTERM);  // 假设我们要阻塞 SIGTERM,解除阻塞其他信号
    
    sigprocmask(SIG_SETMASK, &sigset, NULL);  // 设置新的信号掩码,只阻塞 SIGTERM
    
  4. 保存旧的信号掩码
    如果你想在改变信号掩码之前保存当前的信号掩码,可以使用 oldset 参数:

    sigset_t sigset, oldset;
    sigemptyset(&sigset);
    sigaddset(&sigset, SIGINT);  // 阻塞 SIGINT
    
    sigprocmask(SIG_BLOCK, &sigset, &oldset);  // 阻塞 SIGINT,并保存旧的掩码
    // 在之后的代码中可以恢复旧的信号掩码
    sigprocmask(SIG_SETMASK, &oldset, NULL);
    

小结一下:

  • sigprocmask 允许你控制进程的信号掩码,阻塞或解除阻塞特定的信号。
  • 它是处理信号的关键工具,特别是在需要控制哪些信号能够中断当前进程的操作时。
  • 在使用 sigwait 等函数时,通常需要先通过 sigprocmask 阻塞信号,确保它们不会在等待期间触发默认处理程序。

sigwait

errno = sigwait(&sigalrm_sigchld_sigint, &signal_received);
  • 功能sigwait 用于阻塞进程,直到接收到指定信号集中的某个信号。sigwait 是一个阻塞式的调用,直到接收到指定的信号之一时,函数才会返回。
  • 参数
    • const sigset_t *set:指定要等待的信号集。进程会阻塞,直到集中的某个信号到达。
    • int *sig:指向整数的指针,用来保存接收到的信号编号。
  • 返回值:返回 0 表示成功,errno 会被设置为发生的错误代码(例如 EINTR 表示系统调用被信号中断)。
  • 用途sigwait 是一个同步等待函数,它会阻塞进程并等待一个指定信号集中的信号。当信号到达时,sigwait 返回,并将接收到的信号编号存储在 signal_received 中。在 do_watch 函数中,sigwait 用于等待 SIGALRM(定时器超时信号)、SIGCHLD(子进程结束信号)或 SIGINT(用户中断信号),从而控制查询的执行和中断。

这四个函数小结一下:

  • sigemptyset 初始化信号集,将其中的信号清空。
  • sigaddset 向信号集中添加信号,用来指定需要处理的信号。
  • sigprocmask 用来阻塞或解除阻塞指定信号,可以控制进程对信号的响应行为。
  • sigwait 阻塞进程,直到收到指定信号集中的某个信号。

do_watch 函数中,这些信号处理函数结合使用:

  1. 在查询执行期间,阻塞特定的信号,以避免信号在不合适的时候被处理(例如防止定时器和查询执行之间的竞态条件)。
  2. 使用 sigwait 等待信号,并根据信号的类型决定是否继续执行查询或退出程序(例如用户按下 Ctrl+C 或查询已达到最大迭代次数)。

第五步:资源清理和退出

	if (pagerpipe)
	{
		pclose(pagerpipe);
		restore_sigpipe_trap();
	}
	else
	{
		/*
		 * If the terminal driver echoed "^C", libedit/libreadline might be
		 * confused about the cursor position.  Therefore, inject a newline
		 * before the next prompt is displayed.  We only do this when not
		 * using a pager, because pagers are expected to restore the screen to
		 * a sane state on exit.
		 * 
		 * 如果终端驱动程序回显“^C”,libedit/libreadline 可能会对光标位置产生混淆。
		 * 因此,在显示下一个提示之前插入一个换行符。
		 * 我们只在不使用分页器时才这样做,因为分页器应该在退出时将屏幕恢复到正常状态。
		 */
		fprintf(stdout, "\n");
		fflush(stdout);
	}

#ifndef WIN32
	/* Disable the interval timer. */
	// 禁用间隔计时器
	memset(&interval, 0, sizeof(interval));
	setitimer(ITIMER_REAL, &interval, NULL);
	/* Unblock SIGINT, SIGCHLD and SIGALRM. */
	sigprocmask(SIG_UNBLOCK, &sigalrm_sigchld_sigint, NULL);
#endif

	pg_free(title);

do_watch 函数中,使用了三个不同的信号集来处理不同的信号,它们分别是:

  1. sigalrm_sigchld_sigint
  2. sigalrm_sigchld
  3. sigint

这些信号集的作用是为了确保在查询执行和信号处理过程中避免竞争条件和保证正确的信号处理顺序。每个信号集的设计都有其特定的目的。以下是每个信号集的详细分析:

  1. sigalrm_sigchld_sigint
sigemptyset(&sigalrm_sigchld_sigint);
sigaddset(&sigalrm_sigchld_sigint, SIGCHLD);
sigaddset(&sigalrm_sigchld_sigint, SIGALRM);
sigaddset(&sigalrm_sigchld_sigint, SIGINT);
  • 目的:该信号集包含了 SIGALRMSIGCHLDSIGINT 信号。
  • 作用
    • SIGALRM:定时器信号,在时间到达时触发,表示可以执行查询。
    • SIGCHLD:子进程结束信号,在子进程结束时触发,通常与分页器(如 popen 调用的进程)相关。
    • SIGINT:用户中断信号(Ctrl+C),表示用户想要中止操作。
  • 使用场景
    • 阻塞信号:在启动定时器并打开分页器之前,使用 sigprocmask(SIG_BLOCK, &sigalrm_sigchld, NULL) 阻塞这些信号。这避免了定时器和分页器进程在信号处理过程中产生竞态条件。
    • 信号等待:在 sigwait 等待这些信号时,系统可以等待定时器信号(SIGALRM),子进程结束信号(SIGCHLD)以及用户中断信号(SIGINT)来适当响应并决定是否继续执行查询或终止操作。
    • Unblock这三个信号。

  1. sigalrm_sigchld
sigemptyset(&sigalrm_sigchld);
sigaddset(&sigalrm_sigchld, SIGCHLD);
sigaddset(&sigalrm_sigchld, SIGALRM);
  • 目的:该信号集仅包含 SIGALRMSIGCHLD 信号。
  • 作用
    • SIGALRM:定时器信号,定时查询执行。
    • SIGCHLD:子进程结束信号,通常与分页器进程相关。
  • 使用场景
    • 阻塞信号:在定时器启动并在处理分页器时,使用 sigprocmask(SIG_BLOCK, &sigalrm_sigchld, NULL) 阻塞 SIGALRMSIGCHLD 信号。这避免了定时器到时和子进程退出时立即触发不必要的信号处理。
    • 信号等待:在 sigwait 中等待 SIGALRMSIGCHLD 信号,确保定时器触发时可以执行查询,分页器退出时可以结束循环。

  1. sigint
sigemptyset(&sigint);
sigaddset(&sigint, SIGINT);
  • 目的:该信号集仅包含 SIGINT(用户中断信号)。
  • 作用
    • SIGINT:用户发送的中断信号,通常是在终端按下 Ctrl+C 时触发。
  • 使用场景
    • 阻塞信号:在执行查询时,使用 sigprocmask(SIG_BLOCK, &sigint, NULL) 阻塞 SIGINT,避免在查询执行过程中用户中断查询。
    • 信号等待:在 sigwait 中等待 SIGINT 信号,用于捕获用户的中断请求并结束查询循环。
    • 恢复信号处理:在处理完信号后,解除阻塞 SIGINT,使得查询过程中的用户中断请求能被正确处理。

为什么使用多个信号集?

  1. 精确控制信号的阻塞与等待

    • 使用不同的信号集可以精确控制哪些信号在某些阶段需要被阻塞,哪些信号需要在信号处理器中等待。例如,在定时器设置之前需要阻塞 SIGALRMSIGCHLD,确保这些信号不会干扰程序的初始化过程。而 SIGINT 只在查询执行后需要被阻塞,避免用户在执行查询时中断查询。
  2. 避免信号竞争条件

    • 在有多个信号可能同时到达的情况下,通过分开信号集,确保不同的信号按预期顺序被处理。例如,SIGALRM 可以用于触发查询,而 SIGCHLD 用于检测分页器是否结束。这些信号的处理需要分开,以免不同信号相互干扰。
  3. 信号等待机制

    • 使用 sigwait 等待多个信号时,通过精细的信号集管理,可以确保程序能正确响应定时器超时、分页器进程结束以及用户中断等事件,而不会错过或错误响应某些信号。

小结一下,三个信号集的使用主要是为了:

  • 精确控制信号的阻塞与解阻:保证在某些阶段屏蔽特定信号,以防止信号干扰正常的查询执行流程。
  • 确保信号处理的正确性:通过分开处理 SIGALRMSIGCHLDSIGINT,避免它们之间的冲突或竞态条件。
  • 按预期顺序等待和处理信号:确保查询周期、分页器和用户中断信号的处理顺序不发生混乱,保证程序按预期行为进行。

通过这种方式,do_watch 函数能够稳健地进行定期查询并适应用户中断或分页器退出等事件。


上面的信号设置及处理比较复杂,而真正的query执行则内容篇幅讲解较少。下面是PSQLexecWatch的逻辑处理,有兴趣的小伙伴可以自行前去理解 本文不再赘述:

// src/bin/psql/common.c

/*
 * PSQLexecWatch
 *
 * This function is used for \watch command to send the query to
 * the server and print out the result.
 * 该函数用于\watch命令将查询发送到服务器并打印出结果。
 *
 * Returns 1 if the query executed successfully, 0 if it cannot be repeated,
 * e.g., because of the interrupt, -1 on error.
 * 如果查询成功执行则返回 1,如果无法重复(例如由于中断)则返回 0,如果出现错误则返回 -1。
 */
int
PSQLexecWatch(const char *query, const printQueryOpt *opt, FILE *printQueryFout, int min_rows)
{
	bool		timing = pset.timing;
	double		elapsed_msec = 0;
	int			res;

	if (!pset.db)
	{
		pg_log_error("You are currently not connected to a database.");
		return 0;
	}

	SetCancelConn(pset.db);

	res = ExecQueryAndProcessResults(query, &elapsed_msec, NULL, true, min_rows, opt, printQueryFout);

	ResetCancelConn();

	/* Possible microtiming output */
	if (timing)
		PrintTiming(elapsed_msec);

	return res;
}

http://www.kler.cn/a/512904.html

相关文章:

  • Java 中 HashSet 集合元素的去重
  • 2025寒假备战蓝桥杯01---朴素二分查找的学习
  • jvm_threads_live_threads 和 jvm_threads_states_threads 这两个指标之间存在一定的关系,但它们关注的维度不同
  • 数据结构——栈
  • .Net WebApi 中的Token参数校验
  • JavaScript语言的多线程编程
  • RabbitMq原生接口详解
  • C++编译时间可视化
  • 合并两个img栅格影像——arcgis
  • 解决GB28181对接RTSP倍速播放导致FFmpeg缓冲区满导致花屏问题
  • LangGraph:基于图结构的智能系统开发与实践
  • Java 大视界 -- 深入剖析 Java 在大数据内存管理中的优化策略(49)
  • 数据结构 链表1
  • 力扣hot100之螺旋矩阵
  • 深度学习篇---AnacondaLabelImg
  • Spring 6 第4章——原理:手写IoC
  • 《开源与合作:驱动鸿蒙Next系统中人工智能技术创新发展的双引擎》
  • STM32单片机学习记录(1.17)
  • Failed to load API definition
  • vue 如何判断每次进入都会刷新页面
  • 【WPF】WPF设置自定义皮肤主题
  • 数据结构初 - 链表
  • 第01章 11 分别使用DCMTK和gdcm库,解析DICOM文件系列的dicom标准数据信息
  • SpringBoot 搭建 SSE
  • Numpy基础01(Jupyter基本用法/Ndarray创建与基本操作)
  • vue.draggable 拖拽