为工作项排队:一次就足够

http://hi.baidu.com/guodxu/blog/item/5a374439d0af02f8b311c700.html

 

工作项是一个类型为 IO_WORKITEM 的结构(与工作例程和上下文区域关联)。驱动程序将一个工作项添加到系统工作队列;系统工作线程在以后移除该项并以 PASSIVE_LEVEL 运行关联的例程。但是,为已经在工作队列中的工作项排队将会破坏队列并导致驱动程序中出现很难检测的问题。

 

为工作项排队会怎样破坏工作队列?

  在内部,系统在一个双向链表中维护排队的工作项。当您调用 IoQueueWorkItem(或 ExQueueWorkItem)给队列添加一个项时,系统会检查链接。在可调式版本 (checked build) 中,如果项已经被链接到链表中,系统会发出断言。但是,在自由版本 (free build) 中,项被添加到链表,函数返回一个成功状态,并且链表被破坏。在以后的某些时候,破坏的链表可以导致严重和不可预期的问题。(在 Windows Vista 和 Windows Server 2008 中,试图对工作项进行第二次排队将导致错误检查 0xE4, WORKER_INVALID。)

  如果您熟悉延迟过程调用 (DPC) 队列,那么这种行为可能会让您感到惊讶,因为您可能期望两种类型的队列的操作方式相同。但是,DPC 队列的操作与工作队列不同。如果您试图为一个已经在 DPC 队列中的 DPC 排队,那么 KeInsertQueueDpc 会返回 FALSE。它不会对 DPC 进行第二次排队,并且 DPC 只运行一次。

 

什么时候会发生这种问题?

  如果驱动程序在给定的驱动程序例程每次运行时都对相同的工作项排队,那么它可能会在工作项仍然在队列中的时候对其再次排队。最可能发生的场景包括从以 DISPATCH_LEVEL 运行的例程对工作项排队,但是也可能在以 PASSIVE_LEVEL 排队的工作项上发生。

  例如,DPC 有时会在短时间(在驱动程序处理一系列中断期间)内被重复调用。DPC 以 DISPATCH_LEVEL 运行,通常对工作项排队以在以后以 PASSIVE_LEVEL 执行任务。但是,在单处理器机器上,只要队列中有任何 DPC 在等待以 DISPATCH_LEVEL 运行(或者只要有任何其他工作保持在 DISPATCH_LEVEL),系统就不会将 IRQL 降低到 PASSIVE_LEVEL。因此,如果 DPC 已经被第二次排队,那么系统工作线程将不会有机会运行工作项。在多处理器系统上,此问题出现的可能性比较低(虽然仍然可能发生)。只要 DPC 队列不为空,系统就会调度 DPC 来在下一个可用的处理器上运行。因此,不管在单处理器还是多处理器硬件上,DPC 都可以在系统工作线程运行之前多次运行。如果这样的话,原始的工作项仍然在队列中,并且在 DPC 试图再次排队时队列被破坏。

  要运行一个工作项,系统将其从队列中移除并重置链接。工作项被从队列中移除之后,您可以再次对其安全地排队。请记住,如果您第二次成功地对工作项排队,那么它可以在另一个处理器上或者在单处理器上的不同线程上下文中并发运行。您必须保护工作项例程使用的任何共享的、可写的数据。

 

在您的驱动程序中防止工作项队列破坏。

  因为这种错误依赖于计时、硬件活动和瞬态条件,所以在驱动程序中检测它会很困难。一个线索是由无效地址引起的错误检查,该无效地址在取消对工作项排队的函数位于堆栈上并且工作例程已经释放工作项时产生。但是,最好的方法是对您的驱动程序进行编码来防止这种问题。

  要避免破坏队列,驱动程序应该留意工作项是否在工作队列中。但是,驱动程序不能使用事件或相似的同步机制来确定工作例程何时开始,因为以 DISPATCH_LEVEL 运行的排队例程不能等待一个非零时期。也不能在循环中执行一个长度为零的等待。在单处理器系统上,长度为零的等待会导致死锁,因为工作线程只有在排队例程以 DISPATCH_LEVEL 运行完成后才能以 PASSIVE_LEVEL 运行。

  相反,驱动程序应该使用如下技术:

 
  • 驱动程序维护工作线程的一个工作列表。
 
  • 工作例程在每次运行时执行列表中的所有工作。

 

  • 当新工作到达时,驱动程序只在列表为空时才对工作项排队。

 

  • 如果排队例程需要工作线程已经处理的数据,那么驱动程序可以在每次对工作项排队时创建一个上下文或通过使用循环缓冲区或类似技术来维护更改历史记录。

  Windows DDK 的 SMB 电池示例展示了如何实现这种技术。设备发出警报来响应某些情况(例如电量不足),并且驱动程序维护一个挂起的警报的列表。驱动程序每次被通知警报时,SmbBattAlarm 函数都会添加一个记录到列表中。工作例程 (SmbBattWorkerThread) 从列表中读取警报记录并依次处理它们,直到列表为空。驱动程序并不会在每次被通知警报时对工作项排队。而只在新警报到达时列表为空的情况下才对工作项排队。

  下面的函数摘录自这个示例。随 Windows DDK 安装的完整示例位于 src/wdm/acpi/smbbatt/smbbatt.c。

VOID SmbBattAlarm ( IN PVOID    Context, IN UCHAR    Address, IN USHORT   Data )

 {

   PSMB_ALARM_ENTRY        newAlarmEntry;

  ULONG                   compState;
PSMB_BATT_SUBSYSTEM     subsystemExt  = (PSMB_BATT_SUBSYSTEM) Context;
// // Allocate a new list structure from non-paged pool. //
newAlarmEntry = ExAllocatePoolWithTag (NonPagedPool, sizeof (SMB_ALARM_ENTRY), 'StaB');

  if (!newAlarmEntry)

  {

     BattPrint (BAT_ERROR, ("SmbBattAlarm:couldn't" "allocate alarm structure/n"));

    return;

  }
newAlarmEntry->Data     = Data;

  newAlarmEntry->Address  = Address;

// // Add this alarm to the alarm list //
ExInterlockedInsertTailList( &subsystemExt->AlarmList, &newAlarmEntry->Alarms, &subsystemExt->AlarmListLock );

// // Add 1 to the WorkerActive value.

  // If this is the first alarm queue a work item. //
if (InterlockedIncrement (&subsystemExt->WorkerActive) == 1)

  {

     IoQueueWorkItem (subsystemExt->WorkerThread, SmbBattWorkerThread, DelayedWorkQueue, subsystemExt);

  }
BattPrint(BAT_TRACE, ("SmbBattAlarm:EXITING/n"));

}

VOID SmbBattWorkerThread ( IN PDEVICE_OBJECT   Fdo, IN PVOID            Context )
{

   PSMB_ALARM_ENTRY        alarmEntry;

   PLIST_ENTRY             nextEntry;

  ULONG                   selectorState;

  ULONG                   batteryIndex;
BOOLEAN                 charging = FALSE;

  BOOLEAN                 acOn = FALSE;
PSMB_BATT_SUBSYSTEM     subsystemExt = (PSMB_BATT_SUBSYSTEM) Context;

do {
// // Check to see if we have more alarms to process.

  // If so retrieve the next one and decrement the

  // worker active count. //
nextEntry = ExInterlockedRemoveHeadList( &subsystemExt->AlarmList, &subsystemExt->AlarmListLock );
ASSERT (nextEntry != NULL);
alarmEntry = CONTAINING_RECORD (nextEntry, SMB_ALARM_ENTRY, Alarms);

// // Code here handles the specific alarm conditions.

  // It's been deleted for this tip because it's

  // device-specific. //
. . .

// // Free the alarm structure //
ExFreePool (alarmEntry);
} while (InterlockedDecrement (&subsystemExt->WorkerActive) != 0);

BattPrint(BAT_TRACE, ("SmbBattWorkerThread:EXITING/n"));

}

 

  当新警报到达时,SmbBattAlarm 函数被调用。这个函数创建一个描述警报的记录,将其添加到警报列表,并递增 WorkerActive 变量,该变量跟踪列表中的记录数量。SmbBattWorkerThread 在从列表中移除记录时递减这个变量。因此,如果变量的值是零,那么驱动程序可以确定工作项不在队列中。驱动程序使用 InterlockedXxx 函数来递增和递减 WorkerActive 变量,因为 SmbBattAlarm 和 SmbBattWorkerThread 都会改变这个值。在这个例子中,SmbBattAlarm 不使用与 SmbBattWorkerThread 函数已经处理的警报相关的数据,因此既不需要循环缓冲区也不需要上下文。

 

您应该做什么?

了解工作项是否已经被排队。

设计工作例程来在它们每次运行时执行所有可用的工作。

如果您的驱动程序对一个来自 DPC(或其他 DISPATCH_LEVEL 例程)的工作项重复排队,那么不要使用事件或类似的同步机制来等待工作项完成。

如果工作项和对其排队的例程共享可写的数据,那么应该同步访问以确保两个例程都操作正确的数据。

如果排队例程需要工作项已经处理的数据,那么分配一个上下文或维护一个更改历史记录。

<think> 我们正在讨论一个设置为一分钟执行一次的定时任务,但该任务只进行了初始化,没有自动执行并更新值的问题。根据您提供的引用,可能涉及到数据库的作业调度设置(如Oracle的job_queue_processes和job_queue_interval)或者类似MongoDB的定时窗口设置。不过,您的问题更偏向于程序未按预期执行。 可能的原因及排查步骤: 1. **调度配置错误**: - 检查cron表达式或固定间隔设置是否正确。例如,在Spring中,`@Scheduled(cron = "0 * * * * ?")`表示每分钟的0秒执行(即每分钟一次)[^2]。 - 如果是固定延迟,注意`fixedDelay`和`fixedRate`的区别:`fixedRate`是上一次开始后间隔固定时间执行,而`fixedDelay`是上一次结束后间隔固定时间执行。如果任务执行时间超过1分钟,使用`fixedDelay`会导致实际间隔大于1分钟。 2. **调度器未启动或线程池问题**: - 确保调度器已经启动(在Spring中,需要添加`@EnableScheduling`注解)。 - 检查线程池大小:如果所有线程都被占用,新任务将无法执行。可以通过配置增加线程池大小。 3. **任务阻塞**: - 如果任务中有阻塞操作(如长时间的同步I/O操作),可能导致后续任务无法按时启动。检查任务内部逻辑。 4. **数据库作业调度设置(如Oracle)**: - 根据引用[1],Oracle中`job_queue_processes`参数决定了可以同时运行的作业数量。如果设置为0,则作业不会自动运行。需要确保该参数大于0(如您设置的39)[^1]。 - `job_queue_interval`参数(以秒为单位)决定了作业调度的刷新频率。如果设置过大(如默认的几秒变成10秒),可能影响作业的启动精度,但通常不会导致完全不执行。 5. **任务未提交或未激活**: - 如果是数据库作业(如Oracle的DBMS_JOB或DBMS_SCHEDULER),确保作业状态是启用的(ENABLED)并且已提交。 6. **日志与错误追踪**: - 检查应用日志或数据库作业日志,看是否有错误信息。例如,任务执行时抛出异常可能导致后续执行被跳过。 7. **系统时间与时区问题**: - 确保服务器时间正确,并且调度器使用的时区与应用预期一致。 8. **初始化后未触发**: - 有些调度框架需要显式启动调度器,或者第一次执行需要等待第一个周期到达。检查是否配置了初始延迟(initialDelay),如果设置了较长的初始延迟,则第一次执行会延后。 9. **配置覆盖问题**: - 如果使用配置文件,确保实际生效的配置是修改后的配置(如检查是否有多份配置、配置是否被覆盖)。 10. **依赖注入问题**(Spring等框架): - 定时任务所在的类是否被Spring容器管理(如是否有`@Component`等注解)? 针对您的情况,请按照以下步骤排查: **步骤1:确认任务配置** - 如果是Spring的`@Scheduled`,检查cron表达式或fixedRate/fixedDelay的值。 - 示例:`@Scheduled(fixedRate = 60*1000)`表示每隔60毫秒?注意单位是毫秒,所以60*1000才是60秒(1分钟)。 **步骤2:检查调度器状态** - 查看应用启动日志,确认调度器是否已启用。 **步骤3:查看任务执行日志** - 在任务方法开始处添加日志输出,观察是否有日志产生,以及执行时间戳是否符合预期。 **步骤4:检查数据库作业设置(如果使用数据库作业调度)** - 对于Oracle,查询参数: ```sql SELECT value FROM v$parameter WHERE name = 'job_queue_processes'; ``` 确保值大于0(如39)。 - 检查作业状态: ```sql SELECT job_name, enabled, state FROM user_scheduler_jobs; ``` **步骤5:模拟简单任务** - 创建一个最简单的定时任务(如输出当前时间),看是否能按预期执行,以排除任务内部逻辑的影响。 如果您提供了具体的代码或配置,我们可以更精确地分析。 §§相关问题§§ 1. 如何查看Spring定时任务的线程池使用情况? 2. 数据库作业调度(如Oracle)的常见故障排查方法有哪些? 3. 如何确保分布式环境下定时任务不会重复执行? </think> 根据您的描述,设置为一分钟执行一次的任务只进行了初始化但未自动执行并更新值,可能由以下原因导致: ### 原因分析及解决方案 1. **调度器未激活** - 数据库调度器可能未启动(如 Oracle 的 `job_queue_processes` 参数) - **检查**: ```sql SHOW PARAMETER job_queue_processes; -- Oracle 检查 SELECT name, value FROM v$parameter WHERE name = 'job_queue_processes'; ``` - **解决方案**: ```sql ALTER SYSTEM SET job_queue_processes=10 SCOPE=BOTH; -- 启用调度器进程[^1] ``` 2. **时间窗口配置错误** - 定时任务可能被限制在特定时间段执行(如 MongoDB 的 `activeWindow`) - **检查**: ```javascript use config db.settings.find({_id: "balancer"}); // MongoDB 检查时间窗口[^2] ``` - **解决方案**: 确保时间窗口覆盖当前时间: ```javascript db.settings.update({_id:"balancer"}, {$set:{activeWindow:{start:"00:00", stop:"23:59"}}}) ``` 3. **任务状态异常** - 任务可能处于暂停(PAUSED)或错误(BROKEN)状态 - **检查**(Oracle 示例): ```sql SELECT job_name, enabled, state FROM user_scheduler_jobs; SELECT job, broken, failures FROM user_jobs; ``` 4. **权限问题** - 执行用户缺少必要权限: ```sql GRANT CREATE JOB TO your_user; -- Oracle 授权 GRANT SCHEDULER_ADMIN TO your_user; ``` 5. **执行间隔冲突** - 当任务执行时间 > 调度间隔时: - `fixedRate`:新任务会排队导致延迟执行 - `fixedDelay`:会等待前次任务完成 - **解决方案**: ```java // Spring 示例:改用异步执行 @Async @Scheduled(fixedRate = 60_000) // 1分钟 public void scheduledTask() {...} ``` ### 诊断流程图 ```mermaid graph TD A[任务未执行] --> B{检查调度器状态} B -->|job_queue_processes=0| C[启用调度器进程] B -->|值正常| D{检查任务状态} D -->|状态异常| E[修复任务状态] D -->|状态正常| F{检查时间窗口} F -->|窗口不匹配| G[扩展时间窗口] F -->|窗口正常| H{检查执行日志} H -->|发现错误| I[修复任务逻辑] H -->|无错误| J[检查权限配置] ``` ### 推荐排查步骤 1. **验证调度器状态** ```sql -- Oracle SELECT * FROM v$scheduler_running_jobs; ``` 2. **查看任务错误日志** ```sql SELECT log_date, status, error# FROM dba_scheduler_job_run_details WHERE job_name = 'YOUR_JOB_NAME'; ``` 3. **手动触发测试** ```sql EXEC DBMS_SCHEDULER.RUN_JOB('YOUR_JOB_NAME'); -- Oracle 手动执行 ``` 4. **简化任务逻辑** 创建测试任务仅包含日志输出: ```sql BEGIN DBMS_OUTPUT.PUT_LINE('Test executed: '||SYSTIMESTAMP); END; ``` > **关键提示**:在 Oracle 中,`job_queue_interval`(默认秒级)需设置足够小的值(如10秒)才能支持1分钟级调度[^1]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值