目录
1 进程间通信
在Linux学习(十八):进程概念及创建中我们将fork函数中,我们说到子进程是父进程的一个复制品,它从父进程处继承了整个进程的地址空间。包括进程上下文、代码段、进程堆栈、内存信息、打开的文件描述符、信号处理函数、进程优先级、进程组号、当前工作目录、根目录、资源限制和控制终端等等,而子进程所独有的只有它的进程号、资源使用和计时器等。那是否可以直接使用全局变量来通信呢?我们来看一个例子
#include <stdio.h>
#include <unistd.h>
int x;
int main(int argc, const char *argv[])
{
pid_t pid = fork();
if(pid == -1)
{
perror("fork error");
return -1;
}
else if(pid == 0)
{
//子进程
x = 5;
printf("child &x = %p, x = %d\n",&x,x);
}
else
{
//父进程
sleep(2);
printf("parent &x = %p, x = %d\n",&x,x);
}
while(1);
return 0;
}
程序执行结果
可以看到,虽然两个进程中x的地址是一样的,但是在父进程中,x的值并没有改变。这是为何?这是因为这个地址只是虚拟地址,并不是真正的物理地址,所以虽然就会出现这样的结果,也说明进程间通信并不能使用全局变量的方式。
UNIX平台通信方式,早期进程间通信方式
AT&T的贝尔实验室,对Unix早期的进程间通信进行了改进和扩充,形成了“system V IPC”,其通信进程主要局限在单个计算机内。
BSD(加州大学伯克利分校的伯克利软件发布中心),跳过了该限制,形成了基于套接字(socket)的进程间通信机制。Linux继承了上述所有的通信方式。
常用的进程间通信方式
1、传统的进程间通信方式
无名管道(pipe)、有名管道(fifo)和信号(signal)
2、System V IPC对象
共享内存(share memory)、消息队列(message queue)和信号灯(semaphore)
3、BSD
套接字(socket)
2、管道通信
管道是Linux中进程通信的一种方式,它把一个程序的输出直接连接到另一个程序的输入,Linux的管道主要包括两种:无名管道和有名管道。
2.1 无名管道
无名管道具有如下特点:
1、只能用于具有亲缘关系的进程之间的通信
2、半双工的通信模式,具有固定的读端和写端。
3、管道可以看成是一种特殊的文件,对于它的读写可以使用文件IO(不能使用标准IO)如read、write函数。无名管道并不是普通的文件,不属于任何文件系统,并且只存在于内存中。也就是在文件系统中不可见的。
2.1.1 无名管道的创建
管道是基于文件描述符的通信方式。当一个管道建立时,它会创建两个文件描述符fd[0]和fd[1]。其中fd[0]固定
用于读管道,而fd[1]固定用于写管道。
创建函数pipe
无名管道例程:从终端读取输入,写入管道中,然后再从管道中读出,并输出到终端中,写入quit时两个进程退出。
/*无名管道:用于具有亲缘关系的父子进程间通信*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#if 0
int pipe(int pipefd[2]);
功能:创建无名管道
参数:pipefd[0]作为读端 pipefd[1]作为写端
返回:成功返回0,失败-1
注意:只能使用文件IO,read/write
#endif
#define N 32
int main(int argc, const char *argv[])
{
char buf[N];
int fd[2];
//创建无名管道
if(pipe(fd) == -1)
{
perror("pipe error"); //fd[0]:读端 fd[1]:写端
exit(1);
}
//创建子进程
pid_t pid = fork();
if(pid == -1)
{
perror("fork error");
exit(1);
}
else if(pid == 0)
{
//子
while(1)
{
//从终端获取数据
fgets(buf, N, stdin);
//写到管道中
write(fd[1], buf, N);
//字符串比较
if(strncmp(buf, "quit", 4) == 0)
{
exit(0);
}
}
}
else
{
//父
while(1)
{
//从管道读取数据
read(fd[0], buf, N);
//字符串比较
if(strncmp(buf, "quit", 4) == 0)
{
exit(0);
}
//打印到终端
printf("---> ");
fputs(buf, stdout);
}
}
return 0;
}
在创建管道要注意,要在fork()函数之前创建;若在fork()函数之后再创建,那每个进程都会创建管道,也就无法正常使用了。
当管道中无数据时,读操作会阻塞。
向管道中写入数据时,linux将不保证写入的原子性,管道缓冲区一有空闲区域,写进程就会试图向管道写入数据。如果读进程不读走管道缓冲区中的数据,那么写操作将会一直阻塞。
只有在管道的读端存在时,向管道中写入数据才有意义。否则,向管道中写入数据的进程将收到内核传来的SIGPIPE信号(通常Broken pipe错误)。
2.2 有名管道
有名管道(FIFO)是对无名管道的改进,具有如下特点:
1、它可以使互不相关的两个进程实现彼此通信
2、该管道可以通过路径名来实现,并且在文件中可见的。在建立管道后,两个进程就可以把它当做普通文件一样进程读写操作。当然这里要明白的,它的文件属性是管道文件p,在Linux学习(四):Linux文件系统及其shell命令中讲了Linux文件的七种类型,创建的管道属于管道文件。
3、FIFO遵循先进先出原则,不支持lseek操作。
4、对于读进程,若当前FIFO中无数据,会一直阻塞到有数据写入或FIFO写入端关闭
5、对于写进程,只要FIFO有空间就可写入。若空间不足,写进程就会阻塞,直到有空间为止
2.2.1 创建有名管道mkfifo
创建FIFO的出错信息
对于错误码errno的使用,可以参考Linux学习(十六):文件IO。对于mkfifo()函数,EEXIST是比较常见的错误信息,表明FIFO已经创建,直接使用即可(这里可以理解为一个正常信息)。
有名管道例程:一个进程负责从终端读取数据,并写入进程,另一个进程从管道读取数据并向终端发送数据,若终端数据quit,两进程退出。
写管道:
/*有名管道:可以用于没有关系的进程间通信,先进先出规则*/
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#if 0
int mkfifo(const char *pathname, mode_t mode);
功能:创建一个有名管道文件
参数:pathname:指定创建文件的路径文件名
mode :文件权限(mode & ~umask)
返回:成功返回0,失败返回-1
注意:管道文件不会重复创建,所以已存在是允许发生的错误
#endif
#define N 32
int main(int argc, const char *argv[])
{
if(mkfifo("fifo", 0664) == -1)
{
//已存在
if(errno == EEXIST)
{
puts("fifo exist");
}
else
{
perror("mkfifo error");
exit(1);
}
}
int fd_w = open("fifo", O_RDWR);
if(fd_w == -1)
{
perror("open error");
exit(1);
}
//从终端读取数据,写到管道内,遇到"quit"退出
char buf[N];
while(1)
{
fgets(buf, N, stdin);
write(fd_w, buf, N);
if(strncmp(buf, "quit", 4) == 0)
{
break;
}
}
close(fd_w);
return 0;
}
读管道:
/*有名管道:可以用于没有关系的进程间通信,先进先出规则*/
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#if 0
int mkfifo(const char *pathname, mode_t mode);
#endif
#define N 32
int main(int argc, const char *argv[])
{
if(mkfifo("fifo", 0664) == -1)
{
//已存在
if(errno == EEXIST)
{
puts("exist");
}
else
{
perror("mkfifo error");
exit(1);
}
}
int fd_r = open("fifo", O_RDWR);
if(fd_r == -1)
{
perror("open error");
exit(1);
}
//从管道读取数据,打印到终端,遇到"quit"不输出并退出
char buf[N];
while(1)
{
read(fd_r, buf, N);
if(strncmp(buf, "quit", 4) == 0)
{
break;
}
fputs(buf, stdout);
}
close(fd_r);
return 0;
}
任意执行一个进程后,会创建一个fifo文件,文件属性为p
两个进程的执行结果
3 信号通信
信号是在软件层次上对中断机制的一种模拟,是一种异步通信方式
信号可以直接进行用户空间进程和内核进程之间的交互,内核进程也可以利用它来通知用户空间进程发生了哪些系统事件。
如果该进程当前并未处于执行态,则该信号就由内核保存起来,直到该进程恢复执行再传递给它;如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程 。
信号事件的产生有硬件来源和软件来源,常用的信号相关函数有kill()、alarm()、setitimer()、sigqueue()。
进程可以通过3种方式来相应信号
(1)忽略信号,对信号不做任何处理,SIGKILL和SIGSTOP两个信号不能忽略
(2)捕捉信号,定义信号处理函数,当信号发生时,执行相应操作
(3)执行默认操作。默认操作如下所示
3.1 信号发送函数kill
kill函数同读者熟知的kill系统命令一样,可以发送信号给进程或进程组(实际上,kill系统命令只是kill函数的一个
用户接口)。
kill –l 命令查看系统支持的信号列表
raise函数允许进程向自己发送信号
kill函数
kill例程:杀死一个进程
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#include <signal.h>
#if 0
int kill(pid_t pid, int sig);
功能:给某一个进程或进程组发送信号sig
参数:sig:要发送的信号
pid:pid > 0:给指定的进程发送信号
pid = 0:给同组下的所有进程发送信号(进程本身)
pid =-1:给所有进程发送信号(除了1号init进程)
pid <-1:给指定组下的所有进程发送信号,PGIG=|pid|
返回:成功返回0,失败-1
atoi
功能:将数值型字符串转化成整数
#endif
int main(int argc, const char *argv[]) //argv[0] argv[1]
{
if(kill(atoi(argv[1]), SIGKILL) == -1)
{
perror("kill error");
exit(1);
}
puts("kill ok!");
return 0;
}
执行结果
我们将进程号2778的while进程(一个空的while(1))杀死
进程在收到SIGKILL信号后会在终端打印Killed。
3.2 信号设置signal()
signal()函数语法要点
所需头文件:#include<signal.h>
函数原型: typedef void(* sighandler_t)(int);
sighandler_t signal(int signum,sighandler_t handler)
参数: 参数1:指定信号
参数2:SIG_IGN 忽略该信号,其中SIGKILL和SIGSTOP不能忽略
SIG_DEFL 采用默认方式处理指定信号
自定义的信号处理函数,(sighandler_t 是一个重定义的函数指针,函数要求返回值为空,参数 为int)
返回值: 成功:信号处理函数地址
出错:SIG_ERR
例程:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#if 0
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
功能:捕捉一个指定的信号,并作相应的处理
参数:signum :要捕捉的信号
handler:SIG_IGN 忽略
SIG_DFL 执行默认操作
fun 执行相应的函数代码
返回:失败返回SIG_ERR
注意:SIGKILL和SIGSTOP不能被捕捉
#endif
void fun(int sig)
{
if(sig == SIGINT)
{
puts("catch SIGINT");
}
if(sig == SIGTSTP)
{
puts("catch SIGTSTP");
}
}
int main(int argc, const char *argv[])
{
if(signal(SIGINT, fun) == SIG_ERR) //ctrl+C SIGINT
{
perror("signal error");
exit(1);
}
if(signal(SIGTSTP, fun) == SIG_ERR) //ctrl+\ SIGQUIT
{
perror("signal error");
exit(1);
}
if(signal(SIGQUIT, SIG_DFL) == SIG_ERR) //ctrl+Z SIGTSTP
{
perror("signal error");
exit(1);
}
puts("signal");
while(1);
return 0;
}
执行结果
我们用的ctrl+c和ctrl+z都不能正常结束程序了。而是执行相应的函数
4、IPC通信介绍
以上三种属于传统进程间通信方式,下面介绍消息队列、共享内存和信号灯,这属于System V IPC方式。我们也称这三种通信方式为IPC对象通信。
三种通信方式都需要一个标识符,共享内存标识符、消息队列标识符、信号灯标识符。 标识符只是IPC对象的内部名,如果多个进程需要在同一个IPC对象上进行回合通信,需要一个外部名。为此,使用了键值 (key值)。
4.1 创建键值函数ftok()
头文件: #include <sys/types.h>
#include <sys/ipc.h>
函数原型: key_t ftok(const char *pathname, int proj_id);
参数: 参数1:路径名 参数2 任意一个非0整形数据
返回值: 成功:返回键值,失败:-1
4.2 IPC通信步骤
对于消息队列、共享内存和信号灯通信来讲,第一步是打开或创建IPC通道。第二步就是去操作相应的IPC对象。部分函数是相近的(如打开或创建IPC通道函数,控制函数),方便记忆和使用。
4.3 IPC相关shell命令
1.ipcs命令用于查看系统中的IPC对象 ipcs -q,ipcs -m,ipcs -s 查看消息队列/共享内存/信号灯
2.ipcrm命令用于删除系统中的IPC对象
ipcrm -M shmkey 移除用shmkey创建的共享内存段
ipcrm -m shmid 移除用shmid标识的共享内存段
ipcrm -Q msgkey 移除用msqkey创建的消息队列
ipcrm -q msqid 移除用msqid标识的消息队列
ipcrm -S semkey 移除用semkey创建的信号
ipcrm -s semid 移除用semid标识的信号
5、消息队列
消息队列是IPC对象的一种。消息队列由消息队列ID来唯一标识。消息队列就是一个消息的列表。用户可以在消息队列中添加消息、读取消息等。消息队列可以按照类型来发送/接收消息。我们之前学的管道通信和消息队列比较类似,但消息队列可以区别不同的消息(如消息类型1,消息类型2),管道中所有数据没有类别区分。
5.1 创建或打开消息队列
参数flag和open()函数中的flag类似。IPC_CREAT标志为创建一个先的IPC对象(若存在则不创建新的),IPC_EXCL也是创建一个新的IPC对象,若存在返回出错。后面再或上创建的IPC对象的权限,同样可用八进制表示。
5.2 发送消息
5.3 接收消息
5.4 消息队列控制函数
例程:
发送消息文件,发送三个消息
/*消息队列:*/
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#if 0
int msgget(key_t key, int msgflg);
功能:创建并打开一个消息队列
返回:成功返回消息队列的ID(非负数),失败-1
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
功能:向消息队列发送一条消息
参数:msqid :消息队列的ID
msgp :发送消息的地址
msgsz :消息中消息信息的大小
msgflg:0阻塞,IPC_NOWAIT非阻塞模式
查看:ipcs -q
删除:ipcrm -q ID号
#endif
struct msgbuf{
long mtype; //消息类型(>0)
int a;
float b;
char c;
};
#define N sizeof(struct msgbuf) - sizeof(long)
int main(int argc, const char *argv[])
{
//产生一个key值
key_t key = ftok(".", 1);
//创建并打开消息队列
int msgid = msgget(key, IPC_CREAT|0664);
if(msgid == -1)
{
perror("msgget error");
exit(1);
}
//发送消息
struct msgbuf msgbuf;
msgbuf.mtype = 1;
msgbuf.a = 10;
msgbuf.b = 12.34;
msgbuf.c = 'A';
msgsnd(msgid, &msgbuf, N, 0);
msgbuf.mtype = 2;
msgbuf.a = 20;
msgbuf.b = 22.34;
msgbuf.c = 'B';
msgsnd(msgid, &msgbuf, N, 0);
msgbuf.mtype = 3;
msgbuf.a = 30;
msgbuf.b = 32.34;
msgbuf.c = 'C';
msgsnd(msgid, &msgbuf, N, 0);
return 0;
}
接收消息:接收消息类型为3的消息
/*消息队列:*/
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#if 0
int msgget(key_t key, int msgflg);
功能:创建并打开一个消息队列
返回:成功返回消息队列的ID(非负数),失败-1
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
功能:向消息队列发送一条消息
参数:msqid :消息队列的ID
msgp :发送消息的地址
msgsz :消息中消息信息的大小
msgflg:0阻塞,IPC_NOWAIT非阻塞模式
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
功能:接收消息
参数:msgtyp:=0 接收第一条消息
>0 接收类型为msgtyp的第一条消息
<0 接收小于等于|msgtyp|中最小的第一条数据
msgflg:0阻塞,IPC_NOWAIT非阻塞模式
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
功能:删除消息队列、获取属性信息、设置属性
参数:cmd:IPC_RMID IPC_STAT IPC_SET
返回:成功返回0 失败返回-1
查看:ipcs -q
删除:ipcrm -q ID号
#endif
struct msgbuf{
long mtype; //消息类型(>0)
int a;
float b;
char c;
};
#define N sizeof(struct msgbuf) - sizeof(long)
int main(int argc, const char *argv[])
{
//产生一个key值
key_t key = ftok(".", 1);
//创建并打开消息队列
int msgid = msgget(key, IPC_CREAT|0664);
if(msgid == -1)
{
perror("msgget error");
exit(1);
}
//接收消息
struct msgbuf msgbuf;
msgrcv(msgid, &msgbuf, N, 3, 0);
printf("a = %d, b = %.2f, c = %c\n",msgbuf.a, msgbuf.b, msgbuf.c);
//删除消息队列
system("ipcs -q");
msgctl(msgid, IPC_RMID, NULL);
system("ipcs -q");
return 0;
}
执行结果:
6、共享内存
共享内存是一种最为高效的进程间通信方式,进程可以直接读写内存,而不需要任何数据的拷贝。为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间。进程就可以直接读写这一内存区而不需要进行数据的拷贝,从而大大提高的效率。由于多个进程共享一段内存,因此也需要依靠某种同步机制,如互斥锁和信号量等 。
共享内存我们可以类比成普通程序中的全局变量,全局变量对于所有函数都是可见的,而共享内存对于多个进程都是可操作的,当然需要有一定的步骤,如下:
1、创建/打开共享内存
2、映射共享内存,即把指定的共享内存映射到进程的地址空间用于访问
3、撤销共享内存映射
4、删除共享内存对象
6.1 创建共享内存
6.2 映射共享内存
6.3 撤销共享内存
6.3 共享内存控制
7、信号灯
信号灯主要用于进程间的同步,对于信号量,我们可以用停车场外面指示牌简单类比一下,停车场是一个公用的资源,但能停的车是有限的,比如一个停车场最多可以停10辆车;停车场的指示牌最初显示是10,这是车辆可以进入停车场,每次进一辆指示牌就减一,当指示牌显示为0的时候,外面的车就不能再进入停车场,也就是不能占用这项资源。但此时如果有停车场内的车出来了,指示牌就可以加1,停车场这个资源又可以使用了。
信号灯使用步骤
1、创建信号量
2、初始化信号量
3、进行信号量PV操作,P操作也就是占用一个资源,信号量减1。V操作,释放一个资源,信号量加1。
4、删除信号量
7.1 创建信号量
所需头文件:#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数原型:int semget(key_t key, int nsems, int semflg);
功能:创建并打开信号灯
参数:key:键值;nsems:指定信号灯中信号量的个数。semflg:权限
返回:成功返回ID,失败-1
7.2 信号量控制
int semctl(int semid, int semnum, int cmd, union semun);
参数:semid 信号量ID
semnum:信号灯中信号量的编码(从0开始)
cmd :IPC_RMID(删除) IPC_STAT(获取信号量状态) IPC_SET
SETVAL(设置信号量值) GETVAL(获取信号量值)
semun :使用SETVAL使用第四个参数
7.3 操作信号量
int semop(int semid, struct sembuf *sops, unsigned nsops);
功能:执行PV操作
参数:nsops:指定同时操作信号量的个数
sops :
unsigned short sem_num; //信号量的编码
short sem_op; //正数执行V操作,负数执行P操作
short sem_flg; //0阻塞,IPC_NOWAIT非阻塞
返回:成功返回0,失败-1
例程:获取终端输入的数据,放入共享内存中,再打印出来。通过信号灯进行同步。
读取终端数据程序
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <string.h>
#define N 32
union semun{
int val;
};
int main(int argc, const char *argv[])
{
//key值
key_t key = ftok(".", 1);
/***************** 共享内存 ********************/
//创建共享内存
int shmid = shmget(key, N, IPC_CREAT|0664);
if(shmid == -1)
{
perror("shmget error");
exit(1);
}
//映射
char *p = NULL;
p = (char *)shmat(shmid, NULL, 0);
/****************** 信号灯 *********************/
//创建信号灯
int semid = semget(key, 1, IPC_CREAT|0664);
if(semid == -1)
{
perror("semget error");
exit(1);
}
//设置信号量值
union semun semun;
semun.val = 0;
semctl(semid, 0, SETVAL, semun);
//V操作(增加)
struct sembuf sembuf;
sembuf.sem_num = 0; //信号量编码
sembuf.sem_op = 1; //增加信号量值
sembuf.sem_flg = 0; //阻塞模式
/******************* 通信 **********************/
while(1)
{
//从终端获取数据写到共享内存
fgets(p, N, stdin);
//执行V操作(增加)
semop(semid, &sembuf, 1);
if(strncmp(p ,"quit",4) == 0)
{
break;
}
}
//解除映射
shmdt(p);
return 0;
}
从共享内存获取数据,并打印到终端中去
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <string.h>
#define N 32
int main(int argc, const char *argv[])
{
//key值
key_t key = ftok(".", 1);
/***************** 共享内存 ********************/
//创建共享内存
int shmid = shmget(key, N, IPC_CREAT|0664);
if(shmid == -1)
{
perror("shmget error");
exit(1);
}
//映射
char *p = NULL;
p = (char *)shmat(shmid, NULL, 0);
/****************** 信号灯 *********************/
//创建信号灯
int semid = semget(key, 1, IPC_CREAT|0664);
if(semid == -1)
{
perror("semget error");
exit(1);
}
//P操作(减少)
struct sembuf sembuf;
sembuf.sem_num = 0; //信号量编码
sembuf.sem_op = -1; //增加信号量值
sembuf.sem_flg = 0; //阻塞模式
/******************* 通信 **********************/
while(1)
{
//执行P操作(减少)
semop(semid, &sembuf, 1);
if(strncmp(p, "quit", 4) == 0)
{
break;
}
//从共享内存读取数据打印到终端
fputs(p, stdout);
}
/******************* 删除 **********************/
//解除映射
shmdt(p);
//删除共享内存
shmctl(shmid, IPC_RMID, NULL);
system("ipcs -m");
//删除信号灯
semctl(semid, 0, IPC_RMID);
system("ipcs -s");
return 0;
}