Linux中常用的通信方式有:
- 信号(signal):信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
- 无名管道(pipe):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常指父子进程的关系。
- 命名管道FIFO:命名管道也是半双工的通信方式,但是它允许无亲缘关系的进程间的通信。
- 消息队列(message queue):消息队列是由消息的链表存放在内核中,并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限的问题。
- 共享内存:就是映射一段能被其他进程所访问的内存,这段内存由一个进程创建,但是多个进程都能访问。是最快的IPC方式。往往与其他通信机制进行配合使用(如信号量),来实现进程间的同步和通信。
- 信号量:是一个计数器,控制进程对共享资源的访问,主要作为进程间的同步手段。
- 套接字(socket):可以用于不同机器间的进程间通信。
信号
信号可以在任何时候发送给某一进程,而无需知道进程的状态。如果进程并未处于执行状态,则由内核保存起来,待进程恢复执行并传递给它为止。
如果要使用linux提供的信号,通常需要包含<signal.h>头文件
使用系统命令 kill -l 可以查看系统提供的所有信号类型
操作
信号产生后用户进程有如下三种操作:
- 执行默认操作。
- 捕捉信号。自定义信号处理函数,信号产生时执行相应的处理函数。
- 忽略信号。不对信号做任何处理。
其中SIGKILL和SIGSTOP是无法捕捉和忽略的。
常见的信号及默认操作
信号名 | 含义 | 默认操作 |
---|---|---|
SIGINT | 用户按下Ctrl+C时系统会向终端相应的进程发送该信号 | 终止 |
SIGQUIT | 用户按下Ctrl+\时系统会向终端相应的进程发送该信号 | 终止 |
SIGILL | 当一个进程企图执行非法指令时发出(比如堆栈溢出) | 终止 |
SIGFPE | 在发生致命的算术错误运算时发出 | 终止 |
SIGALRM | 在定时器计时完成时发出,定时器可由进程调用alarm函数来设置 | |
SIGTSTP | 用户按下Ctrl+Z时系统会向终端相应的进程发送该信号 | |
SIGCHLD | 在子进程结束时向父进程发出。只有在父进程运行wait函数时才会被捕捉。 |
函数
函数名 | kill |
---|---|
头文件 | #include<sys/types.h> #include<signal.h> |
功能 | 发送信号给指定的进程 |
原型 | int kill(pid_t pid, int sig) |
传入值说明 | 参数pid的几种情况: pid>0 或 pid<-1,将信号发送给进程识别码为pid绝对值的进程 pid=0,将信号发送给和目前进程相同进程组的所有进程 pid=-1,将信号广播给所有的进程 |
函数返回值 | 执行成功返回0,如果有错误返回-1 |
函数名 | raise |
---|---|
头文件 | #include<signal.h> |
功能 | 给当前进程发送信号 |
原型 | int raise(int sig) |
函数返回值 | 执行成功返回0,如果有错误返回-1 |
函数名 | signal |
---|---|
头文件 | #include<signal.h> |
功能 | 设置信号处理方式 |
原型 | void(*signal(int signum, void(*handler)(int)))(int); |
传入值说明 | signum是指定的信号类型 handler是自定义的处理函数 如果handler不是函数指针,则必须是以下两个常数之一: SIG_IGN:忽略信号 SIG_DFL:恢复默认处理方式 |
函数返回值 | 返回先前的信号处理函数指针,有错误则返回SIG_ERR(-1) |
示例程序
实现用户按下两次ctrl+z才会退出
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void fun_ctrl_z2()
{
printf("你第二次按下了ctrl+z,程序将退出\n");
exit(0);
}
void fun_ctrl_z1()
{
printf("你第一次按下了ctrl+z,如果再次按下程序将会退出\n");
signal(SIGTSTP, fun_ctrl_z2);
}
int main()
{
signal(SIGTSTP, fun_ctrl_z1);
while (1)
{
printf("程序正在运行...\n");
sleep(1);
}
return 0;
}
无名管道
管道主要用于父子进程间的通信。通过pipe()系统调用创建并打开无名管道,当最后一个使用它的进程关闭对它的引用时,将自动撤销。
操作
- 父进程调用pipe函数开辟管道,得到两个文件描述符指向管道的两端
- 父进程调用fork函数创建子进程,子进程也会得到两个文件描述符指向管道的两端
- 进程通过文件描述符向管道中写入或读取数据。父子进程都可以读写管道,但是为了同步,一般只允许一个进程读,以及另一个进程写。
函数
函数名 | pipe |
---|---|
头文件 | #include<unistd.h> |
功能 | 建立管道 |
函数原型 | int pipe(int filedes[2]); |
传入值说明 | 执行后filedes[0]是读取端的文件描述符,filedes[1]是写入端的文件描述符 |
返回值 | 成功返回0,错误返回-1 |
示例程序
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main()
{
int fd[2];
pipe(fd); //创建管道
int cnt, res = fork();
char buf[105];
if (res == 0) //子进程
{
memset(buf, 0, sizeof(buf));
while (1)
{
cnt = read(fd[0], buf, 100); //当管道中没有内容的时候read会返回0
if (cnt == 0) //如果没有读取到内容就继续循环(说明父进程还没写入)
{
sleep(1);
continue;
}
else if (cnt == -1)
perror("读取管道内容失败!\n");
printf("子进程读取了%d个字节,内容为:\n%s\n", cnt, buf);
break;
}
close(fd[0]), close(fd[1]); //及时关闭文件流
}
else //父进程
{
memset(buf, 0, sizeof(buf));
for (int i = 0; i < 100; i++)
buf[i] = 'a' + i % 26;
cnt = write(fd[1], buf, 100);
if (cnt != -1)
printf("父进程写入了%d个字节,内容为:\n%s\n", cnt, buf);
else
perror("写入管道失败!\n");
close(fd[0]), close(fd[1]); //及时关闭文件流
}
return 0;
}
运行结果:
父进程写入了100个字节,内容为:
abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuv
子进程读取了100个字节,内容为:
abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuv
命名管道
和无名管道的工作机制很类似,只是命名管道有名字,相当于在磁盘上创建了一个中介文件用于几个进程间的通信。因此在管道的创建、打开、删除上和无名管道有一定的区别,和普通文件的操作更加类似。当一个命名管道不再被任何进程打开时,它并没有消失,还可以被再次打开。可以像普通文件一样将其删除。如果删除的时候还有其他进程正在使用该管道,则会等到 89所有的进程都结束后再删除管道。
要注意的是, 当一个进程用open以只读方式打开命名管道时,会一直阻塞到另一个进程以只写方式打开该命名管道,反之亦然。
操作
- 调用函数mkfifo建立一个命名管道
- 根据读写方式用open函数打开这个命名管道
- 应用宏建立文件描述符集合,设定等待时间,使用函数select实现非阻塞传送
- 使用read、write读写管道
- 读写完成关闭管道
函数
函数名 | select |
---|---|
头文件 | #include<sys/select.h> |
功能 | 用于监视一些文件描述符的变化情况——读写或异常 |
函数原型 | int select(int maxfdp, fd_set *readfds, fd_set *writefds, fd_set *errorfds, struct timeval *timeout) |
传入值说明 | int maxfdp:集合中所有文件描述符的范围,即所有描述符的最大值+1 fd_set *readfds:可读文件描述符集合 fd_set *writefds:可写文件描述符集合 fd_set *errorfds:错误异常文件描述符集合 struct timeval *timeout:超时时间,超出timeout的时间select将返回0 |
返回值 | 没有可操作的文件或超出timeout时间返回0,存在可操作的文件返回正值,发生错误返回负值 |
备注 | struct fd_set是文件描述符的集合,可以通过以下宏进行操作: FD_ZERO(fd_set *):清空集合 FD_SET(int, fd_set *):将一个给定的文件描述符加入到集合中 FD_CLR(int, fd_set *):将一个给定的文件描述符从集合中删除 FD_ISSET(int, fd_set *):检查集合中给定的文件描述符是否可读写 struct timeval *timeout若传入NULL,则select将一直阻塞直到出现可操作的文件描述符为止 |
函数名 | mkfifo |
---|---|
头文件 | #include<sys/types.h> #include<sys/stat.h> |
功能 | 建立命名管道 |
函数原型 | int mkfifo(const char *pathname, mode_t mode) |
传入值说明 | 根据参数pathname建立特殊的FIFO文件,该文件必须不存在,而参数mode为该文件的权限,因此umask的值也会影响到FIFO文件的权限,用mkfifo创建的FIFO文件,其他进程都可以用读写一般文件的方式进行存取。 |
返回值 | 成功返回0,失败返回-1 |
示例程序
由于使用到select函数的场景比较复杂,这里只介绍简单的命名管道应用
以下程序实现了一个进程读取文件写入命名管道,另一个进程从命名管道内读取内容并写入到copy文件里
//进程A
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <fcntl.h>
int main()
{
char buf[105];
int fd1, fd2, cnt;
mkfifo("/tmp/fifotest5", 0666);
fd1 = open("./test.txt", O_RDONLY);
if (fd1 == -1)
{
perror("打开test.txt文件失败\n");
return 1;
}
fd2 = open("/tmp/fifotest5", O_WRONLY);
if (fd2 == -1)
{
perror("打开命名管道失败\n");
return 1;
}
while (1)
{
cnt = read(fd1, buf, 100);
if (cnt <= 0)
break;
buf[cnt] = 0;
printf("进程A向管道内写入了:%s\n", buf);
write(fd2, buf, cnt);
}
printf("进程A写入管道完成\n");
close(fd1);
close(fd2);
unlink("/tmp/fifotest5");
return 0;
}
//进程B
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <fcntl.h>
int main()
{
char buf[105];
int fd1, fd2, cnt;
fd1 = open("./test_copy.txt", O_WRONLY | O_CREAT);
if (fd1 == -1)
{
perror("打开test_copy.txt文件失败\n");
return 1;
}
fd2 = open("/tmp/fifotest5", O_RDONLY);
if (fd2 == -1)
{
perror("打开命名管道失败\n");
return 1;
}
while (1)
{
cnt = read(fd2, buf, 100);
if (cnt <= 0)
break;
write(fd1, buf, cnt);
}
printf("进程B读取管道完成\n");
close(fd1);
close(fd2);
unlink("/tmp/fifotest5");
return 0;
}
高级管道操作
popen函数可以用创建管道的方式启动一个进程并调用shell来执行command命令。
优点:可以完成复杂的shell命令。并且是C函数库的,支持更多的读写方式。
缺点:对于每个popen调用,不仅要启动一个被请求的程序,还要启动一个shell,比正常方式要慢一点。
当调用popen的父进程退出时,子进程也会退出。
操作
调用popen函数fork一个子进程并得到一个文件描述符,对文件描述符进行读写实现和子进程的通信。
函数
函数名 | popen |
---|---|
头文件 | #include<stdio.h> |
功能 | 建立管道IO |
函数原型 | FILE *popen(const char *command, const char *type) |
传入值说明 | 调用fork产生子进程,然后子进程调用/bin/sh -c来执行command命令。 参数type为"r"则文件指针连接到command的标准输出,参数type为"w"则文件指针连接到command的标准输入。 |
返回值 | 成功返回管道的文件流指针,否则返回NULL |
示例程序
以下程序的功能是父进程调用popen获取命令ls -l
的执行结果并写入到无名管道,子进程从无名管道中读取结果并调用popen执行grep 7-5
,将执行结果输出到屏幕。
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main()
{
int fd[2];
pipe(fd);
int res = fork();
FILE *fp;
char buf[10000];
memset(buf, 0, sizeof(buf));
if (res == 0)
{
close(fd[1]);
fp = popen("grep 7-5", "w");
while (1)
{
int cnt = read(fd[0], buf, 10000);
if (cnt > 0)
{
printf("子进程成功读取运行结果,以下为 \"grep 7-5\" 的运行结果:\n");
fprintf(fp, "%s\n", buf);
break;
}
sleep(1);
}
}
else
{
close(fd[0]);
fp = popen("ls -l", "r");
int cnt = fread(buf, sizeof(char), 10000, fp);
write(fd[1], buf, cnt);
pclose(fp);
printf("以下为父进程 \"ls -l\" 的运行结果:\n");
printf("%s\n", buf);
}
return 0;
}
运行结果:
消息队列
消息队列的特点是非实时性,发送方不必等待接收方受到了消息就可以继续工作,接收方也不必等待发送方发送了消息就可以继续工作。
操作
- 使用ftok函数得到一个编号key
- 应用key值作为msgget函数的参数,产生一个消息队列
- 进程通过msgsnd来向消息队列发送信息,也可以通过msgrcv读取。两种操作都有可能被中途打断导致操作失败。
- msgctl可以删除消息队列
注意点:
消息队列的个数是有限的,如果到达了上限,msgget会调用失败,产生的错误代码提示为"no space left on device"
如果消息队列满了msgsnd会调用失败。
函数
函数名 | ftok |
---|---|
头文件 | #include<sys/types.h> #include<sys/ipc.h> |
功能 | 获取一个用于建立IPC通讯的ID值 |
函数原型 | key_t ftok(char *pathname, char id) |
传入值说明 | pathname:文件名含路径 id:子序号 |
返回值 | 成功则返回key_t值,否则返回-1 |
函数名 | msgget |
---|---|
头文件 | #include<sys/types.h> #include<sys/ipc.h> #include<sys/msg.h> |
功能 | 建立消息队列 |
函数原型 | int msgget(key_t key, int msgflg) |
传入值说明 | 参数msgflg用来决定消息队列的存取权限,取值如下: IPC_CREAT:如果消息队列对象不存在则创建,否则进行打开操作 IPC_EXCL:如果消息对象不存在则创建,否则产生一个错误并返回,需要用 | 连接IPC_CREAT一起使用,不可单独使用 |
返回值 | 成功返回消息队列的识别号,否则返回-1 |
函数名 | msgsnd |
---|---|
头文件 | #include<sys/types.h> #include<sys/ipc.h> #include<sys/msg.h> |
功能 | 向消息队列中发送消息 |
函数原型 | int msgsnd(int msqid, struct msgbuf *msgp, int msgsz, int msgflg) |
传入值说明 | msqid:消息队列的识别码 msgp:指向消息缓冲区的指针,此位置用于暂存发送和接收的消息,是一个用户可定义的通用结构,形态见下方代码 msgsz:消息的大小,用来指定消息数据的长度 msgflg:用来指明核心程序在队列没有数据的情况下所应采取的行动 |
返回值 | 成功返回0,否则返回-1 |
struct msgbuf{ //struct msgbuf形态
long mtype; //消息类型,必须大于0
char mtext[]; //消息文本
};
msgflg说明:
- 0:当消息队列满时,msgsnd将会阻塞,直到消息能写进消息队列
- IPC_NOWAIT:当消息队列已满的时候,msgsnd函数不等待立即返回
- MSG_NOERROR:若发送的消息大于size字节,则把该消息截断,截断部分将被丢弃,且不通知发送进程。(某些内核版本可能无法使用)
函数名 | msgrcv |
---|---|
头文件 | #include<sys/types.h> #include<sys/ipc.h> #include<sys/msg.h> |
功能 | 从消息队列中读取信息 |
函数原型 | int msgrcv(int msqid, struct msgbuf *msgp, int msgsz, long msgtyp, int msgflg) |
传入值说明 | msqid:消息队列的识别码 msgp:指向消息缓冲区的指针,此位置用于暂存发送和接收的消息,是一个用户可定义的通用结构,形态见下方代码 msgsz:消息的大小,用来指定消息数据的长度 msgtyp:用来指定所要读取的信息种类。若等于0,返回队列内第一项消息;若大于0,返回队列内第一项类型相同的消息;若小于0,返回队列内第一项小于或等于该绝对值的消息 msgflg:用来指明核心程序在队列没有数据的情况下所应采取的行动 |
返回值 | 成功返回实际读取的消息数据长度,否则返回-1 |
msgflg说明:
- 0: 阻塞式接收消息,没有该类型的消息msgrcv函数一直阻塞等待
- IPC_NOWAIT:如果没有返回条件的消息调用立即返回,此时错误码为ENOMSG
- MSG_EXCEPT:与msgtype配合使用返回队列中第一个类型不为msgtype的消息(某些内核版本可能无法使用)
- MSG_NOERROR:如果队列中满足条件的消息内容大于所请求的size字节,则把该消息截断,截断部分将被丢弃(某些内核版本可能无法使用)
示例程序
以下程序实现了多个进程向消息队列中写,一个进程从消息队列中读。
如果在运行时提示没有权限访问,则需要在root权限下运行。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
struct msgbuf
{
long msg_type;
char msg_text[600];
};
int main()
{
key_t key = ftok("/tmp/msgqueue6", 1);
if (key == -1)
perror("获取ID出错\n");
int qid = msgget(key, IPC_CREAT);
if (qid == -1)
perror("打开消息队列出错\n");
if (key != -1 && qid != -1)
{
struct msgbuf msg;
msg.msg_type = getpid();
memcpy(msg.msg_text, "123456", 6);
if (msgsnd(qid, &msg, strlen(msg.msg_text), IPC_NOWAIT) == -1)
perror("发送失败\n");
else
printf("发送成功\n");
}
return 0;
}
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
struct msgbuf
{
long msg_type;
char msg_text[501];
};
int main()
{
key_t key = ftok("/tmp/msgqueue6", 1);
if (key == -1)
perror("获取ID出错\n");
int qid = msgget(key, IPC_CREAT);
if (qid == -1)
perror("打开消息队列出错\n");
if (key != -1 && qid != -1)
{
struct msgbuf msg;
while (msgrcv(qid, &msg, 500, 0, IPC_NOWAIT) != -1)
{
printf("收到来自进程 %lld 的消息:%s\n", msg.msg_type, msg.msg_text);
}
}
return 0;
}
运行截图: