进程间通讯 IPC
InterProcess Communication
1.进程间通信方式
1.早期进程间通信:
无名管道(pipe)、有名管道(fifo)、信号(signal)
2.system V IPC:
共享内存(shared memory)、消息队列(message queue)、信号灯集(semaphore set)
3.BSD:
套接字(socket)
2.无名管道
2.1 特点
- 只能用于具有亲缘关系的进程之间的通信
- 半双工的通信模式,具有固定的读端fd[0]和写端fd[1]。
- 管道可以看成是一种特殊的文件,对于它的读写可以使用文件IO如read、write函数。
- 管道是基于文件描述符的通信方式。当一个管道建立时,它会创建两个文件描述符 fd[0]和fd[1]。其中fd[0]固定用于读管道,而fd[1]固定用于写管道。
2.2 函数接口
int pipe(int fd[2])
功能:创建无名管道
参数:文件描述符 fd[0]:读端 fd[1]:写端
返回值:成功 0
失败 -1
头文件:
#include <unistd.h>
创建无名管道进行读写:
#include <unistd.h>
#include <stdio.h>//无名管道
int main(int argc, char const *argv[])
{char buf[65536] = "";int fd[2] = {0}; //fd[0]表示读端,fd[1]表示写端if (pipe(fd) < 0){perror("pipe err");return -1;}printf("%d %d\n", fd[0], fd[1]);//读写操作write(fd[1], "hello", 5);read(fd[0], buf, 32);//结构类似队列,先进先出//1.当管道中没数据时,读阻塞//read(fd[1], "hello", 5);//printf("%s\n", buf);//但是关闭写端就不一样//如果管道中有数据,关闭写端可以读出数据//无数据,关闭写端读操作会立即返回//write(fd[1], "hello", 32);//close(fd[1]);//read(fd[0], buf, 32);//printtf("%s\n", buf);//2.当管道中写满数据时,写阻塞,管道空间大小为64K//write(fd[1], buf, 65536);//printf("full\n");//write(fd[1], "1", 1);//printf("after\n");//写满一次后,当管道中至少有4K空间时(也就是再读4K)才可以继续写,否则会阻塞//read(fd[0], buf, 4096);//write(fd[1], "6", 1);//printf("after\n");//当读端关闭,向管道中写入数据无意义,会造成管道破裂,//进程会收到内核的SIGPIPE//close(fd[0]);//write(fd[1], "hello", 5);return 0;
}
用gdb调试可以看到管道破裂信号:
2.3 注意事项
- 当管道中无数据,读操作会阻塞
当管道中有数据关闭写端,可以将数据读出。
当管道中无数据关闭写端,读操作会立即返回。
- 管道中写满(管道大小64K)数据写操作会阻塞,一旦用4K空间,写继续
- 只有当管道读端存子时,向管道中写入数据才有意义。否则会导致管道破裂,向管道中写入数据进程会收到来自内核的SIGPIPE信号(通常时Broken pipe错误)
练习:父子进程实现通信,父进程循环从终端输入数据,子进程循环打印数据,当输入quit结束。
提示:不需要加同步机制, 因为pipe无数据时读会阻塞。
考虑:创建管道是在fork之前还是之后? 先pipe再fork
思路:
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>int main(int argc, char const *argv[])
{pid_t pid;char buf[32] = "";int fd[2] = {0};if (pipe(fd) < 0){perror("pipe err");return -1;}if ((pid = fork()) < 0){perror("fork err");return -1;}else if (pid == 0){//循环打印while (1){//读管道内容进bufread(fd[0], buf, 32);//判断quit结束if (strcmp(buf, "quit") == 0)break;//打印buf到终端printf("%s\n", buf);}}else{//循环输入while (1){//scanf输入进bufscanf("%s", buf);//把buf内容写入管道write(fd[1], buf, strlen(buf)+1); //+1是为了写入最后的\0//判断quit结束if (strcmp(buf, "quit") == 0)break;}}wait(NULL);return 0;
}
3.有名管道
3.1 特点
- 有名管道可以使互不相关的两个进程互相通信。
- 有名管道可以通过路径名来指出,并且在文件系统中可见,但内容存放在内存中。但是读写数据不会存在文件中,而是在管道中。
- 进程通过文件IO来操作有名管道
- 有名管道遵循先进先出规则
- 不支持如lseek() 操作
3.2 函数接口
int mkfifo(const char *filename,mode_t mode);
功能:创健有名管道
参数:filename:有名管道文件名
mode:权限
返回值:成功:0
失败:-1,并设置errno号
注意对错误的处理方式:
如果错误是file exist时,注意加判断,如:if(errno == EEXIST)
头文件:
#include <sys/types.h>
#include <sys/stat.h>
注:函数只是在路径下创建管道文件,往管道中写的数据依然写在内核空间。
先创建有名管道,然后用文件IO操作:打开、读写和关闭。
3.3 注意事项
- 只写方式打开阻塞,一直到另一个进程把读打开
- 只读方式打开阻塞,一直到另一个进程把写打开
- 可读可写,如果管道中没有数据,读阻塞
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>int main(int argc, char const *argv[])
{int fd;char buf[32] = "";if (mkfifo("fifo", 0666) < 0){if (errno == EEXIST)printf("file exist!\n");else{perror("mkfifo err");return -1;}}printf("mkfifo success\n");//打开文件fd = open("fifo", O_RDWR);printf("fd:%d\n", fd);//读写操作write(fd, "hello", 5);read(fd, buf, 32);printf("%s\n", buf);return 0;
}
练习:通过两个进程实现cp功能。
./input srcfile
./output destfile
input.c 读源文件
//创建有名管道
//打开管道文件,打开源文件
//循环读源文件,把读到内容写进管道
/* src源文件
练习:通过两个进程实现cp功能。./input srcfile./output destfile
*/
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>int main(int argc, char const *argv[])
{//创建管道文件if (mkfifo("fifo", 0666) < 0){if (errno == EEXIST){printf("file exist!\n");}else{perror("mkfifo err");return -1;}}printf("mkfifo success\n");char buf[32] = "";int n = 0;//打开fifo文件int fd = open("fifo", O_WRONLY);//打开源文件int src = open("t3.c", O_RDONLY);if (src < 0 || fd < 0){perror("open err");return -1;}//循环读源文件写入管道while (1){while (n = read(src, buf, 32)){write(fd, buf, n);}break;}close(fd);close(src);return 0;
}
/* dest目标文件
练习:通过两个进程实现cp功能。./input srcfile./output destfile
*/
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>int main(int argc, char const *argv[])
{char buf[32] = "";int n = 0;//打开fifo文件int fd = open("fifo", O_RDONLY);//打开目标文件int dest = open("dest.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);if (fd < 0 || dest < 0){perror("open err");return -1;}while (1){while (n = read(fd, buf, 32)){write(dest, buf, n);}break;}close(fd);close(dest);return 0;
}
3.4 有名管道和无名管道的区别
无名管道 | 有名管道 | |
使用场景 | 具有亲缘关系的进程间 | 不相干的两个进程也可以 |
特点 | 半双工通信方式 固定读端fd[0]和写端fd[1] 看作一种特殊的文件 通过文件IO操作 | 在文件系统中会存在管道文件,但是数据放在内核空间 通过文件IO进行操作 遵循先进先出,不支持lseek操作 |
函数 | pipe() 直接read、write | mkfifo() 先打开open,再读写read、write |
读写特性 | 当管道中无数据读阻塞 (当关闭写端无数据读立即返回,有数据能读出来) 当管道写满时,写阻塞,直到有4K空间才能继续写 关闭读端写会导致管道破裂 | 只写方式打开会阻塞,一直到另一个进程把读方式打开 只读方式打开会苏俄,一直到另一个进程把写方式打开 可读可写,如果管道中无数据读会阻塞 |
4.信号
kill -l: 显示系统中的信号
kill -num PID:给指定进程发送信号
4.1 概念
1)信号是在软件层次上对中断机制的一种模拟,是一种异步通信方式
2)信号可以直接进行用户空间进程和内核进程之间的交互,内核进程也可以利用它来通知用户空间进程发生了哪些系统事件。
3)如果该进程当前并未处于执行态,则该信号就由内核保存起来,直到该进程恢复执行再传递给它;如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程。
信号的生命周期:
4.2 信号的相应方式
1)忽略信号:对信号不做任何处理,但是有两个信号不能忽略:即SIGKILL及SIGSTOP。
2)捕捉信号:定义信号处理函数,当信号发生时,执行相应的处理函数。
3)执行缺省操作:Linux对每种信号都规定了默认操作
4.3 信号种类
SIGINT(2):中断信号,Ctrl-C 产生,用于中断进程
SIGQUIT(3):退出信号, Ctrl-\ 产生,用于退出进程并生成核心转储文件
SIGKILL(9):终止信号,用于强制终止进程。此信号不能被捕获或忽略。
SIGALRM(14):闹钟信号,当由 alarm() 函数设置的定时器超时时产生。
SIGTERM(15):终止信号,用于请求终止进程。此信号可以被捕获或忽略。termination
SIGCHLD(17):子进程状态改变信号,当子进程停止或终止时产生。
SIGCONT(18):继续执行信号,用于恢复先前停止的进程。
SIGSTOP(19):停止执行信号,用于强制停止进程。此信号不能被捕获或忽略。
SIGTSTP(20):键盘停止信号,通常由用户按下 Ctrl-Z 产生,用于请求停止进程。
4.4 函数接口
4.4.1 信号发送和挂起
#include <signal.h>
int kill(pid_t pid, int sig);
功能:给指定进程发送信号
参数:
pid:指定进程
sig:要发送的信号
返回值:
成功:0
失败:-1
int raise(int sig);
功能:向当前进程发送信号
参数:sig:信号
返回值:
成功:0
失败:-1
相当于:kill(getpid(),sig);
int pause(void);
功能:用于将调用进程挂起,直到收到被捕获处理的信号为止。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>int main(int argc, char const *argv[])
{//kill(getpid(),SIGKILL); //给指定的进程发信号,此例子是给当前进程发信号raise(SIGKILL); //给当前进程发送信号//while(1);pause(); //将进程挂起,作用和死循环类似但是不占用CPUreturn 0;
}
4.4.2 定时器
unsigned int alarm(unsigned int seconds)
功能:在进程中设置一个定时器。当定时器指定的时间到了时,它就向进程发送SIGALARM信号。系统对SIGALARM信号默认处理方式是退出进程。
参数:seconds:定时时间,单位为秒
返回值:如果调用此alarm()前,进程中已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,否则返回0。
注意:一个进程只能有一个闹钟时间。如果在调用alarm时已设置过闹钟时间,则之前的闹钟时间被新值所代替。
常用操作:取消定时器alarm(0),返回旧闹钟余下秒数。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>int main(int argc, char const *argv[])
{printf("%d\n", alarm(10)); //设置到闹钟10秒后响铃sleep(1);printf("%d\n", alarm(3)); //重新设置闹钟3秒后响铃,返回上一个闹钟剩余的秒数,以最后一次定闹钟为准pause(); //闹钟响后进程直接退出,所以挂起肯定也结束了return 0;
}
系统SIGALARM信号的响应默认是结束进程,如果不对闹钟信号进行捕捉那默认情况闹钟响了进程就直接结束了。
4.4.3 信号处理函数signal()
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
功能:信号处理函数
参数:signum:要处理的信号
handler:信号处理方式
SIG_IGN:忽略信号 (忽略 ignore)
SIG_DFL:执行默认操作 (默认 default)
handler:捕捉信号 (handler为函数名,可以自定义)
void handler(int sig){} //函数名可以自定义, 参数为要处理的信号
返回值:成功:设置之前的信号处理方式
失败:-1
补充: typedef 给数据类型重命名
#include <stdio.h>
//给普通数据类型int重命名
typedef int size4;
//给指针类型int* 重命名
typedef int *int_p;
//给数组类型int [10]重命名
typedef int intArr10[10];
//给函数指针void (*)()重命名
typedef void (*fun_p)();
void fun()
{
printf("fun\n");
}
int main(int argc, char const *argv[])
{
size4 a = 10; //相当于int a
int_p p = &a; //相当于int* p
intArr10 arr = {1, 2, 3}; //相当于int arr[10]
fun_p fp = fun; //相当于 void (* fp)();
printf("%d\n", *p);
printf("%d\n", arr[0]);
fp();
return 0;
}
总而言之,定义变量的变量名写在哪里,用typedef给数据类型重命名的新名字就写在哪里。然后使用新名字定义变量的格式直接就可以为:新名字 变量名;
用信号的知识实现司机和售票员问题。
1)售票员捕捉SIGINT(代表开车)信号,向司机发送SIGUSR1信号,司机打印(let's gogogo)
2)售票员捕捉SIGQUIT(代表停车)信号,向司机发送SIGUSR2信号,司机打印(stop the bus)
3)司机捕捉SIGTSTP(代表到达终点站)信号,向售票员发送SIGUSR1信号,售票员打印(please get off the bus)
4)司机等待售票员下车,之后司机再下车。
分析:司机(父进程)、售票员(子进程)
售票: 捕捉:SIGINT SIGQUIT SIGUSR1
忽略:SIGTSTP
司机: 捕捉:SIGUSR1 SIGUSR2 SIGTSTP
忽略:SIGINT SIGQUIT
/*
用信号的知识实现司机和售票员问题。
1)售票员捕捉SIGINT(代表开车)信号,向司机发送SIGUSR1信号,司机打印(let's gogogo)
2)售票员捕捉SIGQUIT(代表停车)信号,向司机发送SIGUSR2信号,司机打印(stop the bus)
3)司机捕捉SIGTSTP(代表到达终点站)信号,向售票员发送SIGUSR1信号,售票员打印(please get off the bus)
4)司机等待售票员下车,之后司机再下车。分析:司机(父进程)、售票员(子进程)
售票: 捕捉:SIGINT SIGQUIT SIGUSR1忽略:SIGTSTP
司机: 捕捉:SIGUSR1 SIGUSR2 SIGTSTP忽略:SIGINT SIGQUIT*/#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>pid_t pid;void saler(int sig)
{if(sig == SIGINT){kill(getppid(), SIGUSR1);}else if (sig == SIGQUIT){kill(getppid(), SIGUSR2);}else if (sig == SIGUSR1){printf("[saler] please get off the bus\n");exit(0);}
}void driver(int sig)
{if (sig == SIGUSR1){printf("[driver] let's gogogo!\n");}else if (sig == SIGUSR2){printf("[driver] stop the bus!\n");}else if (sig == SIGTSTP){kill(pid, SIGUSR1);wait(NULL);exit(0);}
}int main(int argc, char const *argv[])
{pid = fork();if(pid < 0){perror("fork err");return -1;}else if (pid == 0){printf("i am saler\n");signal(SIGINT, saler);signal(SIGQUIT, saler);signal(SIGUSR1, saler);signal(SIGTSTP, SIG_IGN);}else{printf("i am driver\n");signal(SIGUSR1, driver);signal(SIGUSR2, driver);signal(SIGTSTP, driver);signal(SIGINT, SIG_IGN);signal(SIGQUIT, SIG_IGN);}while(1)pause(); //不能只发送一个信号进程就结束,所以可以循环挂起//不占用CPUreturn 0;
}
5.共享内存
5.1 特点
1)共享内存是一种最为高效的进程间通信方式,进程可以直接读写内存,而不需要任何数据的拷贝。
2)为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程
将其映射到自己的私有地址空间。进程就可以直接读写这一内存区而不需要进行数据的拷贝,从而大大提高的效率。
- 由于多个进程共享一段内存,因此也需要依靠某种同步机制,如互斥锁和信号量等
5.2 步骤
- 创建key值: ftok()
- 创建或打开共享内存: shmget()
- 映射共享内存到用户空间: shmat()
- 撤销映射:shmdt()
- 删除共享内存:shmctl()
5.3 函数接口
key_t ftok(const char *pathname, int proj_id);
功能:创建出来的具有唯一映射关系的一个key值,帮助操作系统用来标识一块共享内存
参数:
Pathname:已经存在的可访问文件的名字
Proj_id:一个字符(因为只用低8位)
返回值:成功:key值
失败:-1
// 将文件的索引节点号取出ls -i,前面加上proj_id得到key_t的返回值。如指定文件的索引节点号为65538,换算成16进制为 0x010002取后4位,而你指定的ID值为38,换算成16进制为0x26,则最后的key_t返回值为0x26010002。中间的01是系统编号。
int shmget(key_t key, size_t size, int shmflg);
功能:创建或打开共享内存
参数:
key 键值
size 共享内存的大小
shmflg IPC_CREAT|IPC_EXCL|0777
返回值:成功 shmid
出错 -1
//当IPC_CREAT | IPC_EXCL时, 如果没有该块共享内存,则创建,并返回共享内存ID。若已有该块共享内存,则返回-1。
void *shmat(int shmid,const void *shmaddr,int shmflg); //attaches
功能:映射共享内存,即把指定的共享内存映射到进程的地址空间用于访问
参数:
shmid 共享内存的id号
shmaddr 一般为NULL,表示由系统自动完成映射
如果不为NULL,那么有用户指定
shmflg:SHM_RDONLY就是对该共享内存只进行读操作
0 可读可写
返回值:成功:完成映射后的地址,
出错:-1(地址)
用法:if((p = (char *)shmat(shmid,NULL,0)) == (char *)-1)
int shmdt(const void *shmaddr); //detaches
功能:取消映射
参数:要取消的地址
返回值:成功0
失败的-1
int shmctl(int shmid,int cmd,struct shmid_ds *buf); //control
功能:(删除共享内存),对共享内存进行各种操作
参数:
shmid 共享内存的id号
cmd IPC_STAT 获得shmid属性信息,存放在第三参数
IPC_SET 设置shmid属性信息,要设置的属性放在第三参数
IPC_RMID:删除共享内存,此时第三个参数为NULL即可
buf shmid所指向的共享内存的地址,空间被释放以后地址就赋值为null
返回:成功0
失败-1
用法: shmctl(shmid,IPC_RMID,NULL);
创建key值:
#include <sys/types.h>
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <errno.h>int main(int argc, char const *argv[])
{key_t key;int shmid;char *p;//创建key值key = ftok("shm.c", 'd');if (key < 0){perror("ftok err");return -1;}printf("key: %#x\n", key);//创建或打开共享内存shmid = shmget(key, 128, IPC_CREAT | IPC_EXCL | 0666); //没有则创建共享内存,已有则返回-1if (shmid <= 0){if (errno == EEXIST) //如果已存在则直接打开共享内存shmid = shmget(key, 128, 0666); //直接打开共享内存,返回共享内存idelse{perror("shmget err");return -1;}}printf("shmid: %d\n", shmid);//映射共享内存到用户空间p = (char *)shmat(shmid, NULL, 0);if (p == (char *)-1){perror("shmat err");return -1;}//操作共享内存scanf("%s", p);printf("%s\n", p);//取消内存映射shmdt(p);//删除共享内存shmctl(shmid,IPC_RMID,NULL);return 0;
}
5.4. 命令
ipcs -m: 查看系统中的共享内存
ipcrm -m shmid:删除共享内存
ps: 可能不能直接删除掉还存在进程使用的共享内存。
这时候可以用ps -ef对进程进行查看,kill掉多余的进程后,再使用ipcs查看。
练习:两个进程实现通信,一个进程循环从终端输入,另一个进程循环打印,当输入quit时结束
这两个标志在两个进程里,是不共享的,所以为了共享标志位可以和buf封装到一个结构体里作为共享内存。
创建两个文件,一个文件实现循环输入,另一个文件实现循环输出
循环输入
//练习:两个进程实现通信,一个进程循环从终端输入,另一个进程循环打印,当输入quit时结束#include <sys/types.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <sys/shm.h>
#include <errno.h>
#include <string.h>struct msg
{int flag;char buf[32];
};int main(int argc, char const *argv[])
{//创建key值key_t key = ftok("t3.txt", 'd');if (key < 0){perror("ftok err");return -1;}printf("key : %#x\n", key);//创建或打开共享内存int shmid = shmget(key, sizeof(struct msg), IPC_CREAT | IPC_EXCL | 0666);//没有则创建共享内存,已有则返回-1if (shmid <= 0){if (errno == EEXIST)shmid = shmget(key, sizeof(struct msg), 0666);else{perror("shmget err");return -1;}}printf("shmid: %d\n", shmid);//映射共享内存到用户空间struct msg *me = (struct msg *)shmat(shmid, NULL, 0);if (me == (struct msg *)-1){perror("shmat err");return -1;}me->flag = 0;//操作共享内存while(1){if(0 == me->flag){scanf("%s", me->buf);me->flag = 1;}if (strcmp(me->buf,"quit") == 0){break;}}//取消内存映射shmdt(me);//删除共享内存shmctl(shmid, IPC_RMID, NULL);//return 0;
}
循环输出
//练习:两个进程实现通信,一个进程循环从终端输入,另一个进程循环打印,当输入quit时结束#include <sys/types.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <sys/shm.h>
#include <errno.h>
#include <string.h>struct msg
{int flag;char buf[32];
};int main(int argc, char const *argv[])
{//创建key值key_t key = ftok("t3.txt", 'd');if (key < 0){perror("ftok err");return -1;}printf("key : %#x\n", key);//创建或打开共享内存int shmid = shmget(key, 128, IPC_CREAT | IPC_EXCL | 0666);//没有则创建共享内存,已有则返回-1if (shmid <= 0){if (errno == EEXIST)shmid = shmget(key, 128, 0666);else{perror("shmget err");return -1;}}printf("shmid: %d\n", shmid);//映射共享内存到用户空间struct msg *me = (struct msg *)shmat(shmid, NULL, 0);if (me == (struct msg *)-1){perror("shmat err");return -1;}//操作共享内存while(1){if(1 == me->flag){printf("%s\n", me->buf);if (strcmp(me->buf,"quit") == 0){break;}me->flag = 0;}}//取消内存映射shmdt(me);//删除共享内存shmctl(shmid, IPC_RMID, NULL);//return 0;
}