104-信号引起的竞争错误

本文探讨了使用alarm信号实现带超时IO函数时产生的竞争错误,并通过引入pselect函数来解决这一问题。通过对比传统的解决方案,展示了如何利用pselect函数有效避免在特定时间窗口内可能出现的alarm信号干扰。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 引言

之前我们学习过使用 alarm 信号这种奇技淫巧来实现带超时的 IO 函数,一直以来,我们写的这种程序都带有一个隐含的 bug.

举例来说,我们可能经常会写下面这样的代码:

alarm(2);
for(;;) {
  addrlen = sizeof(cliaddr); 
  // 1. 如果信号在 recvfrom 执行前产生
  nr = recvfrom(sockfd, buf, 4096, 0, (struct sockaddr*)&cliaddr, &addrlen);   
  // 2. 如果信号在 recvfrom 返回后产生                                        
  if (nr < 0) {
    if (errno == EINTR) break;      
    ERR_EXIT("recvfrom");
  }
  // ...
}

如果,alarm 信号在注释 1 和 2 两者描述的情况中产生,这意味着 recvfrom 永远都不会被 alarm 信号打断。看起来这种情况似乎不太可能发生,但是谁又能保证一定不会发生呢?哪怕只有万分之一的可能性,我们也得避免。

2. 修改方案

一个直观的想法就是在 recvfrom 调用前后将信号阻塞掉,看起来像下面这样(伪代码):

// 0. 添加阻塞
sigprocmask(SIG_BLOCK, SIGALRM);
alarm(2);
for(;;) {
  addrlen = sizeof(cliaddr); 
  // 1. 解除阻塞
  sigprocmask(SIG_UNBLOCK, SIGALRM);
  nr = recvfrom(sockfd, buf, 4096, 0, (struct sockaddr*)&cliaddr, &addrlen);   
  // 2. 添加阻塞
  sigprocmask(SIG_BLOCK, SIGALRM);

  if (nr < 0) {
    if (errno == EINTR) break;      
    ERR_EXIT("recvfrom");
  }
  // ...
}

看起来似乎没什么问题,但是,在 sigprocmask <-> recvfrom <-> sigprocmask 之间,仍然有一个非常小的时间窗(time window),我们仍然不能保证在此期间不产生 alarm 信号,要是 sigprocmask <-> recvfrom <-> sigprocmask 是一个整体那就完美了——换句话说,我们希望第一次 sigprocmask 和 recvfrom 能同时执行,在 recvfrom 返回时同时执行第二次 sigprocmask。

这似乎不太可能,但 linux 提供了额外的方案,那就是 pselect 函数。

2.2 pselect 函数

pselect 函数只比 select 函数多一个参数

int pselect(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timespec *timeout, const sigset_t *sigmask);

前面那些参数我们都非常熟悉了,这里只讲最后一个 sigmask,它是一个信号集。

该参数非常类似 sigsuspend 的参数,如果该 sigmask 不为 NULL,pselect 做两件事(原子的):

  • 将当前阻塞信号集替换成 sigmask 指定的信号集
  • 执行 select 函数

当 pselect 返回时,会恢复旧的阻塞信号集。

2.2 使用 pselect 修改竞争错误

sigset_t stalarm = {SIGALARM};
sigset_t stempty = {}; // 空
rfds = {sockfd};
maxfd = sockfd;

sigprocmask(SIG_BLOCK, &stalarm); // 先阻塞
alarm(2);
for(;;) {
  addrlen = sizeof(cliaddr);      
  FD_SET(sockfd, &rfds);
  // 当调用 pselect 时会阻塞,同时愿意接收 alarm 信号,这两步是原子的。
  // 直接有数据到来或者被信号打断。
  ret = pselect(maxfd + 1, &rfds, NULL, NULL, NULL, &stempty);
  if (ret < 0) {
    if (errno == EINTR) break;      
    ERR_EXIT("pselect");
  }
  nr = recvfrom(sockfd, buf, 4096, 0, (struct sockaddr*)&cliaddr, &addrlen);
  // ...
}

3. 实验

本文使用的程序工具托管在 gitos 上:http://git.oschina.net/ivan_allen/unp

本文实验程序路径:unp/program/broadcast/raceconditions

3.1 实验步骤

上一篇文章一样,在三台不同主机上开启 udp 服务器,然后在其中一台机器上发广播。

为了能演示出竞争错误,你可以在代码中加入 sleep 函数(当然,我们这个正确的版本是不会出现问题的):


这里写图片描述
图1 加入 sleep,等等信号发生


这里写图片描述
图2 仍然能够正常运行

你要是想看到有问题的版本,你可以在上一篇文章中相应的位置加入 sleep 函数,就能看到竞争错误的情况。

4. 总结

  • 知道信号带来的竞争错误
  • 知道为什么为产生竞争错误
  • 掌握 pselect 函数,解决竞争问题
<think>嗯,我现在要设计一个基于74192的倒计时电路,并且需要防止竞争-冒险的问题。首先,我得回忆一下74192是什么芯片,它的功能是什么。74192是一个同步十进制可逆计数器,可以进行加法和减法计数,并且有异步清零和并行加载的功能。倒计时的话,应该用的是它的减法计数模式。 接下来,竞争-冒险现象是怎么回事呢?我记得在数字电路中,当信号传输路径不同导致门电路输入端的变化有时间差,可能会产生短暂的错误输出,比如毛刺。这在计数器这样的时序电路中尤其需要注意,因为毛刺可能导致误触发,影响整个系统的稳定性。 那么,在基于74192的倒计时电路中,哪些地方可能会出现竞争-冒险呢?可能的环节包括计数器的输出变化、进位/借位信号的生成、或者外部控制信号的切换过程中。比如,当计数器从某个状态转换到另一个状态时,各个位的翻转速度可能不一致,导致中间状态出现短暂的错误值。如果这时候其他电路(比如显示译码器或者下一级计数器)捕捉到这个中间状态,就会产生错误操作。 那如何防止这种情况呢?常见的解决方法可能有以下几种: 1. 使用同步设计:确保所有信号的变化都在同一个时钟沿触发,减少不同步带来的问题。74192本身是同步计数器,所以时钟信号的质量和同步性很重要。需要确保时钟信号没有抖动,且布线合理,避免时钟偏移。 2. 添加滤波电容:在关键信号线上并联小容量电容,滤除高频毛刺。不过这可能影响信号边沿的速度,需要权衡。 3. 引入同步寄存器:在输出端加一级触发器,用时钟的边沿来锁存稳定的输出值,避免毛刺传递到后续电路。例如,在计数器输出到显示译码器之间加一个D触发器,用同一个时钟驱动,这样只有当信号稳定后才会被读取。 4. 合理设计状态转换逻辑:检查是否存在可能导致竞争的逻辑,例如多个输入信号同时变化的情况。可能的话,通过逻辑简化或调整时序来消除。 5. 利用74192的预置功能:在计数器加载新值时,确保加载信号(LOAD)在时钟稳定时有效,避免异步加载导致的竞争。 具体到74192的应用中,假设倒计时电路的工作流程是:预置初始值,开始倒计时,当计数到零时产生借位信号,可能触发其他操作。这里需要注意的竞争点可能出现在借位信号(Borrow Out)和计数器状态变化的时刻。例如,当计数器从0000借位变为9999(十进制模式下)时,各个位的翻转可能不同步,导致中间状态出现,此时若借位信号立即用于触发其他电路,可能会出现问题。 可能的解决方案包括: - 将借位信号通过一个触发器同步后再输出,确保在时钟的下一个上升沿时才传递借位信号,这时计数器状态已经稳定。 - 检查时钟信号的边沿是否干净,避免振铃或噪声引起的误触发。可能需要使用施密特触发器整形时钟信号- 在计数器输出到显示或其他逻辑电路之间加入缓冲器或锁存器,确保后续电路在计数器稳定后才读取数据。 此外,电源去耦也很重要,在芯片的电源引脚附近添加足够的去耦电容,减少电源噪声对芯片内部逻辑的影响,从而降低产生竞争-冒险的可能性。 另外,布局布线时,尽量缩短关键信号线的长度,特别是时钟线和借位信号线,减少信号传输延迟差异。如果使用多片74192级联扩展位数,需要确保级联时的进位/借位信号正确同步,可能每级的借位输出都经过同步处理后再传递给下一级的时钟输入。 总结一下,防止竞争-冒险的措施包括同步设计、信号滤波、添加锁存器、优化布局布线和电源去耦。具体到74192倒计时电路,重点处理借位信号和计数器输出的同步,确保状态变化稳定后再传递到后续电路。</think>基于74192的倒计时电路防止竞争-冒险的步骤如下: --- ### **1. 竞争-冒险的成因分析** 在倒计时电路中,竞争-冒险可能由以下场景引发: - **计数器状态跳变**:当计数器从某个状态(如`0000`)借位到下一状态(如`9999`)时,各比特位翻转速度不同,产生短暂中间状态。 - **异步信号触发**:借位输出(`Borrow Out`)或控制信号(如`LOAD`、`CLEAR`)的延迟差异可能导致毛刺。 - **级联电路时序不同步**:多片74192级联时,借位信号的传递延迟可能引发级间竞争--- ### **2. 关键防护措施** #### **2.1 同步设计优化** - **统一时钟边沿**:确保所有74192芯片和后续电路(如触发器、译码器)使用同一时钟信号,且时钟布线等长以减少偏移。 - **借位信号同步**:将`Borrow Out`信号通过D触发器同步后再传递至下级计数器或控制逻辑,确保仅在时钟上升沿更新状态。 **电路示例**: ```plaintext 74192 Borrow Out → D触发器(CLK同步) → 下级计数器CLK输入 ``` #### **2.2 输出稳定性增强** - **输出端添加锁存器**:在计数器输出与译码器/逻辑电路之间插入锁存器(如74HC573),用时钟控制锁存时机,仅在计数器稳定后读取数据。 **代码示例(逻辑描述)**: ```verilog always @(posedge CLK) begin latched_output <= counter_output; // 时钟上升沿锁存 end ``` #### **2.3 信号滤波与整形** - **RC低通滤波**:在关键控制信号(如`LOAD`)上并联10~100pF电容,滤除高频毛刺。 - **施密特触发器整形**:对时钟或异步输入信号使用施密特门(如74HC14),改善边沿质量。 #### **2.4 级联电路的时序设计** - **同步级联方式**:多片74192级联时,采用同步借位传递模式,而非异步模式。 **连接方式**: ```plaintext 第一片Borrow Out → 同步触发器 → 第二片CLK输入 ``` - **统一预置信号**:所有级联芯片的`LOAD`信号需同步操作,避免部分芯片提前加载导致的逻辑冲突。 #### **2.5 电源与布局优化** - **去耦电容**:在每片74192的VCC和GND引脚间添加0.1μF陶瓷电容,抑制电源噪声。 - **关键信号短路径**:缩短时钟、借位信号的布线长度,优先布局靠近目标芯片。 --- ### **3. 验证与调试** - **示波器检测毛刺**:观察计数器输出和借位信号波形,确认毛刺是否消除。 - **静态功能测试**:手动触发`LOAD`和`CLEAR`,验证计数器状态跳变是否符合预期。 - **动态压力测试**:在极限频率下运行电路,检查是否出现计数错误或显示抖动。 --- ### **4. 典型应用电路示例** ```plaintext CLK ────────┬──────► 74192(1) CLK │ ▲ Borrow Out ───► D触发器 ───► 74192(2) CLK 10μF电容 │ └─ 输出接74HC573锁存器 → 译码器/显示 去耦电容 ▲ └─────── 74192 VCC/GND ``` --- 通过上述设计,可有效抑制74192倒计时电路的竞争-冒险现象,确保系统稳定运行。实际应用中需结合具体场景调整滤波参数和同步时序。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值