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 --helpUsage:watch [options] commandOptions:-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 exitFor 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 10current_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.cHELP0("  \\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);elsesnprintf(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);  // 假设我们要阻塞 SIGINTsigprocmask(SIG_BLOCK, &sigset, NULL);  // 阻塞 SIGINT
    
  2. 解除阻塞信号
    如果你想解除对某些信号的阻塞,可以使用 SIG_UNBLOCK

    sigset_t sigset;
    sigemptyset(&sigset);
    sigaddset(&sigset, SIGINT);  // 假设我们要解除阻塞 SIGINTsigprocmask(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);  // 阻塞 SIGINTsigprocmask(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);
#endifpg_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.mzph.cn/diannao/67692.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

当当网书籍信息爬虫

1.基本理论 1.1概念体系 网络爬虫又称网络蜘蛛、网络蚂蚁、网络机器人等&#xff0c;可以按照我们设置的规则自动化爬取网络上的信息&#xff0c;这些规则被称为爬虫算法。是一种自动化程序&#xff0c;用于从互联网上抓取数据。爬虫通过模拟浏览器的行为&#xff0c;访问网页…

计算机网络 (50)两类密码体制

前言 计算机网络中的两类密码体制主要包括对称密钥密码体制&#xff08;也称为私钥密码体制、对称密码体制&#xff09;和公钥密码体制&#xff08;也称为非对称密码体制、公开密钥加密技术&#xff09;。 一、对称密钥密码体制 定义&#xff1a; 对称密钥密码体制是一种传…

【PowerQuery专栏】实现JSON数据的导入

Json 格式数据是在互联网数据格式传输使用的非常频繁的一类数据,图7.44为Json数据格式中比较典型的数据格式。 PowerQuery进行Json数据解析使用的是Json.Document进行数据解析,Json.Document目前有2个参数。 参数1为内容数据,数据类型为二进制类型,值为需要解析的Json数据参…

软件授权管理中的软件激活向导示例

软件激活向导示例 在软件许可中&#xff0c;提供许可应该是简单和安全的。这适用于想要在中央许可证服务器上创建新许可证的软件开发人员&#xff0c;也适用于需要在其设备上获得许可证的最终用户。如果所讨论的系统有互联网连接&#xff0c;或是暂时的连接&#xff0c;就可以…

模型部署工具01:Docker || 用Docker打包模型 Build Once Run Anywhere

Docker 是一个开源的容器化平台&#xff0c;可以让开发者和运维人员轻松构建、发布和运行应用程序。Docker 的核心概念是通过容器技术隔离应用及其依赖项&#xff0c;使得软件在不同的环境中运行时具有一致性。无论是开发环境、测试环境&#xff0c;还是生产环境&#xff0c;Do…

【云岚到家】-day03-门户缓存方案选择

【云岚到家】-day03-门户缓存方案选择 1.门户常用的技术方案 什么是门户 说到门户马上会想到门户网站&#xff0c;中国比较早的门户网站有新浪、网易、搜狐、腾讯等&#xff0c;门户网站为用户提供一个集中的、易于访问的平台&#xff0c;使他们能够方便地获取各种信息和服务…

网络安全VS数据安全

关于网络安全和数据安全&#xff0c;我们常听到如下两种不同声音&#xff1a; 观点一&#xff1a;网络安全是数据安全的基础&#xff0c;把当年做网络安全的那一套用数据安全再做一遍。 观点二&#xff1a;数据安全如今普遍以为是网络安全的延伸&#xff0c;实际情况是忽略数据…

Python----Python高级(文件操作open,os模块对于文件操作,shutil模块 )

一、文件处理 1.1、文件操作的重要性和应用场景 1.1.1、重要性 数据持久化&#xff1a; 文件是存储数据的一种非常基本且重要的方式。通过文件&#xff0c;我们可 以将程序运行时产生的数据永久保存下来&#xff0c;以便将来使用。 跨平台兼容性&#xff1a; 文件是一种通用…

Ubuntu 24.04 LTS 安装 Docker Desktop

Docker 简介 Docker 简介和安装Ubuntu上学习使用Docker的详细入门教程Docker 快速入门Ubuntu版&#xff08;1h速通&#xff09; Docker 安装 参考 How to Install Docker on Ubuntu 24.04: Step-by-Step Guide。 更新系统和安装依赖 在终端中运行以下命令以确保系统更新并…

python+pygame+pytmx+map editor开发一个tiled游戏demo 05使用object层初始化player位置

代码 import mathimport pygame# 限制物体在屏幕内 import pytmxdef limit_position_to_screen(x, y, width, height):"""限制物体在屏幕内"""x max(0, min(x, SCREEN_WIDTH - width)) # 限制x坐标y max(0, min(y, SCREEN_HEIGHT - height))…

函数递归的介绍

1.递归的定义 在C语言中&#xff0c;递归就是函数自己调用自己 上面的代码就是 main 函数在函数主体内 自己调用自己 但是&#xff0c;上面的代码存在问题&#xff1a;main 函数反复地 自己调用自己 &#xff0c;不受限制&#xff0c;停不下来。 最终形成死递归&#xff0c;…

【PCIe 总线及设备入门学习专栏 6.1 -- PCIe MCTP】

文章目录 1 什么是 MCTP?2 MCTP 消息在 PCIe 中的传输特点3 PCIe MCTP 的局限性(1) 出站(Outbound)MCTP 消息分解的限制(2) 入站(Inbound)MCTP 消息组装的限制4 MCTP 消息的实际使用流程发送端处理流程接收端处理流程5 实际使用场景例 1:管理命令传输例 2:监控数据报告例…

Text2Sql:开启自然语言与数据库交互新时代(30/30)

一、Text2Sql 简介 在当今数字化时代&#xff0c;数据处理和分析的需求日益增长。对于众多非技术专业人员而言&#xff0c;数据库操作的复杂性常常成为他们获取所需信息的障碍。而 Text2Sql 技术的出现&#xff0c;为这一问题提供了有效的解决方案。 Text2Sql&#xff0c;即文…

【机器学习实战入门项目】MNIST数字分类机器学习项目

Python 深度学习项目&#xff1a;手写数字识别 为了使机器更加智能&#xff0c;开发者们正在深入研究机器学习和深度学习技术。人类通过不断练习和重复来学习执行某项任务&#xff0c;从而记住如何完成这些任务。然后&#xff0c;大脑中的神经元会自动触发&#xff0c;他们能够…

快速入门:如何注册并使用GPT

文章目录 ProtonMail邮箱步骤 1&#xff1a;访问Proton官网步骤 2&#xff1a;创建ProtonMail账户步骤 3&#xff1a;选择注册免费账户步骤 4&#xff1a;填写邮箱地址和手机号&#xff08;可选&#xff09;步骤 5&#xff1a;邮箱验证&#xff08;必须进行验证&#xff09;步骤…

C# 给定欧氏平面中的一组线可以形成的三角形的数量

给定欧氏平面中的一组线可以形成的三角形的数量(Number of Triangles that can be formed given a set of lines in Euclidean Plane) 给定欧氏平面上的 n 条不同直线的集合 L {l 1 , l 2 , ………, l n }。第i 条直线由形式为 a i x b i y c i的方程给出。求出可以使用集合…

从CRUD到高级功能:EF Core在.NET Core中全面应用(三)

目录 IQueryable使用 原生SQL使用 实体状态跟踪 全局查询筛选器 并发控制使用 IQueryable使用 在EFCore中IQueryable是一个接口用于表示可查询的集合&#xff0c;它继承自IEnumerable但具有一些关键的区别&#xff0c;使得它在处理数据库查询时非常有用&#xff0c;普通集…

uniapp中h5的微应用解决办法

考虑过用wujie&#xff0c;参考官网Vue组件封装 | 无界的教程&#xff0c;虽然没报错&#xff0c;但是子应用的vue节点根本没挂载上&#xff0c;不知道什么原因&#xff0c;如下图所示 后面采用iframe方式将子应用导入进来&#xff1a; 父应用&#xff1a; <template>&…

数据分析及应用:经营分析中的综合指标解析与应用

目录 1. 市场份额(Market Share) 2. 客户获取成本(Customer Acquisition Cost, CAC) 3. 客户生命周期价值(Customer Lifetime Value, CLV) 4. 客户留存率(Customer Retention Rate, CRR) 5. 净推荐值(Net Promoter Score, NPS) 6. 转化率(Conversion Rate) …

【Java 学习】深度剖析Java多态:从向上转型到向下转型,解锁动态绑定的奥秘,让代码更优雅灵活

&#x1f4ac; 欢迎讨论&#xff1a;如对文章内容有疑问或见解&#xff0c;欢迎在评论区留言&#xff0c;我需要您的帮助&#xff01; &#x1f44d; 点赞、收藏与分享&#xff1a;如果这篇文章对您有所帮助&#xff0c;请不吝点赞、收藏或分享&#xff0c;谢谢您的支持&#x…