进程间的通信方式,信号(signal,sigaction)以及管道(有名管道,无名管道)
1.进程间通信方式:
1.1为什么进行通信?
linux操作系统多进程操作系统
数据的传输
资源共享
通知事件
进程控制
1.2进程通信的分类?
信号:软中断模拟机制,类似于通知
管道:可以进行数据传输,具有单向导通性以及阻塞
共享内存:多个进程共享一块数据,可以随时读取以及更改
消息队列:最符合通信思想,单纯收发数据
信号量集:同步保护资源
网络编程:不同设备通信
1.3同步和互斥:
同步:
是指在不同任务之间的若干个程序片段,他们的运行必须严格按照规定的某种顺序来运行,这种顺序依赖于要完成任
务的特点。
场景:
多个进程在运行时需要协同步调,按预定的顺序
执行,比如A任务依赖B任务产生的数据
互斥:
场景:一个公共资源同时只能被一个进程使用
1.4进程间通信:
同一个设备之间:
管道信号共享内存消息队列信号量集
不同设备之间:
网络编程
2.信号
2.1信号的本质?
软中断信号(signal)通知进程发生异步事件
在原理上,进程接收到一个信号和处理器接收到一个
中断请求是一样的
2.2信号的作用:
信号==》使:
用户进程《==》内核进程交互
(内核空间告知用户空间发生的系统事件)
2.3信号的查看:
kill -l
信号列表:
1~31不可靠信号信号可能丢失,为非实时信号
34~64可靠信号支持排队不会丢失实时信号
信号类型:
/usr/include/asm-generic/signal.h
SIGHUP 1 A 终端挂起或者控制进程终止
SIGINT 2 A 键盘中断(如break键被按下)
ctrl +c
SIGQUIT 3 C 键盘的退出键被按下
ctrl +
SIGILL 4 C 非法指令
SIGABRT 6 C 由abort(3)发出的退出指令
SIGFPE 8 C 浮点异常
SIGKILL 9 AEF Kill信号
kill -9
SIGSEGV 11 C 无效的内存引用
SIGPIPE 13 A 管道破裂: 写一个没有读端口的管道
SIGALRM 14 A 由alarm(2)发出的信号
SIGTERM 15 A 终止信号
SIGUSR1 10 A 用户自定义信号1
SIGUSR2 12 A 用户自定义信号2
SIGCHLD 20,17,18 B 子进程结束信号
SIGCONT 19,18,25 进程继续(曾被停止的进程)
SIGSTOP 17,19,23 DEF 终止进程
SIGTSTP 18,20,24 D 控制终端(tty)上按下停止键
SIGTTIN 21,21,26 D 后台进程企图从控制终端读
SIGTTOU 22,22,27 D 后台进程企图从控制终端写
字母说明:
A 缺省的动作是终止进程
B 缺省的动作是忽略此信号,将该信号丢弃,不做处理
C 缺省的动作是终止进程并进行内核映像转储(dump core),内核映像转储是指将进程数据在内存的映像和进程在内核结构中的部分内容以一定格式转储到文件系统,并且进程退出执行,这样做的好处是为程序员提供了方便,使得他们可以得到进程当时执行时的数据值,允许他们确定转储的原因,并且可以调试他们的程序。
D 缺省的动作是停止进程,进入停止状况以后还能重新进行下去,一般是在调试的过程中(例如ptrace系统调用)
E 信号不能被捕获
F 信号不能被忽略
产生信号的时机:
可以在任何时候发送给某一进程,无需考虑进程状态
未执行==》信号由内核保存==》执行
阻塞==》信号传递延时==》阻塞取消
周期:
信号产生
信号在进程中注册
信号在进程中注销
执行信号处理函数
2.3.收发信号:
信号发送:
kill
功能
向指定的进程发指定的信号
原型
int kill(pid_t pid, int sig);
所属头文件
#include <sys/types.h>
#include <signal.h>
参数
Pid :指定的进程
pid>0 将信号发送给进程ID为pid的进程
pid0将信号发送给同组进程
pid<0 将信号发送给其进程组ID等于pid绝对值的进程
pid-1将信号发送给所有进程
Sig:信号
返回值
成功返回0 失败返回-1
案例演示:
主函数:main.c
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
printf("pid = %d\n",getpid());
while(1);
}
发送信号函数:kill.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>
int main(int argc,char *argv[])
{
pid_t pid = atoi(argv[1]);
int sig = atoi(argv[2]);
kill(pid,sig);
perror("kill");
return 0;
}
运行结果:
主函数运行进入死循环等待信号,发送信号函数发送信号:
说明:
绝大多数信号用来结束进程,此外还有忽略和捕捉。
raise
功能
向进程本身发送信号
原型
int raise(int sig);
所属头文件
#include <sys/types.h>
#include <signal.h>
参数
Sig:信号类型
返回值
成功返回0 失败返回-1
只能给自己发信号,没有别的区别:
演示代码:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
//void fun(int n)
//{
// printf("捕捉到的%d信号\n",n);
//}
int main()
{
int i=0;
// signal(2,fun);//捕捉信号
while(1)
{
i++;
sleep(1);
if(i==5)
{
raise(2);//进程自己给自己发信号,结束进程
}
}
return 0;
}
运行结果:自己给自己发信号结束进程(也可以对信号进行捕捉)
5s后自动结束。
闹钟函数alarm
功能
设置时间值,到达时间产生SIGALRM信号,不捕捉的时候默认终止进程(给进程本身发送)
原型
unsigned int alarm(unsigned int seconds);
所属头文件
#include<unistd.h>
参数
seconds:经过指定的时间后产生信号
返回值
如果调用此alarm()前,进程已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,否则返回0。
作用:每间隔多久之后发送一个信号:
演示代码:间隔2s后发送指定信号:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void fun(int n)
{
printf("捕捉到的%d信号\n",n);
}
int main()
{
int i=0;
signal(14,fun);//捕捉信号14
alarm(2);//14信号在2s之后产生
while(1)
{
i++;
printf("i=%d\n",i);
sleep(1);
}
return 0;
}
运行结果:
如果:
//signal(14,fun);//捕捉信号14
注释掉,没有设置要发送的信号,则结束进程
如:
pause
功能
使调用进程挂起直至捕捉到一个信号
使终端挂起–任意信号都可以打断阻塞
原型
int pause(void);
所属头文件
#include<unistd.h>
参数
无
返回值
-1
作用:等待一个信号
演示代码:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void fun(int n)
{
printf("捕捉到的%d信号\n",n);
}
int main()
{
int i=0;
signal(14,fun);//捕捉信号14
while(1)
{
i++;
printf("i=%d\n",i);
alarm(1);//14信号在1s之后产生
pause();//挂起,等待一个信号//不然一直while循环
}
return 0;
}
运行结果:
2.4.信号的处理:
用户对信号相应方式:
默认处理SIG_DFL
绝大数信号的系统默认动作是终止该进程
忽略SIG_IGN
大多数信号按照这种方式处理,但SIGKILL,SIGSTOP除外,原因是它们向超级用户提供了一种终止或停止进程的方法
捕捉信号
定义信号处理函数,当信号发生时,执行相应的处理函数
注意:
前两种处理方式通过定义宏即可完成,第三种捕捉信号,需要定义处理函数。
函数:signal
功能
设置信号处理方式
原型
sighandler_t signal(int signum, sighandler_t handler);
所属头文件
<signal.h>
参数
signum:要操作的信号
handler:对应信号处理方式
返回值
成功返回处理函数指针(形参),失败返回SIG_ERR
演示代码:信号的忽略,默认,捕捉
main.c:提供进程号,等待接收信号
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
void signal_hadle(int num)
{
printf("%s %d\n",__FUNCTION__,num);
}
int main()
{
printf("pid = %d\n",getpid());
//signal(2,SIG_IGN);//信号2作为忽略信号
//signal(2,SIG_DFL);//信号2作为默认处理(杀死)信号
signal(2,signal_hadle);//信号2作为捕捉信号
while(1);
}
kill.c:发送信号
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>
int main(int argc,char *argv[])
{
pid_t pid = atoi(argv[1]);
int sig = atoi(argv[2]);
kill(pid,sig);
perror("kill");
return 0;
}
运行结果:
注意:
此时结束进程ctrl + c 不能用,因为:ctrl + c产生的就是信号2.
可采用ctrl + ;ctrl + z;或者信号9等杀死进程。
演示代码:使ctrl+c和ctrl+\的终端信号失灵
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
void fun(int num)
{
printf("num = %d\n",num);
}
int main()
{
signal(2,fun);//ctrl+c
signal(3,fun);//ctrl+\
printf("pid=%d\n",getpid());
usleep(1);//需要一定的缓冲时间,才能打印出数据
while(1)
{
};
return 0;
}
运行结果:
案例演示:
先终端输入ctrl+c,在3s内输入ctrl+\:观查先后顺序:
演示代码:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
void fun(int num)
{
printf("ctrl+c---\n");
sleep(3);
printf("ctrl+c---\n");
}
void fun1(int num)
{
printf("ctrl+\\---\n");
sleep(5);
printf("ctrl+\\---\n");// \转义字符
}
int main()
{
signal(2,fun);//ctrl+c
signal(3,fun1);//ctrl+\
//printf("pid=%d\n",getpid());
//usleep(1);//需要一定的缓冲时间,才能打印出数据
while(1)
{
};
return 0;
}
运行结果:
解答:当执行某种信号,当遇到新来信号,转向执行新信号,执行完之后,再回来执行
注意:这也是不可靠信号的特点
函数:sigaction:
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
参数
signum:为需要捕捉的信号
act:是一个结构体,里面包含信号处理函数地址、处理方式等信息
oldact:是一个传出参数,sigaction 函数调用成功后,oldact 里面包含以前对 signum 的处理方式的信息,
通常为 NULL
如果函数调用成功,将返回 0,否则返回-1
比signal多出的功能处理信号的时候不允许被打断
结构体 struct sigaction(注意名称与函数 sigaction 相同)的原型为:
struct sigaction {
void (*sa_handler)(int); //老类型的信号处理函数指针
void (*sa_sigaction)(int, siginfo_t *, void *);//新类型的信号处理函数指针
sigset_t sa_mask; //将要被阻塞的信号集合
int sa_flags; //信号处理方式掩码 ( SA_SIGINFO )
void (*sa_restorer)(void); //保留,不要使用
};
该结构体的各字段含义及使用方式:
1、字段 sa_handler 是一个函数指针,用于指向原型为 void handler(int)的信号处理函数地址,即老类型的信号处理函数(如果用这个再将sa_flags = 0,就等同于 signal()函数)
2、字段 sa_sigaction 也是一个函数指针,用于指向原型为:
void handler(int iSignNum, siginfo_t *pSignInfo, void *pReserved);的信号处理函数,即新类型的信号处理函数
该函数的三个参数含义为:
iSignNum:传入的信号
pSignInfo:与该信号相关的一些信息,它是个结构体
pReserved:保留,现没用,通常为 NULL
3、字段 sa_handler 和 sa_sigaction 只应该有一个生效,如果想采用老的信号处理机制,就应该让 sa_handler
指向正确的信号处理函数,并且让字段 sa_flags 为 0;否则应该让 sa_sigaction 指向正确的信号处理函数,并且让
字段 sa_flags 包含 SA_SIGINFO 选项
4、字段 sa_mask 是一个包含信号集合的结构体,该结构体内的信号表示在进行信号处理时,将要被阻塞的
信号。针对 sigset_t 结构体,有一组专门的函数对它进行处理,它们是:
#include <signal.h>
int sigemptyset(sigset_t * set); //清空信号集合 set
int sigfillset(sigset_t * set); //将所有信号填充进 set 中
int sigaddset(sigset_t * set, int signum); //往 set 中添加信号 signum
int sigdelset(sigset_t * set, int signum); //从 set 中移除信号 signum
int sigismember(const sigset_t * set, int signum); //判断 signnum 是不是包含在 set 中
int sigpending(sigset_t * set); //将被阻塞的信号集合由参数 set 指针返回
例如:如果打算在处理信号SIGINT 时,只阻塞对 SIGQUIT 信号的处理,可以用如下方法:
struct sigaction act;
act.sa_flags = SA_SIGINFO;
act.sa_sigaction = newHandler;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, SIGQUIT);
sigaction(SIGINT, &act, NULL);
6、 字段 sa_flags 是一组掩码的合成值,指示信号处理时所应该采取的一些行为,各掩码的含义如表 5.1 所 示:
演示代码:
#include <stdio.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
void fun_2(int num,siginfo_t *info,void *res)
{
printf("捕捉到信号2~~~~\n");
sleep(3);
printf("捕捉到信号2 函数结束~~~~\n");
}
void fun_3(int num)
{
printf("捕捉到信号3~~~~\n");
sleep(5);
printf("捕捉到信号3 函数结束~~~~\n");
}
int main()
{
struct sigaction act;
act.sa_sigaction = fun_2; //填充 新型的信号处理函数
act.sa_flags = SA_SIGINFO; //sa_flags 0--函数入口是sa_handle
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask,3);
sigaction(SIGINT,&act,NULL);
signal(3,fun_3);
while(1);
return 0;
}
运行代码后,在ctrl+c的3秒内给ctrl+\信号,发现不能够打断正在执行的函数,按照顺序执行
运行结果:
注意:这是可靠信号的特点。
2.5.无名管道:
管道的创建:
函数pipe:
功能
创建无名管道
原型
int pipe(int filedes[2]);
所属头文件
#include <unistd.h>
参数
filedes:接收打开管道文件的文件描述符
filedes[0]:存放管道文件的读端
filedes[1]:存放管道文件的写端
返回值
成功返回0,失败返回-1。
管道属性:
无实际介质文件存在(消息存放在缓冲区,大小64k)
只用于亲缘关系的进程(先建管道,后建进程) 数据读出后就不存在管道
filedes[0]–读端 filedes[1]–写端
单向传输:
直接对得到的文件描述符进行读写操作
阻塞
管道两端同时打开,向满管道写数据阻塞
管道两端同时打开,读取空管道,阻塞
当管道只有一端开着:
1.写端关闭,管道中的数据读取完之后,再次读取,读
到0字节
2.读端关闭,写端产生SIGPIPE信号,write返回-1
半双工的通信模式,固定的读端和写端。
可以看成一种特殊的文件,使用普通的 read、write
写进程从写端写入数据,读进程从读端读数据
管道实例:
demo1.c:代码实现查看无名管道大小:64k
//查看无名管道的大小:64k
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
int fd[2]={
0};
pid_t pid;
char ch;
int i=0;
pipe(fd);
pid=fork();
if(pid>0)
{
close(fd[1]);
wait(NULL);
printf("fujincheng\n");
read(fd[0],&ch,1);
}
else
{
close(fd[0]);
while(1)
{
write(fd[1],"1",1);
i++;
printf("i=%d\n",i);
}
}
return 0;
}
运行结果:65536/1024=64k
案例演示2:利用无名管道实现父进程和子进程通信(单向)
//无名管道练习:实现父子进程通信(单向)
#include<stdio.h>
#include<unistd.h>
int main()
{
int fd[2]; // 两个文件描述符
pid_t pid;
char buff[20];
if(pipe(fd) < 0) // 创建管道
printf("Create Pipe Error!\n");
if((pid = fork()) < 0) // 创建子进程
printf("Fork Error!\n");
else if(pid > 0) // 父进程
{
close(fd[0]); // 关闭读端
write(fd[1], "hello world\n", 12);
}
else
{
close(fd[1]); // 关闭写端
read(fd[0], buff, 20);
printf("%s", buff);
}
return 0;
}
运行结果:
注意事项:
必须在系统调用fork()之前调用pipe(),否则子进程将不会继承文件描述符
只有在管道的读端存在时向管道中写入数据才有意义
向管道中写入数据时,管道缓冲区一有空闲区域,写进程就会试图向管道写入数据。如果读进程不读取管道缓冲区中的数据,那么写操作将会一直阻塞。
父子进程在运行时,它们的先后次序并不能保证,因此,在这里为了保证父进程已经关闭了读描述符,可在子进程中调用 sleep 函数。
2.6.有名管道:
FIFO文件,任意两个进程间通信:
1>适用于任意进程之间
2>存在介质文件FIFO unlink
3>两端同时打开,才可以进行读写操作
FIFO文件和普通文件:
读取FIFO文件的进程只能以RDONLY方式打开FIFO文件
写FIFO文件的进程只能以WDONLY的方式打开FIFO文件
FIFO文件里面的内容被读取后就消失了,但普通文件的内容还会存在
有名管道的创建:
函数mkfifo:
功能
创建有名管道
原型
int mkfifo(const char * pathname,mode_t mode)
所属头文件
#include <sys/stat.h>
#include<sys/types.h>
参数
pathname:要创建的FIFO文件的名字(带路径)
mode:创建的FIFO文件的权限
返回值
成功返回0,失败返回-1。
有名管道的删除:
函数unlink:
功能
删除文件
原型
int unlink(const char * pathname)
所属头文件
#include <unistd.h>
参数
pathname:要删除的FIFO文件的名字(带路径)
返回值
成功返回0,失败返回-1。
通信实例:
写进程:
创建FIFO文件
向FIFO文件写数据(write函数)
退出
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
//int mkfifo(const char *pathname, mode_t mode);
int main()
{
char *str="vnfvjdknvdrij";
int fd=open("./file",O_WRONLY);//仅只写这个管道文件
printf("写入打开成功\n");
write(fd,str,strlen(str));
close(fd);
return 0;
}
读进程
读取FIFO文件的数据
显示数据
退出
删除FIFO
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
//int mkfifo(const char *pathname, mode_t mode);
int main()
{
char buf[30]={
0};
if((mkfifo("./file",0600)==-1)&&errno!=EEXIST)//同时满足错误吗:文件已存在
{
printf("创建失败\n");
perror("why");
}
int fd=open("./file",O_RDONLY);//仅只读这个管道文件
printf("打开成功\n");
int nread=read(fd,buf,30);//一次读20个字节...返回字节数
printf("read %d byte from fifo\ncontext:%s\n",nread,buf);
close(fd);
return 0;
}
案例演示:
实现两个进程之间相互发送信号:实现两个进程之间相互通信(双向)
进程1:
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
int main()
{
char buf[30]={
0};
char *str="vnfvjdknvdrij";
int fd=open("./file",O_WRONLY);
int fd1=open("./file1",O_RDONLY);
printf("打开成功\n");
write(fd,str,strlen(str));
int nread=read(fd1,buf,30);//一次读20个字节...返回字节数
printf("read %d byte from fifo\ncontext:%s\n",nread,buf);
close(fd);
close(fd1);
return 0;
}
进程2:
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
int main()
{
char buf[30]={
0};
char *str="vnfvjdknvdrij";
if((mkfifo("./file",0600)==-1)&&errno!=EEXIST)
{
printf("创建失败\n");
perror("why");
}
if((mkfifo("./file1",0600)==-1)&&errno!=EEXIST)
{
printf("创建失败\n");
perror("why");
}
int fd=open("./file",O_RDONLY);
int fd1=open("./file1",O_WRONLY);
printf("打开成功\n");
int nread=read(fd,buf,30);//一次读20个字节...返回字节数
write(fd1,str,strlen(str));
printf("read %d byte from fifo\ncontext:%s\n",nread,buf);
close(fd);
close(fd1);
return 0;
}