【Linux】进程信号
🌠 作者:@阿亮joy.
🎆专栏:《学会Linux》
🎇 座右铭:每个优秀的人都有一段沉默的时光,那段时光是付出了很多努力却得不到结果的日子,我们把它叫做扎根
目录
- 👉信号入门👈
- 生活中的信号
- Linux信号
- 常见信号
- 👉信号产生👈
- 通过终端按键产生信号
- 核心转储
- 调用系统函数向进程发信号
- 由软件条件产生信号
- 硬件异常产生信号
- 👉阻塞信号👈
- 信号其他相关常见概念
- 信号在内核中的表示
- sigset_t
- 信号集操作函数
- sigpending
- sigprocmask
- 👉捕捉信号👈
- 内核如何实现信号的捕捉
- sigaction
- 👉可重入函数👈
- 👉volatile👈
- 👉SIGCHLD信号👈
- 👉总结👈
👉信号入门👈
信号是一种软件中断,信号在 Linux 操作系统 中提供了一种处理异步事件的方法,可以很好地在多个进程之间进行同步和简单的数据交互。注:信号和信号是两个东西,没有关系!信号只是用来通知某个进程发生了什么事情,但并不给该进程传递任何数据。
生活中的信号
在生活中,我们会收到很多信号,比如:红绿灯、闹钟、转向灯和狼烟等等。那我们为什么会知道这些生活中的信号呢?其实是我们曾经学习过有关这些生活信号的知识并且记住了对应场景下的信号。有关信号的推论如下:
- 当这些信号产生时,我们就能够识别这些信号,并且执行相应的动作。
- 当特定信号没有产生时,我们依旧知道应该如何处理这个信号。
- 当我们收到信号时,我们可能不会立即处理这个信号。
- 当我们无法立即处理信号的时候,信号也一定要先被临时地记住。
Linux信号
什么是 Linux 信号?Linux 信号本质是一种通知机制,用户或操作系统通过发送一定的信号,通知进程某些时间已经发生了,进程可以在后续进行信号处理。
- 进程要处理信号,那么进程必须具备信号识别的能力(收到信号加上相对应的信号处理动作)。
- 为什么进程能够识别信号呢?进程能够识别信号,肯定是设计操作系统的程序员将常见的信号及信号处理动作内置到进程的代码和属性中。
- 信号产生是随机的,当信号产生时,进程可能正在处理某些任务。所以,信号可能不是立即被进程处理的。
- 信号会被临时地记录下来,方便进程后续进行处理。
- 那进程会在什么时候处理信号呢?合适的时候。
- 一般而言,信号的产生相对于进程而言是异步的。异步指两个或两个以上的对象或事件不同时存在或发生(或多个相关事物的发生无需等待其前一事物的完成)。同步指两个或两个以上随时间变化的量在变化过程中保持一定的相对关系。
- 注:信号也有确定的信号,比如:定下闹钟的时间时,那么闹钟一定会在那个时间点响起来。
- 信号处理的常见方式:
- 默认(进程自带的处理动作,该动作是程序员写好的逻辑)
- 忽略(忽略也是信号处理的一种方式)
- 自定义动作(捕捉信号)
常见信号
kill -l #该命令可以查看常见的信号
man 7 signal #查看信号的相关描述
Linux 内核支持 62 种不同的信号,这些信号都有一个名字,这些名字都以三个字符 SIG 开头。在头文件siganl.h
中你能够,这些信号都被定义为正整数,称为信息编号。其中,编号 1 到 31 的信号称为普通信号,编号 34 到 64 的信号称为实时信号,实时信号对处理的要求比较高。
普通信号和实时信号的关系就像分时操作系统和实时操作系统的关系类似,分时操作系统是基于时间片轮转调度的,而实时操作系统要求要有严格的时序,可以认为是一个队列。将一个任务放入该队列中,那么操作系统就尽量快地将该任务处理完。日常生活中使用最多的就是分时操作系统,而实时操作系统常见于特殊的行业,如军工领域和自动驾驶领域等等。
组合键转化成信号
Ctrl + C 的本质就是给进程发送了 2 信号,进程接收到 2 号信号后的默认处理动作是结束进程。
那如何理解组合键变成信号呢?其实键盘的工作方式是通过中断方式进行的。键盘是槽位的,每个槽位都会对应一个编号。因为有键盘驱动,操作系统是能够识别这些编号的。只要按下了一些键,操作系统立马就能够识别到。那么当你按下组合键,操作系统也是可以识别到的。操作系统既然都识别到了你按下了组合键,那么操作系统给特定的进程发送信号,也就是轻而易举的事情了。
既然进程要接收操作系统发送过来的信号,那么进程必须要具有保存信号的相关数据结构,而该数据结构就是位图(unsigned int),使用比特位信息就可以表示操作系统是否有给进程发送信号。比如:最低位比特位为 1,则说明操作系统给该进程发送了 1 号信号;反之,则操作系统没有给该进程发送 1 号信号。注:该位图结构保存在进程的内核数据结构 task_struct 中,只有操作系统才能修改 task_struct。信号产生的方式有很多种,但其发送的本质就是操作系统向目标进程写信号,操作系统修改 task_struct 中的位图结构,完成信号发送的过程。
那么组合键能够转化成信号也就很好理解了。当你按下组合键 Ctrl + C 时,操作系统识别到该组合键并解释该组合键,然后查找到在前台运行的进程,最后操作系统将 Ctrl + C 对应的信号写入到进程内部的位图结构中就完成了信号发送。现在进程已经将操作系统发给它的信号记录下来了,进程就会在合适的时候处理该信号。
注意:
- Ctrl + C 产生的信号只能发给前台进程。一个命令后面加个 & 可以放到后台运行,这样 shell 不必等待进程结束就可以接受新的命令,启动新的进程。
- shell 可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl + C 这种组合键产生的信号。
- 前台进程在运行过程中用户随时可能按下 Ctrl + C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous) 的。
👉信号产生👈
通过终端按键产生信号
在上面的内容已经提及到,按下组合键 Ctrl + C 可以前台进程发送 2 号信号,那我们可以通过 signal 函数来验证一下。
signal 函数的原型如下:
使用 signal 函数后,当进程接收到 signum 信号时,进程会调用 handler 函数(handler 是回调函数,handler 是 函数指针类型,该函数的返回值是 void,参数是 int)并将 signum 传递给 handler 函数,其实 signal 函数相当于可以自定义捕捉某个信号。signal 函数的返回值是对于 signum 信号的旧的处理方法。
#include <iostream>
#include <signal.h>
#include <unistd.h>using namespace std;void catchSignal(int signal)
{cout << "捕捉到了一个信号: " << signal << endl;
}int main()
{// signal(2, catchSignal); // 这种写法也可以// catchSignal是自定义捕捉signal(SIGINT, catchSignal); // 特定信号的处理动作一般只有一个while(true){cout << "我是一个进程,我的pid是: " << getpid() << endl;sleep(2);}return 0;
}
注:signal 函数仅仅是修改进程对特定信号的后续处理动作,并不是直接调用对应的处理动作。而是当进程接收到特定信号时,才会去调用对应的处理动作。如果后续没有产生 SIGINT 信号,catchSignal 函数就不会被调用,signal 函数往往放在最前面,先注册特定信号的处理方法。
现在就无法通过 Ctrl + C(2 号信号)终止该进程了,那么我们可以通过 Ctrl + \ (3 号信号)终止该进程。如果你也将 3 号信号也自定义捕捉了,那么可以发生 8 号信号(浮点数异常)来终止进程。
核心转储
首先解释什么是核心转储(Core Dump)。当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是 core,这种行为就叫做核心转储(Core Dump)。
进程异常终止通常是因为有 Bug,比如非法内存访问导致段错误,事后可以用调试器检查 core 文件以查清错误原因,这叫做 Post-mortem Debug(事后调试)。一个进程允许产生多大的 core 文件取决于进程的 Resource Limit(这个信息保存在 PCB 中)。默认是不允许产生 core 文件的,因为 core 文件中可能包含用户密码等敏感信息,不安全。在开发调试阶段可以用 ulimit 命令改变这个限制,允许产生 core 文件。 首先用 ulimit 命令改变 shell 进程的 Resource Limit,允许 core 文件最大为1024K:$ ulimit -c 1024。
一般而言,云服务器(生产环境)的核心转储功能是关闭的。程序员写代码的环境称为开发环境,测试人员的环境是测试环境(测试 Realease 版本),产品上线后用户可以使用的环境就成为生产环境(有对应的服务器)。我们所购买的云服务器是集开发、测试、发布、部署于一体的机器。
打开云服务器的核心转储功能后,我们来验证一下是否真的会产生 core 文件。
注:只有核心转储才会生成 core 文件。
core 文件是以进程 ID 作为后缀,通常该文件是比较大的。生产环境一般会关闭核心转储功能是为了防止生成大量的 core 文件占用磁盘空间。如果磁盘中充满大量的 core 文件,可能会导致服务器无法重启或操作系统挂掉。
通过生成的 core 文件来进行 Debug
验证进程等待中的 core dump 标记位
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
#include <cassert>using namespace std;// 验证进程等待中的core dump标记位
int main()
{int id = fork();// 子进程if(id == 0){sleep(2);int a = 100;a /= 0;exit(0);}int status = 0;int ret = waitpid(id, &status, 0);assert(ret != -1);(void)ret;cout << "父进程: " << getpid() << " 子进程: " << id << " exit signal: " \<< (status & 0x7F) << " is core: " << ((status >> 7) & 1) << endl;// 父进程return 0;
}
将核心转储功能关闭,就不会生成 core 文件,core dump 标记位始终为 0;当进程不是收到核心转储信号终止进程的,也不会生成 core 文件,core dump 的标记位也始终为 0。
调用系统函数向进程发信号
通过系统调用实现 mykill 命令
系统调用 kill 函数可以想指定的进程发送指定的信号。
// mykill.cc
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <cstring>
#include <stdlib.h>using namespace std;static void Usage(string proc)
{cout << "Usage:\r\n\t" << proc << " -SignalNumber ProcessID" << endl;
}// 通过系统调用向进程发送信号(设计mykill命令)
// ./mykill -2 pid
int main(int argc, char* argv[])
{if(argc != 3){Usage(argv[0]);exit(1);}int signal = atoi(argv[1] + 1);int id = atoi(argv[2]);kill(id, signal);return 0;
}
raise 函数可以给调用该函数的进程发信号,
raise(sig)
等价于kill(getpid(), sig)
。
#include <iostream>
#include <signal.h>
#include <unistd.h>using namespace std;int main()
{cout << "我正在运行中..." << endl;sleep(2);raise(8);return 0;
}
abort 函数给调用该函数的进程发送6号信(SIGABRT)终止进程,6 号信号会引起核心转储,通常用来终止进程。就像 exit 函数一样,abort 函数总是会成功的,所以没有返回值。
#include <iostream>
#include <unistd.h>
#include <stdlib.h>using namespace std;int main()
{cout << "我正在运行中..." << endl;sleep(2);abort();return 0;
}
如何理解通过系统调用向进程发信号?用户调用系统接口,执行操作系统对应的系统调用代码,操作系统提取参数或设置特定的数值(信号编号和进程 ID),操作系统向目标进程写信号(修改对应进程的位图结构),进程后续处理信号执行相应的处理动作。
由软件条件产生信号
学习管道的时候,我们说过:当管道读端关闭,写端一直在写,操作系统会自动终止对应的写端进程。操作系统是通过发送 13 号信号(SIGPIPE)来终止写端进程的!
那现在我们来按照一下步骤来验证一下!
-
- 创建匿名管道
-
- 让父进程进行读取,子进程进行写入
-
- 父子进程通行一段时间(该步骤可以省略)
-
- 让父进程先关闭读端,子进程只有一直写入就行
-
- 父进程通过 waitpid 等待子进程拿到子进程的退出信息
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <cassert>
#include <string>
#include <cstring>using namespace std;int main()
{// 创建匿名管道int pipefd[2] = {0};int n = pipe(pipefd);assert(n != -1);(void)n;cout << "创建匿名管道成功" << endl;// 创建子进程int id = fork();if(id == 0){// 子进程// 关闭子进程的读端close(pipefd[0]);char send_buffer[128] = {'\0'};string s = "我是子进程,我正在给你发消息";int count = 0;while(1){// 构造变化的字符串snprintf(send_buffer, sizeof(send_buffer), "%s id:%d %d", s.c_str(), getpid(), count++);write(pipefd[1], send_buffer, strlen(send_buffer));sleep(1);}}// 父进程// 关闭父进程的写端close(pipefd[1]);char read_buffer[128] = {'\0'};int count = 0;while(1){ssize_t s = read(pipefd[0], read_buffer, sizeof(read_buffer) - 1);if(s > 0){read_buffer[s] = '\0';++count;cout << "父进程:" << getpid() << " 收到消息" << read_buffer << endl;}else{cout << "写端已经关闭,读取结束" << endl;break;}// 循环5次,关闭读端if(count == 5){close(pipefd[0]);cout << "父进程的读端已关闭!" << endl;break;}}// 获取子进程的退出信息int status = 0;int ret = waitpid(id, &status, 0);assert(ret != -1);cout << "等待子进程成功 " << "子进程id:" << id << " signal:" << (status & 0x7F) << endl; return 0;
}
父进程的读端已经关闭,子进程的写端再进行写入也没有任何的意义,那么操作系统就向子进程发送 13 号信号(SIGPIPE)。像管道的读端关闭写端还在写的这样情况,其实就是不符合软件条件(管道通信的条件,管道也是一种软件),那么操作系统就会向不符合软件条件的进程发送特定的信号终止进程。
alarm 函数可以设定一个闹钟,也就是告诉操作系统在 seconds 秒后给当前进程发送 14 号信号(SIGALRM),该信号的默认处理动作是终止当前进程。
这个函数的返回值是 0 或者是以前设定的闹钟时间还余下的秒数。打个比方,某人要小睡一觉,设定闹钟为 30 分钟之后响,20 分钟后被人吵醒了,还想多睡一会儿。于是重新设定闹钟为 15 分钟之后响,以前设定的闹钟时间还余下的时间就是 10 分钟。如果 seconds 值为 0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数。
#include <iostream>
#include <unistd.h>using namespace std;int main()
{alarm(1);int count = 0;// 验证1s内,count++会进行多少次// cout + 网络 = IOwhile(true){cout << "count: " << count++ << endl;}return 0;
}
通过上图可以看到,count 一定被加加了 7w+ 次,这次数是比较少的,其实是由 cout 和网络传输数据慢导致的。如果想单纯看看计算的算力,可以通过下面的程序。
#include <iostream>
#include <unistd.h>
#include <signal.h>using namespace std;unsigned int count = 0;void catchSignal(int signal)
{cout << "count:" << count << endl;
}int main()
{signal(SIGALRM, catchSignal);alarm(1);while(true){++count;}return 0;
}
注:设定了一个闹钟,这个闹钟一旦被处罚,就会自动被移除。
下面的代码可以做到每隔一秒就发送 SIGALRM
#include <iostream>
#include <unistd.h>
#include <signal.h>using namespace std;unsigned int count = 0;void catchSignal(int signal)
{cout << "count:" << count << endl;alarm(1);
}int main()
{signal(SIGALRM, catchSignal);alarm(1);while(true){++count;}return 0;
}
以上的代码就简单地实现了定时器的功能,每隔一秒钟做指定的一件事。
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <functional>
#include <vector>using namespace std;unsigned int count = 0;
typedef function<void ()> func; // func为函数类型
vector<func> callBacks;void showCount()
{cout << "count:" << count << endl;
}
void showLog()
{cout << "这个是日志功能" << endl;
}
void logUser()
{// 创建子进程执行who命令if(fork() == 0){execl("/usr/bin/who", "who", nullptr);exit(1);}wait(nullptr);
}void catchSignal(int signal)
{for(auto &func : callBacks){func();}cout << "-------------------------" << endl;alarm(1);
}int main()
{signal(SIGALRM, catchSignal);alarm(1);callBacks.push_back(showCount);callBacks.push_back(showLog);callBacks.push_back(logUser);while(true) ++count;return 0;
}
如何理解软件条件给进程发送信号?操作系统先识别到某种软件条件触发或者不满足,然后操作系统构建信号发送给指定的进程。注:闹钟也是结构体,操作系统通过特定的数据结构来管理闹钟。当闹钟超时了,操作系统就会给闹钟结构体中存储的进程 id 发送 SIGALRM 信号。
硬件异常产生信号
#include <iostream>
#include <unistd.h>
#include <signal.h>using namespace std;void handler(int signum)
{sleep(1);cout << "收到了一个信号: " << signum << endl;
}int main()
{signal(SIGFPE, handler);int a = 100;a /= 0;while(1) sleep(1);return 0;
}
将程序运行起来,就会发现程序在死循环打印语句。那为什么会这样呢?如何理解除零呢?进行计算的是 CPU 这个硬件,CPU 内部是有寄存器的,其中有一个寄存器是状态寄存器。该寄存器不进行数值保存,它只用来保存 CPU 本次计算的状态,其结构也是位图,有着对应的状态标记位(溢出标记位)。当状态寄存器的溢出标记位为 0,操作系统就将计算结果写回到内存中;而当溢出标记位为 1时,操作系统就会意识到有除零错误(溢出问题),操作系统会找到当前哪个进程在运行,向该进程发送 SIGFPE 信号,进程会在合适的时候处理该信号。
当出现硬件异常时,进程不一定会退出!一般默认是退出,但是我们即使不退出,我们也做不了什么!那为什么上面的程序会死循环呢?虽然我们捕捉了 SIGFPE 信号也处理了该信号,但是寄存器中的异常一直没有被解决!寄存器中的数据是进程的上下文,当进行进程切换的时候,寄存器的数据也被保存下来了。当该进程被调度时,操作系统又立马就识别到该进程出现了异常,所以就一直给进程发送 SIGFPE 信号,那么就出现了死循环打印的现象。
#include <iostream>
#include <unistd.h>
#include <signal.h>using namespace std;void handler(int signum)
{sleep(1);cout << "收到了一个信号: " << signum << endl;
}int main()
{// SIGSEGV 段错误(11号信号)signal(SIGSEGV, handler);int a = 100;a /= 0;while(1) sleep(1);return 0;
}
如何理解野指针或者越界访问问题呢?
- 无论是野指针还是越界访问,都必须通过地址来找到目标位置
- 语言层面上的地址全部都是虚拟地址。当对某个数据进行访问时,首先要将虚拟地址转化成物理地址
- 虚拟地址通过页表和 MMU(Memory Manager Unit 内存管理单元,硬件)来转换成物理地址
- 当野指针或越界访问时,使用的地址都是非法地址,那么 MMU 进行转化的时候,就一定会报错。只有 MMU 报错,操作系统就能识别当前进程出现了硬件异常,将该硬件异常转化成对应的信号发送给进程。
- 出现死循环的原因和除零错误出现死循环的原因类似
小总结:所有的信号都有它的来源,但最终全部都是被操作系统识别、解释并发送给对应的进程的。
👉阻塞信号👈
信号其他相关常见概念
- 实际执行信号的处理动作称为信号递达(Delivery),信号处理动作有默认、忽略、自定义捕捉。
- 信号从产生到递达之间的状态,称为信号未决(Pending),也就是进程收到了一个信号但该信号还未被处理,信号被保存在位图(Pending 位图)中。
- 进程可以选择阻塞 (Block )某个信号。
- 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
- 注意:阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
信号在内核中的表示
为了表示信号递达、未决和阻塞三个概念,那么操作系统就要用一定的结构去表示它们。操作系统就使用了三张表来表示这三个概念,如下图所示:
其中 pending 表就是保存信号的位图结构(unsigned int),1 表示收到了信号,0 表示没有收到信号;handler 表是函数指针数组,数组的下标就是信号编号,数组中存的是信号的处理动作;block 表也是位图结构,1 表示该信号被阻塞,0 表示该信号未被阻塞。
信号处理的过程:操作系统给目标进程就是修改 pending 位图,这样信号就完成发送了。进程在合适的时候处理信号,遍历 pending 位图看哪些比特位为 1。当发现比特位为 1 时,就去看对应的 block 位图上的比特位是否为 1。如果是 1,则说明该信号被阻塞着,进程不会去处理该信号,也不会将 pending 位图的比特位从 1 改成 0;而如果是 0,则说明该信号没有被阻塞,进程可以处理该信号,处理完成后还需要将 pending 位图上的比特位从 1 改成 0,表示该信号已经处理完成。
如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1 允许系统递送该信号一次或多次。Linux 是这样实现的:普通信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里,本篇博客不讨论实时信号。
sigset_t
- 编程语言都会给我们提高 .h 或者 .hpp 和语言本身的定义类型;操作系统也会给我们提供 .h 和操作系统自定义的类型,像 pid_t 和 key_t 等。如果要访问硬件,那么语言类的头文件也会包含对应的系统调用接口,将系统调用封装起来给我们使用。
- sigset_t 也是操作系统自定义的类型,该类型是位图结构,用以表示上图的 pending 表和 block 表。用户不能直接通过位操作来修改位图(unsigned int),需要使用操作系统提供的方法来修改位图。
- 用户可以直接使用 sigset_t 类型,和使用内置类型和自定义类型没有任何差别。
- 每个信号只有一个比特位的未决标志,非 0 即 1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型 sigset_t 来表示。sigset_t 称为信号集,这个类型可以表示每个信号的有效或无效状态,在阻塞信号集中有效和无效的含义是该信号是否被阻塞,而在未决信号集中有效和无效的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的屏蔽应该理解为阻塞而不是忽略。
信号集操作函数
sigset_t 类型对于每种信号用一个比特位表示有效或无效状态,至于这个类型内部如何存储这些比特位则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作 sigset_ t 变量,而不应该对它的内部数据做任何解释,比如用 printf 直接打印 sigset_t 变量是没有意义的。
#include <signal.h>
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 sigismember(const sigset_t *set, int signo);
- 函数 sigemptyset 初始化 set 所指向的信号集,使其中所有信号的对应比特位清零,表示该信号集不包含任何有效信号。
- 函数 sigfillset 初始化 set 所指向的信号集,使其中所有信号的对应比特位置 1,表示该信号集的有效信号包括系统支持的所有信号。
- sigaddset 函数将 signo 信号对应的比特位置为 1,sigdelset 函数将 signo 信号对应的比特位置为 0。
- sigismember 函数可以判断 signo 信号是否在信号集中,如果 signo 信号在信号集中,返回 1;如果不在,返回 0;出错则返回 -1。
- 注意:在使用 sigset_ t 类型的变量之前,一定要调用sigemptyset 或 sigfillset 函数做初始化,使信号集处于确定的状态。初始化 sigset_t 变量之后就可以在调用 sigaddset 和 sigdelset 在该信号集中添加或删除某种有效信号。
sigpending
sigpending 函数通过输出型参数 set 获取当前进程的未决信号集,调用成功返回 0,出错则返回 -1。
sigprocmask
sigprocmask 函数可以帮助我们读取或更改进程的信号屏蔽字(阻塞信号集),调用成功返回 0,出错则返回 -1。
如果 oldset 是非空指针,则读取进程的当前信号屏蔽字通过 oldset 参数传出。如果 set 是非空指针,则更改进程的信号屏蔽字,参数 how 指示如何更改。如果 oldset 和 set 都是非空指针,则先将原来的信号屏蔽字备份到 oldset 里,然后根据 set 和 how 参数更改信号屏蔽字。假设当前的信号屏蔽字为 mask,下表说明了 how 参数的可选值。
如果我们对所有的信号都进行了信号捕捉,那我们是不是就写了一个不会被异常终止或者用户杀掉的进程呢?我们通过代码来验证一下!
#include <iostream>
#include <signal.h>
#include <unistd.h>using namespace std;void catchSig(int signum)
{cout << "捕捉到了一个信号: " << signum << endl;
}int main()
{for(int i = 1; i <= 31; ++i)signal(i, catchSig);while(1) sleep(1);return 0;
}
操作系统的设计者也考虑到了上述的情况,所以就让 9 号信号无法捕捉,9 号信号是管理员信号。
如果我们将 2 号信号 block 掉,并且不断地获取并打印当前进程的 pending 信号集。如果我们突然发送一个 2 号信号,我们应该就能看到 pending 信号集中 2 号信号的比特位由 0 变成 1。
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cassert>using namespace std;static void showPending(sigset_t& pending)
{for(int signal = 31; signal >= 1; --signal){if(sigismember(&pending, signal))cout << '1';elsecout << '0';}cout << endl << "----------------" << endl;
}int main()
{// 定义并初始化信号集sigset_t set, oldset;sigemptyset(&set);sigemptyset(&oldset);// 将2号信号添加到信号屏蔽集中sigaddset(&set, 2);// 将信号屏蔽集设置到当前进程的PCB中// 默认情况下,进程不会对任何信号进行blockint n = sigprocmask(SIG_BLOCK, &set, &oldset);assert(n == 0); // assert本质是一个宏(void)n;cout << "block 2 号信号成功......" << endl;// 重复打印当前进程的pending信号集sigset_t pending;sigemptyset(&pending);while(true){// 获取当前进程的pending信号集sigpending(&pending);// 打印pending信号集showPending(pending);sleep(2);}return 0;
}
给当前进程发送 2 号信号时,可以看到该进程的 pending 表中 2 号信号的比特位由 0 变成 1。如果我们在若干秒后,解除对 2 号信号的 block,那么 2 号信号就会被递达而终止当前进程。如果我们对 2 号信号进程捕捉,那么进程也就不会被终止了。
没有捕捉 2 号信号
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cassert>using namespace std;static void showPending(sigset_t& pending)
{for(int signal = 31; signal >= 1; --signal){if(sigismember(&pending, signal))cout << '1';elsecout << '0';}cout << endl << "----------------" << endl;
}int main()
{// 定义并初始化信号集sigset_t set, oldset;sigemptyset(&set);sigemptyset(&oldset);// 将2号信号添加到信号屏蔽集中sigaddset(&set, 2);// 将信号屏蔽集设置到当前进程的PCB中// 默认情况下,进程不会对任何信号进行blockint n = sigprocmask(SIG_BLOCK, &set, &oldset);assert(n == 0); // assert本质是一个宏(void)n;cout << "block 2 号信号成功......" << "id:"<< getpid() << endl;// 重复打印当前进程的pending信号集sigset_t pending;sigemptyset(&pending);int count = 0;while(true){// 获取当前进程的pending信号集sigpending(&pending);// 打印pending信号集showPending(pending);sleep(2);++count;if(count == 6){cout << "解除对 2 号信号的 block" << endl;// 默认情况下,解除对于2号信号的block的时候,2号信号确实会递达// 但是2号信号的默认处理动作是终止进程!n = sigprocmask(SIG_SETMASK, &oldset, nullptr);assert(n == 0);(void)n;}}return 0;
}
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cassert>using namespace std;static void showPending(sigset_t& pending)
{for(int signal = 31; signal >= 1; --signal){if(sigismember(&pending, signal))cout << '1';elsecout << '0';}cout << endl << "----------------" << endl;
}void catchSig(int signum)
{cout << "捕捉到了一个信号: " << signum << endl;
}int main()
{signal(2, catchSig);// 定义并初始化信号集sigset_t set, oldset;sigemptyset(&set);sigemptyset(&oldset);// 将2号信号添加到信号屏蔽集中sigaddset(&set, 2);// 将信号屏蔽集设置到当前进程的PCB中// 默认情况下,进程不会对任何信号进行blockint n = sigprocmask(SIG_BLOCK, &set, &oldset);assert(n == 0); // assert本质是一个宏(void)n;cout << "block 2 号信号成功......" << "id:"<< getpid() << endl;// 重复打印当前进程的pending信号集sigset_t pending;sigemptyset(&pending);int count = 0;while(true){// 获取当前进程的pending信号集sigpending(&pending);// 打印pending信号集showPending(pending);sleep(2);++count;if(count == 5){cout << "解除对 2 号信号的 block" << endl;n = sigprocmask(SIG_SETMASK, &oldset, nullptr);assert(n == 0);(void)n;}}return 0;
}
- 打印解除 block 语句和捕捉的顺序就是一个打印的顺序问题。
- 所有的信号发送方式,都是修改 pending 位图的过程,我们只需要通过 sigpending 接口来获取 pending 位图即可。
如果我们将所有的信号都进行 block,我们是不是就写了一个不会被异常终止或者用户杀掉的进程呢?我们也通过代码来验证一下!
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cassert>using namespace std;static void showPending(sigset_t& pending)
{for(int signal = 31; signal >= 1; --signal){if(sigismember(&pending, signal))cout << '1';elsecout << '0';}cout << endl << "----------------" << endl;
}static void blockSig(int sig)
{sigset_t set;sigemptyset(&set);sigaddset(&set, sig);int n = sigprocmask(SIG_BLOCK, &set, nullptr);assert(n == 0);(void)n;
}int main()
{// block所有信号for(int i = 1; i <= 31; ++i){blockSig(i);}sigset_t pending;while(1){sigpending(&pending);showPending(pending);sleep(1);}return 0;
}
自动给进程发送信号的脚本语言
#!/bin/bashi=1
id=$(pidof mysignal)
while [ $i -le 31 ]
dokill -$i $idecho "kill -$i $id"let i++sleep 1
done
#!/bin/bashi=1
id=$(pidof mysignal)
while [ $i -le 31 ]
doif [ $i -eq 9 ];thenlet i++continuefiif [ $i -eq 19 ];thenlet i++continuefikill -$i $idecho "kill -$i $id"let i++sleep 1
done
注: 9 号和 19 号信号是无法被 block 的,20 号信号的默认处理动作是忽略。
👉捕捉信号👈
内核如何实现信号的捕捉
在上面提及到,信号产生之后,进程可能无法立即处理,进程需要在合适的时候去处理信号。那这个合适的时候是什么呢?带着这个问题,我们来探究一下信号处理的整个流程!
信号相关的数据字段是在进程的 PCB 内部,PCB 内部属于内核范畴,普通用户无法对信号进行检测和处理。那么要对信号进行处理,就需要在内核状态。当执行系统调用或被系统调度时,进程所处的状态就是内核态;不执行操作系统的代码时,进程所处的状态就是用户态。现在我们已经知道需要在内核态下进行信号处理,那究竟具体是什么时候呢?结论:在内核态中,从内核态返回用户态的时候,进行信号的检测和处理!如何进入内核态呢?进行系统调用或产生异常等。汇编指令int 80
(80 是中断编号)可以进程进入内核态,也就是将代码的执行权限从普通用户转交给操作系统,让操作系统去执行!注:汇编指令int 80
内置在系统调用函数中。
sigaction
- sigaction 函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回 0,出错则返回 -1。signum 是指定的信号编号。若 act 不为空,则根据 act 修改该信号的处理动作。若 oldact 不为空,则通过 oldact 传出该信号原来的处理动作。act 和 oldact 指向 sigaction 结构体。
- 将 sa_handler 赋值为常数 SIG_IGN 传给 sigaction 表示忽略信号;赋值为常数 SIG_DFL 表示执行系统默认动作;赋值为一个函数指针表示用自定义函数捕捉信号或者说向内核注册了一个信号处理函数。该函数返回值为 void,参数为 int,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被 main 函数调用,而是被系统所调用。
- sa_sigaction 是实时信号的处理方法,不需要关心。
Makefile
mysignal:mysignal.ccg++ $^ -o $@ -std=c++11 -fpermissive
.PHONY:cleanrm -f mysignal
#include <iostream>
#include <unistd.h>
#include <signal.h>using namespace std;void handler(int signum)
{ cout << "捕捉到了一个信号: " << signum << endl;
}int main()
{// signal(2, SIG_IGN);// 一下内核数据结构变量是在用户栈定义的// 需要将它们设置进进程的内核中struct sigaction act, oldact;act.sa_flags = 0; // 实时信号的标记位sigemptyset(&act.sa_mask);act.sa_handler = handler;// 设置进当前进程的PCB中sigaction(SIGINT, &act, &oldact);cout << "default action: " << (int)(oldact.sa_handler) << endl;while(true) sleep(1);return 0;
}
处理信号、执行自定义动作的时候,如果在处理信号期间,又来了同样的信号,操作系统该如何处理呢?Linux 的设计方案是在任何时候,操作系统只能处理一层信号,不允许出现信号正在处理又来信号再被处理的情况。操作系统无法决定信号什么时候来,但可以决定什么时候去处理信号。接下来要一起探讨的是为什么要有信号屏蔽字 block!
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用 sa_mask 字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。 sa_flags 字段包含一些选项,本篇博客的代码都把 sa_flags 设0,sa_sigaction 是实时信号的处理函数,本章不详细解释这两个字段,有兴趣的伙伴可以再了解一下。
#include <iostream>
#include <unistd.h>
#include <signal.h>using namespace std;void showPending(sigset_t* pending)
{for(int i = 31; i >= 1; --i){if(sigismember(pending, i)) cout << '1';else cout << '0';}cout << endl << "----------------" << endl;
}void handler(int signum)
{ cout << "捕捉到了一个信号: " << signum << endl;cout << "捕捉到了一个信号: " << signum << endl;cout << "捕捉到了一个信号: " << signum << endl;// 验证2号信号被捕捉期间,再次发送2号信号不会去处理sigset_t pending;int c = 6;while(1){sigpending(&pending);showPending(&pending);--c;if(!c) break;sleep(1);}
}int main()
{// 一下内核数据结构变量是在用户栈定义的// 需要将它们设置进进程的内核中struct sigaction act, oldact;act.sa_flags = 0; // 实时信号的标记位sigemptyset(&act.sa_mask);act.sa_handler = handler;// 设置进当前进程的PCB中sigaction(SIGINT, &act, &oldact);cout << "default action: " << (int)(oldact.sa_handler) << endl;while(true) sleep(1);return 0;
}
处理 2 信号的同时,屏蔽 3 ~ 7 号信号
#!/bin/bashi=2
id=$(pidof mysignal)
while [ $i -le 7 ]
dokill -$i $idecho "kill -$i $id"let i++sleep 1
done
#include <iostream>
#include <unistd.h>
#include <signal.h>using namespace std;void showPending(sigset_t* pending)
{for(int i = 31; i >= 1; --i){if(sigismember(pending, i)) cout << '1';else cout << '0';}cout << endl << "----------------" << endl;
}void handler(int signum)
{ cout << "捕捉到了一个信号: " << signum << endl;cout << "捕捉到了一个信号: " << signum << endl;cout << "捕捉到了一个信号: " << signum << endl;// 验证2号信号被捕捉期间,再次发送2号信号不会去处理sigset_t pending;int c = 7;while(1){sigpending(&pending);showPending(&pending);--c;if(!c) break;sleep(1);}
}int main()
{// 一下内核数据结构变量是在用户栈定义的// 需要将它们设置进进程的内核中cout << "id:" << getpid() << endl;struct sigaction act, oldact;act.sa_flags = 0; // 实时信号的标记位sigemptyset(&act.sa_mask);act.sa_handler = handler;// 处理2号信号期间,3 4 5 6 7号信号也被blocksigaddset(&act.sa_mask, 3);sigaddset(&act.sa_mask, 4);sigaddset(&act.sa_mask, 5);sigaddset(&act.sa_mask, 6);sigaddset(&act.sa_mask, 7);// 设置进当前进程的PCB中sigaction(SIGINT, &act, &oldact);cout << "default action: " << (int)(oldact.sa_handler) << endl;while(true) sleep(1);return 0;
}
👉可重入函数👈
- 信号捕捉并没有创建新的进程或线程。
- main 函数调用 insert 函数向一个链表 head 中插入节点node1,插入操作分为两步。刚做完第一步的时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换到 sighandler 函数,sighandler 也调用 insert 函数向同一个链表 head 中插入节点 node2,插入操作的两步都做完之后从 sighandler 返回内核态,再次回到用户态就从 main 函数调用的 insert 函数中继续往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main 函数和 sighandler 先后向链表中插入两个节点,而最后只有一个节点真正插入链表中了。
- 像上例这样,insert 函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入。insert 函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数。反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。想一下,为什么两个不同的控制流程调用同一个函数,问它的同一个局部变量或参数就不会造成错乱?
- 可重入和不可重入是函数的一种特征,目前我们用的函数,90% 是不可重入的。
如果一个函数符合以下条件之一则是不可重入的:
- 调用了 malloc 或 free,因为 malloc 也是用全局链表来管理堆的。
- 调用了标准 I/O 库函数。标准 I/O 库的很多实现都以不可重入的方式使用全局数据结构。
👉volatile👈
该关键字在 C 语言当中我们已经有所涉猎,今天我们站在信号的角度重新理解一下。
#include <iostream>
#include <unistd.h>
#include <signal.h>using namespace std;int flag = 0;void changeFlag(int signum)
{(void)signum;cout << "flag: " << flag << endl;flag = 1;cout << "flag: " << flag << endl;
}int main()
{signal(2, changeFlag);while(!flag);cout << "进程正常退出后: " << flag << endl;
}
编译器有时候会自动地给我们进行代码优化!
我们看到即使对 flag 进行了修改,也没有办法结束进程。这是为什么呢?正常情况下,每次循环通过 flag 进行检测时,都需要到内存中去数据;但是编译优化(编译的时候已经进行了优化)后,编译器认为 main 函数里的代码没有对 flag 进行修改,所以为了提高效率,第一次过后就不去内存中取数据了,而是直接读取寄存器中的值来进行循环检测。而实际情况是内存中 flag 的值早就被改成了 1 了,所以就出现上图的情况了。
编译器优化会让 CPU 无法看到内存,而关键字 volatile 就是为了保持内存的可见性,每次都取内存中取数据。
👉SIGCHLD信号👈
进程一章讲过用 wait 和 waitpid 函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一 下,程序实现复杂。
其实,子进程在终止时会给父进程发 SIGCHLD 信号,该信号的默认处理动作是忽略,父进程可以自定义 SIGCHLD 信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用 wait 清理子进程即可。
事实上,由于 UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调用 sigaction 或 signal 将 SIGCHLD 的处理动作置为 SIG_IGN,这样 fork 出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用 sigaction 函数自定义的忽略通常是没有区别的,但这是一个特例。此方法对于 Linux 可用,但不保证在其它 UNIX 系统上都可用。
#include <iostream>
#include <unistd.h>
#include <signal.h>using namespace std;void handler(int signum)
{cout << "子进程退出: " << signum << " father: " << getpid() << endl;
}// 证明:子进程退出,会向父进程发送信号
int main()
{signal(SIGCHLD, handler);if(fork() == 0){cout << "child pid: " << getpid() << endl;sleep(1);exit(0);}while(true) sleep(1);}
自动等待子进程
#include <iostream>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <unistd.h>using namespace std;void handler(int sig)
{pid_t id;// -1表示等待任意一个子进程while ((id = waitpid(-1, nullptr, WNOHANG)) > 0){printf("wait child success: %d\n", id);}printf("child is quit! %d\n", getpid());
}int main()
{signal(SIGCHLD, handler);if (fork() == 0){ // childprintf("child : %d\n", getpid());sleep(3);exit(1);}while (1){printf("father proc is doing some thing!\n");sleep(1);}return 0;
}
#include <iostream>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <unistd.h>using namespace std;// 如果我们不想等待子进程,并且我们还想让子进程退出之后,自动释放僵尸子进程
int main()
{// OS 默认就是忽略的signal(SIGCHLD, SIG_IGN); // 手动设置对子进程进行忽略if(fork() == 0){cout << "child: " << getpid() << endl;sleep(5);exit(0);}while(true){cout << "parent: " << getpid() << " 执行我自己的任务!" << endl;sleep(1);}
}
👉总结👈
本篇博客主要讲解了什么是信号、信号如何产生、阻塞信号、捕捉信号、可重入函数以及 volatile 关键字和 SIGCHLD 信号等。那么以上就是本篇博客的全部内容了,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家!💖💝❣️
相关文章:
【Linux】进程信号
🌠 作者:阿亮joy. 🎆专栏:《学会Linux》 🎇 座右铭:每个优秀的人都有一段沉默的时光,那段时光是付出了很多努力却得不到结果的日子,我们把它叫做扎根 目录👉信号入门&…...
SpringBoot 集成Junit单元测试
学习文章: https://www.cnblogs.com/ysocean/p/6889906.html 开发工具: IDEA 2022.1.4 目录 目录 1. 概述 2. 实现步骤 2.1 maven导入依赖 2.2 随意代码演示(不推荐) 2.3 规范代码演示(推荐) 3. Junit相关其他注解 4. 注意事项 5. 结语 1. 概述 接触到Junit,…...
Android开发之简单控件
文章目录一 文本显示1.1 文本设置的两种方式1.2 常见字号单位类型2.2 设置文本的颜色三 视图基础3.1 设置视图的宽高3.2 设置视图的间距3.3 设置视图的对齐方式四常用布局4.1 线性布局LinearLayout4.2 相对布局RelativeLayout4.3 网格布局GridLayout4.4 滚动视图ScrollView五 按…...
树状数组讲解
树状数组 文章目录树状数组引入例题AcWing241.楼兰图腾思路代码AcWing 242. 一个简单的整数问题思路代码AcWing 244. 谜一样的牛思路代码总结引入 树状数组主要维护的是这样一个数据结构: tr[x]表示以x为终点的长度为lowbit(x)的前缀和、最大值、最小值、最大公约数…...
每个Android开发都应需知的性能指标~
无论你是发布一个新的 Android 应用,还是希望提高现有应用的性能,你都可以使用 Android 应用性能指标来帮助你。 在这篇文章中,我将解释什么是 Android 应用性能指标,并列出8个需要考虑跟踪的维度和建议的基线。 什么是 Android…...
MSYS2安装
最近在学习windows上编译FFmpeg,需要用到msys2,在此记录一下安装和配置过程。 点击如下链接,下载安装包: Index of /msys2/distrib/x86_64/ | 清华大学开源软件镜像站 | Tsinghua Open Source Mirror 我下载的是:ms…...
3/3考试总结
时间安排 7:30–7:50 看题,怎么感觉三道构造,T3 貌似有网络流背景。 7:50–8:30 T1,有一些简单的性质,缩减两端点后枚举一下翻转的区间就可以了。然后花了一点时间写 spj 调试。 8:30–10:20 T2,比较纯粹的构造题。有网络流做法,…...
Spark Streaming DStream转换
DStream上的操作与RDD的类似,分为Transformations(转换)和Output Operations(输出)两种,此外转换操作中还有一些比较特殊的算子,如:updateStateByKey()、transform()以及各种Window相…...
水果商城,可运行
文章目录项目介绍一、技术栈二、本项目分为前后台,有管理员与用户两种角色;1、管理员角色包含以下功能:2、用户角色包含以下功能:三、用户功能页面展示四、管理员功能页面展示五、部分代码展示六、获取整套项目源码项目介绍 一、…...
LiveGBS国标GB/T28181国标视频流媒体平台-功能报警订阅配置报警预案告警截图及录像
LiveGBS国标GB/T28181国标视频流媒体平台-功能报警订阅配置报警预案告警截图及录像1、报警信息1.1、报警查询1.2、配置开启报警订阅1.2.1、国标设备编辑1.2.2、选择开启报警订阅1.3、配置摄像头报警1.3.1、配置摄像头报警通道ID1.3.2、配置摄像头开启侦测1.3.3、尝试触发摄像头…...
软件测试---测试分类
一 : 按测试对象划分 1.1 可靠性测试 可靠性(Availability)即可用性,是指系统正常运行的能力或者程度,一般用正常向用户提供软件服务的时间占总时间的百分比表示。 1.2 容错性测试 行李箱 , 四个轮子 , 坏了一个 , 说明这个容错…...
剑指 Offer II 015. 字符串中的所有变位词
题目链接 剑指 Offer II 015. 字符串中的所有变位词 mid 题目描述 给定两个字符串 s和 p,找到 s中所有 p的 变位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。 变位词 指字母相同,但排列不同的字符串。 示例 1: 输…...
【SpringCloud】SpringCloud详细教程之微服务比较
目录前言一.什么是微服务?为什么要使用微服务二.微服务对比三.企业开发场景前言 我会通过实际代码来给展示每个组件的用法 一.什么是微服务?为什么要使用微服务 分布式,把一个项目拆分成多个模块,每一个模块相当于一个服务。 微…...
二.项目使用vue-router,引入ant-design-vue的UI框架,引入less
根据前文《使用Vue脚手架工具搭建vue项目》搭建好脚手架后使用 1.vue-router 2.引入UI框架ant design vue 3.引入less 1.vue-router vue-router分为两种模式(默认为hash模式): hash history hash: 特征: 1.hash会在浏览器路径里带#号&#…...
网络安全怎么学?20年白帽子老江湖告诉你
很多人都知道龙叔是个老程序员,但却不知道其实我也是个H客,20年前我就开始痴迷于H客技术,可以说是网络安全方面的老江湖了。 到现在,我还依然会去研究这一块,偶尔会和一些网安的朋友交流技术,比如说红盟的…...
药房管理系统;药库管理系统
第一,主要功能: 本系统集日常销售、药品进销存、会员积分、GSP管理等药店所需的所有功能于一体,实现店铺管理的全部自动化。第二、新功能: 增加了“按功能查询药品”的功能,使软件用户可以根据客户的症状推荐合适…...
深眸科技|机器视觉提升制造性能,焕发传统企业智造新活力!
随着机器视觉技术的成熟与发展,其在工业制造中得到越来越广泛的应用。机器视觉在工业制造领域的应用朝着智能识别、智能检测、智能测量以及智能互联的完整智能体系方向发展。此外,快速变化的市场需求,不断涌入行业的竞争对手,让传…...
ubuntu安装SSH的方法
Ubuntu安装SSH的方法。14版的ubuntu经过测试,默认没有开启SSH,所以需要安装。 1、虚拟机设置网卡为桥接模式,即NAT。12版虚拟机默认的。 2、查看ubuntu使用的ip。 ifconfig即可查看,14版的ubuntu自带这个命令。 3、查看是否pi…...
哪种蓝牙耳机通话效果好?通话清晰的蓝牙耳机推荐
出门的时候,如果戴耳机和别人通话,就不必把耳机摘下来,接电话变得前所未有的简单。现在的蓝牙耳机,已经不是单纯的用来听音乐了,而是一种更好的功能。下面这四款蓝牙耳机不仅适合听歌,通话还清晰࿰…...
IT运维如何完成一场高质量复盘
复盘的终极目标是:还原事实,找到薄弱点加以改进。 提到复盘,很多人的第一反应是线上故障,有人要背锅了。 复盘真正的价值是还原事实,在薄弱处加以改进。如何做一次高质量的复盘,我们给出3点建议。 1、坦…...
JVM调优面试题——基础知识
文章目录1、JDK,JRE以及JVM的关系2、编译器到底干了什么事?3、类加载机制是什么?3.1、装载(Load)3.2、链接(Link)3.3、初始化(Initialize)4、类加载器有哪些?5、什么是双亲委派机制?6、介绍一下JVM内存划分(…...
三、mongdb 查询
一、 MongoDB文档检索 MongoDB中有多种方式可以检索文档: 1.1 查询过滤器 使用查询过滤器从集合中检索文档。查询过滤器是一组键值对,可按字段值查询文档。 例如: db.col.find({"status":"A"})这个示例查询status等于“A”的文档。 1.2 范围查询操作符…...
python的 ping 网络状态监测方法(含多IP)
ping 基本概念 ping (Packet Internet Groper)是一种因特网包探索器,用于测试网络连接量的程序。Ping是工作在 TCP/IP网络体系结构中应用层的一个服务命令, 主要是向特定的目的主机发送 ICMP(Internet Control Messag…...
【独家】华为OD机试提供C语言题解 - 单词反转
最近更新的博客 华为od 2023 | 什么是华为od,od 薪资待遇,od机试题清单华为OD机试真题大全,用 Python 解华为机试题 | 机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南)华为od机试,独家整理 已参加机试人员的实战技巧文章目录 最近更新的博客使用说明单词…...
Linux docker环境安装,docker-compose安装,jdk17安装
安装docker 删除之前安装的docker yum remove docker \docker-client \docker-client-latest \docker- common \docker-latest \docker-latest-logrotate \docker-logrotate \docker-sqlinux \docker-engine-selinux \docker-engine \docker-ce安装yum工具 yum install -y y…...
界面开发(3)--- PyQt5用户登录界面连接数据库
文章目录数据库账户注册账号登录找回密码为了实现用户登录界面的登录功能,我们必须建立一个数据库,并把账号和对应的密码,存储到数据库中。如果输入的账号和密码与数据库中的一致,那我们就允许用户登录,进入新的界面。…...
以下真的没有任何要写的了,我需要凑字数,请大家原谅
以下真的没有任何要写的了,我需要凑字数,请大家原谅!!!!!!!!!!!!!!!&#…...
2023年 Java 发展趋势
GitHub 语言统计表明,Java在编程语言中排名第二,而在2022年的TIOBE指数中,Java排在第四。 抛开排名,Java是自诞生以来企业使用率最高的编程语言,作为一种编程语言,它比许多竞争对手都有更多的优点…...
Lsof命令介绍
LSOF(List Open Files)是一款功能强大的开源工具,用于列出当前系统上打开的文件和进程。该工具可以帮助系统管理员和开发人员快速查找正在使用某个文件的进程,以及在系统上使用磁盘空间最多的进程。 本文将介绍LSOF的基本用法和常…...
LeetCode题目笔记——1487. 保证文件名唯一
文章目录题目描述题目链接题目难度——中等方法一:哈希表代码/Python代码/C总结题目描述 给你一个长度为 n 的字符串数组 names 。你将会在文件系统中创建 n 个文件夹:在第 i 分钟,新建名为 names[i] 的文件夹。 由于两个文件 不能 共享相同…...
网站开发怎么学/海阳seo排名优化培训
CSDN、51cto、ITeye、Java学习者论坛这几个网站目前是我觉得还不错的,都有丰富的资料和一些问题解答,都是很方便的哦今天主要介绍一个论坛先,后续会一一介绍其他的网站!Java学习者论坛,最方便的学习社区,它…...
做微信的微网站费用/什么平台可以免费打广告
代理缓存服务介绍Squid是Linux系统中最为流行的一款高性能代理服务软件,通常用作Web网站的前置缓存服务,能够代替用户向网站服务器请求页面数据并进行缓存。 Squid服务程序具有配置简单、效率高、功能丰富等特点,它能支持HTTP、FTP、SSL等多种…...
关于做网站的/营销软文范文
bug出现环境背景 使用Cocospod安装RN的时候,报错 bug分析 [!] Unable to satisfy the following requirements: - Yoga ( 0.44.0.React) required by React/Core (0.44.0) None of your spec sources contain a spec satisfying the dependency: Yoga ( 0.44.0.Reac…...
如何做同城信息网站/网站搭建需要什么
文章目录文章参考获取dom节点高宽的通用方法案例说明getBoundingClientRect() 获取DOM 的高度和宽度的办法?文章参考 原生js操作dom添加删除替换class你了解getBoundingClientRect()? 获取dom节点高宽的通用方法 dom.style.widt…...
wordpress google翻译插件/我赢网提供的高水平网页设计师
一、 一般测试场景: 1. 所有必填字段都应校验并用星号“*”标注 2. 验证错误提示信息应在正确的位置合理显示 3. 所有的错误信息都应用相同的CSS样式显示(如:红色) 4. 一般性的确认信息应该用错误消息意外的CSS样…...
网站运行费用一般多少/百度指数行业排行
码云静态网页1 介绍2 搭建2.1 建仓库2.2 开启Gitee Pages功能3 图片3.1 头像参考1 介绍 码云是开源中国社区2013年推出的基于 Git 的代码托管服务,目前已经成为国内最大的代码托管平台,致力于为国内开发者提供优质稳定的托管服务。码云 Pages 是一个免费…...