【背景】
程序运行起来后,每个模块都有自己的进程,那么不同的模块如何进行通讯或者数据交换呢?
上面这张图说明了linux的ipc是继承最初的Unix 的IPC逻辑的,那么具体关系和概述讲解,请参考此链接的原文:https://www.cnblogs.com/52php/p/5877691.html 这里不再赘述
那么对于linux 下的ipc方法都有哪些呢?如下:
Linux下进程间通信的几种主要手段简介:
- 管道(Pipe)及有名管道(named pipe):管道可用于具有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信;
- 信号(Signal):信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;Linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又能够统一对外接口,用sigaction函数重新实现了signal函数);
- 报文(Message)队列(消息队列):消息队列是消息的链接表,包括Posix消息队列system V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。
- 共享内存:使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。
- 信号量(Semaphore):主要作为进程间以及同一进程不同线程之间的同步手段。
- 套接口(Socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支开发出来的,但现在一般可以移植到其它类Unix系统上:Linux和System V的变种都支持套接字
一般来说,Linux下的进程包含以下几个关键要素:
- 有一段可执行程序;
- 有专用的系统堆栈空间;
- 内核中有它的控制块(进程控制块),描述进程所占用的资源,这样,进程才能接受内核的调度;
- 具有独立的存储空间
进程和线程有时候并不完全区分,而往往根据上下文理解其含义
【信号】
这里为什么要再次说明呢?因为我们这次主要是基于linux c讲的,那么肯定有c的部分,最主要的也是Linux c本身的部分,针对c的信号部分,可以参考013_C标准库函数之<signal.h>-CSDN博客
下面我们主要说明下linux c的信号部分,当然大家也可以参考此链接,我觉得这个作者讲的很不错:https://www.cnblogs.com/52php/p/5813867.html
1.接收并处理信号
【sigaction】
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
sigaction()会依参数signum指定的信号编号来设置该信号的处理函数act。参数signum可以指定SIGKILL和SIGSTOP以外的所有信号,该函数与signal()函数一样,用于设置与信号signum关联的动作
入参:
int signum:具体信号值
const struct sigaction *act:用于设置指定信号的动作(也就是具体信号handler函数实现)
struct sigaction *oldact:不是空指针的话,就保存原先对该信号的动作的位置
结构体:
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
-
sa_handler
:这是一个函数指针,指向处理信号的函数。如果设置为SIG_IGN
,则忽略该信号;如果设置为SIG_DFL
,则执行信号的默认动作。 -
sa_sigaction
:这是一个可选的函数指针,当sa_flags
中包含SA_SIGINFO
标志时,使用这个字段来指定一个带有额外信息的信号处理器。这个处理器接收三个参数:信号编号、一个指向siginfo_t
的指针(包含了信号的额外信息),以及一个指向信号上下文的指针。 -
sa_mask
:这是一个信号集,用于指定在信号处理器执行期间要被阻塞的信号集合。这可以防止在处理一个信号时被其他信号中断。 -
sa_flags
:这个字段包含了一系列标志,用于改变信号处理的行为。例如,SA_RESTART
可以使某些被打断的系统调用在信号处理函数返回后自动重新发起。 -
sa_restorer
:这个字段通常不需要,它是为了兼容某些老版本的系统,在现代系统中一般不使用。
【testcode00】
1.基础使用
2.sa.mask使用
此外,现在有一个这样的问题,我们使用signal()或sigaction()函数来指定处理信号的函数,但是如果这个信号处理函数建立之前就接收到要处理的信号的话,进程会有怎样的反应呢?它就不会像我们想像的那样用我们设定的处理函数来处理了。sa_mask就可以解决这样的问题,sa_mask指定了一个信号集,在调用sa_handler所指向的信号处理函数之前,该信号集将被加入到进程的信号屏蔽字中,设置信号屏蔽字可以防止信号在它的处理函数还未运行结束时就被接收到的情况,即使用sa_mask字段可以消除这一竞态条件。
2.发送信号
关于信号,我们上面说的signal, sigaction都是接收到信号,然后做出处理动作,那么,假如需要发出一个信号给其他进程呢?
【kill】
在Linux和Unix系统中,kill
命令用于发送信号到程序或进程。信号是一种软件中断,可以用来通知接收进程某个事件已经发生
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
<pid>
:进程ID,即您想要发送信号的进程的ID。<signal>
:信号的名称或编号。如果不指定,默认发送的是SIGTERM(终止信号)。
它的作用把信号sig发送给进程号为pid的进程,成功时返回0。kill()调用失败返回-1,调用失败通常有三大原因:
1、给定的信号无效(errno = EINVAL)
2、发送权限不够( errno = EPERM )
3、目标进程不存在( errno = ESRCH )
进程可以通过kill函数向包括它本身在内的其他进程发送一个信号,一般向别人进程发送信号可能会失败,极大概率原因是因为没有权限!但是要注意,如果是root用户的话,那就是无极的存在,肯定会成功,不会失败
【注意事项】
- 您需要有足够的权限来发送信号到其他用户的进程。通常,只有root用户或进程的所有者才能发送信号到该进程。
- 发送信号前,请确保您知道该信号的作用,因为某些信号(如SIGKILL和SIGSTOP)会立即终止或停止进程,且这些进程无法恢复。
- 在发送信号前,您可以通过
ps
命令来查看进程的状态和PID。 - 如果进程已经退出,
kill
命令可能会返回错误消息,如“没有这样的进程”。 - 在使用
kill
命令时,确保您使用的是正确的进程ID,以避免意外终止重要的系统进程。
以上就是kill
命令的基本用法。在使用时,请谨慎操作,以避免对系统稳定性造成影响。
【testcode】
在代码中使用fork()调用复制了一个新进程,在子进程中,5秒后向父进程中发送一个SIGALRM信号,父进程中捕获这个信号,并用sig_handler_00()函数来处理,变改flag_sig的值,然后退出循环。从结果中我们也可以看到输出了5个There is a string...之后,程序就收到一个SIGARLM信号,然后结束了进程
注:如果父进程在子进程的信号到来之前没有事情可做,我们可以用函数pause()来挂起父进程,直到父进程接收到信号。当进程接收到一个信号时,预设好的信号处理函数将开始运行,程序也将恢复正常的执行。这样可以节省CPU的资源,因为可以避免使用一个循环来等待。以本例子为例,则可以把while循环改为一句pause();
【alarm】
在Unix和Linux系统中,alarm
函数是一个用于设置定时器(闹钟)的函数,它会在指定的秒数后发送SIGALRM信号到调用进程。如果进程没有处理该信号的处理器,那么默认的行为是终止进程
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
seconds
:指定多少秒后发送SIGALRM信号。如果值为0,则取消任何已设置的闹钟。
【返回值】
alarm
函数返回上一个闹钟设置的剩余秒数,如果没有设置过闹钟,则返回0。如果返回值是非零,这意味着之前已经设置了一个闹钟,而且它还没有超时。
进程可以调用alarm()函数在经过预定时间后向发送一个SIGALRM信号。
alarm()函数用来在seconds秒之后安排发送一个SIGALRM信号,如果seconds为0,将取消所有已设置的闹钟请求。alarm()函数的返回值是以前设置的闹钟时间的余留秒数,如果返回失败返回-1
【testcode】
在这个例子中,程序将设置一个3秒的闹钟,并在闹钟超时时打印一条消息。然而,由于主循环是无限循环,如果没有其他方式终止程序,它将永远不会退出。实际上,SIGALRM的默认行为是终止进程,但由于我们注册了一个信号处理函数,所以它将调用sig_handler_02函数而不是终止进程。
需要注意的是,由于alarm
函数是基于实时时间的,如果进程在睡眠状态(例如调用了sleep
或pause
),SIGALRM信号会在进程恢复运行时立即触发。此外,在一个进程中只能设置一个闹钟,调用alarm
会覆盖之前的设置。
3.信号函数集
在C语言中,信号处理是通过特定的函数集来实现的,这些函数在 <signal.h>
头文件中声明。以下是在C语言中处理信号的常用函数:
-
signal()
- 用途:用于设置一个信号的处理函数。
- 原型:
void (*signal(int sig, void (*func)(int)))(int);
-
sigaction()
- 用途:用于检查或修改与指定信号相关联的处理动作。
- 原型:
int sigaction(int sig, const struct sigaction *act, struct sigaction *oldact);
-
kill()
- 用途:用于向指定进程发送信号。
- 原型:
int kill(pid_t pid, int sig);
-
raise()
- 用途:用于向调用进程发送信号。
- 原型:
int raise(int sig);
-
alarm()
- 用途:用于设置一个定时器,在指定的秒数后发送
SIGALRM
信号给调用进程。 - 原型:
unsigned int alarm(unsigned int seconds);
- 用途:用于设置一个定时器,在指定的秒数后发送
-
pause()
- 用途:用于使调用进程挂起,直到收到一个信号。
- 原型:
int pause(void);
-
sigpause()
- 用途:用于在挂起进程的同时设置信号掩码。
- 原型:
int sigpause(int sigmask);
-
sigpending()
- 用途:用于获取当前被阻塞的信号集。
- 原型:
int sigpending(sigset_t *set);
-
sigprocmask()
- 用途:用于改变进程的信号掩码。
- 原型:
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
- 调用这个函数才能真正改变进程的屏蔽字,之前的函数都是为改变一个变量的值而已,并不会真正影响进程的屏蔽字。
-
sigemptyset()
- 用途:用于初始化信号集,将其所有信号清零。
- 原型:
int sigemptyset(sigset_t *set);
-
sigfillset()
- 用途:用于将信号集的所有信号置位。
- 原型:
int sigfillset(sigset_t *set);
-
sigaddset()
- 用途:用于将一个信号添加到信号集中。
- 原型:
int sigaddset(sigset_t *set, int sig);
-
sigdelset()
- 用途:用于从信号集中删除一个信号。
- 原型:
int sigdelset(sigset_t *set, int sig);
-
sigismember()
- 用途:用于检查一个信号是否在信号集中。
- 原型:
int sigismember(const sigset_t *set, int sig);
这些函数提供了C语言中信号处理的强大工具集,可以用于信号的产生、发送、阻塞、处理等操作。在使用这些函数时,需要注意信号的安全性和原子性,避免竞态条件和不可预料的行为。
在Linux操作系统中,信号是用于进程间通信(IPC)的一种机制,它们用于通知接收进程某个事件已经发生。Linux的信号函数集主要涉及信号的发送、接收和处理。以下是Linux中常用的信号处理相关函数:
-
setitimer()
- 设置间隔定时器,可以更精确地控制信号的发送。
-
sigqueue()
- 发送信号给指定进程,并可以附带一个整数值。
-
sigsuspend()
- 用于在临时替换信号屏蔽字的同时挂起进程,直到捕捉到一个信号。
-
pthread_kill()
- 用于向线程发送信号。
-
sigwait()
- 用于等待一个或多个信号的发生。
以下是信号处理相关的结构和类型:
-
sigset_t
- 用于表示信号集的数据类型。
-
siginfo_t
- 用于描述信号的信息。
-
struct sigaction
- 用于描述信号的处理动作。
一些常用的信号及其默认行为:
SIGABRT
:进程中止信号。SIGALRM
:定时器超时信号。SIGFPE
:浮点异常信号。SIGHUP
:挂起信号。SIGILL
:非法指令信号。SIGINT
:中断信号。SIGKILL
:杀死信号。SIGPIPE
:管道破裂信号。SIGQUIT
:退出信号。SIGSEGV
:段错误信号。SIGTERM
:终止信号。SIGUSR1
、SIGUSR2
:用户定义的信号。
这些函数和结构在 <signal.h>
头文件中声明,是Linux信号处理的核心组成部分。在使用这些函数时,应当注意信号的同步和异步特性,以及信号处理函数中应避免调用的那些不可重入的函数。
【注】以上c和linux信号函数集大部分是通用的,所以可以一起来看,我并没有将各自的一一列出,总的来看就行了
【testcode_00】
首先,我们能过sigaction()函数改变了SIGINT信号的默认行为,使之执行指定的函数sig_handler_03,所以输出了语句:Welcome to sig_handler_03... 然后,通过sigprocmask()设置进程的信号屏蔽字,把SIGINT信号屏蔽起来,所以过了6秒之后,用sigpending()函数去获取被阻塞的信号集时,检测到了被阻塞的信号SIGINT,输出The SIGINT signal has ignored...最后,用函数sigdelset()函数去除 先前用sigaddset()函数加在sigset上的信号SIGINT,再调用函数sigsuspend(),把进程的屏蔽字再次修改为sigset(不包含SIGINT),并挂起进程。由于先前的SIGINT信号停留在待处理状态,而现在进程已经不再阻塞该信号,所以进程马上对该信号进行处理,从而在最后,你不用输入 Ctrl+C 也会出现后面的处理语句,最后过了3秒程序就成功退出了。
【总结】
关于信号的学习及简单练习就先这么着,我们只是简单的演示和操作了一些接口,关于信号的复杂运用及注意事项,大家可以自行练习,由于篇幅关系我们没有将所有接口一一举例进行演示,但是大部分都演示了,大家可以根据此思维继续去演练,加油