企业网站建立流程的第一步是,网站维护简单吗,手机套 东莞网站建设,青岛北京网站建设#x1f3ac; 个人主页#xff1a;谁在夜里看海. 
#x1f4d6; 个人专栏#xff1a;《C系列》《Linux系列》《算法系列》 
⛰️ 时间不语#xff0c;却回答了所有问题 目录 
#x1f4da;前言 
#x1f4da;一、信号的本质 
#x1f4d6;1.异步通信 
#x1f4d6;2.信… 个人主页谁在夜里看海. 个人专栏《C系列》《Linux系列》《算法系列》 
⛰️ 时间不语却回答了所有问题 目录 
前言 
一、信号的本质 
1.异步通信 
2.信号列表 
3.信号发送的本质 
二、信号的产生 
1.终端按键 
core dump 
2.系统调用 
kill() 
raise() 
abort() 
alarm() 
三、信号的捕获 
1.signal信号处理 
函数原型 
示例 
2.sigaction信号处理 
函数原型 
示例  
四、信号的阻塞与未决 
1.阻塞的本质 
未决信号 
阻塞信号 
⚠️阻塞≠忽略 
2.阻塞的应用 
函数原型 
示例 
五、可重入函数 
1.定义 
特性 
示例 
2.volatile关键字 
-O1优化  前言 
上一章我们谈论了进程间的通信机制由于不同进程之间有时需要相互协作所以引入了进程间通信的概念使得不同的进程可以共享同一份资源完成数据的传输。进程间通信时是用户层面的通信为什么这么说呢因为在这个过程中用户空间的进程通过特定的通信机制来传递数据或信息而这些操作并不涉及操作系统的内核执行逻辑通信的内容也是由用户自定义的。 
然而系统层面也需要通信比如一个进程执行异常时操作系统必须对异常进程进行处理那么有哪些处理方式呢 
✅①直接“杀死”该进程将进程从CPU运行队列中移除但是存在隐患比如文件还没关闭资源还未释放有内存泄漏的风险 
✅②告知进程退出让进程主动退出这样可以保证资源被释放。 
系统告知进程退出不就是系统与进程间的通信嘛这一类通信不需要用户层的参与在操作系统底层完成并且通信的内容也不由用户定义而是由系统定义这种通信方式就叫做信号。 
一、信号的本质 
1.异步通信 
在用户层的通信中比如管道通信我们可以明确通信何时发生数据何时进行交换吗答案是可以管道通信是借助一个管道文件完成的进程需要对管道文件进行打开和关闭操作而这些操作在执行流中是确定的也就是说用户规定了进程在何时发送数据何时接收数据我们称这种通信为同步通信。 
然而在系统层的通信中我们可以明确通信何时发生吗就比如程序执行异常时需要发送信号告知程序终止我们能确定程序何时异常吗并不能确定如果能确定的话我们就完全可以避免异常的发生了就不会引发异常了我们称这种通信为异步通信。 
2.信号列表 
❓为什么操作系统需要与进程进行通信呢 
✅通常进程和操作系统之间的通信发生在一些特殊情况下而不是无缘无故的通信。这些特殊情况往往是有限的比如异常、外部中断、资源不足等。针对这些情况操作系统有相应的处理方法例如遇到异常时操作系统会终止进程当外部中断发生时操作系统会中断进程的执行并进行相应处理。因此操作系统只需将所有可能出现的特殊情况及其对应的处理办法保存在一个集合中当进程遇到某种情况时操作系统就发送相应的信号进行处理这个集合被称为信号列表。 
通过kill -l命令可以查看系统定义的信号列表 我们可以看到每个信号都有一个编号以及宏定义名称这些宏定义可以signal.h中找到。其中编号32号以下的信号是标准信号它们用于处理进程中的常见情况如进程终止、异常、退出等32号以上的信号是是实时信号通常由用户定义并用于更精细的进程控制。 
3.信号发送的本质 
在操作系统中每一个信号都有一个对应的编号这些编号的范围是1~64所以在信号传输的过程中我们并不需要传输具体的信号宏名称只需要一个位图即可实现比如传输SIGINT信号时只需要将位图中第二位置为1即可完成信号的传输。因此每一个进程都会维护一个用于表示信号集的位图每一位对应一个信号的状态。 
理解了信号的本质我们来谈谈信号的操作即信号是如何产生的进程对信号又是如何处理的 
二、信号的产生 
1.终端按键 
其实我们早就接触过信号了平常我们写代码时偶尔会遇到程序陷入死循环或卡住的情况这个时候我们按住ctrlc就可以退出进程结束死循环其实按下ctrlc的过程就是向进程发送一个SIGINT信号用于通知进程中断当前操作这就是第一种信号产生的方式终端按键产生。 
不止ctrlc可以产生信号其他按键也可以产生相应的信号例如ctrl\用于发送SIGQUIT信号该信号可以强制终止进程这与SIGINT类似但不同的是SIGQUIT可以生成核心转储文件core dump以便于调试 core dump 
Core dump核心转储是操作系统在进程崩溃时将该进程的内存内容保存到一个文件中的机制能够为开发者提供有关崩溃时程序状态的详细信息。 
⚠️触发core dump时系统会向磁盘写入一个核心转储文件用于保存程序崩溃的完整快照但是并不是所有程序崩溃的场景都需要一个快照因为这样会导致磁盘空间被大量占用当磁盘空间被大量占用时可能会引发比程序崩溃更严重的问题所以默认情况下禁用core dump文件。 
然而可以通过ulimit命令接触限制 
① 使用 ulimit -c 命令查看当前系统的核心转储限制。如果输出为 0则表示禁用了核心转储。 
② 通过 ulimit -c unlimited 命令可以取消核心转储大小限制允许生成核心转储文件。 
2.系统调用 
kill() 
通常情况下我们是通过系统调用 kill 来实现信号的发送的例如 
kill -SIGINT (进程pid)
# 或者
kill -2 (进程pid) 
这种方式等价与按键ctrlc都是向指定进程发送SIGINT信号。 
除了在终端输入kill指令我们还可以在函数内部调用kill()函数 
#include stdio.h
#include signal.h
#include unistd.hint main() {pid_t pid  getpid();  // 获取当前进程IDprintf(Sending SIGINT signal to process %d\n, pid);// 向当前进程发送 SIGINT 信号if (kill(pid, SIGINT)  -1) {perror(Error sending signal);}// 除了向当前进行发送还可以向其他进程发送 SIGINT 信号只要知道它的pidreturn 0;
}❓为什么是kill 
kill是“杀死”的意思为什么发送信号的指令要取名为“杀死”呢? 
✅这大概是因为在信号被设计之初其作用主要就是“杀死”一个进程所以当时直接将信号发送取名为kill到了后面信号的种类越来越多但是kill这个指令名称被保留了下来沿用至今。但是不要因为指令叫做kill就认为它只能“杀死”进程实际上它用于发送多种不同类型的信号。 
除了kill()之外其他一些与信号相关的系统调用和库函数也能触发信号的发送或控制信号的行为。  
raise() 
raise()函数用于向当前进程发送信号自己给自己发信号 
#include signal.h
int raise(int sig);sig表示要发送的信号。 
abort() 
abort()函数也可以向当前进程发送信号但与raise不同的是它只能发送SIGABRT信号因此函数内部不包含任何参数 
#include stdio.h
#include stdlib.hint main() {printf(Before abort\n);abort();  // 终止进程并生成核心转储printf(This line will never be executed.\n);return 0;
}alarm() 
alarm()函数的作用是在指定时间之后发送一个SIGALRM信号 
#include unistd.hunsigned int alarm(unsigned int seconds);参数 seconds设置定时器的时间单位为秒当 alarm() 被调用时系统会在 seconds 秒后向当前进程发送 SIGALRM 信号。 
返回值 如果成功设置定时器它返回剩余的时间。如果定时器已经存在返回值是该定时器剩余的时间。 
示例 
#include iostream
using namespace std;
#include unistd.h
#include signal.hint main()
{alarm(5);while(1){coutI am a process...endl;sleep(1);}return 0;} 三、信号的捕获 
进程对于信号的处理有以下三种方式 
① 忽略信号进程可以选择忽略某个信号即在接收该信号时不执行任何操作 
② 处理信号进程可以指定一个信号处理函数来处理指定信号 
③ 默认处理如果没有指定默认的信号处理函数操作系统会采用该信号的默认行为。 
1.signal信号处理 
为了捕捉并处理信号我们通常调用signal()函数通过该函数我们可以自定义信号处理行为。 
函数原型 
#include signal.htypedef void (*sighandler_t)(int);sighandler_t signal(int signum, sighandler_t handler);signum是我们要捕获的信号编号 
对于handler参数我们有两种选项 
① 采用关键字SIG_IGN表示当前信号被忽略SIG_DFL表示采用默认处理方式 
② 传入自定义信号处理函数函数内部带有一个int型参数表示信号编号 
示例 
#include iostream
using namespace std;
#include unistd.h
#include signal.hvoid sigcb(int sig)
{if(sig  2)coutcatch a sig: sigendl;
}int main()
{signal(2,sigcb); // 调用自定义的信号处理函数coutThis is a process,pid is: getpid()endl;while(1);return 0;
} 
此时通过ctrlc发生SIGINT信号时程序执行自定义行为 此时我们将signal的第二个参数修改成SIG_IGN表示忽略当前信号 
#include iostream
using namespace std;
#include unistd.h
#include signal.hint main()
{signal(2,SIG_IGN);coutThis is a process,pid is: getpid()endl;while(1);return 0;
} 
此时输入ctrlcSIGINT信号被忽略不执行任何行为 2.sigaction信号处理 
sigaction函数不仅可以指定信号处理程序还可以设置额外的选项来控制信号处理的细节。 
函数原型 
#include signal.hint sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);参数 
① signum 指定要操作的信号编号 
② act指向一个struct sigaction 类型的结构体用于定义信号处理行为可设置为NULL 
③ oldact保存之前的信号处理行为结构体可设置为NULL。 
struct sigaction 
该结构体用于定义信号处理行为 
struct sigaction {void     (*sa_handler)(int);     // 信号处理函数或以下宏之一// SIG_DFL使用默认行为// SIG_IGN忽略信号void     (*sa_sigaction)(int, siginfo_t *, void *); // 备用信号处理函数sigset_t sa_mask;                // 在处理此信号时需要阻塞的其他信号集合int      sa_flags;               // 信号处理的标志void     (*sa_restorer)(void);   // 已废弃
};结构体成员 ① sa_handler指定信号处理函数简单处理 ② sa_sigaction用于处理更复杂信号信息需要设置 SA_SIGINFO 标志 ③ sa_mask在处理信号时需要阻塞的其他信号 ④ sa_flags控制信号处理行为的选项。常用值         SA_RESTART自动重启被信号中断的系统调用         SA_SIGINFO启用 sa_sigaction传递更多信号信息         SA_NOCLDWAIT阻止僵尸进程的生成用于 SIGCHLD         SA_NODEFER在处理信号时不自动阻塞该信号 示例  
设置对SIGINT信号的处理行为 
#include iostream
using namespace std;
#include unistd.h
#include signal.hvoid sigcb(int sig)
{coutcatch a sig: sigendl;
}int main()
{struct sigaction sa;sa.sa_handler  sigcb;sigemptyset(sa.sa_mask);sa.sa_flags  0;sigaction(2,sa,nullptr);while(1);return 0;
} 四、信号的阻塞与未决 
然而对于进程来说并不是所有信号都需要立即响应的因为在信号到达时进程需要立即停止当前执行流去执行信号处理操作。例如一个进程的部分执行内容是对一个大文件进行拷贝写入如果在写入的途中中断了就会导致文件不完整甚至损坏所以进程希望写入操作执行完毕后再响应信号此时就需要信号阻塞操作。 
1.阻塞的本质 
我们前面提到信号发送的本质是将目标进程的信号集位图中相应位置的标志位置为1实际上进程要维护两个这样信号集一个为阻塞信号集一个为未决信号集 
未决信号 
当系统向进程发送信号时修改的就是未决信号集中的指定标志位在未决信号是指已经由系统发送但进程因为某些原因还未处理的信号一个信号要想被进程处理的第一个条件就是在未决信号集Pending中标志位为1 
阻塞信号 
阻塞信号Block就是进程希望延迟执行的信号例如在文件写入完毕之后再执行SIGINT信号此时就需要先将SIGINT信号阻塞写入完毕后再取消阻塞未决信号想要被进程执行还需要判断是否阻塞即在阻塞信号集中的标志位是否为1为1则阻塞 所以一个信号想要被执行需要满足 
① 未决位为1表示信号已产生 
② 阻塞位为0表示信号不被阻塞 
③ 其他条件例如进程空闲没有在执行其他信号... 
⚠️阻塞≠忽略 
信号被阻塞与信号被忽略是两个不同的概念 
信号被阻塞表示信号不会被进程接收如果产生则处于未决状态Pending 
信号被忽略是进程对信号的一种处理方式表示进程接收信号时忽略该信号不做处理 
2.阻塞的应用 
函数原型 
sigprocmask() 是一个用于设置或获取当前进程阻塞信号集的系统调用。它可以阻塞、解除阻塞或替换进程的阻塞信号集。 
#include signal.hint sigprocmask(int how, const sigset_t *set, sigset_t *oldset);参数 
① how指定对信号集的操作方式可以取以下值 SIG_BLOCK将 set 中的信号添加到当前阻塞信号集中 SIG_UNBLOCK从当前阻塞信号集中移除 set 中的信号 SIG_SETMASK将当前阻塞信号集替换为 set。 
② set指向包含要阻塞或解除阻塞的信号集的指针。 
③ oldset存储之前的阻塞信号集为NULL表示不作存储。 
返回值 
成功返回 0失败返回 -1并设置errno。 
除了上述函数我们还需要一系列的信号集操作函数 
#include signal.h
// sigset_t set; // 调用下面函数前需要先定义一个函数集变量
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismemberconst sigset_t *set, int signo); 
下面对这些函数进行一一介绍 
① sigemptyset 
初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。 
② sigfillset 
初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号包括系统支持的所有信号。 
③ sigaddset 
向set指向的信号集中添加信号signo即将signo对应的标志位置为1。 
④ sigdelset 
将set指向的信号集中的signo信号删除即将signo对应的标志位置为0。 
⑤ sigismember 
判断set指向的信号集中是否存在信号signo也可以理解成返回signo对应的标志位。 
示例 
设置阻塞信号2与40观察发生信号时的状态 
#include iostream
using namespace std;
#include unistd.h
#include signal.hvoid printsigset(sigset_t *set)
{for(int i0;i64;i){if(sigismember(set,i))putchar(1);elseputchar(0);}coutendl;
}int main()
{coutThis is a process,pid is: getpid()endl;sigset_t m,p;sigemptyset(m);sigaddset(m,2);sigaddset(m,40);sigprocmask(SIG_BLOCK,m,NULL);while(1){sigpending(p);printsigset(p);sleep(3);}return 0;
} 
此时阻塞了信号2与40并通过打印未决信号集观察情况 发现发送信号2和40后未决信号集的第2位和第40位置为1表示信号2和40为未决信号。 
五、可重入函数 
信号处理函数执行过程中可能被任意中断因此信号处理函数需要使用可重入函数以避免数据竞争、资源冲突和未定义行为。 
1.定义 
可重入函数是指函数在执行过程中可以被中断并且在中断期间又可以安全地被调用且两次调用互不干扰。 
特性 
① 无全局变量的依赖 函数内部不依赖于全局或静态变量或者对这些变量的操作是线程安全的原子性操作 
② 无静态数据的修改 函数不能修改其内部的静态变量因为多次调用可能导致状态不一致 
③ 无不可重入函数调用 函数只能调用其他可重入函数 
④ 使用局部资源 函数仅使用局部变量或参数不会影响其他调用的状态。  
示例 
情况一两个线程并发地执行以下代码假设a是全局变量初始为0那么输出结果会是什么 void foo(){aa1;printf(%d ,a);} 
✅答案1 1 、1 2 、2 1 、2 2 都有可能 
分析 
aa1并不是原子性操作怎么理解呢 
原子性操作只有未执行和执行完毕两种情况而aa1的操作流程是 
①将a从全局数据区提取到寄存器中②在寄存器中完成1操作③将a返回到全局数据区 线程之间是共享虚拟地址空间的除了栈区线程之间各自拥有一块栈区空间所以它们在访问全局变量a的时候会产生竞争访问它们可能同时将a提取到各自的寄存器中也有可能进程1先提取修改完毕返回后进程2才提取此时进程2提取的就是已经经历过一次1的a 情况二那如果此时a是局部变量呢 
✅答案只会是 1 1。 
因为线程之间各自拥有一块栈空间a是局部变量的话那么在两个线程之间各有一份即线程会在各自的栈空间中提取a到寄存器并返回1后的结果不存在竞争访问问题执行结果只会是1 1。 
情况一的foo函数是不可重入函数因为存在竞争访问情况二的foo函数是可重入函数因为没有使用全局变量只存在于栈空间不存在竞争访问。 
2.volatile关键字 
看下面代码 
#include iostream
using namespace std;
#include unistd.h
#include signal.hint flag  0;
void handler(int sig)
{if(sig  2){coutchange flag 0 to 1endl;flag  1;}
}int main()
{signal(2,handler);while(!flag);exit(0);
} 
在标准情况下键入CTRL-C ,2号信号被捕捉执行自定义动作修改flag1while条件不满足,退出循 环进程退出 
# makefile
test:volatile.cppg -o $ $^ #标准情况.PHONY:clean
clean:rm -f test 但是在优化情况下键入CTRL-C ,2号信号被捕捉执行自定义动作修改flag1但是while条件依旧满足,进程继续运行 
test:volatile.cppg -o $ $^ -O1 #优化情况.PHONY:clean
clean:rm -f test 这是为什么呢 
-O1优化  
-O1是编译器的一个优化级别在当前级别下编译器会对代码进行性能优化 原本存储在全局数据区的变量flag被直接存储到了寄存器中signal函数调用中对flag值的修改是在另一个寄存器中进行的而while循环中对flag的判断还是在原先的寄存器中提取导致signal函数中对flag的修改没有作用到while循环中键入CTRL-C最终不会退出进程。 
为了解决编译器优化带来的这一潜在问题我们可以使用volatile关键字修饰flag的定义表示flag不被优化这样就保证了signal中对flag的修改可以同步到while循环中 
#include iostream
using namespace std;
#include unistd.h
#include signal.hvolatile int flag  0; // volatile修饰
void handler(int sig)
{if(sig  2){coutchange flag 0 to 1endl;flag  1;}
}int main()
{signal(2,handler);while(!flag);exit(0);
} 
在优化情况下键入CTRL-C ,2号信号被捕捉执行自定义动作修改flag1while条件不满足,退出循 环进程退出 
test:volatile.cppg -o $ $^ -O1 #优化情况.PHONY:clean
clean:rm -f test 以上就是【文件操作的艺术——从基础到精通】的全部内容欢迎指正~  
码文不易还请多多关注支持这是我持续创作的最大动力