进程间通信(InterProcess Communication,IPC)是指在不同进程之间传播或交换信息。
Linux的进程间通信方法有管道(Pipe)和有名管道(FIFO)、信号(Signal)、消息队列(Message Queue)、共享内存(Shared Memory)、信号量(Semaphore)、套接字(Socket)等。
Linux进程间通信由以下几部分发展而来:
1、UNIX进程间通信
2、基于System V 进程间通信
3、POSIX进程间通信
POSIX进程间通信是最新的技术,POSIX表示可移植操作系统接口。
管道是半双工的(即数据单向流动,先进先出),有固定的读端和写端。数据被一个进程读出后,将被从管道删除,其他进程将不能再读到这些数据。
管道提供了简单的流控制机制,进程试图读空管道时,进程将阻塞。同样,管道已经满时,进程再试图向管道写入数据,进程将阻塞。
管道包括无名管道(Pipe)和有名管道(FIFO)两种。无名管道用于父进程和子进程间的通信,有名管道可用于运行于同一系统中的任意两个进程间的通信。
无名管道
函数用法 | #include int pipe(int fd[2]); |
函数功能 | 创建无名管道 fd:文件描述符,fd[0]用于读管道,fd[1]用于写管道 |
函数返回值 | 成功返回 0 , 失败返回 -1 并产生errno |
当一个无名管道(Pipe)建立时,它会创建两个文件描述符:fd[0]用于读管道
,fd[1]
用于写管道。
要关闭管道只需要关闭这两个文件描述符即可。
管道用于不同进程间通信。通常先创建一个管道,再通过fork函数创建一个子进程,该子进程会继承父进程所创建的管道。必须在系统调用fork( )前调用pipe( ),否则子进程将不会继承文件描述符。
实例:使用管道实现数据的发送与接收
#include
#include
#include
#include
#include
#include
void ReadPipe(int fd)//读管道
{
int ret;
char buf[32] = {0};
while(1)
{
ret = read(fd, buf, sizeof(buf));
if (-1 == ret)
{
perror("read");
exit(1);
}
if (!strcmp(buf, "bye"))
{
break;
}
printf("read from pipe: %s\n", buf);
memset(buf, 0, sizeof(buf));
}
close(fd);
}
void WritePipe(int fd)//写管道
{
int ret;
char buf[32] = {0};
while (1)
{
scanf("%s", buf);
ret = write(fd, buf, strlen(buf));
if (-1 == ret)
{
perror("write");
exit(1);
}
if (!strcmp(buf, "bye"))
{
break;
}
memset(buf, 0, sizeof(buf));
}
close(fd);
}
int main()
{
int fd[2];
int ret;
pid_t pid;
ret = pipe(fd); //创建管道
if (-1 == ret)
{
perror("pipe");
exit(1);
}
pid = fork(); //创建进程
if (-1 == pid)
{
perror("fork");
exit(1);
}
else if (0 == pid)
{
close(fd[1]); //关闭写端口
ReadPipe(fd[0]); //fd[0]读数据
}
else
{
close(fd[0]); //关闭读端口
WritePipe(fd[1]); //fd[1]写数据
int status;
wait(&status);
}
return 0;
}
有名管道
函数用法 | #include int mkfifo(const char *pathname, mode_t mode); |
函数功能 | 创建有名管道 pathname:文件名 mode:设定创建的文件的权限 |
函数返回值 | 成功返回 0 , 失败返回 -1 并产生errno |
命名管道(FIFO)与无名管道的区别在于它提供一个路径与之关联,以FIFO的文件形式存储于文件系统中。命名管道是一个设备文件。因此,即使进程与创建FIFO的进程不存在亲缘关系,只要可以访问该路径,就能够通过FIFO相互通信。
实例:使用有名管道,实现一个终端发送数据,一个终端接收数据
fifo_read.c
#include
#include
#include
#include
#include
#include
#include
int main()
{
char buf[32] = {0};
int fd;
int ret;
fd = mkfifo("fifo.tmp", S_IRWXU);//创建有名管道
if (-1 == fd)
{
perror("mkfifo");
exit(1);
}
fd = open("fifo.tmp", O_RDONLY);//只读方式打开文件fifo.tmp
if (-1 == fd)
{
perror("open");
exit(1);
}
while(1)
{
ret = read(fd, buf, sizeof(buf));//从文件中读取数据
if (-1 == ret)
{
perror("read");
exit(1);
}
if (!strcmp(buf, "bye"))//读取到bye结束循环
{
break;
}
printf("read:%s\n", buf);
memset(buf, 0, sizeof(buf));
}
close(fd);
unlink("fifo.tmp");//在管道使用结束后删除文件fifo.tmp
return 0;
}
fifo_write.c
#include
#include
#include
#include
#include
#include
#include
int main()
{
int fd;
int ret;
char buf[32] = {0};
fd = open("fifo.tmp", O_WRONLY);//以只写方式打开文件fifo.tmp
while (1)
{
scanf("%s", buf);
ret = write(fd, buf, strlen(buf));//写入数据到文件
if (-1 == ret)
{
perror("write");
exit(1);
}
if (!strcmp(buf, "bye"))//输入bye结束写入
{
break;
}
memset(buf, 0, sizeof(buf));
}
close(fd);
return 0;
}
注:在运行测试时候必须先运行创建了管道的程序,本文中要先运行fifo_read.c
信号(signal)机制是Unix系统中最为古老的进程间通信机制,很多条件可以产生一个信号:
(1)当用户按某些按键时,产生信号。
(2)硬件异常产生信号:除数为0、无效的存储访问等等。这些情况通常由硬件检测到,将其通知内核,然后内核产生适当的信号通知进程,例如,内核对正访问一个无效存储区的进程产生一个SIGSEGV信号 。
(3)进程用kill函数将信号发送给另一个进程。
(4)用户可用kill命令将信号发送给其他进程。
当某信号出现时,将按照下列三种方式中的一种进行处理:
(1)忽略此信号,大多数信号都按照这种方式进行处理,但有两种信号决不能被忽略,它们是: SIGKILL\SIGSTOP。 这是因为这两种信号向超级用户提供了一种终止或停止进程的方法。
(2)执行系统默认动作,对大多数信号的系统默认动作是终止该进程。
(3)执行用户希望的动作,通知内核在某种信号发生时,调用一个用户函数。在用户函数中,执行用户希望的处理。
在终端中输入 kill -l 可以查看到系统设定的信号的宏定义。
发送信号的函数有:kill(向任意进程发送信号)、raise(只能向当前进程发送信号)、abort(发送SIGABRT信号,可以让进程异常终止)、alarm(发送SIGALRM闹钟信号)
实例:实现0-59的计时,计时到60时变为0.
使用alarm函数可以设置一个时间值(闹钟时间),当所设置的时间到了时,产生SIGALRM信号。然后使用signal函数捕捉SIGALRM信号,并且自定义信号处理方式。
#include
#include
#include
#include
int t;
void print()
{
system("clear");//清屏
alarm(1);//1秒后给当前进程发送一个SIGALRM信号,一直在执行
printf("%d\n", (t++)%60);
}
int main()
{
system("clear");//清屏
alarm(1);//1秒后给当前进程发送一个SIGALRM信号,只执行一次
signal(SIGALRM, print);
while (1);//必须有,不然进程就直接结束了
return 0;
}
消息队列(Message Queue),就是一个消息的链表,是一系列保存在内核中消息的列表。用户进程可以向消息队列添加消息,也可以向消息队列读取消息。
消息队列与管道通信相比,其优势是对每个消息指定特定的消息类型,接收的时候不需要按照队列次序,而是可以根据自定义条件接收特定类型的消息。
上图中进程A指定了类型1,进程C指定了类型2,,进程B指定了类型1。按上图演示,最后进程B会接收到进程A发送的hello,进程C的world接收不到,因为B和C不在同一类型。
实例:使用消息队列和fork,实现两个终端的收发数据,既能收,又能发。
msg_send.c
#include
#include
#include
#include
#include
#include
#include
#include
#define MSGKEY 1234
struct msgbuf
{
long mtype; /* message type, must be > 0 */
char mtext[100]; /* message data */
};
int main()
{
int msgid;
int ret;
struct msgbuf mbuf;
pid_t pid;
msgid = msgget(MSGKEY, IPC_CREAT | IPC_EXCL); //创建消息队列
if (-1 == msgid)
{
perror("msgget");
exit(1);
}
pid = fork();
if (-1 == pid)
{
perror("fork");
exit(1);
}
else if (0 == pid) //子进程发送数据
{
while (1)
{
memset(mbuf.mtext, 0, sizeof(mbuf.mtext));
scanf("%s", mbuf.mtext);
mbuf.mtype = 1;
ret = msgsnd(msgid, &mbuf, sizeof(mbuf.mtext), 0);
if (-1 == ret)
{
perror("msgsnd");
exit(1);
}
if (!strcmp(mbuf.mtext, "bye"))
{
mbuf.mtype = 2;
msgsnd(msgid, &mbuf, sizeof(mbuf.mtext), 0);
break;
}
}
}
else //父进程接收数据
{
while (1)
{
memset(mbuf.mtext, 0, sizeof(mbuf.mtext));
ret = msgrcv(msgid, &mbuf, sizeof(mbuf.mtext), 2,0);
if (-1 == ret)
{
perror("msgrcv");
exit(1);
}
if (!strcmp(mbuf.mtext, "bye"))
{
kill(pid, 2);
break;
}
printf("\t%s\n", mbuf.mtext);
}
}
sleep(1);
msgctl(msgid, IPC_RMID, NULL);//销毁消息队列
return 0;
}
msg_write.c
#include
#include
#include
#include
#include
#include
#include
#include
#define MSGKEY 1234
struct msgbuf
{
long mtype; /* message type, must be > 0 */
char mtext[100]; /* message data */
};
int main()
{
int msgid;
int ret;
struct msgbuf mbuf;
pid_t pid;
msgid = msgget(MSGKEY, 0); //打开消息队列
if (-1 == msgid)
{
perror("msgget");
exit(1);
}
pid = fork();
if (-1 == pid)
{
perror("fork");
exit(1);
}
else if (0 == pid) //子进程发送数据
{
while (1)
{
memset(mbuf.mtext, 0, sizeof(mbuf.mtext));
scanf("%s", mbuf.mtext);
mbuf.mtype = 2;
ret = msgsnd(msgid, &mbuf, sizeof(mbuf.mtext), 0);
if (-1 == ret)
{
perror("msgsnd");
exit(1);
}
if (!strcmp(mbuf.mtext, "bye"))
{
mbuf.mtype = 1;
msgsnd(msgid, &mbuf, sizeof(mbuf.mtext), 0);
break;
}
}
}
else //父进程接收数据
{
while (1)
{
memset(mbuf.mtext, 0, sizeof(mbuf.mtext));
ret = msgrcv(msgid, &mbuf, sizeof(mbuf.mtext), 1,0);
if (-1 == ret)
{
perror("msgrcv");
exit(1);
}
if (!strcmp(mbuf.mtext, "bye"))
{
kill(pid, 2);
break;
}
printf("\t%s\n", mbuf.mtext);
}
}
return 0;
}
注:测试程序时先启动创建了消息队列的程序,本文中应该是先启动msg_send.c
共享内存允许一个或多个进程共享一个给定的物理存储区,这一个给定的物理存储区可以被两个或两个以上的进程映射至自身的地址空间中。
采用共享内存进行通信的一个主要好处是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝,对于像管道和消息队里等通信方式,则需要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次:一次从输入文件到共享内存区,另一次从共享内存到输出文件。
使用共享内存的步骤:
(1)申请共享内存 shmget
(2)映射 shmat
(3)使用
(4)解除映射 shmdt
(5)销毁共享内存 shmctl
实例:
shm.c
#include
#include
#include
#include
#include
#include
#define SHMKEY 1234
#define SHMSIZE 4096
int main()
{
int shmid;
void *shmaddr;
int count = 0;
shmid = shmget(SHMKEY, SHMSIZE, IPC_CREAT | IPC_EXCL);//创建共享内存
if (-1 == shmid)
{
perror("shmget");
exit(1);
}
shmaddr = shmat(shmid, NULL, 0);//映射到虚拟地址空间
if (NULL == shmaddr)
{
perror("shmat");
exit(1);
}
*(int *)shmaddr = count;//把数据写到内存
while(1)
{
count = *(int *)shmaddr;//从内存读数据
//usleep(100000);
if (count > 100)
{
break;
}
printf("A:%d\n", count);
count++;
*(int *)shmaddr = count;//数据写回内存
usleep(100000);
}
shmdt(shmid); //解除映射
shmctl(shmid, IPC_RMID, NULL);//销毁映射
return 0;
}
shm2.c
#include
#include
#include
#include
#include
#include
#define SHMKEY 1234
#define SHMSIZE 4096
int main()
{
int shmid;
void *shmaddr;
int count = 0;
shmid = shmget(SHMKEY, SHMSIZE, 0);//获取共享内存
if (-1 == shmid)
{
perror("shmget");
exit(1);
}
shmaddr = shmat(shmid, NULL, 0);//映射到虚拟地址空间
if (NULL == shmaddr)
{
perror("shmat");
exit(1);
}
while(1)
{
count = *(int *)shmaddr;//从内存读数据
//usleep(100000);
if (count > 100)
{
break;
}
printf("B:%d\n", count);
count++;
*(int *)shmaddr = count;//把数据写回共享内存
usleep(100000);
}
return 0;
}
注:在测试时,应该先启动创建了共享内存的那个程序,也就是先启动shm.c。
测试现象是只启动了shm.c时数字刷新步长为1 (也就是 1 2 3 4 5 这样变),在shm.c运行时启动了shm2.c后,数字刷新步长为2 (也就是 1 3 5 7 这样变)
思考:如果将例子里的 延时usleep(100000); 挪到从内存读数据后面(也就是注释掉的那串代码)会发生什么现象?过程是怎么样子的?怎么解决?(解决方案见信号量实例)
信号量(Semaphore),又名信号灯,主要用途是保护临界资源。进程可以根据它来判定是否能够访问某些共享资源。除了用于访问控制外,还可用于进程同步。
二值信号灯:信号灯的值只能取0或1,类似于互斥锁。 但两者有不同:
信号灯强调共享资源,只要共享资源可用,其他进程同样可以修改信号灯的值;
互斥锁更强调进程,占用资源的进程使用完资源后,必须由进程本身来解锁。
计数信号灯:信号灯的值可以取任意非负值。
使用信号量的步骤
(1)创建(获取) 信号量 semget
(2)初始化信号量 semctl
(3)使用(P操作/V操作) semop
(4)销毁信号量 semctl
实例:共享内存的例子的思考的解决方案。
sem.c
#include
#include
#include
#include
#include
#include
#include
#define SHMKEY 1234
#define SHMSIZE 4096
#define SEMKEY 1234
union semun
{
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO(Linux specific) */
};
void sem_p(int semid)
{
int ret;
struct sembuf sbuf;
sbuf.sem_num = 0;//第一个
sbuf.sem_op = -1;//P操作
sbuf.sem_flg = SEM_UNDO;//SEM_UNDO进程异常自动UNDO
ret = semop(semid, &sbuf, 1);
if (ret == -1)
{
perror("semop");
return;
}
}
void sem_v(int semid)
{
int ret;
struct sembuf sbuf;
sbuf.sem_num = 0;//第一个
sbuf.sem_op = 1;//V操作
sbuf.sem_flg = SEM_UNDO;//SEM_UNDO进程异常自动UNDO
ret = semop(semid, &sbuf, 1);
if (ret == -1)
{
perror("semop");
return;
}
}
int main()
{
int shmid;
void *shmaddr;
int count = 0;
int semid;
union semun unsem;
semid = semget(SEMKEY, 1, IPC_CREAT | IPC_EXCL); //创建信号量
if (-1 == semid)
{
perror("semget");
exit(1);
}
unsem.val = 1;//初始化成二值信号量
semctl(semid, 0, SETVAL, unsem);//初始化信号量
shmid = shmget(SHMKEY, SHMSIZE, IPC_CREAT | IPC_EXCL);//创建共享内存
if (-1 == shmid)
{
perror("shmget");
exit(1);
}
shmaddr = shmat(shmid, NULL, 0);//映射到虚拟地址空间
if (NULL == shmaddr)
{
perror("shmat");
exit(1);
}
*(int *)shmaddr = count;//把数据写到内存
while(1)
{
sem_p(semid);//P操作
count = *(int *)shmaddr;//从内存读数据
usleep(100000);
if (count > 100)
{
break;
}
printf("A:%d\n", count);
count++;
*(int *)shmaddr = count;//数据写回内存
//usleep(100000);
sem_v(semid);//V操作
}
shmdt(shmaddr); //解除映射
sleep(1);
shmctl(shmid, IPC_RMID, NULL);//销毁映射
semctl(semid, 0, IPC_RMID);//销毁信号量
return 0;
}
sem2.c
#include
#include
#include
#include
#include
#include
#include
#define SHMKEY 1234
#define SHMSIZE 4096
#define SEMKEY 1234
union semun
{
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO(Linux specific) */
};
void sem_p(int semid)
{
int ret;
struct sembuf sbuf;
sbuf.sem_num = 0;//第一个
sbuf.sem_op = -1;//P操作
sbuf.sem_flg = SEM_UNDO;//SEM_UNDO进程异常自动UNDO
ret = semop(semid, &sbuf, 1);
if (ret == -1)
{
perror("semop_p");
return;
}
}
void sem_v(int semid)
{
int ret;
struct sembuf sbuf;
sbuf.sem_num = 0;//第一个
sbuf.sem_op = 1;//V操作
sbuf.sem_flg = SEM_UNDO;//SEM_UNDO进程异常自动UNDO
ret = semop(semid, &sbuf, 1);
if (ret == -1)
{
perror("semop_v");
return;
}
}
int main()
{
int shmid;
void *shmaddr;
int count = 0;
int semid;
union semun unsem;
semid = semget(SEMKEY, 1, 0); //获取信号量
if (-1 == semid)
{
perror("semget");
exit(1);
}
//unsem.val = 1;
//semctl(semid, 0, SETVAL, unsem);//初始化信号量
shmid = shmget(SHMKEY, SHMSIZE, 0);//获取共享内存
if (-1 == shmid)
{
perror("shmget");
exit(1);
}
shmaddr = shmat(shmid, NULL, 0);//映射到虚拟地址空间
if (NULL == shmaddr)
{
perror("shmat");
exit(1);
}
while(1)
{
sem_p(semid);//P操作
count = *(int *)shmaddr;//从内存读数据
usleep(100000);
if (count > 100)
{
break;
}
printf("B:%d\n", count);
count++;
*(int *)shmaddr = count;//数据写回内存
//usleep(100000);
sem_v(semid);//V操作
}
shmdt(shmaddr); //解除映射
return 0;
}
进程间通信方面的系统调用远不止这些,想了解更多请转https://blog.csdn.net/qq_42379345/article/details/81710027有对大部分常用系统调用和系统调用派生函数的简单介绍。