【Linux内核系列】:信号(上)

🔥 本文专栏:Linux

🌸作者主页:努力努力再努力wz


💪 今日博客励志语录山顶的风景从不挑剔攀登者的身份,只承认坚持的脚步


引入

那么正式在谈我们信号之前,那么我们先来谈一下我们生活中的信号,那么生活中的信号的例子有很多,比如我们的红绿灯或者说上课铃以及门铃以及军队的哨响,那么这些例子都是我们生活中的信号,而这些信号必然至少会涉及到两个对象,分别是信号的发出者以及信号的接收者,那么以红绿灯来说,那么该信号的发出者就是交通信号灯,那么接收者就是司机,那么当接收者受到信号时,那么它一般会对该信号做出对应的反应,比如司机受到红灯信号,那么它会立马踩刹车减速,而如果学生收到了上课铃这个信号,那么学生会立刻返回到教室去上课,那么军人听到了哨响,那么会立马集合,而之所以要有这些红绿灯以及上课铃等信号的出现,我们发现这些信号的作用本质上就是为了管理接收信号的对象,来维持秩序,比如红绿灯的存在是为了管理通行的车辆从而维护交通秩序,而上课铃则是为了管理学生从而维护课堂的秩序,那么只要这些对象能够正确执行这些信号对应的响应动作,那么秩序就能够得到维护,整个系统就能够稳定

此时我们再来引入Linux的信号,那么其实Linux的信号和我们的现实生活中的信号的场景是相似的,那么在现实生活中,那么接收信号的对象是我们人类,而在计算机世界中,那么接收信号的则是进程,那么为什么进程要接收信号呢?那么我们还是以我们现实世界的生活场景举例子,为什么生活要有交通信号灯的存在,那么交通信号灯出现的原因就是因为某些司机到了人行横道或者到了十字交叉路口,那么司机根本不管人行横道是否有路人,或者十字交叉路口此时是否有车辆通行,司机不管当前的路况是怎样,还是继续踩油门前进,所以就需要有交通信号灯来提醒或者说警告司机,此时道路面前是红灯,你必须给我踩刹车停下来,因为面前有行人或者行车通过,所以信号的出现的更为本质的原因就是因为某些对象会做出一些破坏秩序的行为或者动作出现,比如司机看到行人还是依然踩刹车,或者学生就一直在外面玩耍不回到教室学习,那么同理对于计算机世界来说,我们接收信号的对象变成了进程,那么进程为什么会接收信号,因为进程也会做出某些不合法的行为,比如说非法访问内存等等

而上文我说计算机世界的信号和现实生活中的信号是相似但不是相同,那么它们之间的区别在哪里呢?那么我们可以发现生活的信号,通常一个信号的发出,此时接收信号的对象可能并没有做出不合法的行为,比如在斑马线停车的司机,他眼睛也会看到红灯,意味着他也会接收这个信号,但是他此时并没有做出不合法行为,而一直在教室里面学习的学生,他也会受到回到教室的上课铃的信号,而相比之下,对于Linux的信号来说,那么是进程一定是得做出不合法行为之后,那么它才会受到信号,而不会出现进程正常运行,没有发生任何异常情况然后也受到信号,所以一定要注意这点,所以Linux的信号,那么它的作用就是对于已经做出某种不合法行为的进程的一个管理,从而来维护系统的稳定。

信号

那么知道了信号的意义之后,那么信号必然得有一个发送方和一个接收方,那么这个发送方可以是我们的内核也就是操作系统,而接收方就是进程,有的读者在写代码然后运行进程的时候可能碰到过信号,那么有的读者可能就会存在这样一个疑惑,因为他知道进程的各种行为是由用户决定的,因为进程底层的一行一行代码就决定着进程的各种行为,而这一行行代码是由用户编写的,所以进程的各种行为是由用户决定的,而之前说到,信号会有一个发送方比如内核,那么假设内核发送了信号比如强制让进程终止的9号信号发送给了一个进程,因为该进程此时做出了一个不合法的行为,但是我们发现用户编写代码的时候,本身并没有编写关于信号处理逻辑的相关代码,但是事实上,该进程还确实收到了信号并且还执行了该信号对应的处理逻辑,因为进程的确是终止没有运行了,这正是对应9号信号的一个内容,所以这里给人的感觉的就是我明明没有做这件事的动机和行为,但是这件事怎么就发生了呢,那么要解释清楚这个原因,那么就得跳出进程的视角,也就是得涉及硬件才能理解,那么在讲解之前,那么这里我还是会先借助一个生活中的例子来引入

那么我们知道我们现实生活中存在这样的人,那么他就是喜欢干一些违法事情,比如有些人作为司机在开车的时候,那么他看到红灯,这个信号灯就是让他踩刹车停车,但是他却是还是继续踩着油门前进,那么假设他开的是一个非常智能的电动汽车,那么此时电动汽车的部件也就是传感器识别到了此时当前为红灯路段,但是汽车仍然向前行驶,那么此时该传感器就会将该信息反馈给交警,那么交警确认到信息之后,知道有个汽车正在闯红灯,然后立马行驶交通管理权,然后远程连接来接管该车辆,让车辆采取制动措施强制停止在路中间,那么在这个例子中,我们的司机就是我们的进程,而我们车辆的传感器等汽车部件就是对应计算机的硬件,那么这个例子中,司机此时他进行踩刹车以及踩油门和转动方向盘的行为,你看似是他自己完成的,其实这些行为只是一个请求,因为这个动作的真正的执行者,其实是汽车本身。

而进程底层的各种代码,那么这些代码就对应这该进程的各种行为,那么这个行为发出之后,能否真正完成,你得看一个人的脸色,那就是CPU,因为CPU是来执行这些代码所对应的指令,那么这些指令在我们看来其实只是个请求而已,那么假设你有一条代码涉及到非法访问内存,比如解引用了野指针,那么当CPU运行到这行代码所对应的指令时,CPU会将地址码交给CPU的内存管理单元MMU,那么MMU的作用就是访问内存的页表将该虚拟地址转化为物理地址,当MMU一查表,发现你这个地址不在该进程的有效的地址范围内,也就是该地址不合法,那么此时就会给CPU发送一个物理信号,那么CPU获取到该信号之后,知道此时出现了异常情况,那么CPU就会停止运行你这行指令以及之后的所有的指令,那么它会立马将该进程给切换,切换成内核态

那么再结合刚才的例子,那么刚才例子中的司机就是进程,那么传感器就是MMU,那么车辆的控制系统就是CPU,那么交警就是内核,那么我们司机的各种行为比如踩刹车和油门,那么都是要经过车辆的控制系统来处理,那么在没闯红灯之前,那么是由司机来控制该车辆,那么该车辆能够执行该司机的各种行为比如踩刹车以及油门,但是当司机冲红灯,也就对应进程非法访问内存,那么MMU识别到无效地址,那么就好比汽车传感器检查到当前异常,然后立马让通知交警,接着交警接管车辆的控制权,那么这里就对应CPU切换为内核态

而MMU会给CPU传递一个物理信号,那么该硬件上的物理信号我们有一个专有名词就是中断号 ,那么CPU获取到中断号会将其保存到某个寄存器中,然后CPU再将这个中断号交给内核,那么内核内部会记录每一个对应的中断号的处理逻辑的函数,也就是说内核内部会维护一个函数指针数组 ,那么中断号就是该函数指针数组的一个索引,那么通过中断号执行对应的处理函数,比如某个对应中断号的函数的内容,就是终止进程,然后设置进程的退出码,然后将进程的task_struct结构体移除就绪队列,然后清理资源,所以这就是为什么,我们虽然没有自己在代码层面上编写信号的处理的逻辑函数,但是进程就自然的执行了信号所对应的内容,那么这就和硬件层面有关系,或者说信号的执行和处理不是涉及用户态,而是涉及内核态和底层的硬件

而我们Linux中内核的信号其实就是模拟的是上文硬件之间的物理信号也就是中断号,所以我们要彻底搞清楚内核中存在的信号,那么我们就得知道底层的硬件中的中断号是怎么一回事

硬件物理信号

那么我们知道计算机底层的各种硬件单元,我们可以理解为一个生产车间里面负责不同岗位不同内容的工人,那么这些工人都是独立执行各自的任务,那么此时就要有一个领导能够站出来,指挥这些工人协同工作从而能够形成一个流水线,那么在计算机底层,那么担任这个领导者的角色就是CPU,那么既然要能够领导各个不同岗位的工人,那么领导就要知道这些工人的情况,那么同理,这里CPU也需要能够通过某种方式能够获取到各个不同的硬件的情况,而如果读者仔细看过或者观察过CPU,那么你会发现CPU的底部会有很多个物理针脚,那么这些物理针脚会插入进电脑的主板,而这每一个物理针脚对应这一条线路然后连接着对应的硬件单元,那么比如磁盘或者键盘等

那么如果某一个硬件有情况,那么此时硬件会传送一个电信号到对应的物理针脚,然后CPU的中断控制器再根据特定位置的物理针脚,将这个电信号给转化为有实际意义的中断号,那么以键盘为例,那么我们的代码中,经常会调用scanf或者read函数来获取键盘的输入,那么当我们用户敲击键盘,此时有输入了,那么此时键盘就会向其对应的物理针脚发送一个电信号,然后再有CPU的中断控制器转化为中断号告诉给CPU然后保存到CPU的寄存器中,那么CPU获取到中断号,那么此时就要切换为内核态,因为CPU就只是一个干事的,它本身并不知道中断号是什么以及有什么用,而内核是管理者,管理软硬件资源,说白了就是管事的,那么它会获取到中断号,并且内核记录了每一种中断号对应的处理方式是怎么样的,也就是一个中断向量表,本质上就是一个函数指针数组,那么它根据中断号,执行对应的函数,从而内核直到自己接下来应该获取键盘的输入,然后将其写入内存的缓存区中,同理,当我们从磁盘中读取数据,那么我们知道磁盘定位数据需要一定的时间,那么内核也不可能傻傻的等你取数据,啥也不干,因为它还有自己的其他很多事情要做,那么这里当磁盘定位到数据,也会传送一个电信号,然后转化为中断号,告诉给内核,内核再查中断向量表,知道这时候我应该将磁盘的数据给拷贝进内存的缓冲区中

那么操作系统是软硬件资源的管理者,那么它要管理硬件资源就需要知道各个硬件的状态,但是它不可能自己亲自去检查,因为操作系统太忙了,要做的事情太多了,所以解决方案就是通过硬件发送中断号来告知内核当前硬件的状态,这就是中断号的意义

内核信号

那么知道了物理信号之后,那么我们再来与内核的信号来建立联系,那么我们知道每一个硬件连接CPU的某个特定的针脚来传递得到不同的中断号,然后内核根据中断号从而知道对应的执行逻辑,而这里内核中也存在多种不同的信号,那么总共有64种信号,那么我们可以根据kill -l指令来获取全部的64种信号,而前31中信号是标准信号而后面的则是实时信号,而至于标准信号和实时信号有什么区别,那么我下文会有解释

那么这里的每一种信号就是如同硬件上特定的中断号,有着各自自己的执行逻辑,而内核则是通过一个中断向量表,本质就是一个函数指针数组,那么中断号就是这个数组的下标,那么同理这里信号也有自己的执行逻辑,所以这里内核也会存在有一个信号向量表,其同样也是函数指针数组,那么信号的编号就是数组的索引,其中对应着特定执行逻辑的函数,所以这就是为什么内核的信号本质模拟的实硬件层面上的中断号

cpp 复制代码
// 位于 Linux 内核源码的 include/linux/sched.h

struct task_struct {
    // ...
    
    /* 信号处理表 */
    struct sigaction            sigaction[_NSIG];  // 每个信号对应的处理方式(64个条目)
    
    /* 阻塞和待处理信号 */
    sigset_t                    blocked;           // 被阻塞的信号集(signal mask)
    struct sigpending           pending;           // 待处理信号队列(位图 + 实时信号链表)
    
    /* 信号处理上下文 */
    struct signal_struct        *signal;           // 进程间共享的信号状态(如终端控制、资源限制)
    struct sighand_struct       *sighand;          // 信号处理函数表(可共享)
    
    // ...
};

那么接下来就是解析内核层面上的信号的一个处理的全过程了,在上文我们就说过了,那么信号的接收和处理那么不涉及用户态,那么只会涉及到内核态和硬件层面的关联,那么主要是内核态,而信号的处理流程我们可以简单的概括三个大步骤,首先是信号的产生,接着是信号的保存,最后是信号的处理或者叫信号的捕捉

信号的保存

从上文,我们知道了即使我们没有写任何信号处理逻辑的代码,那么进程也会接收并处理信号,那么信号的接收一定在内核态而不是用户态,而每一个进程都有对应的task_sruct结构体,那么我们该task_struct结构体就会有一个字段,其中会指向一个位图和一个结构体链表,而这个位图是31位,对应的就是标准信号,那么意味着当我们给某个特定的进程发出标准信号,那么首先内核层面的处理就是定位获取到该进程的task_struct结构体,然后会修改一个TIF_SIGPENDING标记位,那么这个标记位的具体含义现阶段的知识还讲不清楚,只能在线程阶段才能说明,那么就可以简单理解为这个标记的作用就是标记当前进程受到信号,那么接下来就是找到位图,然后将对应的比特位设置为1,而这里我什么要将信号的接收和保存一起讲,其实我们发现这两个动作几乎都是一起完成的,接收到信号然后立马就保存,那么要细分就是设置那个标记位的过程就对应信号的接收,而之后的内容则是信号的保存

cpp 复制代码
struct task_struct {
    //...
    struct signal_struct *signal;  // 信号处理相关信息
    sigset_t blocked;             // 被阻塞的信号集
    struct sigpending pending;    // 待处理信号队列
    //...
};
// 位于 Linux 内核源码的 include/linux/signal.h

struct sigpending {
    struct list_head list;  // 实时信号队列(链表)
    sigset_t signal;        // 标准信号的位图(pending 位图)
};

而我们也可以通过Linux的kill命名来发出信号

格式: kill -信号编号 进程pid

那么这个指令能够发出64种信号的绝大部分信号,而之前我们说了信号的分为了标准信号和实时信号,那么这里我们也知道了标准信号和实时信号是采取了不同的方式来存储,一个采取位图,另一个采取链表,那么两者的区别就是一个进程能接收不同种类的标准信号,但是每一种标准信号只能接收一次,因为一旦你进程在接收到了同一种标准信号,那么内核检查位图发现对应位已经被设置好,那么当前信号就会被舍弃,而实时信号就是一个链表,那么接收的每一个信号就是链表中的一个节点,这样实时信号能够允许同时接收同一种信号,这就是两者的区别

信号的处理

那么进程接收保存完信号之后,那么接下来有三种处理信号的方式,分别是++执行默认动作以及忽略和自定义行为++,那么执行默认动作和忽略我们都好理解,就好比有的人看到红灯就停,而有的人即使看到红灯也不踩刹车继续前进,而这里的自定义行为则类似于,有的司机看到红灯就条件反射唱歌或者读书,也就是既不是不是停车也不是前进而是做自己的事情

所以这里我们就先来见一下这里最特殊的处理方式,也就是自定义行为,那么内核也为我们提供了自定义行为的signal函数:

  • signal
  • 头文件:<signal.h>
  • 函数声明:void signal(int signum,size_t size,void(handler)(int));
  • 返回值:成功时返回之前的信号处理函数指针,失败时返回 SIG_ERR 。

那么这里的第一个参数就是信号的编号,那么第二个参数就是我们自定义行为的函数,那么也就是一个函数指针,那么我们知道内核存在一个信号向量表,里面记录了每一种信号对应的函数,那么这个signal函数则是拿到这个函数指针和编号然后将信号向量表的对应位置其替换为用户自定义的函数的函数指针,所以由于这里能够自定义信号的处理逻辑,那么这里的信号向量表不是全局的,而是每一个进程有所属于自己的一个信号向量表,对应着task_struct结构体的一个字段

那么我可以简单编写一个代码来验证一下这个函数的功能,那么我们自定义二号信号,如果该进程接收到二号信号,那么就会执行我们自定义的函数指针指向的内容:

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
void myheadler(int sigid)
{
   printf("%d 接收到了%d信号\n",getpid(),sigid);
}
int main()
{
   void(*func)(int)=myheadler;
    signal(2,func);
    while(1)
    {
        printf("I am %d,hiii\n",getpid());
        sleep(1);
      }
     return 0;
}

运行截图:


那么接下来有些读者的疑问可能在于怎么执行信号,那么有上文的信号的接收的经验,这里不用多说,信号的处理肯定也是在内核态,那么有的读者可能就会说,假设存在这样的场景,那么我编写了一个代码,那么该代码的逻辑就是死循环调用printf不断向显示器打印字符串,那么这里理论上来说,这个进程不会终止,会一直占用CPU资源,那么这里如果我们没有调用相关的系统调用,那么意味着CPU运行当前进程的没有时机切换为内核态,那么怎么执行信号对应的处理逻辑呢?

那么这个问题是很好的问题,会涉及到硬件方面的内容,首先这里其实printf本身内部的函数实现会涉及到调用write函数,也就是有系统调用可以切换到内核态,那么我们假设就存在这样的场景,没有任何系统调用,那么即使这样,我还是能向你保证内核能够正常处理信号,那么在刚才的例子中,你认为这个进程死循环打印字符串就会一直占用CPU资源了,但是其实不是,内核存在那么多要运行的进程,那么是不会允许一个进程一直占用CPU资源的,那么CPU有一个时钟芯片,那么里面记录了每一个进程占用CPU的时间,比如这个打印死循环的进程占用CPU 10s,那么10s一到,那么此时就会切换运行其他的进程,但是进程之间的切换并不是无缝切换的,而是中间有一个间隙,那么这个间隙就是用来切换到内核,因为操作系统本质上也是一个进程,那么此时切换的这个间隙,那么内核就会检查遍历组织的task_struct结构体,然后检查标记,如果有标记,代表该进程受到信号,那么就会处理该进程的信号,然后调整进程的pc计数器,然后将pc计数器原本指向用户态的某个代码,此时给设置为指向该信号向量表的函数指针位置处,从而执行信号对应的处理逻辑


补充

前后台进程

我们可以来做一个简单的实验,那么就是我们可以编写一个程序,那么该程序就是向显示器死循环打印字符串,那么当我们运行该程序的时候,我们会发现一个现象,那么就是我们向显示器输入的各种指令比如ls或者cd指令,那么此时再运行该进程之后,那么此时我们发现指令并没有执行

那么想必有的读者会对这个现象感到疑惑,因为上文我们就才刚刚提到过,那么每一个进程不可能一直占据CPU资源,因为每一个进程都有自己的时间片,也就是规定占据CPU多长时间,时间一到就切换进程,而如果有的读者有之前有自己模拟实现过bash也就是命令行解释器,那么他知道bash进程会先打印一个命令行提示符,接着会获取用户的输入,那么这里这个死循环进程运行了一定的时间之后,一定会切回bash进程,那么执行bash进程的代码,那么其中bash进程一定能够获取到刚才我们在内核缓冲区中保存的指令,接着bash会调用fork创建子进程,然后再调用进程替换的接口比如execve将该子进程替换为指令对应进程的上下文,但是事实上,你可以验证,无论过多久,那么你不管在键盘输入什么指令,那么bash都不会执行对应的指令,所以这里就得引出一个前台进程和后台进程的概念,那么Linux中,只允许有一个前台进程,准确来说应该是前台进程组,由于进程组是后面的概念,那么这里我们就可以简单理解只允许一个前台进程,而最开始bash就是这个前台进程,那么只有前台进程才能获取键盘输入,而当我们创建一个新的进程,那么由于Linux只允许一个前台进程,那么bash就将这个前台进程的角色交给了这个打印死循环的进程,而bash就成了后台进程,也就是发生了权利的交接,由于这里的进程只是打印字符串,不会读取键盘输入,所以我们输入的各种指令就会被存放到内存的缓冲区中

那么有之前模拟实现bash经验的小伙伴会知道,我们创建的这些进程都是bash的子进程,那么bash在获取到用户的键盘输入之后,那么会先解析字符串,分成指令部分和参数部分,然后再分析该指令是否是内置命令,不是,那么那么就调用fork创建子进程然后再进程的替换,那么这里在fork之后,进程替换之前,那么bash就会做一个权利交接的工作,那么它会调用一个tcsetpgrp接口,那么这个接口就是给改进程设置标志位,因为每一个进程要访问键盘的内核缓冲区,那么内核都要检查它是否是前台进程,那么检查的方式就是内核会记录当前的前台进程是哪一个进程,如果匹配,那么就能够访问,而如果我们要让bash进程还要能够执行指令,那么就必须得让bash进程继续担任前台进程,所以在运行该打印死循环的进程变成后台进程,那么就是在运行进程的指令后加一个&

cpp 复制代码
//bash
setpgid(0, 0);  // 让子进程自立门户,成为新进程组的组长
tcsetpgrp(tty_fd, child_pid);  // 通知终端:"现在子进程是前台进程组"
cpp 复制代码
struct tty_struct {
    // ...
    struct pid *foreground_pgrp;  // 当前前台进程组的 PGID
    // ...
};

内核检查该进程的  pgrp  是否等于  tty->foreground_pgrp 。
 
如果是 → 允许读取。
 
如果不是 → 发送  SIGTTIN

./process & 创建一个后台进程

那么这里对于前后台进程的介绍就简单讲解到这里,这里的介绍只是大致的了解了前后台进程,那么前后台进程还有很多的细节这里还没有讲到,那么会在后面的学习中逐步补充,那么这里就先做一个铺垫

模拟实现kill指令

原理

那么文章的最后,那么我们就来利用实现一个上文介绍的kill指令,那么简单的实现一个我们自己的kill指令,那么我们知道用户最开始输入的kill指令只是一个字符串,然后用空格分开参数部分,那么bash接收到字符串,然后分割出指令部分和参数部分,那么接下来判断完当前指令不是内置指令,那么就需要调用fork创建子进程,然后再调用execve函数,因为kill指令本质上是存储在特定路径下的一个可执行文件,那么这里execve将分割出的指令部分和参数部分传递给该替换后的进程

所以这里我们在实现的时候,那么这里我们的main函数就得有参数,分别是总的参数个数argc包括指令部分和参数部分,以及一个字符数组,每一个元素是一个字符指针,指向分割后的指令部分和参数部分的字符串:

cpp 复制代码
int main(int argc,char* argv[])

那么这里的kill指令底层就是调用了kill系统调用接口,那么接下来我们来认识这个接口:

  • kill
  • 头文件: <sys/types.h>,<signal.h>
  • 函数声明:int kill(pid_t pid, int sig);
  • 返回值:成功时返回0,失败时返回 -1并设置errno

那么这里的参数就是接收进程pid以及信号的编号,那么上文我们讲的信号的接收以及保存的那部分内容,就是kill接口的原理,那么内核会根据传递的参数pid,然后持有该pid定位到对应的进程的task_struct结构体,如果是标准信号就利用位运算,然后将位图对应的比特位设置为1,如果是实时信号就添加进链表中

那么我们知道kill指令的格式是:

kill -信号编号 pid

kill 信号编号 pid

那么kill指令的参数总个数是三个,也就是argc一定是三,如果不是三,那么我们就得终端打印一个错误信息,并且退出,那么终端打印这个错误信息,我定义了一个instruct函数

cpp 复制代码
void instruct(char* s)
{
      cout<<"usage:"<<s<<"is not right"<<endl;
}
int main(int argc,char* argv[])
{
    if(argc!=3)
    {
        instruct(argv[0]);
        return 2;
    }

那么接下来我们发现kill指令的第二个参数就是信号编号,并且Linux允许前面可以有下划线并且有前导0,同理pid也允许有前导0,那么这里我们就得对信号编号以及进程编号做一个处理,对于信号编号的字符串来说,那么这里我们额外定义一个指针s1来判断,那么这里先判断当前是否是空字符串,也就是判断当前字符串的第一个字符是不是'\0',如果是,那么直接打印错误信息退出,然后再判断是否有下划线的情况,如果有下划线,那么指针往后移动一旦单位,由于信号编号允许前导0,那么接下来就要处理前导0,那么如果当前是前导0,那么指针要持续后移知道移到第一个非0字符,但是有可能有的信号编号是这样的:"-000000"或者"0000",就是只有前导0,而信号编号是从1开始,那么这里明显就不合法,所以如果指针移到结尾也就是\0处,那么就可以判断这里的字符串只有前导0,那么直接打印错误信息并且退出

那么指针移到第一个非0字符处,那么我们就要判断之后的字符是否合法,也就是不能出现字母等其他字符,那么就做一个循环判断即可

cpp 复制代码
char* s1=argv[1];
    if((*s1)=='-')
    {
        s1++;
    }
      if((*s1)=='\0') 
      {
        instruct(argv[0]);
        return 2;
      }else
      {
        while((*s1)!='\0')//去除前导0
        {
            if((*s1)=='0')
            {
                s1++;
            }else
            {
                break;
            }
        }
        if(*s1=='\0')
        {
            instruct(argv[0]);
            return 2;
        }
      }
      char* p1=s1;
    while((*p1)!='\0')
    {
         if('0'<=(*p1)&&(*p1)<='9')
         {
            p1++;
         }else
         {
            instruct(argv[0]);
            return 2;
         }
    }

那么这里对于进程pid来说,那么就一样的逻辑,判断当前是否为空字符串,去除前导0,再检查字符串的合法性

cpp 复制代码
char* s2=argv[2];
    if((*s2)=='\0')
    {
        instruct(argv[0]);
        return 2;
    }else
    {
        while(*s2!='\0')
        {
            if(*s2=='0')
            {
                s2++;
            }else
            {
                break;
            }
        }
        if(*s2=='\0')
        {
            instruct(argv[0]);
            return 2;
        }
    }
    char* p2=s2;
    while((*p2)!='\0')
    {
         if('0'<=(*p2)&&(*p2)<='9')
         {
            p2++;
         }else
         {
            instruct(argv[0]);
            return 2;
         }
    }

最后调用stoi将字符串转换为整数,然后调用kill接口即可


源码

cpp 复制代码
#include <sys/types.h>
#include <signal.h>
#include<iostream>
#include<cstring>
#include<string>
#include<cstdlib>
#include<cerrno>
#include<cstdio>
using std::cout;
using std::endl;
void instruct(char* s)
{
      cout<<"usage:"<<s<<"is not right"<<endl;
}
int main(int argc,char* argv[])
{
    if(argc!=3)
    {
        instruct(argv[0]);
        return 2;
    }
    char* s1=argv[1];
    if((*s1)=='-')
    {
        s1++;
    }
      if((*s1)=='\0') 
      {
        instruct(argv[0]);
        return 2;
      }else
      {
        while((*s1)!='\0')//去除前导0
        {
            if((*s1)=='0')
            {
                s1++;
            }else
            {
                break;
            }
        }
        if(*s1=='\0')
        {
            instruct(argv[0]);
            return 2;
        }
      }
      char* p1=s1;
    while((*p1)!='\0')
    {
         if('0'<=(*p1)&&(*p1)<='9')
         {
            p1++;
         }else
         {
            instruct(argv[0]);
            return 2;
         }
    }
    char* s2=argv[2];
    if((*s2)=='\0')
    {
        instruct(argv[0]);
        return 2;
    }else
    {
        while(*s2!='\0')
        {
            if(*s2=='0')
            {
                s2++;
            }else
            {
                break;
            }
        }
        if(*s2=='\0')
        {
            instruct(argv[0]);
            return 2;
        }
    }
    char* p2=s2;
    while((*p2)!='\0')
    {
         if('0'<=(*p2)&&(*p2)<='9')
         {
            p2++;
         }else
         {
            instruct(argv[0]);
            return 2;
         }
    }
    size_t sig=std::stoi(s1);
    size_t pid=std::stoi(s2);
    size_t result= kill(pid, sig);
    if(result<0)
    {
        perror("kill");
        return 1;
    }
    return 0;
}

运行截图:

结语

那么这就是本期博客的全部内容,那么我下一期还会继续补充信号的更多的细节,我会持续更新,希望你能够多多关注,如果本文对你有帮组的话,那么还请三连加关注,你的支持就是我创作的最大的动力!

相关推荐
欧的曼几秒前
cygwin+php教程(swoole扩展+redis扩展)
开发语言·redis·后端·mysql·nginx·php·swoole
snow@li3 分钟前
VSCode:基础使用 / 使用积累
java·servlet·jar
巴拉巴巴巴拉4 分钟前
Spring Boot 整合 Thymeleaf
java·spring boot·后端
智江鹏11 分钟前
Android 之 Kotlin中的符号
android·开发语言·kotlin
爱喝水的鱼丶13 分钟前
SAP-ABAP: Open SQL集合函数COUNT(统计行数)、SUM(数值求和)、AVG(平均值)、MAX/MIN(极值)深度指南
运维·数据库·sql·sap·报表·abap·程序
凤年徐13 分钟前
【数据结构与算法】刷题篇——环形链表的约瑟夫问题
c语言·数据结构·c++·算法·链表
AI 嗯啦22 分钟前
linux的用户操作(详细介绍)
linux·运维·服务器
AOwhisky36 分钟前
云计算一阶段Ⅱ——12. SELinux 加固 Linux 安全
linux·安全·云计算
牟同學39 分钟前
深入理解 C++ 中的stdpriority_queue:从原理到实战的高效优先级管理
数据结构·c++·priority_queue
Star在努力43 分钟前
20-C语言:第21~22天笔记
java·c语言·笔记