首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
299 阅读
2
从零开始配置 vim(15)——状态栏配置
176 阅读
3
nvim番外之将配置的插件管理器更新为lazy
165 阅读
4
2018总结与2019规划
154 阅读
5
wsl 启动报错:Wsl/0x80080005
130 阅读
软件与环境配置
读书笔记
编程
Thinking
FIRE
菜谱
翻译
登录
Search
标签搜索
c++
c
emacs
elisp
学习笔记
windows
文本操作术
编辑器
NeoVim
Vim
win32
读书笔记
VimScript
linux
文本编辑器
投资理财
Java
反汇编
OLEDB
数据库编程
Masimaro
累计撰写
468
篇文章
累计收到
33
条评论
首页
栏目
软件与环境配置
读书笔记
编程
Thinking
FIRE
菜谱
翻译
页面
归档
友情链接
关于
搜索到
4
篇与
的结果
2026-10-26
《elisp 每日一练》——定时器与进程调用综合练习
在前面一系列文章中,我们讨论了定时器以及同步和异步方式调用进程。本篇是回顾和综合练习篇回顾首先我们介绍了两种定时器:run-at-time 创建普通定时器run-with-idle-timer 创建空闲定时器接着我们讨论了同步进程调用的方式:shell-command-to-stringprocess-linescall-process后面我们讨论了异步调用进程的方式早期版本使用 start-process 新版本使用控制粒度更细的 make-process最后介绍了两种解析json的方式:早期版本使用纯elisp实现的 json-read 和 json-read-from-string。后来新版本使用的接口是基于 Jansson 实现的 json-parse-string、json-parse-buffer练习本节的综合练习,我们来实现一个基于 curl 的异步http 调用功能。虽然 Emacs原生有一个 url-retrieve 的方案可以支持http的异步访问,但是它的问题也比较明显:相比较curl来说,它的使用显得十分诡异,异步函数 url-retrieve 在某些复杂场景并不能做到真正的异步。所以在一些对网络请求有较高要求的场景通常是通过调用 curl 程序实现通用的外部http请求。它的调用方式如下:(let ((target-url "https://api.ipify.org?format=json")) (my-async-http-request :url target-url :method "GET" :data nil :headers '(:Accept "application/json") :response (lambda (response) (condition-case err (let* (((json json-parse-string response :object-type 'alist)) (ip (alist-get 'ip json))) (message "解析成功:您的ip是 :%s" ip)) (error (message "json 解析失败"))))))header 是一个 alist / plist 结构,最终需要将这种风格的键值对转化为字符串形式的header参数,用来调用 curl。我们先封装一个函数用于将alist / plist 结构的header转化为对应的字符串(defun my--plist-to-curl-args(headers) "把 HEADERS(形如 (:Accept \"application/json\" :X-Foo \"bar\") 的 plist, 或 alist ((. \"Accept\") . \"application/json\") 结构)转换成 curl -H 参数列表。" (let ((args)) (cond ;; plist风格 (:Key "val" :Key2 "val2") ((and headers (keywordp (car headers))) (let ((rest headers)) (while rest (let ((k (car rest)) (v (cdr rest))) (push "-H" args) (push (format "%s: %s" (substring (symbol-name k) 1) v) args)) (setq rest (cddr rest))))) ;; alist 风格 ((\"Accept\" . \"application/json\") ...) ((listp headers) (dolist (pair headers) (push "-H" args) (push (format "%s: %s" (car pair) (cdr pair)) args)))) (nreverse args)))对于 alist 风格的数据转化比较好理解,它实际上就是一个列表,列表的每一项都是一个cons cell。所以我们只需要遍历它的每一子项,针对子项分别调用 car 和 cdr 就可以获取每一项。但是对于 plist 形式的数据处理的稍微复杂一些。我们在前面提到过 plist 本身也是一个个list, '(:key value) 的数据实际上是一个拥有两个成员的list,list的第一个元素是符号 :key,第二个是数据 value。所以我们可以使用通过 car cdr 分别取它的键和值,对于下一个键值对可以使用 cddr 获取。这里也是采用循环的思路获取每一项,需要注意的是 key 符号本身包含了最左边的 : 所以我们需要去掉它关于调用 curl 来说,跟我们之前编写的cmake调用的思路类似,这里就不多啰嗦了。(defun my-async-http-request (&rest param) "向远程接口发起异步HTTP请求 参数: :url 目标url (必填) :method HTTP 请求方法字符串,默认 \"GET\" :headers plist 或者 alist 结构,每一项对应着http header 中的一个键值对 :data 请求体 :response 接收响应 body 字符串的回调函数 " (let* ((url (plist-get param :url)) (method (or (plist-get param :method) "GET")) ;;默认是get请求 (headers (plist-get param :headers)) (data (plist-get param :data)) (callback (plist-get param :response)) (buf-name (format "*Async-HTTP-%s*" (format-time-string "%H%M%S-%3N"))) (output-buf (get-buffer-create buf-name)) (curl-args (list "-s" "-X" method url))) ;;拼接header 头部 (setq curl-args (append curl-args (my--plist-to-curl-args headers))) ;; 如果有请求的data,追加 -d 参数 (when data (setq curl-args (append curl-args (list "-d" data)))) ;;初始化输出缓冲区 (with-current-buffer output-buf (erase-buffer)) (message "正在向 [%s] 发起异步%s请求..." url method) (message "curl-args= %s" curl-args) ;;正式使用curl 发起请求 (make-process :name "my-async-curl-proc" :buffer output-buf :command (append '("curl") curl-args) :filter nil ;; 退出时根据退出状态负责调用用户回调 :sentinel (lambda (proc event) (when (eq (process-status proc) 'exit) (let ((exit-code (process-exit-status proc)) (buf (process-buffer proc))) ;;正常则读取返回的http响应包 (if (and (zerop exit-code) (buffer-live-p buf)) (let ((response-str (with-current-buffer buf (buffer-substring (point-min) (point-max))))) ;; 存在回调函数则调用回调 (when callback (funcall callback response-str)) ;;清理临时buffer (kill-buffer buf)) (message "请求失败,错误码:%d" exit-code) (when (buffer-live-p buf) (kill-buffer buf)))))))))这里我们将buffer的所有数据都通过一个参数传给回调函数,更加合理的设计是传入三个参数: status_code、response_header、response_data。调用者就不需要自己拆分http响应体的内容了。但是这部分的工作就交给各位读者去尝试了
2026年10月26日
2 阅读
0 评论
0 点赞
2026-10-23
《elisp 每日一练》——异步进程基础与生命周期管理
在上一节,我们讨论了Emacs中同步方式调用进程,它比较简单,适合那些能快速返回的命令行程序,对于一些耗时操作,如果下一步确实依赖耗时操作的返回,那么等待可能是必须的。但是某些时候,如果因为一些功能增强的后台应用导致界面卡顿,例如拼写检查、lsp 代码分析 如果耗时太久反而影响用户编写代码的体验。对于这种我们可以考虑将它们放到后台作为异步进程背景知识Emacs 提供了两个启动异步进程的函数: start-process 和 make-process。前者是早期Emacs提供的方式,它比较简单和传统,函数签名如下:(start-process NAME BUFFER PROGRAM &rest PROGRAM-ARGS)它的功能比较简单,它启动一个异步进程并将进程输出放到指定的buffer中,这种情况下我们无法对进程生命周期和输出进行精准的控制,如果需要更精确的控制,更推荐使用 make-process(make-process &rest ARGS)它的签名看着简单,似乎只有一个 ARGS 作为可选,但是实际上可选的参数十分丰富。我们传入一些关键字参数来精确的控制异步进程。最关键的几个参数如下::name 进程名称,Emacs必要时会自动修改这个名称,确保唯一;请注意这个名称是我们为这个进程对象取得名字,并不是可执行程序得名称:command 这个是进程真实执行得命令行,可以是一个个命令行参数组成得字符串列表:buffer 指定进程输出关联的缓冲区,可以是缓冲区名称或者对象:filter 输出过滤函数,如果指定了函数,有输出时会调用该函数进行过滤,并将过滤后的内容输出到指定buffer中:sentinel 进程状态变更的回调函数:stderr 标准错误输出的buffer调用这些函数之后,会返回一个进程对象。我们后面可以以对象作为参数,调用下面的函数来控制它的生命周期:process-live-p: 检查进程当前是否还在活跃运行process-status: 获取进程状态,例如 'run、'exit、'signalinterrupt-process: 向进程发送中断信号kill-process: 强行终止进程练习我们实现一个调用cmake编译的小插件。假设我有一个项目在Windows上,我执行编译的命令如下:cmake . -B build -G"Visual Studio 18 2026" -T "v143" -A x64 -DCMAKE_BUILD_TYPE=Debug -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDebugDLL cmake --build build将编译的正常输出放到名为 *compile-build* 的buffer中,将错误和警告信息放到 *compile-error* 我们可以在该buffer中开启 compilation-minor-mode,这样我们就可以根据行号快速打开报错的位置(defun my-cmake-msvc-build (source-dir) "打开当前buffer所在目录,执行cmake编译" (interactive "D选择项目根目录:") (let* ((default-directory (file-name-as-directory source-dir)) (build-dir "build") (out-buf (get-buffer-create "*compile-build*")) (err-buf (get-buffer-create "*compile-error*")) (config-proc nil) ;; 构建进程 (build-proc nil)) ;; 编译进程 (with-current-buffer err-buf ;;启用 compilation-minor-mode 支持从错误信息跳转到具体位置 (erase-buffer) (compilation-minor-mode t)) (with-current-buffer out-buf (erase-buffer)) (message "开始cmake 配置") (setq config-proc (make-process :name "cmake-config" :command (list "cmake" "." "-B" build-dir "-G" "Visual Studio 18 2026" "-T" "v143" "-A" "x64" "-DCMAKE_BUILD_TYPE=Debug" "-DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDebugDLL") :buffer out-buf :stderr err-buf :noquery t :sentinel ;; 在构建成功后执行编译 (lambda (p event) (when (memq (process-status p) '(exit signal)) (if (zerop (process-exit-status p)) (progn (message "构建成功,开始编译...") (setq build-proc (make-process :name "cmake-build" :command (list "cmake" "--build" build-dir) :buffer out-buf :stderr err-buf :noquery t :sentinel ;; 通知结果 (lambda (p1 event1) (when (memq (process-status p1) '(exit signal)) (if (zerop (process-exit-status p1)) (message "构建成功") (message "构建失败,查看*compile-error*") (display-buffer err-buf)))))))))))) config-proc))这个例子,我们演示了 make-process 的基本用法,并且给出了状态变更的回调函数的使用。在Windows上实测时因为msvc将编译时的错误信息和正常输出都作为标准输出了,所以读者可能会发现报错出现在 *compile-build* 缓冲。为了适配Windows,读者可以尝试在 *compile-build* 也加上 compilation-minor-mode总结本节我们介绍了异步方式调用的进程的方法,本节的相关函数如下:make-processprocess-live-pprocess-statusinterrupt-processkill-process
2026年10月23日
7 阅读
0 评论
0 点赞
2026-10-22
《elisp 每日一练》—— 同步进程调用
在前面我们了解了定时器的使用机制,但在日常使用中,通常是利用Emacs配合外部命名行工具的方式使用。所以我们今天讨论如何利用Emacs调用外部命令行工具基础知识我们今天主要讨论同步的方式调用,虽然它会阻塞Emacs的主线程,但是对于搜索、编译等工作来说,我们可能就是需要等待它们的执行结果。这种情况下使用同步的方式,反而简单一些。对于同步方式调用,Emacs提供了三个不同的接口。shell-command-to-stringprocess-linescall-processshell-command-to-string 的签名如下:(shell-command-to-string COMMAND)它通过系统的标准shell,执行 COMMAND 参数中输入的命令,并且等待结果,将结果以字符串的形式返回。当然我们也可以传入管道符号,对输出进行过滤,例如(shell-command-to-string "gcc -c main.c -o main | grep \\"error\\"")process-lines 与 shell-command-to-string 类似,但是它会自动将程序输出按行分隔为字符串列表返回(process-lines PROGRAM &rest ARGS)同时它也支持将程序的参数以多个字符串参数的形式输入。例如 (process-lines "git" "status" "--porcelain")call-process 是比较底层的函数,它不经过shell,直接创建一个新的进程,调用指定的二进制可执行程序,并将参数以列表形式逐个传入。它的签名如下:(call-process PROGRAM \&optional INFILE DESTINATION DISPLAY &rest ARGS)PROGRAM 是被调用的程序名称,如果是相对路径,Emacs 会在变量 exec-path 列出的目录中查找,需要注意的是这个参数应该只包含程序名,不能包含命令行参数INFILE 指定子进程的标准输入来源,如果为 nil 则从空设备(/dev/null) 中读取,否则应该是一个文件名,程序从指定文件中读取。DESTINATION 决定程序的输出,可以指定标准输出或者错误输出。DISPLAY 结果显示方式,如果是 nil,仅在函数完全结束后,结果才可见; 非 nil 会根据程序输出,即时刷新屏幕ARGS 是真正需要传递给子进程的参数DESTINATION 的取值可以有下面几个:缓冲区对象或名称,此时它会将输出打印到指定缓冲区光标所在位置的后面t 将输出插入到当前缓冲区中nil 丢弃所有输出0 丢弃输出并立即返回,不等待子进程结束。此时实际上是在异步执行,但是Emacs认为子进程已经执行完毕, Windows 上不支持(:file 文件名) 此时会将输出放入到一个指定文件中我们在上面说,它可以分别重定向标准输出和标准错误,它可以使用 (标准输出目标 标准错误目标) 这样的形式分别指定。各位读者如果对操作系统、子进程、shell等概念比较熟悉的话,应该很容易区分这些函数的使用场景。这里就不再深入讨论了。练习我们来实现一个简单的 git 信息显示的功能,以此来演示一下这些函数的用法。我们实现以下功能:通过 shell-command-to-string 调用 git branch --show-current 显示当前branch通过 process-lines 调用 git status --porcelain 获取当前修改的文件列表, 并显示到临时buffer中在用户选择上一步中某个文件之后,通过 call-process 调用 git diff, 查看差异(defun my/git-current-branch() (interactive) (let* ((output (shell-command-to-string "git branch --show-current")) (branch (string-trim output))) (if (string-empty-p branch) (message "当前目录可能不是一个git 仓库") (message "当前分支: %s" branch)))) (defun my/git-status() "在临时缓冲区中列出当前仓库修改的文件" (interactive) (let ((lines (process-lines "git" "status" "--porcelain")) (buf (get-buffer-create my/git-status-buffer-name))) (with-current-buffer buf (let ((inhibit-read-only t)) (erase-buffer) ;;插入提示信息 (insert (format "Branch: %s\n" (my/git-current-branch))) (insert (make-string 60 ?-) "\n") (if lines (dolist (line lines) (insert line "\n")) (goto-char (point-min))))) (pop-to-buffer buf))) (defun my/git-status--path-at-point () "从当前行解析出文件路径;若不是文件行则返回 nil。" (save-excursion (beginning-of-line) (let ((line (buffer-substring-no-properties (line-beginning-position) (line-end-position)))) ;; 跳过头部信息行 (when (and (>= (length line) 4) (not (string-prefix-p "Branch:" line)) (not (string-prefix-p "---" line))) (let ((path (substring line 3))) ; 去掉 "XY " 前缀 ;; 处理重命名 "OLD -> NEW" (when (string-match " -> " path) (setq path (substring path (match-end 0)))) ;; 处理含空格的路径(porcelain 会用引号包裹) (when (string-prefix-p "\"" path) (setq path (string-trim path "\"" "\""))) path))))) (defun my/git-show-diff-at-point() (interactive) (let ((path (my/git-status--path-at-point))) (if (not path) (message "当前行没有文件路径") (let ((diff-buff (get-buffer-create my/git-diff-buffer-name))) (with-current-buffer diff-buff (let ((inhibit-read-only t)) (erase-buffer) (insert (format "diff for: %s" path)) (insert (make-string 60 ?-) "\n") (call-process "git" nil t nil "diff" "HEAD" "--" path) ;; DESTINATION 设置为t,直接显示内容到当前buffer (goto-char (point-min)))) (display-buffer diff-buff)))))总结本节我们了解了Emacs中同步方式调用进程的几种方式,它们各有千秋:shell-command-to-stringprocess-linescall-process
2026年10月22日
4 阅读
0 评论
0 点赞
2016-09-06
windows 多任务与进程
多任务,进程与线程的简单说明多任务的本质就是并行计算,它能够利用至少2处理器相互协调,同时计算同一个任务的不同部分,从而提高求解速度,或者求解单机无法求解的大规模问题。以前的分布式计算正是利用这点,将大规模问题分解为几个互不不相关的问题,将这些计算问题交给局域网中的其他机器计算完成,然后再汇总到某台机器上,显示结果,这样就充分利用局域网中的计算机资源。相对的,处理完一步接着再处理另外一步,将这样的传统计算模式称为串行计算。在提高处理器的相关性能主要有两种方式,一种是提高单个处理器处理数据的速度,这个主要表现在CPU主频的调高上,而当前硬件总有一个上限,以后再很难突破,所以现在的CPU主要采用的是调高CPU的核数,这样CPU的每个处理器都处理一定的数据,总体上也能带来性能的提升。在某些单核CPU上Windows虽然也提供了多任务,但是这个多任务是分时多任务,也就是每个任务只在CPU中执行一个固定的时间片,然后再切换到另一个任务,由于每个任务的时间片很短,所以给人的感觉是在同一时间运行了多个任务。单核CPU由于需要来回的在对应的任务之间切换,需要事先保存当前任务的运行环境,然后通过轮循算法找到下一个运行的任务,再将CPU中寄存器环境改成新任务的环境,新任务运行到达一定时间,又需要重复上述的步骤,所以在单核CPU上使用多任务并不能带来性能的提升,反而会由在任务之间来回切换,浪费宝贵的资源,多任务真正使用场合是多核的CPU上。windows上多任务的载体是进程和线程,在windows中进程是不执行代码的,它只是一个载体,负责从操作系统内核中分配资源,比如每个进程都有4GB的独立的虚拟地址空间,有各自的内核对象句柄等等。线程是资源分配的最小单元,真正在使用这些资源的是线程。每个程序都至少有一个主线程。线程是可以被执行的最小的调度单位。进程的亲缘性进程或者线程只在某些CPU上被执行,而不是由系统随机分配到任何可用的CPU上,这个就是进程的亲缘性。例如某个CPU有8个处理器,可以通过进程的亲缘性设置让该进程的线程只在某两个处理器上运行,这样就不会像之前那样在8个CPU中的任意几个上运行。在windows上一般更倾向于优先考虑将线程安排在之前执行它的那个处理器上运行,因为之前的处理器的高速缓存中可能存有这个线程之前的执行环境,这样就提高的高速缓存的命中率,减少了从内存中取数据的次数能从一定程度上提高性能。需要注意的是,在拥有三级高速缓存的CPU上,这么做意义就不是很大了,因为三级缓存一般作为共享缓存,由所有处理器共享,如果之前在2号处理器上执行某个线程,在三级缓存上留下了它的运行时的数据,那么由于三级缓存是由所有处理器所共享的,这个时候即使将这个线程又分配到5号处理器上,也能访问这个公共存储区,所以再设置线程在固定的处理器上运行就显得有些鸡肋,这也是拥有三级缓存的CPU比二级缓存的CPU占优势的地方。但是如果CPU只具有二级缓存,通过设置亲缘性可以发挥高速缓存的优势,提高效率。亲缘性可以通过函数SetProcessAffinityMask设置。该函数的原型如下:BOOL WINAPI SetProcessAffinityMask( __in HANDLE hProcess, __in DWORD_PTR dwProcessAffinityMask );第一个参数是设置的进程的句柄,第二个参数是DWORD类型的指针,是一个32位的整数,每一位代表一个处理器的编号,当希望设置进程中的线程在此处理器上运行的话,将该位设置为1,否则为0。为了设置亲缘性,首先应该了解机器上CPU的情况,根据实际情况来妥善安排,获取CPU的详细信息可以通过函数GetLogicalProcessInformation来完成。该函数的原型如下:BOOL WINAPI GetLogicalProcessorInformation( __out PSYSTEM_LOGICAL_PROCESSOR_INFORMATION Buffer, __in_out PDWORD ReturnLength );第一个参数是一个结构体的指针,第二个参数是需要缓冲区的大小,也就是说这个函数我们可以采用两次调用的方式来合理安排缓冲区的大小。结构体SYSTEM_LOGICAL_PROCESSOR_INFORMATION的定义如下: typedef struct _SYSTEM_LOGICAL_PROCESSOR_INFORMATION { ULONG_PTR ProcessorMask; LOGICAL_PROCESSOR_RELATIONSHIP Relationship; union { struct { BYTE Flags; } ProcessorCore; struct { DWORD NodeNumber; }NumaNode; CACHE_DESCRIPTOR Cache; ULONGLONG Reserved[2]; }; } SYSTEM_LOGICAL_PROCESSOR_INFORMATION, *PSYSTEM_LOGICAL_PROCESSOR_INFORMATION;ProcessorMask是一个位掩码,每个位代表一个逻辑处理器,第二个参数是一个标志,表示该使用第三个共用体中的哪一个结构体,这三个分别表示核心处理器,NUMA节点,以及高速缓存的信息。下面是使用的一个例子代码:#include <windows.h> #include <tchar.h> #include <stdio.h> #include <locale.h> DWORD CountBits(ULONG_PTR uMask); typedef BOOL (WINAPI *CPUINFO)(PSYSTEM_LOGICAL_PROCESSOR_INFORMATION Buffer, PDWORD ReturnLength); int _tmain(int argc, TCHAR *argv[]) { _tsetlocale(LC_ALL, _T("chs")); CPUINFO pGetCpuInfo = (CPUINFO)GetProcAddress(GetModuleHandle(_T("kernel32")), "GetLogicalProcessorInformation"); if (NULL == pGetCpuInfo) { _tprintf(_T("系统不支持该函数,程序结束\n")); return 0; } PSYSTEM_LOGICAL_PROCESSOR_INFORMATION plpt = NULL; DWORD dwLength = 0; if (!pGetCpuInfo(plpt, &dwLength)) { if (ERROR_INSUFFICIENT_BUFFER == GetLastError()) { plpt = (PSYSTEM_LOGICAL_PROCESSOR_INFORMATION)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwLength); }else { _tprintf(_T("函数调用失败\n")); return 0; } } if (!pGetCpuInfo(plpt, &dwLength)) { _tprintf(_T("函数调用失败\n")); return 0; } DWORD dwSize = dwLength / sizeof(SYSTEM_LOGICAL_PROCESSOR_INFORMATION); PSYSTEM_LOGICAL_PROCESSOR_INFORMATION pOffsite = plpt; DWORD dwCacheCnt = 0; DWORD dwCoreCnt = 0; DWORD dwPackageCnt = 0; DWORD dwNodeCnt = 0; for (int i = 0; i < dwSize; i++) { switch (pOffsite->Relationship) { case RelationCache: dwCacheCnt++; break; case RelationNumaNode: dwNodeCnt++; break; case RelationProcessorCore: { if (pOffsite->ProcessorCore.Flags) { dwCoreCnt++; }else { dwCoreCnt += CountBits(pOffsite->ProcessorMask); } } break; case RelationProcessorPackage: dwPackageCnt++; break; default: break; } pOffsite++; } _tprintf(_T("处理器个数:%d\n"), dwPackageCnt); _tprintf(_T("处理器核数:%d\n"), dwCoreCnt); _tprintf(_T("NUMA个数:%d\n"), dwNodeCnt); _tprintf(_T("高速缓存的个数:%d\n"), dwCacheCnt); return 0; } DWORD CountBits(ULONG_PTR uMask) { DWORD dwTest = 1; DWORD LSHIFT = sizeof(ULONG_PTR) * 8 -1; dwTest = dwTest << LSHIFT; DWORD dwCnt = 0; for (int i = 0; i <= LSHIFT; i++) { dwCnt += ((uMask & dwTest) ? 1 : 0); dwTest /= 2; } return dwCnt; }这个程序首先采用两次调用的方式分配一个合适的缓冲区,用来接收函数的返回,然后分别统计CPU数目,物理处理器数目以及逻辑处理器数量。我们根据MSDN上面得到的信息,首先判断缓冲区中有多少个结构体,也就是由多少条处理器的信息,然后根据第二个成员的值,来判断当前存储的是哪种信息。并将对应的值加1,当计算逻辑处理器的数目时需要考虑超线程的问题,所谓超线程就是intel提供的一个新的技术,可以将一个处理器虚拟成多个处理器来使用,已达到多核处理器的效果,如果它支持超线程,那么久不能简单的根据是否为核心处理器而加1,这个时候需要采用计算位掩码的方式来统计逻辑存储器,根据MSDN上说的当flag为1时表示支持超线程。windows下的进程windows中进程是已装入内存中,准备或者已经在执行的程序,磁盘上的exe文件虽说可以执行,但是它只是一个文件,并不是进程,一旦它被系统加载到内存中,系统为它分配了资源,那么它就是一个进程。进程由两个部分组成,一个是系统内核用来管理进程的内核对象,一个是它所占的地址空间。windows下的进程主要分为3大类:控制台,窗口应用,服务程序。写过控制台与窗口程序的人都知道,控制台的主函数是main,而窗口应用的主函数是WinMain,那么是否可以根据这个来判断程序属于那种呢,很遗憾,windows并不是根据这个来区分的。在VS编译器上可以通过设置将Win32 控制台程序的主函数指定为WinMain,或者将窗口程序的主函数指定为main,设置方法:属性-->连接器-->系统-->子系统,将这项设置为/SUBSYSTEM:CONSOLE,那么它的主函数为main,设置为/SUBSYSTEM:WINDOWS那么它的主函数为WinMain,甚至我们可以自己设置主函数。我们知道在C/C++语言中main程序是从main函数开始的,但是这个函数只是语法上的开始,并不是真正意义上的入口,在VC++中,系统会首先调用mainCRTStartup,在这个函数中调用main或者WinMain, 这个函数主要负责对C/C++运行环境的初始化,比如堆环境或者C/C++库函数环境的初始化。如果需要自定义自己的入口,那么这些环境将得不到初始化,也就意味着我们不能使用C/C++库函数。只能使用VC++提供的API,这么做也有一定的好处,毕竟这些库函数都是在很早之前产生的,到现在来看有很多问题,有许多有严重的安全隐患,使用API可以避免这些问题。下面是一个简单的例子:#include <Windows.h> #include <tchar.h> #include <strsafe.h> #define PRINTF(...) \ {\ TCHAR szBuf[4096] = _T("");\ StringCchPrintf(szBuf, sizeof(szBuf), __VA_ARGS__);\ size_t nStrLen = 0;\ StringCchLength(szBuf, STRSAFE_MAX_CCH, &nStrLen);\ WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE), szBuf, nStrLen, NULL, NULL);\ } int LHMain() { PRINTF(_T("this is a %s"), _T("test hello world\n")); return 0; }这个例子中,入口函数是LHMain我们完全使用API的方式来编写程序,如果想要做到自定义入口,那么需要进行这样的设置:属性-->高级-->入口点,在入口点中输出我们希望作为入口点的函数名称即可。入口参数各个参数的含义现在这部分主要说明main函数以及WinMain函数。int main()这是main函数的一种原型,这种原型不带入任何参数int main(int argc, char *argv[])这种原型主要接受命令行输入的参数,参数以空格隔开,第一个参数表示输入命令行参数的总数,第二个是一个字符串指针数组,每个字符串指针成员代表的是具体输入的命令以及参数,这些信息包括我们输入的程序的名称,比如我们输入test.exe -s -a -t 来启动程序,那么argc = 4 ,argv[0] = "test.exe" argv[1] = "-s" argv[2] = "-a" argv[3] = "-t"int main(int argc, char argv[], char envs[])这个原型中第一个和第二个参数的函数与上述的带有两个参数的main函数相同,多了一个表示环境变量的指针数组,它会将环境变量以”变量名=值“的形式来组织字符串。int WINAPI WinMain(HANDLE hInstance, HANDLE hPrevInstance, LPSTR lpszCmdLine, int nCmdShow);函数中的WINAPI表示函数的调用约定为__stdcall的调用方式,函数的调用方式主要与参数压栈方式以及环境回收的方式有关。在这就不再说明这部分的内容,有兴趣的可以看本人另外一篇博客专门讲解了这部分的内容,点击这里hInstance:进程的实例句柄,该值是exe文件映射到虚拟地址控件的基址hPrevInstance:上一个进程的实例句柄,为了防止进程越界访问,这个是16位下的产物,在32位下,这个没有作用。lpszCmdLine:启动程序时输入的命令行参数nCmdShow:表示程序的显示方式。进程的环境变量与工作路径进程的环境变量可以通过main函数的第三个参数传入,也可以在程序中利用函数GetEnvrionmentStrings和GetEnvrionVariable获取,下面是获取进程环境变量的简答例子: setlocale(CP_ACP, "chs"); TCHAR **ppArgs = NULL; int nArgCnt = 0; ppArgs = CommandLineToArgvW(GetCommandLine(), &nArgCnt); for (int i = 0; i < nArgCnt; i++) { _tprintf(_T("%d %s\n"), i, ppArgs[i]); } HeapFree(GetProcessHeap(), 0, ppArgs);函数的第一个参数是开启这个进程需要的完整的命令行字符串,这个字符串使用函数GetCommandLine来获取,第二个参数是一个接受环境变量的字符串指针数组。函数返回数组中元素个数。一般情况下不推荐使用环境变量的方式来保存程序所需的数据,一般采用文件或者注册表的方式,但是最好的办法是采用xml文件的方式来村粗。至于进程的工作目录可以通过函数GetCurrentDirectory方法获取。进程创建在windows下进程创建采用API函数CreateProcess,该函数的原型如下:BOOL CreateProcess( LPCWSTR pszImageName, LPCWSTR pszCmdLine, LPSECURITY_ATTRIBUTES psaProcess, LPSECURITY_ATTRIBUTES psaThread, BOOL fInheritHandles, DWORD fdwCreate, LPVOID pvEnvironment, LPWSTR pszCurDir, LPSTARTUPINFOW psiStartInfo, LPPROCESS_INFORMATION pProcInfo ); 该函数参数较多,下面对这几项做重点说明,其余的请自行查看MSDN。pszImageName:表示进程对应的exe文件所在的完整路径或者相对路径pszCmdLine:启动进程传入的命令行参数,这是一个字符串类型,需要注意的是,这个命令行参数可以带程序所在的完整路径,这样就可以将第一个参数设置为NULL。参数中的安全描述符表示的是创建的子进程的安全描述符,与当前进程的安全描述符无关。fInheritHandles表示,可否被继承,这个继承关系是指父进程中的信息能否被子进程所继承psiStartInfo规定了新进程的相关启动信息,主要有这样几个重要的值:对于窗口程序: LPTSTR lpTitle; DWORD dwX; DWORD dwY; DWORD dwXSize; DWORD dwYSize;这些值规定了窗口的标题和所在的屏幕位置与长高的相关信息,对于控制台程序,主要关注: HANDLE hStdInput; HANDLE hStdOutput; HANDLE hStdError;标准输入、输出、以及标准错误 下面是一个创建控制台与创建窗口的简单例子: STARTUPINFO si = {0}; si.dwXSize = 400; si.dwYSize = 300; si.dwX = 10; si.dwY = 10; PROCESS_INFORMATION pi = {0}; SECURITY_ATTRIBUTES sa = {sizeof(SECURITY_ATTRIBUTES)}; //创建一个窗口应用的进程,其中szExePath表示进程所在exe文件的路径,而szAppDirectory表示exe所在的目录 CreateProcess(szExePath, szExePath, &sa, &sa, FALSE, 0, NULL, szAppDirectory, &si, &pi); WaitForSingleObject(pi.hProcess, INFINITE); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); //启动控制台窗口,与父进程公用输入输出环境 ZeroMemory(szExePath, sizeof(TCHAR) * (MAX_PATH + 1)); StringCchPrintf(szExePath, MAX_PATH, _T("%s%s"), szAppDirectory, _T("SubConsole.exe")); ZeroMemory(&si, sizeof(STARTUPINFO)); ZeroMemory(&pi, sizeof(PROCESS_INFORMATION)); CreateProcess(szExePath, szExePath, &sa, &sa, FALSE, 0, NULL, szAppDirectory, &si, &pi); WaitForSingleObject(pi.hProcess, INFINITE); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); //启动控制台,在新的控制台上做输入输出 ZeroMemory(&si, sizeof(STARTUPINFO)); ZeroMemory(&pi, sizeof(PROCESS_INFORMATION)); CreateProcess(szExePath, szExePath, &sa, &sa, FALSE, CREATE_NEW_CONSOLE, NULL, szAppDirectory, &si, &pi); WaitForSingleObject(pi.hProcess, INFINITE); CloseHandle(pi.hProcess); CloseHandle(pi.hThread);上述程序中,对于窗口程序,在创建时没有给出特别的创建标志,窗口本身就是一个个独立的,并且我们通过指定si的部分成员指定了窗口的显示位置,而对于控制台,如果在创建时不特别指定创建的标志,那么它将与父进程共享一个输入输出控制台。为了区分子进程和父进程的输入输出,一般通过标志CREATE_NEW_CONSOLE为新进程新建一个另外的控制台。进程输入输出重定向输入输出重定向的实现可以通过函数CreateProcess在参数psiStartInfo中的HANDLE hStdInput; HANDLE hStdOutput; HANDLE hStdError中指定,但是需要注意的是,在父进程中如果采用了Create之类的函数创建了输入输出对象的句柄时一定要指定他们可以被子进程所继承。下面是一个重定向的例子: //启动控制台,做输入输出重定向到文件中 TCHAR szFilePath[MAX_PATH + 1] = _T(""); //指定文件对象可以被子进程所继承,以便子进程可以使用这个内核对象句柄 sa.bInheritHandle = TRUE; ZeroMemory(&si, sizeof(STARTUPINFO)); ZeroMemory(&pi, sizeof(PROCESS_INFORMATION)); StringCchPrintf(szFilePath, MAX_PATH, _T("%s%s"), szAppDirectory, _T("input.txt")); HANDLE hInputFile = CreateFile(szFilePath, GENERIC_READ | GENERIC_WRITE, 0, &sa, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); ZeroMemory(szFilePath, sizeof(szFilePath)); StringCchPrintf(szFilePath, MAX_PATH, _T("%s%s"), szAppDirectory, _T("output.txt")); HANDLE hOutputFile = CreateFile(szFilePath, GENERIC_READ | GENERIC_WRITE, 0, &sa, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); si.hStdInput = hInputFile; si.hStdOutput = hOutputFile; si.dwFlags = STARTF_USESTDHANDLES; CreateProcess(szExePath, szExePath,NULL, NULL, TRUE, DETACHED_PROCESS, NULL, szAppDirectory, &si, &pi); WaitForSingleObject(pi.hProcess, INFINITE); CloseHandle(hOutputFile); CloseHandle(hInputFile); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); //启动ping命令,并将输出重定向到管道中 StringCchPrintf(szExePath, MAX_PATH, _T("ping 127.0.0.1")); DWORD dwLen = 0; BYTE byte[1024] = {0}; HANDLE hReadP = NULL; HANDLE hWriteP = NULL; sa.bInheritHandle = TRUE; CreatePipe(&hReadP, &hWriteP, &sa, 1024); si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW; si.wShowWindow = SW_HIDE; si.hStdOutput = hWriteP; CreateProcess(NULL, szExePath, NULL, NULL, TRUE, DETACHED_PROCESS, NULL, szAppDirectory, &si, &pi); //关闭管道的写端口,不然读端口会被阻塞 CloseHandle(hWriteP); dwLen = 1000; DWORD dwRead = 0; while (ReadFile(hReadP, byte, dwLen, &dwRead, NULL)) { if ( 0 == dwRead ) { break; } //写入管道的数据为ANSI字符 printf("%s\n", (char*)byte); ZeroMemory(byte, sizeof(byte)); }进程的退出进程在遇到如下情况中的任意一种时会退出:进程中任意一个线程调用函数ExitProcess进程的主线程结束进程中最后一个线程结束调用TerminateProcess在这针对第2、3中情况作特别的说明:这两种情况看似矛盾不是吗,当主线程结束时进程就已经结束了,这个时候还会等到最后一个线程吗。其实真实的情况是主线程结束,进程结束这个限制是VC++上的,之前在自定义入口的时候说过,main函数只是语法上的,并不是实际的入口,在调用main之前会调用mainCRTStartup,这个函数会负责调用main函数,当main函数调用结束后,这个函数会隐式的调用ExitProcess结束进程,所以只有当我们自定了程序入口才会看到3所示的现象,下面的例子说明了这点:DWORD WINAPI ThreadProc(LPVOID lpParam); #define PRINT(s) \ {\ size_t sttLen = 0;\ StringCchLengthA(s, 1024, &sttLen);\ WriteConsoleA(GetStdHandle(STD_OUTPUT_HANDLE), s, sttLen, NULL, NULL);\ } int LHMain(int argc, char *argv[]) { CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL); return 0; } DWORD WINAPI ThreadProc(LPVOID lpParam) { PRINT("main thread is ending,this is child thread\r\n"); return 0; }这种程序,如果我们采用系统中规定的main函数的话,是看不到线程中输出的信息的,因为主线程先结束的话,整个进程就结束了,线程还来不及输出,就被终止了。但是我们采用自定义入口的方式,屏蔽了这个特性,所以它会等到所有线程执行完成后才会结束,这个时候就会看到这句话输出了。进程在终止时会发生以下事件:关闭进程打开的对象句柄,但是对象本身不一定会关闭,这是因为每个对象都有一个计数器,每当有一个线程在使用这个对象时计数器会加1,而释放它的句柄时会减一,只有当计数器为0时才会销毁这个对象。对象是可以跨进程访问的,而且所有相同的对象在内存中只有一份,所以需要一个计数器以便在没有被任何进程访问的时候系统删除它。进程对象的状态设为有信号,以便所有等待该进程对象信号的函数(像函数WaitForSingleObject)能够正常返回。进程的终止状态从STILL_ACTIVE变成进程的退出码,可以通过这个特性判断某个进程是否在运行,具体的方式是通过函数GetExitProcess获取进程的终止码,如果函数返回STILL_ACTIVE,则说明进程仍在运行。
2016年09月06日
34 阅读
0 评论
0 点赞