前言
Linux多线程环境中的信号处理不同于进程的信号处理。一方面线程间信号处理函数的共享性使得信号处理更为复杂,另一方面普通异步信号又可转换为同步方式来简化处理。
本文首先介绍信号处理在进程中和线程间的不同,然后描述相应的线程库函数,在此基础上给出一组示例代码,以讨论线程编程中信号处理的细节和注意事项。文中涉及的代码运行环境如下:
本文通过sigwait()调用来“等待”信号,而通过signal()/sigaction()注册的信号处理函数来“捕获”信号,以体现其同步和异步的区别。
一 概念
1.1 进程与信号
信号是向进程异步发送的软件通知,通知进程有事件发生。事件可为硬件异常(如除0)、软件条件(如闹钟超时)、控制终端发出的信号或调用kill()/raise()函数产生的用户逻辑信号。
当信号产生时,内核通常在进程表中设置一个某种形式的标志,即向进程递送一个信号。在信号产生(generation)和递送(delivery)之间(可能相当长)的时间间隔内,该信号处于未决(pending)状态。已经生成但未递送的信号称为挂起(suspending)的信号。
进程可选择阻塞(block)某个信号,此时若对该信号的动作是系统默认动作或捕捉该信号,则为该进程将此信号保持为未决状态,直到该进程(a)对此信号解除阻塞,或者(b)将对此信号的动作更改为忽略。内核为每个进程维护一个未决(未处理的)信号队列,信号产生时无论是否被阻塞,首先放入未决队列里。当时间片调度到当前进程时,内核检查未决队列中是否存在信号。若有信号且未被阻塞,则执行相应的操作并从队列中删除该信号;否则仍保留该信号。因此,进程在信号递送给它之前仍可改变对该信号的动作。进程调用sigpending()函数判定哪些信号设置为阻塞并处于未决状态。
若在进程解除对某信号的阻塞之前,该信号发生多次,则未决队列仅保留相同不可靠信号中的一个,而可靠信号(实时扩展)会保留并递送多次,称为按顺序排队。
每个进程都有一个信号屏蔽字(signal mask),规定当前要阻塞递送到该进程的信号集。对于每个可能的信号,该屏蔽字中都有一位与之对应。对于某种信号,若其对应位已设置,则该信号当前被阻塞。
应用程序处理信号前,需要注册信号处理函数(signal handler)。当信号异步发生时,会调用处理函数来处理信号。因为无法预料信号会在进程的哪个执行点到来,故信号处理函数中只能简单设置一个外部变量或调用异步信号安全(async-signal-safe)的函数。此外,某些库函数(如read)可被信号中断,调用时必须考虑中断后出错恢复处理。这使得基于进程的信号处理变得复杂和困难。
1.2 线程与信号
内核也为每个线程维护未决信号队列。当调用sigpending()时,返回整个进程未决信号队列与调用线程未决信号队列的并集。进程内创建线程时,新线程将继承进程(主线程)的信号屏蔽字,但新线程的未决信号集被清空(以防同一信号被多个线程处理)。线程的信号屏蔽字是私有的(定义当前线程要求阻塞的信号集),即线程可独立地屏蔽某些信号。这样,应用程序可控制哪些线程响应哪些信号。
信号处理函数由进程内所有线程共享。这意味着尽管单个线程可阻止某些信号,但当线程修改某信号相关的处理行为后,所有线程都共享该处理行为的改变。这样,若某线程选择忽略某信号,而其他线程可恢复信号的默认处理行为或为信号设置新的处理函数,从而撤销原先的忽略行为。即对某个信号处理函数,以最后一次注册的处理函数为准,从而保证同一信号被任意线程处理时行为相同。此外,若某信号的默认动作是停止或终止,则不管该信号发往哪个线程,整个进程都会停止或终止。
若信号与硬件故障(如SIGBUS/SIGFPE/SIGILL/SIGSEGV)或定时器超时相关,该信号会发往引起该事件的线程。其它信号除非显式指定目标线程,否则通常发往主线程(哪怕信号处理函数由其他线程注册),仅当主线程屏蔽该信号时才发往某个具有处理能力的线程。
Linux系统C标准库提供两种线程实现,即LinuxThreads(已过时)和NPTL(Native POSIX Threads Library)。NPTL线程库依赖Linux 2.6内核,更加(但不完全)符合POSIX.1 threads(Pthreads)规范。两者的详细区别可以通过man 7 pthreads命令查看。
NPTL线程库中每个线程拥有自己独立的线程号,并共享同一进程号,故应用程序可调用kill(getpid(), signo)将信号发送到整个进程;而LinuxThreads线程库中每个线程拥有自己独立的进程号,不同线程调用getpid()会得到不同的进程号,故应用程序无法通过调用kill()将信号发送到整个进程,而只会将信号发送到主线程中去。
多线程中信号处理函数的共享性使得异步处理更为复杂,但通常可简化为同步处理。即创建一个专用线程来“同步等待”信号的到来,而其它线程则完全不会被该信号中断。这样就可确知信号的到来时机,必然是在专用线程中的那个等待点。
注意,线程库函数不是异步信号安全的,故信号处理函数中不应使用pthread相关函数。
二 接口
2.1 pthread_sigmask
线程可调用pthread_sigmask()设置本线程的信号屏蔽字,以屏蔽该线程对某些信号的响应处理。
#include <signal.h> int pthread_sigmask(int how, const sigset_t *restrict set, sigset_t *restrict oset); |
该函数检查和(或)更改本线程的信号屏蔽字。若参数oset为非空指针,则该指针返回调用前本线程的信号屏蔽字。若参数set为非空指针,则参数how指示如何修改当前信号屏蔽字;否则不改变本线程信号屏蔽字,并忽略how值。该函数执行成功时返回0,否则返回错误编号(errno)。
下表给出参数how可选用的值。其中,SIG_ BLOCK为“或”操作,而SIG_SETMASK为赋值操作。
参数how | 描述 |
SIG_BLOCK | 将set中包含的信号加入本线程的当前信号屏蔽字 |
SIG_UNBLOCK | 从本线程的当前信号屏蔽字中移除set中包含的信号(哪怕该信号并未被阻塞) |
SIG_SETMASK | 将set指向的信号集设置为本线程的信号屏蔽字 |
主线程调用pthread_sigmask()设置信号屏蔽字后,其创建的新线程将继承主线程的信号屏蔽字。然而,新线程对信号屏蔽字的更改不会影响创建者和其他线程。
通常,被阻塞的信号将不能中断本线程的执行,除非该信号指示致命的程序错误(如SIGSEGV)。此外,不能被忽略处理的信号(SIGKILL 和SIGSTOP )无法被阻塞。
注意,pthread_sigmask()与sigprocmask()函数功能类似。两者的区别在于,pthread_sigmask()是线程库函数,用于多线程进程,且失败时返回errno;而sigprocmask()针对单线程的进程,其行为在多线程的进程中没有定义,且失败时设置errno并返回-1。
2.2 sigwait
线程可通过调用sigwait()函数等待一个或多个信号发生。
#include <signal.h> int sigwait(const sigset_t *restrict sigset, int *restrict signop); |
参数sigset指定线程等待的信号集,signop指向的整数表明接收到的信号值。该函数将调用线程挂起,直到信号集中的任何一个信号被递送。该函数接收递送的信号后,将其从未决队列中移除(以防返回时信号被signal/sigaction安装的处理函数捕获),然后唤醒线程并返回。该函数执行成功时返回0,并将接收到的信号值存入signop所指向的内存空间;失败时返回错误编号(errno)。失败原因通常为EINVAL(指定信号无效或不支持),但并不返回EINTR错误。
给定线程的未决信号集是整个进程未决信号集与该线程未决信号集的并集。若等待信号集中某个信号在sigwait()调用时处于未决状态,则该函数将无阻塞地返回。若同时有多个等待中的信号处于未决状态,则对这些信号的选择规则和顺序未定义。在返回之前,sigwait()将从进程中原子性地移除所选定的未决信号。
若已阻塞等待信号集中的信号,则sigwait()会自动解除信号集的阻塞状态,直到有新的信号被递送。在返回之前,sigwait()将恢复线程的信号屏蔽字。因此,sigwait()并不改变信号的阻塞状态。可见,sigwait()的这种“解阻-等待-阻塞”特性,与条件变量非常相似。
为避免错误发生,调用sigwait()前必须阻塞那些它正在等待的信号。在单线程环境中,调用程序首先调用sigprocmask()阻塞等待信号集中的信号,以防这些信号在连续的sigwait()调用之间进入未决状态,从而触发默认动作或信号处理函数。在多线程程序中,所有线程(包括调用线程)都必须阻塞等待信号集中的信号,否则信号可能被递送到调用线程之外的其他线程。建议在创建线程前调用pthread_sigmask()阻塞这些信号(新线程继承信号屏蔽字),然后绝不显式解除阻塞(sigwait会自动解除信号集的阻塞状态)。
若多个线程调用sigwait()等待同一信号,只有一个(但不确定哪个)线程可从sigwait()中返回。若信号被捕获(通过sigaction安装信号处理函数),且线程正在sigwait()调用中等待同一信号,则由系统实现来决定以何种方?div style="border-bottom:1px solid #aaa;margin-bottom:25px">网络采集软件核心技术剖析系列(4)---使用C#语言如何将html网页转换成pdf(html2pdf) - 际为软件事务所 阅读原文»
一 本系列随笔概览及产生的背景
本系列开篇受到大家的热烈欢迎,这对博主是莫大的鼓励,此为本系列第四篇,希望大家继续支持,为我继续写作提供动力。
自己开发的豆约翰博客备份专家软件工具问世3年多以来,深受广大博客写作和阅读爱好者的喜爱。同时也不乏一些技术爱好者咨询我,这个软件里面各种实用的功能是如何实现的。
该软件使用.NET技术开发,为回馈社区,现将该软件中用到的核心技术,开辟一个专栏,写一个系列文章,以飨广大技术爱好者。
本系列文章除了讲解网络采编发用到的各种重要技术之外,也提供了不少问题的解决思路和界面开发的编程经验,非常适合.NET开发的初级,中级读者,希望大家多多支持。
很多初学者常有此类困惑,“为什么我书也看了,C#相关的各个方面的知识都有所了解,但就是没法写出一个像样的应用呢?”
这其实还是没有学会综合运用所学知识,锻炼出编程思维,建立起学习兴趣,我想该系列文章也许会帮到您,但愿如此。
开发环境:VS2008
本节源码位置:https://github.com/songboriceboy/csharphtml2pdf
源码下载办法:安装SVN客户端(本文最后提供下载地址),然后checkout以下的地址:https://github.com/songboriceboy/csharphtml2pdf
系列文章提纲拟定如下:
2.如何使用C#语言获得博文的正文及标题;
3.使用C#语言如何将html网页转换成pdf(html2pdf)
4.如何使用C#语言下载博文中的全部图片到本地并可以离线浏览
5.如何使用C#语言合成多个单个的pdf文件到一个pdf中,并生成目录
6.网易博客的链接如何使用C#语言获取到,网易博客的特殊性;
7.微信公众号文章如何使用C#语言下载;
8.如何获取任意一篇文章的全部图文
9.如何使用C#语言去掉html中的全部标签获取纯文本(html2txt)
10.如何使用C#语言将多个html文件编译成chm(html2chm)
11.如何使用C#语言远程发布文章到新浪博客
12.如何使用C#语言开发静态站点生成器
13.如何使用C#语言搭建程序框架(经典Winform界面,顶部菜单栏,工具栏,左边树形列表,右边多Tab界面)
14.如何使用C#语言实现网页编辑器(Winform)
二 第四节主要内容简介(使用C#语言如何将html网页转换成pdf)
本节示例代码的运行界面如下图所示。点击生成PDF按钮后,程序做了3件事情:
(1)下载网页地址中博文的正文;
(2)下载博文中的全部图片到本地;
(3)将文字和图片用后面所说的工具生成为PDF文档。
点击生成PDF按钮后,该程序后自动下载网址中对应的网页,并在可执行程序所在目录生成一个以博文标题命名的文件夹,如下图所示:
该文件夹中包含了网页正文的html文档(index.html文件),正文中的全部图片,以及最后生成的pdf文档。如下图所示:
生成的pdf文档效果如下图所示:
将html文件转换成pdf文件所需要的技术非常高,我们需要写一个既能解析html文档(类似浏览器功能),又能生成pdf文档(需要掌握pdf文档结构细节)的工具,幸运的是一个有一个好用免费的现成的工具,那就是wkhtmltopdf(http://www.wkhtmltopdf.org/)。
首先,我们来看一下如何通过这个工具来转html文档到pdf文档。下载对应平台的可执行文件(windows平台的可以在我上面的github地址中下载到,在PDFLIB文件夹中)。文件夹中有4个文件,如下图所示:
进入DOS界面,执行命令wkhtmltopdf.exe www.cnblogs.com cnblogs.pdf,第一个参数是要转化的网页地址(www.cnblogs.com),
第二个参数是要保存的pdf文件名称(cnblogs.pdf),这个命令执行成功之后会发现在当前路径下生成了一个cnblogs.pdf文件(我这里的路径是d:/PDFLIB/)。
生成的PDF文件如下:
接下来我们需要做的是将这个生成过程整合到我们自己开发的软件中。
由上面的做法可以很容易得出,我们需要使用C#语言来实现进程间通信,即在我们自己的代码中启动wkhtmltopdf.exe进程,并传递给wkhtmltopdf.exe其所需要的参数。
C#中启动一个进程可以利用Process类,下面我们就来看一下具体做法,核心代码如下:
{
string strPdfSavedPath = m_strPath;
if (!Directory.Exists(strPdfSavedPath))//判断是否存在
{
Directory.CreateDirectory(strPdfSavedPath);//创建新路径
}
if (!File.Exists(strPdfSavedPath + fileName + ".pdf"))
{
string strHtmlSavedPath = m_strPath;
string file_flvbind = Application.StartupPath + @"\PDFLIB\wkhtmltopdf.exe";
//MoveFolderTo(fileName, Application.StartupPath + @"\PDFLIB\");
//生成ProcessStartInfo
ProcessStartInfo pinfo = new ProcessStartInfo(file_flvbind);
//pinfo.WorkingDirectory = Application.StartupPath + @"\PDFLIB\";
pinfo.WorkingDirectory = strHtmlSavedPath;
//设置参数
StringBuilder sb = new StringBuilder();
sb.Append("--footer-line ");
sb.Append("--footer-center \"powered by 际为软件事务所(http://www.cnblogs.com/ice-river)\" ");
sb.Append("\"" + "index.html\"");
sb.Append(" \"" + strPdfSavedPath + fileName + ".pdf" + "\"");
pinfo.Arguments = sb.ToString();
//隐藏窗口
pinfo.WindowStyle = ProcessWindowStyle.Hidden;
//启动程序
Process p = Process.Start(pinfo);
p.WaitForExit();
//DeleteFiles(Application.StartupPath + @"\PDFLIB\");
if (p.ExitCode == 0)
{
DelegatePara dp = new DelegatePara();
dp.strLog = "生成 [" + fileName + "
没有评论:
发表评论