Node.js监听SIG信号时createReadStream致process.exit()挂起
解决Node.js监听/dev/input设备时自定义信号监听器导致的进程无法退出问题
我之前也碰到过类似的坑——当处理/dev下的字符设备文件时,Node.js的ReadStream会陷入阻塞式系统调用,单纯调用stream.close()或者fs.closeSync()根本没法中断这个调用,直接导致事件循环卡壳,进程死活退不出去。结合你的代码,这里有几个关键修复点:
核心修复步骤
- 用
stream.destroy()替代stream.close():它能直接中断底层的阻塞读取,比close()的清理更彻底 - 清除定时任务:你的
setInterval会让Node.js事件循环一直保持活跃,必须手动清除 - 统一资源清理逻辑:把设备断开、进程退出时的资源释放抽成通用方法,避免遗漏
- 给事件循环留缓冲时间:确保流销毁、文件描述符关闭的操作完成后再退出进程
修改后的完整代码
const fs = require('fs'); let intervalId; // 保存定时器ID,方便后续关闭 let monitoredDevices = { devices: [], addDevice: function(path) { fs.open(path, 'r', (e, fd) => { if(e) return; let dev = { path: path, fd: fd, stream: null}; dev.stream = fs.createReadStream(null, { fd: dev.fd, mode: 'r'}); const deviceIndex = this.devices.push(dev) - 1; // 修正索引,确保splice能正确删除 if(dev.stream) { dev.stream.on('data', (data) => { console.log('DATA('+dev.path+'):', data); }); dev.stream.on('error', (e) => { if(e.code == 'ENODEV') { // 设备断开时自动清理 this.cleanupDevice(dev, deviceIndex); } }); } }); }, hasDevice: function(path) { return this.devices.some(x => x.path === path); }, // 新增统一的设备清理方法,复用逻辑 cleanupDevice: function(dev, index) { // 先销毁流,中断底层阻塞调用 if(dev.stream) { dev.stream.destroy(); dev.stream = null; } // 关闭文件描述符,容错处理避免报错 if(dev.fd !== null) { try { fs.closeSync(dev.fd); } catch (err) { console.error(`关闭设备${dev.path}的文件描述符失败:`, err); } dev.fd = null; } // 从设备列表中移除并清理对象 if(index !== undefined && index >= 0) { this.devices.splice(index, 1); } delete dev; } }; // 保存定时器ID,方便退出时清除 intervalId = setInterval(function() { fs.readdir('/dev/input/by-id', (err, files) => { if(err) return; files.forEach(file => { const fullPath = '/dev/input/by-id/' + file; try { if(fs.lstatSync(fullPath).isDirectory()) return; if(monitoredDevices.hasDevice(fullPath)) return; monitoredDevices.addDevice(fullPath); console.log('EVENT:', file); } catch (statErr) { console.error(`检查设备文件${file}失败:`, statErr); } }); }); }, 500); let _shutdownInProcess = false; function exitHandler(signal='') { if(!_shutdownInProcess) { _shutdownInProcess = true; console.log('\033[31m 捕获退出信号 ' + signal + ', 正在清理资源... \x1b[0m'); // 第一步:清除定时器,让事件循环有机会结束 clearInterval(intervalId); // 第二步:批量清理所有监听的设备 monitoredDevices.devices.forEach((dev, index) => { console.log(`正在关闭设备: ${dev.path} (文件描述符: ${dev.fd})`); monitoredDevices.cleanupDevice(dev, index); }); // 第三步:给事件循环留100ms处理清理回调,再强制退出 setTimeout(() => { process.exit(0); }, 100); } } // 保留你的信号监听器 process.on('SIGINT', () => exitHandler('SIGINT')); process.on('SIGUSR1', () => exitHandler('SIGUSR1')); process.on('SIGUSR2', () => exitHandler('SIGUSR2')); process.on('SIGTERM', () => exitHandler('SIGTERM'));
关键修复点说明
stream.destroy()的必要性
/dev/input下的设备是字符设备,Node.js读取这类设备时会发起阻塞的read()系统调用。close()只是关闭流的上层接口,无法中断底层的系统调用;而destroy()会直接终止这个调用,彻底释放相关资源。清除定时器的作用
Node.js的事件循环会因为存在活跃的定时器而持续运行,必须用clearInterval移除定时任务,否则即使所有设备资源都清理了,进程还是会卡在事件循环里。统一清理逻辑的好处
把设备断开和进程退出的清理逻辑抽成通用方法,既能避免重复代码,也能确保两种场景下的资源释放逻辑一致,不会出现遗漏。延迟退出的原因
流的销毁、文件描述符的关闭都是异步操作,给100ms的缓冲时间能让这些操作完成,避免进程在资源还没完全释放时就强制退出。
这样修改后,你按下CTRL+C或者执行killall node时,进程应该能正常退出,不会再出现终端挂起需要kill -9才能结束的情况。
内容的提问来源于stack exchange,提问作者AJ Cole
相关产品推荐
相关产品推荐

