Chronaxis用户手册English
本页目录

chronaxis.batch.process_tree

源码基线:8904c2f744951857659abd40aaa3535d57faf53c

1"""管理本次工作进程树;Windows使用作业对象,Linux使用已确认身份的pidfd进程组。23父进程持有本次任务的作业句柄或pidfd;工作进程完成登记后才运行用户计划。4关闭时回收本任务的剩余后代,不扫描或接管其他任务的进程。5"""67import ctypes8from ctypes import wintypes9import os10import signal11import sys12from time import monotonic, sleep1314from ..results.budget import _cleanup_owner151617class _BasicLimits(ctypes.Structure):18    """对应Windows基本作业限制结构;仅设置关闭句柄时结束整个作业。"""1920    _fields_ = [('per_process', ctypes.c_longlong), ('per_job', ctypes.c_longlong),21                ('flags', wintypes.DWORD), ('min_ws', ctypes.c_size_t),22                ('max_ws', ctypes.c_size_t), ('active_limit', wintypes.DWORD),23                ('affinity', ctypes.c_size_t), ('priority', wintypes.DWORD),24                ('scheduling', wintypes.DWORD)]252627class _ExtendedLimits(ctypes.Structure):28    """对应Windows扩展作业限制,未使用的资源额度保持零值。"""2930    _fields_ = [('basic', _BasicLimits), ('io', ctypes.c_ulonglong * 6),31                ('process_memory', ctypes.c_size_t), ('job_memory', ctypes.c_size_t),32                ('peak_process', ctypes.c_size_t), ('peak_job', ctypes.c_size_t)]333435class ProcessTree:36    """持有一个工作进程树的关闭责任;Linux在GO前完成双向身份核验。"""3738    def __init__(self, pid, *, registration=None, process=None):39        """登记实际PID。Windows沿原pid接口;Linux另需registration和借用process。返回新对象;平台、握手或系统错误保原异常。"""40        self._linux = sys.platform.startswith('linux')41        if self._linux:42            self._register_linux(pid, registration, process)43            return44        if os.name != 'nt':45            raise NotImplementedError('进程树管理需要Windows或Linux 6.9及以上')46        api = ctypes.WinDLL('kernel32', use_last_error=True)47        api.CreateJobObjectW.argtypes = [ctypes.c_void_p, wintypes.LPCWSTR]48        api.CreateJobObjectW.restype = wintypes.HANDLE49        api.SetInformationJobObject.argtypes = [wintypes.HANDLE, ctypes.c_int, ctypes.c_void_p, wintypes.DWORD]50        api.SetInformationJobObject.restype = wintypes.BOOL51        api.OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD]52        api.OpenProcess.restype = wintypes.HANDLE53        api.AssignProcessToJobObject.argtypes = [wintypes.HANDLE, wintypes.HANDLE]54        api.AssignProcessToJobObject.restype = wintypes.BOOL55        api.CloseHandle.argtypes = [wintypes.HANDLE]56        api.CloseHandle.restype = wintypes.BOOL57        # 两个实际句柄先登记;构造尚未返回也必须能从原异常恢复关闭。58        self._api, self._handle, self._process_handle = api, None, None59        self._closed = False60        try:61            self._handle = api.CreateJobObjectW(None, None)62            if not self._handle:63                raise ctypes.WinError(ctypes.get_last_error())64            limits = _ExtendedLimits()65            limits.basic.flags = 0x2000  # JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;不授予后代脱离权限。66            if not api.SetInformationJobObject(self._handle, 9, ctypes.byref(limits), ctypes.sizeof(limits)):67                raise ctypes.WinError(ctypes.get_last_error())68            self._process_handle = api.OpenProcess(0x0101, False, pid)  # SET_QUOTA | TERMINATE。69            if not self._process_handle or not api.AssignProcessToJobObject(self._handle, self._process_handle):70                raise ctypes.WinError(ctypes.get_last_error())71            # 登记成功后只关本次借助的进程句柄,保留作业句柄直到工作池结束。72            self._close_process_handle()73        except BaseException as error:74            _cleanup_owner(self, error, 'process_tree_cleanup_owner', 'Windows进程树构造')75            raise7677    def _register_linux(self, pid, registration, process):78        """通过本次进程的双向握手登记pidfd。7980        参数: pid为实际PID;registration拥有启动通道;process为借用的Process。81        返回: None;能力与身份核验完成后才发GO。82        异常: 无效身份、握手或系统错误原样传播;未确认fd只关闭,不发送组信号。83        """84        self._fd = None85        self._confirmed = False86        self._closed = False87        self._process = process88        try:89            if registration is None or process is None or type(pid) is not int or (pid != process.pid) or (pid <= 0):90                raise ValueError('Linux进程树只能登记本次启动握手对应的Process')91            registration.close_child()92            expected = b'READY' + registration.token + pid.to_bytes(8, 'big') + pid.to_bytes(8, 'big')93            if registration.receive(process) != expected or pid == os.getpgrp():94                raise ValueError('工作进程组READY身份不匹配')95            # 只打开临时fd;收到打开之后的ACK以前不能向该fd发送组信号。96            self._fd = os.pidfd_open(pid)97            registration.parent.send_bytes(b'PROBE' + registration.token)98            if registration.receive(process) != b'ACK' + registration.token:99                raise ValueError('工作进程组ACK身份不匹配')100            if os.getpgid(pid) != pid or pid == os.getpgrp():101                raise ValueError('实际工作进程未建立独立进程组')102            try:103                signal.pidfd_send_signal(self._fd, 0, None, 4)104            except OSError as error:105                if error.errno in (22, 38):106                    raise NotImplementedError('并行任务需要Linux 6.9及以上的pidfd进程组信号') from error107                raise108            # ACK证明原worker仍活着,实际组身份和内核能力通过后才授予业务执行。109            self._confirmed = True110            registration.parent.send_bytes(b'GO' + registration.token)111        except BaseException as error:112            _cleanup_owner(self, error, 'process_tree_cleanup_owner', 'Linux进程树登记')113            raise114115    def _close_linux(self):116        """关闭已确认的进程组并核对实际消失。117118        参数: 无。119        返回: None;未确认fd仅关闭。120        异常: 组未消失或信号失败时保留责任;Linux close本身报错也不重试已释放编号。121        """122        if self._fd is None:123            self._process = None124            self._closed = True125            return126        if self._confirmed:127            try:128                signal.pidfd_send_signal(self._fd, signal.SIGKILL, None, 4)129            except ProcessLookupError:130                pass131            # 回收本次组长僵尸;不接管或扫描其他进程。132            process = self._process133            if process is not None and (not getattr(process, '_closed', False)):134                process.join(timeout=5)135                if process.is_alive():136                    raise TimeoutError('进程树组长尚未退出')137            deadline = monotonic() + 5138            while True:139                try:140                    signal.pidfd_send_signal(self._fd, 0, None, 4)141                except ProcessLookupError:142                    break143                if monotonic() >= deadline:144                    raise TimeoutError('进程组尚未全部退出,保留清理责任')145                sleep(0.01)146        # 组已不存在;Linux close可能先释放编号再报错,因此只调用一次。147        descriptor = self._fd148        self._fd = None149        self._process = None150        self._closed = True151        os.close(descriptor)152153    def _close_process_handle(self):154        """关闭登记期间真实打开的进程句柄。参数: 无。返回: None。异常: 失败仍保留句柄。"""155        if self._process_handle:156            if not self._api.CloseHandle(self._process_handle):157                raise ctypes.WinError(ctypes.get_last_error())158            self._process_handle = None159160    def close(self):161        """关闭本次平台资源;Windows失败保留实际句柄,Linux只重试尚未结束的组。162163        参数: 无。返回: None;重复关闭安全。异常: 第一个关闭错误,其余写入notes。164        Windows作业关闭后结束残余后代;登记句柄失败仍尝试结束作业。165        Linux组确认消失后只close一次pidfd;错误向上传播,不重试已释放编号。166        """167        if self._closed:168            return169        if getattr(self, '_linux', False) is True:170            self._close_linux()171            return172        errors = []173        try:174            self._close_process_handle()175        except BaseException as error:176            errors.append(error)177        if self._handle:178            try:179                if not self._api.CloseHandle(self._handle):180                    raise ctypes.WinError(ctypes.get_last_error())181                self._handle = None182            except BaseException as error:183                errors.append(error)184        self._closed = not self._handle and not self._process_handle185        if errors:186            for error in errors[1:]:187                errors[0].add_note('Windows进程树另有句柄关闭失败: ' + type(error).__name__)188            raise errors[0]189
Chronaxis 用户手册 · 离线阅读