进程与线程(三)
- 进程间通信
- 传统间的进程间通信机制
- 无名管道
- 无名管道的特征
- 无名管道的创建
- 父子进程通信
- 测试管道的大小
- 管道读写易出现的问题
- 有名管道
- 创建有名管道
- 有名管道的写端代码
- 有名管道的读端代码
- 信号
- 信号的特征
- 产生信号
- 硬件来源
- 软件来源
- 发送信号的函数:kill() raise()
- 定时器信号
- 响应信号
- 信号安装:
- 忽略信号
- 执行缺省操作
- 捕捉信号
进程间通信
常用的进程间通信方式
1、传统的进程间通信方式
无名管道(pipe)、有名管道(fifo)和信号(signal)
2、System V IPC对象
共享内存(share memory)、消息队列(message queue)和信号灯(semaphore)
3、BSD
套接字(socket)
传统间的进程间通信机制
无名管道
无名管道的特征
这里所说的管道主要指无名管道,它具有如下特点:
1、只能用于具有亲缘关系的进程之间的通信
2、半双工的通信模式,具有固定的读端和写端
3、管道可以看成是一种特殊的文件,对于它的读写可以使用文件IO如read、write函数。
无名管道的创建
父子进程通信
(1)创建无名管道
(2)创建子进程
(3)完成父子通信业务
通信流程:子进程发消息给父进程
思路:fd[0]读端 fd[1]写端 ----》固定的
子进程:发消息(fd[1]有效) fd[0]关闭掉
父进程:收消息(fd[0]有效) fd[1]关闭掉
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>
int main(int argc, const char *argv[])
{
//1,创建无名管道
//定义数组,存储读写的文件描述符
int fd[2];
if(pipe(fd) < 0)
{
perror("pipe error");
return -1;
}
printf("pipe ok!\n");
//2,创建子进程
pid_t pid = fork();
if(pid < 0)
{
perror("fork error");
close(fd[0]);
close(fd[1]);
return -1;
}
else if(0 == pid)
{
//子进程的地址空间
//关闭读端
close(fd[0]);
//发消息(write)
//定义消息文本
char Message[100] = {0};
printf("请输入需要发送的消息:");
fgets(Message, sizeof(Message), stdin);
//使用write写入到管道中
write(fd[1], Message, strlen(Message));
printf("send ok!\n");
}
else
{
//父进程的地址空间
//关闭写端
close(fd[1]);
//收消息(read)
//定义接收消息的地址空间
char Message[100] = {0};
read(fd[0], Message, sizeof(Message));
printf("来自于子进程:%s\n",Message);
}
return 0;
}
测试管道的大小
思路:往管道中不管写入,直到写不进去,此时写入的字节数就是管道容量大小
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>
int main()
{
//测试管道大小
//创建管道
int fd[2];
if(pipe(fd) < 0)
{
return -1;
}
printf("create pipe ok!\n");
//写入数据,不断写入
//1KB == 1024byte
char buf[1024] = {0};
int size = 0;
while(1)
{
int wr_count = write(fd[1], buf, 1024);//每次写入1024个字节 即:1KB大小
if(wr_count < 0)
{
perror("write error");
break;
}
else if(0 == wr_count)
{
printf("nothing wai be written...\n");
break;
}
else
{
size++;
printf("%dKB\n",size);
}
}
close(fd[0]);
close(fd[1]);
return 0;
}
管道读写易出现的问题
当管道中无数据时,读操作会阻塞
向管道中写入数据时,linux将不保证写入的原子性,管道缓冲区一有空闲区域,写进程就会试图向管道写入数据。如果读进程不读走管道缓冲区中的数据,那么写操作将会一直阻塞。
只有在管道的读端存在时,向管道中写入数据才有意义。否则,向管道中写入数据的进程将收到内核传来的SIGPIPE信号(通常Broken pipe错误)。
案例:验证管道断裂
#include <stdio.h>
#include <sys/types.h>
#inckude <unistd.h>
#include <string.h>
int main()
{
int fd[2];
if(pipe(fd) < 0)
{
return -1;
}
printf("pipe ok!\n");
//关闭读端 (保证写入管道时,读端不存在---》才会出现管道断裂)
close(fd[0]);
pid_t pid = fork();
if(pid < 0)
{
return -1;
}
else if(0 == pid)
{
//子进程
//写入数据到管道
char data[1024];
printf("Please input:");
fgets(data, sizeof(data), stdin);
write(fd[1], data, strlen(data));
printf("写入管道成功!\n");
exit(100);
}
else
{
//父进程
//使用wait阻塞等待子进程退出时回收其资源
int wstatus;//保存子进程的退出状态值
wait(&wstatus);
//当执行该40行时,说明子进程已经退出(收到信号异常退出还是正常执行exit(100)退
出?)
//分类讨论
if(WIFSIGNALED(wstatus))
{
//收到信号导致子进程退出
printf("导致子进程退出的信号
是:%d\n",WTERMSIG(wstatus));//SIGPIPE==13号
}
else if(WIFEXITED(wstatus))
{
//正常退出---》执行了exit(100)
printf("子进程的退出状态数值为:%d\n",WEXITSTATUS(wstatus));
//父进程
char buf[1024] = {0};
read(fd[0], buf, sizeof(buf));
printf("读取的结果是:%s\n",buf);
}
}
return 0;
}
有名管道
无名管道只能用于具有亲缘关系的进程之间,这就限制了无名管道的使用范围
1、有名管道可以使互不相关的两个进程互相通信。
2、有名管道可以通过路径名来指出,并且在文件系统中可见
3、进程通过文件IO来操作有名管道
4、有名管道遵循先进先出规则,不支持如lseek() 操作
创建有名管道
有名管道的写端代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
int main(int argc, const char *argv[])
{
//创建有名管道(同时也创建好了管道文件)
if(mkfifo("./fifo.txt", 0664) < 0 && errno != EEXIST)
{
perror("mkfifo error");
return -1;
}
printf("mkfifo ok!\n");
//以只写方式打开创建好的管道文件
int fw = open("./fifo.txt", O_WRONLY);
if(fw < 0){
perror("open_write error");
return -1;
}
printf("open_write ok!\n");
char buf[1024] = {0};
while(1)
{
//对于buf做清空
bzero(buf, sizeof(buf));
printf("客户端:");
fgets(buf, sizeof(buf), stdin);
//判断客户端是否办理完毕业务
if(0 == strncasecmp("quit", buf, 4))
{
printf("业务办理结束!\n");
break;
}
//发送消息
int wr_count = write(fw, buf, strlen(buf));
if(wr_count < 0){
perror("write error");
break;
}
else if(0 == wr_count)
{
printf("未写入任何内容...\n");
break;
}
else
{
printf("实际发送%d字节内容!\n",wr_count);
printf("send ok!\n");
}
}
//关闭写端
close(fw);
return 0;
}
有名管道的读端代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
int main(int argc, const char *argv[])
{
//创建有名管道(同时也创建好了管道文件)
if(mkfifo("./fifo.txt", 0664) < 0 && errno != EEXIST)
{
perror("mkfifo error");
return -1;
}
printf("mkfifo ok!\n");
//以只读方式打开创建好的管道文件
int fr = open("./fifo.txt", O_RDONLY);
if(fr < 0){
perror("open_read error");
return -1;
}
printf("open_read ok!\n");
char buf[1024] = {0};
while(1)
{
//对于buf做清空
bzero(buf, sizeof(buf));
//接收消息
int rd_count = read(fr, buf, sizeof(buf));
if(rd_count < 0)
{
perror("read error");
break;
}
else if(0 == rd_count)
{
printf("客户端业务处理结束!\n");
break;
}
else
{
printf("成功接收%d个字节!\n",rd_count);
printf("来自于客户端:%s\n",buf);
}
}
close(fr);
return 0;
}
信号
信号的特征
信号是在软件层次上对中断机制的一种模拟,是一种异步通信方式
信号可以直接进行用户空间进程和内核进程之间的交互,内核进程也可以利用它来通知用户空间进程发生了哪些系统事件。
如果该进程当前并未处于执行态,则该信号就由内核保存起来,直到该进程恢复执行再传递给它;如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程
同步:发送方发送数据,等待接收方响应之后才发下一个数据包的通讯方式
异步:发送方发送数据,不等待接收方发回响应,接着发送下一个数据包的通讯方式
产生信号
硬件来源
从键盘输入:
ctrl c : SIGINT信号 ,默认处理方式为进程终止
ctrl : SIGQUIT信号,作用与SIGINT类似,默认处理方式为进程终止
ctrl z: SIGTSTP信号,默认处理方式为进程暂停
SIGSTOP信号:只能通过kill产生,默认处理方式进程暂停
软件来源
发送信号的函数:kill() raise()
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
功能:不仅可以终止进程,也可以向进程发送其他信号
#include <signal.h>
int raise(int sig);
功能:只允许进程向自身发送信号
案例:首先使用fork创建子进程,在子进程中使用raise()向自身发送SIGSTOP信号,使子进程暂停;接下来在父进程中当时间戳超过10秒之后,调用kill()向子进程发送SIGKILL信号杀死子进程并回收子进程的退出
资源,且检查导致子进程退出是否因为收到了信号,如果是,打印出该信号是几号?如果子进程是正常退出,则打印出子进程退出的状态值是多少?(模拟正常退出:例如在终端给子进程发送SIGCONT信号来唤醒子进程,此时子进程继续执行,直到执行了exit()退出)
#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <sys/wait.h>
int main(int argc, const char *argv[])
{
//功能:练习kill()和raise()
pid_t pid = fork();
if(0 == pid)
{
//子进程
printf("child PID = %d\tparent PID = %d\n", \
getpid(), getppid());
//使用raise()函数给子进程自己发送SIGSTOP,让其暂停
raise(SIGSTOP);
printf("I am wakeup...\n");
printf("hahahah\n");
exit(88);
}
else
{
//父进程
//定义保存时间的变量
time_t start_time, current_time;
time(&start_time);
int wstatus;
//回收子进程退出资源
while(1)
{
//获取当前的系统时间(更新的)
time(¤t_time);
if(current_time - start_time > 10)
{
//使用kill()函数给子进程发送SIGKILL信号,让其终止
kill(pid, SIGKILL);
}
int exitID = waitpid(-1, &wstatus, WNOHANG);
if(exitID < 0)
{
perror("waitpid error");
break;
}
else if(0 == exitID)
{
printf("子进程还未结束...\n");
}
else
{
//已成功回收子进程
printf("已成功回收子进程的退出资源...\n");
if(WIFSIGNALED(wstatus))
{
printf("异常退出,造成子进程退出的信号是:%d号信号!\n",WTERMSIG(wstatus));
}
else if(WIFEXITED(wstatus))
{
printf("子进程正常退出,退出状态数值为:%d\n",WEXITSTATUS(wstatus));
}
break;
}
//每隔1秒 轮询一次
sleep(1);
}
}
return 0;
}
定时器信号
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
参数:秒数
当定时器指定的时间到了时,它就向进程发送SIGALARM信号,信号的默认操作是结束进程.
注意:每个进程只能有一个闹钟时间。如果在调用alarm时,以前已为该进程设置过闹钟时间,而且它还没有超时,则该闹钟时间的剩余时间值作为本次alarm函数调用的值返回,以前登记的闹钟时间则被新值代换。如果有以前登记的尚未超过的闹钟时间,而新设的闹钟时间值为0,则取消以前的闹钟时间,其剩余时间值仍作为函数的返回值。
扩充:将调用进程挂起函数:
#include <unistd.h>
int pause(void);
功能:会造成进程主动挂起(处于阻塞状态,并主动放弃CPU),并且等待信号将其唤醒
案例:使用alarm()和pause()函数
#include <stdio.h>
#include <unistd.h>
int main(int argc, const char *argv[])
{
printf("hello!\n");
//设定闹钟 5秒
alarm(5);
printf("I will.....\n");
//进程挂起
pause();
printf("java!\n");
printf("exit!\n");
return 0;
}
响应信号
信号安装:
signal函数原型:
#include <signal.h>
void (*signal(int signum, void (*handler)(int)))(int);
优化原型:看的更清楚。
//给函数指针类型取别名
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
功能:进行信号安装
参数:
参数1:需要安装的信号名称
参数2:对于该信号的处理函数(该函数是用户自定义的,且返回值类型void ,形参是一个
int的函数的地址)
PS:参数2的位置可以传入:忽略(SIG_IGN) 执行缺省操作(SIG_DFL),信号处理函数的地址
返回值:成功安装完毕的信号处理函数的地址。
信号的处理方式可以有:
1、忽略信号
2、执行缺省操作(默认操作)
3、捕捉信号(自定义信号处理函数)
忽略信号
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
int main(int argc, const char *argv[])
{
//安装信号
signal(SIGINT, SIG_IGN);//忽略信号
//执行一段代码
while(1)
{
printf("hello!\n");
sleep(1);
}
return 0;
}
执行缺省操作
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
int main(int argc, const char *argv[])
{
//安装信号
//signal(SIGINT, SIG_IGN);//忽略信号
signal(SIGINT, SIG_DFL);//缺省操作(信号默认处理方式)
//执行一段代码
while(1)
{
printf("hello!\n");
sleep(1);
}
return 0;
}
捕捉信号
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void handler_func(int signum)
{
printf("不处理哦!\n");
}
int main(int argc, const char *argv[])
{
//安装信号
//signal(SIGINT, SIG_IGN);//忽略信号
//signal(SIGINT, SIG_DFL);//缺省操作(信号默认处理方式)
signal(SIGINT, &handler_func);
//执行一段代码
vwhile(1)
{
printf("hello!\n");
sleep(1);
}
return 0;
}
注意:一个信号处理函数响应多个信号
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void handler_func(int signum)
{
//按照信号的类别进行控制
switch(signum)
{
case SIGINT:
{
printf("paly music...\n");
break;
}
case SIGTSTP:
{
printf("paly vedio...\n");
break;
}
case SIGQUIT:
{
printf("quit...\n");
break;
}
default:
{
printf("信号无法响应...\n");
}
}
return;
}
int main(int argc, const char *argv[])
{
//安装信号
//signal(SIGINT, SIG_IGN);//忽略信号
//signal(SIGINT, SIG_DFL);//缺省操作(信号默认处理方式)
//signal(SIGINT, &handler_func);//捕捉信号--》自定义信号处理函数
//安装3个信号
signal(SIGINT, &handler_func);//播放音乐
signal(SIGTSTP, &handler_func);//播放视频
signal(SIGQUIT, &handler_func);//退出app
//执行一段代码
while(1)
{
printf("hello!\n");
sleep(1);
}
return 0;
}