Linux进程间通信的方式
Linux信号
Linux信号是Linux进程间通信的一种古老的方式,是一种异步的通信方式,进程向另一个进程发送一个信号,另一个进程接收到信号之后会触发一个处理事件,比如说Ctrl+C中断。
kill -l/-L
:查看信号信息
zouren@ubuntu:~$ kill -L
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
编号1到31为传统UNIX信号,为不可靠信号,编号32到64为后来扩充的,为可靠信号,前者不支持排队,后者支持排队,前者一般都有特殊的定义,而后者通常提供给用户定义
信号产生方式
- 硬件产生,浮点数异常,除数为0,无效内存访问…
- Ctrl+C等用户操作
- 软件异常产生信号,当检测到某种软件条件已发生,并将其通知有关进程时,产生信号。
- kill()函数发送信号
- kill命令发送信号,常用此命令终止一个失控的后台进程
kill函数
头文件
#include<sys/types.h>
#include<signal.h>
函数原型
int kill(pid_t pid, int signum);
注意:使用kill函数时,接收信号和发送信号的进程的所有者必须相同,或者发送信号进程的所有者是超级用户
参数 | 描述 |
---|---|
pid | pid > 0 发送信号给进程号为pid的进程,pid = 0 将信号发送给当前进程所在进程组中的所有进程,pid = -1 将信号发送给系统内所有的进程,pid < -1 信号传递给指定进程组的所有进程,进程组号为pid的绝对值 |
signum | 信号编号(可以是整数也可以是宏定义) |
返回值
返回值 | 描述 |
---|---|
0 | 成功 |
-1 | 失败 |
测试代码
从父进程种fork出一个子进程,子进程一直输出信息,3秒后父进程向子进程发送中断信号
#include<iostream>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<signal.h>
using namespace std;
int main(int argc, char* argv[]){
pid_t pid;
pid = fork(); // create son process
if(pid < 0){
perror("error to fork");
}
int i = 0;
if(pid == 0){
// son process;
while(1){
cout<<"i am son!"<<endl;
sleep(1);
}
}else if(pid > 0){
while(1){
cout<<"i am father"<<endl;
sleep(1);
i++;
if(i == 3){
kill(pid, SIGINT); // send the SIGINT signal to son process
}
}
}
return 0;
}
zouren@ubuntu:~/IPC/signal$ ./signal_kill_exe
i am father
i am son!
i am son!
i am father
i am son!
i am father
i am father
i am father
i am father
i am father
i am father
i am father
等待信号
头文件
#include<unistd.h>
函数原型
int pause(void);
等待信号的到来,再信号到来之前,程序会被阻塞,直到捕捉到信号位置,用于判断信号是否已到
返回值
直到捕获到信号才返回-1,errno被置为EINTR
测试代码
程序输出一个信息,并调用pause,等待信号,在收到Ctrl+C之后退出程序
#include<unistd.h>
#include<stdio.h>
int main(int argc, char *argv[]){
printf("i am waiting for signal\n");
pause();
return 0;
}
zouren@ubuntu:~/IPC/signal$ ./pause_test_exe
i am waiting for signal
^C
zouren@ubuntu:~/IPC/signal$
信号处理的三种方式
进程收到信号之后,会执行三种操作种的一种
- 执行默认操作,一般是终止进程
- 忽略该信号
- 执行自己定义的函数
SIGKILL
和SIGSTOP
不能更改信号的处理方式,因为其向用户提供了终止进程的一种可靠方法。
signal()函数连接信号处理函数
头文件
#include<signal.h>
函数原型
sighandler_t signal(signum, sighandler_t handler);
sighandler_t
是一个函数,它的形式为typedef void (*sighandler_t)(int); // 回调函数
signal会返回一个函数地址,handler有三个参数
自定义函数地址、SIG_DEF(默认操作)、SIG_IGN(忽略)
自定义函数的格式为void funname(int signal){ ..... }
这个自定义函数是可重入函数
返回值
情况 | 返回值 |
---|---|
成功 | 第一次返回NULL,第二次返回信号处理函数的地址 |
失败 | SIG_ERR |
测试代码
#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;
// 信号处理函数
void signal_handler(int signal){
if(signal == SIGINT){
cout<<"receive signal SIGINT"<<endl;
}else if(signal == SIGQUIT){
cout<<"receive signal SIGQUIT"<<endl;
}
}
int main(int argc, char *argv[]){
cout<<"Waiting for SIGINT or SIGQUIT"<<endl;
/*
SIGINT: Ctrl + C
SIGQUIT: Ctrl + \
*/
// 信号注册函数
signal(SIGINT, signal_handler);
signal(SIGQUIT, signal_handler);
// 等待信号
pause();
pause();
return 0;
}
zouren@ubuntu:~/IPC/signal$ ./signal_link_exe
Waiting for SIGINT or SIGQUIT
^Creceive signal SIGINT
^\receive signal SIGQUIT
测试代码2
测试signal的返回值
#include<unistd.h>
#include<signal.h>
#include<iostream>
using namespace std;
void signal_handler1(int signal){
cout<<"i am fun1"<<endl;
}
void signal_handler2(int signal){
cout<<"i am fun2"<<endl;
}
int main(int argc, char* argv[]){
sighandler_t fun_addr = NULL;
fun_addr = signal(SIGINT, signal_handler1);
// 第一次返回NULL
if(fun_addr == NULL){
cout<<"return fun addr is NULL"<<endl;
}
// 第二次返回fun1的地址
fun_addr = signal(SIGINT, signal_handler2);
if(fun_addr == signal_handler1){
cout<<"return fun addr is fun1"<<endl;
}
// 第三次返回fun2的地址
fun_addr = signal(SIGINT, signal_handler1);
if(fun_addr == signal_handler2){
cout<<"return fun addr is fun2"<<endl;
}
return 0;
}
zouren@ubuntu:~/IPC/signal$ ./signal_ret_exe
return fun addr is NULL
return fun addr is fun1
return fun addr is fun2
信号集与信号阻塞集
程序要对多个信号进程处理,这些信号存储在信号集种,信号集是表示多个信号定义的数据类型
sigset_t
,定义路径为/usr/include/i386-linux-gnu/bits/sigset.h
相关函数
#include<signal.h>
int sigemptyset(sigset_t *set); // 清空信号集
int sigfillset(sigset_t, *set); // 将参数set进行初始化
int sigismember(const sigset_t *set, int signum); // 判断signum是否在信号集中
int sigaddset(sigset_t *set, int signum); // 在信号集中添加信号signum
int sigdelset(sigset_t *set, int signum); // 在信号集中删除信号signum
测试代码
#include<signal.h>
#include<iostream>
using namespace std;
int main(int argc, char *argv[]){
sigset_t set; // 定义信号集
int ret = 0;
sigemptyset(&set); // 清空信号集中的内容
// sigismember(); 判断信号是否在信号集中,存在返回1, 不存在返回0
ret = sigismember(&set, SIGINT);
if(ret == 0){
cout<<"SIGINT is not a member of set \nret="<<ret<<endl;
}else{
cout<<"SIGINT is a member of set\nret="<<ret<<endl;
}
// 添加信号集
sigaddset(&set, SIGINT);
sigaddset(&set, SIGQUIT);
// 继续判断
ret = sigismember(&set, SIGINT);
if(ret == 1){
cout<<"SIGINT is a member of set\nret="<<ret<<endl;
}
// 移除SIGQUIT信号
sigdelset(&set, SIGQUIT);
ret = sigismember(&set, SIGQUIT);
if(ret == 0){
cout<<"SIGQUIT is not a member of set\nret="<<ret<<endl;
}
return 0;
}
zouren@ubuntu:~/IPC/signal$ ./sigset_exe
SIGINT is not a member of set
ret=0
SIGINT is a member of set
ret=1
SIGQUIT is not a member of set
ret=0
信号阻塞集
每一个进程都有一个自己的信号阻塞集,又称为(屏蔽集、掩码),用于描述那些信号递送到该进程的时候被阻塞,(信号被记录,等到进程空闲准备好之后再执行它),子进程会继承父进程的信号阻塞集
阻塞不是进制传递,而是延缓传递信号,可以通过
sigprocmask()
来修改当前的信号掩码要改变信号的阻塞情况
头文件
#include<signal.h>
函数原型
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
根据
how
的值,对信号信号阻塞集进行检查或修改,set为处理后新的信号阻塞集,oldset为之前的信号阻塞集(如果oldset不为NULL的话)
how
:
-
SIG_BLOCK: 向信号阻塞集中添加set集,新的信号掩码是两者的并集
set U oldset
-
SIG_UNBLOCK:从信号阻塞集中删除set集
-
SIG_SETMASK:信号阻塞集设置为set,原来的信号阻塞集放到oldset,之前的信号组赛集会清空
set
:
- 需要操作的信号集的地址
oldset
:
- 原来的信号阻塞集的地址
返回值
描述 | 返回值 |
---|---|
成功 | 0 |
失败(how非法) | -1EINVAL |
不能阻塞SIGKILL和SIGSTOP信号,并且阻塞SIGFPE可能会导致意想不到的后果
测试代码
#include<iostream>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
using namespace std;
int main(int argc, char *argv[]){
sigset_t set; // 定义信号集
int i = 0;
sigemptyset(&set); // 清空信号集
sigaddset(&set, SIGINT); // 添加SIGINT信号集
while(1){
// set集假如阻塞集,没有移除前,SIGINT被阻塞
sigprocmask(SIG_BLOCK, &set, NULL);
for(i = 0; i<5 ; ++i){
cout<<"SIGINT signal is blocked!"<<endl;
sleep(1);
}
// 从信号阻塞集中移除SIGINT
// 假如SIGINT信号在被阻塞时发生了
// 此时SIGINT信号立马生效,中断进程
sigprocmask(SIG_UNBLOCK, &set, NULL);
for(i = 0; i<5 ; ++i){
cout<<"SIGINT signal is unblocked"<<endl;
sleep(1);
}
}
return 0;
}
zouren@ubuntu:~/IPC/signal$ ./sigset_block_exe
SIGINT signal is blocked!
SIGINT signal is blocked!
^CSIGINT signal is blocked!
SIGINT signal is blocked!
SIGINT signal is blocked!
zouren@ubuntu:~/IPC/signal$
可靠信号(1-32):
不支持排队,多次发送相同的信号,但是进程只能收到一次,可能会丢失
不可靠信号(33-64):
支持排队,发多少次,接收多少次
signal函数只能提供简单的函数安装操作,主要用于不可靠的信息,不支持传递信息
sigaction函数
sigaction()
用来检查和更改信号处理操作,支持可靠,不可靠信号,同时支持信号传递信息
头文件
#include<signal.h>
函数原型
int sigqueue(pid_t pid, int signal, const union sigval value);
给指定进程发送信号
pid
:进程号
signal
:信号编号
value
:传递的信息sigval是一个共用体
union sigval{ int sigval_int; void *sigval_ptr; // 一个指针地址 }
成功返回0,失败返回-1
int sigcation(int signum, const struct sigcation *act, struct sigaction *oldact);
检查或修改指定信号的设置
signum
:信号编号
act
: 要设置的对信号的新的处理方式
oldact
:原来的对信号的处理方式成功返回0,失败返回-1
信号设置结构体
struct sigaction{
/*旧的信号处理函数指针*/
void (*sa_handler)(int signum);
/*新的信号处理函数指针*/
void (*sa_sigaction)(int signum, siginfo_t *info, void *context);
/*信号阻塞集*/
sigset_t sa_mask;
/*信号处理形式*/
int sa_flags;
}
sig_handler和sa_sigaction
:处理方式SIG_DEF,SIG_IGN,自定义
sa_mask
:信号阻塞集
sa_flags
:指定信号处理的行为,值按照位或组合
值 描述 SA_RESTART
使被信号打断的系统调用自动重新发起(已废弃) SA_NOCLDSTOP
使父进程在它的子进程暂停或继续运行时不会收到SIGCHLD信号 SA_NOCLDWAIT
使父进程在它的子进程退出时不会收到 SIGCHLD 信号,这时子进程如果退出也不会成为僵尸进程 SA_NODEFER
使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号。 SA_RESETHAND
信号处理之后重新设置为默认的处理方式 SA_SIGINFO
使用sa_sigaction作为信号处理函数
sa_sigaction信号处理函数:
void sa_sigaction(int signum, siginfo_t *info, void *context)
signum
: 不赘述
siginfo_t
: 记录信号发送进程信息的结构体
context:
可以赋给执行ucontext_t类型的一个对象的指针,以引用在传递信号时被终端的接收进程或线程的上下文
测试代码
一个进程是接收程序,另外一个进程给该进程发送整数数据,默认为100
#include<stdio.h>
#include<signal.h>
#include<sys/types.h>
#include<unistd.h>
/*********************
发送SIGINT信号和携带的值给指定进程
参数 argv[1] 目标进程号
argv[2] 发送的值
返回值 0
*********************/
int main(int argc, char *argv[]){
if(argc >= 2){
pid_t pid, pid_self;
union sigval tmp;
pid = atoi(argv[1]); // 目标进程号
if(argc >= 3){
tmp.sival_int = atoi(argv[2]);
}else{
tmp.sival_int = 100;
}
// 给进程pid发送SIGINT信号,并发送数值
sigqueue(pid, SIGINT, tmp);
pid_self = getpid();
printf("pid = %d, pid_self = %d\n", pid, pid_self);
}
return 0;
}
// 接收程序
#include<signal.h>
#include<stdio.h>
#include<unistd.h>
void signal_handler(int signum, siginfo_t *info, void *ptr){
printf("signum = %d\n", signum);
printf("info->pid = %d\n", info->si_pid);
printf("info->si_sigval = %d\n", info->si_value.sival_int);
}
int main(int argc, char *argv[]){
struct sigaction act, oldact;
act.sa_sigaction = signal_handler;
sigemptyset(&act.sa_mask); // 清空阻塞集
act.sa_flags = SA_SIGINFO; // 指定调用signal_handler
// 注册
sigaction(SIGINT, &act, &oldact);
while(1){
printf("i am waiting for message!\nmy pid is %d\n", getpid());
pause(); // 等待信号
}
return 0;
}
# 接收程序
zouren@ubuntu:~/IPC/signal$ ./recv_sigact_exe
i am waiting for message!
my pid is 29759
signum = 2
info->pid = 29767
info->si_sigval = 3000
i am waiting for message!
my pid is 29759
signum = 2
info->pid = 29768
info->si_sigval = 100
i am waiting for message!
my pid is 29759
^\退出 (核心已转储)
zouren@ubuntu:~/IPC/signal$ ^C
# 发送程序
zouren@ubuntu:~/IPC/signal$ ./send_sigact_exe 29759 3000
pid = 29759, pid_self = 29767
zouren@ubuntu:~/IPC/signal$ ./send_sigact_exe 29759
pid = 29759, pid_self = 29768
Linux无名管道
Linux下IPC通信的古老形式,所有UNIX都支持这种通信机制
特点
- 半双工,数据同一时刻只能在一个方向上流动
- 只能从管道一端写入,一端读出(不能混用)
- 数据遵循先入先出原则
- 所传送的数据是无格式的,需要双方遵循一定的规则
- 管道是由操作系统管理的一小撮内存空间,不存在于文件系统中
- 数据读取是一次性操作,读完之后,数据就从管道中消失了
- 没有名字,只能在具有亲缘关系的进程中传输数据(父进程与子进程、兄弟进程)
使用管道
头文件
#include<unistd.h>
函数原型
int pipe(int filedes[2]);
创建一个无名管道
filedes
:为int型数组的首地址,其存放了管道的文件描述符,filedes[0],filedes[1]
创建两个文件描述符fd[0]和fd[1],
fd[0]
和fd[1]
前者固定用于读取管道中的数据,后者固定用于往管道中写入数据。,一般I/O操作都不可以用来操作管道
返回值
返回值 | 描述 |
---|---|
成功 | 0 |
失败 | -1 |
测试代码:
#include<iostream>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
using namespace std;
int main(int argc, char *argv[]){
int fd_pipe[2] = {0};
pid_t pid;
if(pipe(fd_pipe) < 0){
cerr<<"Pipe create error!"<<endl;
perror("pipe");
}else{
cout<<"Pipe create successfully"<<endl;
}
pid = fork();
if(pid < 0){
perror("fork");
exit(-1);
}else if(pid == 0){
char buf[] = "I am the msg sender";
// 写数据
write(fd_pipe[1], buf, strlen(buf));
}else if(pid > 0){
wait(NULL); // 等待子进程结束,并回收其资源
char str[50] = {0};
// 读数据
read(fd_pipe[0], str, sizeof(str));
printf("str = [%s]\n", str);
}
return 0;
}
zouren@ubuntu:~/IPC/pipe$ ./unname_pipe1_exe
Pipe create successfully
str = [I am the msg sender]
zouren@ubuntu:~/IPC/pipe$
因为管道的模型很类似于生产者与消费者的模型,因为缓冲区是有一定内存空间的,当缓冲区慢了之后,调用write的进程回阻塞到直到缓冲区腾出了空间才继续进行,而当缓冲区中没有数据时,调用read也会阻塞
管道读取也可以设置非阻塞,通过函数
fcnt1(....)
来进行设置
fcntl(fd, F_SETFL, 0); // 阻塞
fcntl(fd, F_SETFL, O_NONBLOCK);
#include<iostream>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<fcntl.h>
using namespace std;
int main(int argc, char *argv[]){
int fd_pipe[2] = {0};
pid_t pid;
if(pipe(fd_pipe) < 0){
cerr<<"Pipe create error!"<<endl;
exit(-1);
}
pid = fork();
if(pid < 0){
cerr<<"Failed to fork son process!"<<endl;
exit(-1);
}
if(pid == 0){
sleep(3);
char buf[] = "hello csu! i love you";
write(fd_pipe[1], buf, strlen(buf)); // 写数据
_exit(0);
}else if(pid > 0){
fcntl(fd_pipe[0], F_SETFL, strlen(buf)); // 写数据
while(1){
char str[50] = {0};
read(fd_pipe[0], str, sizeof(str));
printf("str = [%s]\n", str);
sleep(1);
}
}
return 0;
}
zouren@ubuntu:~/IPC/pipe$ ./unblock_exe
str=[]
str=[]
str=[]
str=[hello, edu]
str=[]
str=[]
^C
zouren@ubuntu:~/IPC/pipe$ ^C
在通信过程中,别的进程先结束后,当前进程都端口关闭后,向管道内写数据时,write所在进程会(收到SIGPIPE)退出,收到SIGPIPE默认动作为中断当前进程
#include<iostream>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
using namespace std;
int main(int argc, char *argv[]){
int fd_pipe[2] = {0};
pid_t pid;
if(pipe(fd_pipe) < 0){
cerr<<"Failed to create Pipe"<<endl;
exit(-1);
}
pid = fork();
if(pid < 0){
cerr<<"Failed to create son process!"<<endl;
exit(-1);
}
if(pid == 0){
_exit(-1);
}else if(pid > 0){
wait(NULL); // 等待子进程结束,回收其资源
close(fd_pipe[0]); // 关闭读端口
char buf[50] = "12345";
// 当前进程读端口关闭,write收到SIGPIPE信号,默认动作:中断当前进程
write(fd_pipe[1], buf, strlen(buf));
while(1);
}
return 0;
}
zouren@ubuntu:~/IPC/pipe$ ./signal_pi_exe
zouren@ubuntu:~/IPC/pipe$