进程间通信就是在不同进程之间传播或交换信息,那么不同进程之间存在着什么双方都可以访问的介质呢?进程的用户空间是互相独立的,一般而言是不能互相访问的,唯一的例外是共享内存区。另外,系统空间是“公共场所”,各进程均可以访问,所以内核也可以提供这样的条件。此外,还有双方都可以访问的外设。在这个意义上,两个进程当然也可以通过磁盘上的普通文件交换信息,或者通过“注册表”或其它数据库中的某些表项和记录交换信息。广义上这也是进程间通信的手段,但是一般都不把这算作“进程间通信”。
特点:
管道到创建:
#include
int pipe(int pipefd[2]);
参数:
pipefd是一个由两个元素的数组首元素地址,用来存储管道的读文件描述符和写文件描述符;
pipefd[0]表示的是管道的读端,pipefd[1]表示的是管道的写端;
返回值:
成功返回0;
失败返回-1,且修改errno的值;
1 #include<stdio.h>
2 #include<unistd.h>
3
4 int main()
5 {
6 int fd[2]; // 两个文件描述符
7 pid_t pid;
8 char buff[20];
9
10 if(pipe(fd) < 0) // 创建管道
11 printf("Create Pipe Error!\n");
12
13 if((pid = fork()) < 0) // 创建子进程
14 printf("Fork Error!\n");
15 else if(pid > 0) // 父进程
16 {
17 close(fd[0]); // 关闭读端
18 write(fd[1], "hello world\n", 12);
19 }
20 else
21 {
22 close(fd[1]); // 关闭写端
23 read(fd[0], buff, 20);
24 printf("%s", buff);
25 }
26
27 return 0;
28 }
在内核中创建的管道,同时在文件系统中创建管道的名称,不同的进程可以通过名称去访问同一个管道,从而实现任意进程之间的通信。
特点:
管道创建:
#include
#include
int mkfifo(const char *pathname, mode_t mode);
参数:
参数1:pathname表示所创建管道的名称(绝对路径/相对路径)
参数2:mode表示的管道的访问权限
返回值:
成功返回0;
失败返回-1,且修改errno的值;
PS:
read.c
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<errno.h>
4 #include<fcntl.h>
5 #include<sys/stat.h>
6
7 int main()
8 {
9 int fd;
10 int len;
11 char buf[1024];
12
13 if(mkfifo("myfifo", 0666) < 0 && errno!=EEXIST) // 创建FIFO管道 后面的判断是判断管道是否已经存在,如果已经存在就跳过创建
14 perror("Create FIFO Failed");
15
16 if((fd = open("myfifo", O_RDONLY)) < 0) // 以只读打开FIFO文件
17 {
18 perror("Open FIFO Failed"); //判断是否打开成功
19 exit(1);
20 }
21
22 while((len = read(fd, buf, 1024)) > 0) // 读取FIFO管道
23 printf("Read message: %s", buf);
24
25 close(fd); // 关闭FIFO文件
26 return 0;
27 }
write.c
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<fcntl.h>
4 #include<sys/stat.h>
5 #include<time.h> // time
6
7 int main()
8 {
9 int fd;
10 int n, i;
11 char buf[1024];
12 time_t tp;
13
14 printf("I am %d process.\n", getpid()); // 说明进程ID
15
16 if((fd = open("myfifo", O_WRONLY)) < 0) // 以写打开一个FIFO
17 {
18 perror("Open FIFO Failed");
19 exit(1);
20 }
21
22 for(i=0; i<5; ++i)
23 {
24 time(&tp); // 取系统当前时间
25 n=sprintf(buf,"Process %d's time is %s",getpid(),ctime(&tp));
26 printf("Send message: %s", buf); // 打印
27 if(write(fd, buf, n+1) < 0) // 写入到FIFO中
28 {
29 perror("Write FIFO Failed");
30 close(fd);
31 exit(1);
32 }
33 sleep(1); // 休眠1秒
34 }
35
36 close(fd); // 关闭FIFO文件
37 return 0;
38 }
特征:
#include
#include
int kill(pid_t pid, int sig);
参数:
参数1:pid表示信号接收进程的进程id;
参数2:sig表示发送的信号编号
返回值:
成功返回0;
失败返回-1,且修改errno的值;
通过raise给调用者进程发送信号
#include
int raise(int sig);
参数:sig表示发送的信号编号
通过函数alarm给调用者发送SIGALRM信号;
#include
unsigned int alarm(unsigned int seconds);
参数:seconds表示的是定时器的初始值(以秒为单位);
返回值:
成功:如果进程没有设置闹钟,则返回0;
如果之前已经设置过闹钟,返回的是上一次闹钟的剩余时间;
失败返回-1,且修改errno的值;
信号处理:
通过signal函数实现信号的注册:将信号和信号处理函数关联(当信号产生,就会去执行相应的信号处理函数)
void (*signal(int signum, void (*handler)(int)))(int);
采用的原则:右左原则,
signum:int类型的变量;
handler:指针 函数(参数是int,返回值void) ==> 参数为int,返回值为void的函数指针;
signal:函数(参数有两个(int 函数指针), 返回值:指针 函数(参数为int ,返回值void))
typedef void(* handler_t)(int)
hander_t signal(int signum, handler_t handler);
参数:
参数1:signum表示需要注册的信号编号
参数2: handler表示信号的处理方式:
SIG_IGN:信号忽略;
SIG_DFL:缺省处理方式;
自定义信号处理函数,当信号产生,就会去执行信号处理函数
思路:
key --> IPC对象 --> 操作IPC对象
key == 0: 每次创建都会得到一个新的IPC对象;
key != 0: 在创建的时候,如果IPC对象不存在,则创建新的IPC对象,如果IPC对象存在,则访问原有的IPC对象
key的获取:
手动设置:使用大于等于0的整数;
自动生成:
#include
#include
key_t ftok(const char *pathname, int proj_id);
参数:
参数1:pathname表示的是程序的执行路径(相对路径/绝对路径);
参数2:proj_id表示的是程序的项目编号(只需要使用低8位的值);
返回值:
成功返回key
失败返回-1,且修改errno的值。
特点:
实现流程:
#include
#include
int shmget(key_t key, size_t size, int shmflg);
参数:
参数1:key用来申请共享内存
参数2:size表示申请共享内存空间的大小;
参数3: shmflg表示共享内存的访问权限和操作方式,需要创建设置IPC_CREAT
返回值:
成功返回共享内存的id;
失败返回-1,且修改errno的值。
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数:
参数1:shmid表示需要映射共享内存的id号;
参数2:shmaddr表示映射空间的起始地址,
手动设置:给定一个用户空间地址,必须保证足够的空间未被使用,在去完成映射,映射成功的地址就是传递的地址;
自动映射:由系统自动进行分配,用户只需要使用NULL填充;
参数3:shmflg表示映射后对于共享内存的访问权限
shmflg = 0, 表示使用缺省属性,可以进行读写访问;
shmflg = SHM_RDONLY, 表示设置为只读访问;
返回值:
成功返回映射后的起始地址;
失败返回(void *) -1,且修改errno的值。
共享内存的读写,就是直接对指针的操作
共享内存解映射:用户空间不能再去访问内核空间的地址
int shmdt(const void *shmaddr);
参数:shmaddr表示解除映射空间的起始地址
返回值:
成功返回0;
失败返回-1,且修改errno的值。
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:
参数1:shmid表示需要操控的共享内存的id号;
参数2:cmd操控请求命令;
IPC_STAT:获取共享内存的属性,属性值使用buf来接受;
IPC_SET:设置共享的属性,属性值由buf传递;
IPC_RMID:删除共享内存,参数3无意义使用NULL填充;
参数3:buf表示命令的传输数据
返回值:
成功返回0;
失败返回-1,且修改errno的值。
示例代码:
#include
#include
#include
#include
#include
#include
#include
/*初始化共享内存*/
int shm_init(void **shmaddr,size_t size)
{
int ret,shmid;
key_t key;
key = ftok(".",'a');
if(key == -1)
{
perror("ftok");
return -1;
}
/*创建共享内存*/
shmid = shmget(key,size,IPC_CREAT | 0777);
if(shmid == -1)
{
perror("shmget");
return -1;
}
/*映射共享内存*/
*shmaddr = shmat(shmid,NULL,0);
if(*shmaddr == (void *)-1)
{
perror("shmat");
return -1;
}
return shmid;
}
/*取消共享内存*/
int shm_uninit(int shmid,void **shmaddr)
{
int ret;
/*分离共享内存*/
ret = shmdt(*shmaddr);
if(ret == -1)
{
perror("shmdt");
return -1;
}
/*删除共享内存 id号*/
ret = shmctl(shmid,IPC_RMID,NULL);
if(ret == -1)
{
perror("shmctl");
return -1;
}
return 0;
}
int main(void)
{
void *shmaddr; //共享内存的首地址
int ret;
int shmid;
pid_t pid;
shmid = shm_init(&shmaddr,128);
if(shmid == -1)
{
return -1;
}
pid = fork();
if(pid == -1)
{
perror("fork");
return -1;
}
else if(pid == 0) //子进程输入
{
while(1)
{
fgets(shmaddr,128,stdin);
}
exit(EXIT_SUCCESS);
}
while(1) //父进程打印
{
printf("shmaddr:%s\n",(char *)shmaddr);
}
ret = shm_uninit(shmid,&shmaddr);
return 0;
}
以上在父子进程中来展示共享内存,当然也可以任意进程之间
特点:
实现流程:
#include
#include
#include
int msgget(key_t key, int msgflg);
参数:
参数1:key用来申请消息队列
IPC_PRIVATE:(key_t)0,在每次调用msgget都会得到一个新的消息队列
非IPC_PRIVATE:在创建的时候,如果IPC对象不存在,则创建新的IPC对象,如果IPC对象存在,则访问原有的IPC对象
参数2:msgflg表示消息队列的权限和操作方式
如果要创建消息队列,使用IPC_CREAT
返回值:
成功返回消息队列的id号;
失败返回-1,且修改errno的值。
#include
#include
#include
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
参数:
参数1:msqid表示发送消息的消息队列id
参数2:msgq结构体指针,表示发送消息的存储空间的起始地址
struct msgbuf
{
long mtype; /* message type, must be > 0 */
char mtext[1]; /* message data,消息的存储空间,用户需要根据消息的字节数来自定义大小,需要小于等于MSGMNB */
};
参数3: msgsz表示的消息内容数组空间的大小;
参数4:msgflg表示消息发送的方式:
0:表示阻塞方式发送,只有当消息发送完成才会返回;
IPC_NOWAIT:非阻塞方式发送,不管消息是否发送完成,都会立即返回;
返回值:
成功返回0;
失败返回-1,且修改errno的值。
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
参数:
参数1:msqid表示接收消息的消息队列id
参数2:msgq结构体指针,表示接收消息的存储空间的起始地址;
参数3:msgsz表示的消息内容数组空间的大小;
参数4:msgtyp接收消息的类型:
msgtyp = 0, 接收消息对列中的第一条消息;
msgtyp > 0, 接收消息队列中类型为msgtyp的第一条消息;
msgtyp < 0, 接收消息队列中类型<=|msgtyp|中类型最小的第一条消息;
参数5:msgflg表示消息发送的方式:
0:表示阻塞方式接收,只有当消息接收完成才会返回;
IPC_NOWAIT:非阻塞方式接收,不管消息是否接收完成,都会立即返回;
#include
#include
#include
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
参数:
参数1:msqid表示需要操控的消息队列的id号;
参数2:cmd操控请求命令;
IPC_STAT:获取消息队列的属性,属性值使用buf来接受;
IPC_SET:设置消息队列的属性,属性值由buf传递;
IPC_RMID:删除消息队列,参数3无效可以是NULL填充;
参数3:buf表示命令的传输数据
返回值:
成功返回0;
失败返回-1,且修改errno的值。
示例代码:
msgrcv.c
#include
#include
#include
#include
#include
#include
#include
#include
/*mtype 对应才能相互接收发送*/
struct msgbuf {
long mtype; /* message type, must be > 0 */
char mtext[128]; /* message data */
};
/*初始化消息队列*/
int msg_init(void)
{
/*创建IPV key值*/
key_t key=ftok(".", 'b');
if(key == -1)
{
perror("ftok");
return -1;
}
/*创建消息队列*/
int msgid=msgget(key, IPC_CREAT | 0777);
if(msgid == -1)
{
perror("msgget");
return -1;
}
return msgid;
}
/*删除消息队列*/
int msg_uninit(int msgid)
{
int ret = msgctl(msgid,IPC_RMID,NULL);
if(ret == -1)
{
perror("msgctl->IPC_RMTD");
return -1;
}
return 0;
}
int main(void)
{
struct msgbuf buf;
int msgid, ret;
/*初始化消息队列*/
msgid = msg_init();
if(msgid == -1)
{
exit(EXIT_FAILURE);
}
buf.mtype=2;
while(1)
{
ret = msgrcv(msgid,(void *)&buf, 128,buf.mtype,0);
if(ret == -1)
{
perror("msgsnd");
return -1;
}
printf("rcv:%s",buf.mtext);
}
/*删除消息队列ID*/
ret = msg_uninit(msgid);
if(ret == -1)
{
exit(EXIT_FAILURE);
}
return 0;
}
msgsnd.c
#include
#include
#include
#include
#include
#include
#include
#include
struct msgbuf {
long mtype; /* message type, must be > 0 */
char mtext[128]; /* message data */
};
/*初始化消息队列*/
int msg_init(void)
{
/*创建IPV key值*/
key_t key=ftok(".", 'b');
if(key == -1)
{
perror("ftok");
return -1;
}
/*创建消息队列*/
int msgid=msgget(key, IPC_CREAT | 0777);
if(msgid == -1)
{
perror("msgget");
return -1;
}
return msgid;
}
/*删除消息队列*/
int msg_uninit(int msgid)
{
int ret = msgctl(msgid,IPC_RMID,NULL);
if(ret == -1)
{
perror("msgctl->IPC_RMTD");
return -1;
}
return 0;
}
int main(void)
{
struct msgbuf buf;
int msgid, ret;
/*初始化消息队列*/
msgid = msg_init();
if(msgid == -1)
{
exit(EXIT_FAILURE);
}
while(1)
{
scanf("%ld",&(buf.mtype));
getchar();
fgets(buf.mtext,128,stdin);
ret = msgsnd(msgid, (void*)&buf, 128, 0);
if(ret == -1)
{
perror("msgsnd");
return -1;
}
}
/*删除消息队列ID*/
ret = msg_uninit(msgid);
if(ret == -1)
{
exit(EXIT_FAILURE);
}
return 0;
}
实现流程:
#include
#include
#include
int semget(key_t key, int nsems, int semflg);
参数:
参数1:key用来申请信号灯集
参数2:nsems表示信号灯集合中信号灯的数量
参数3:semflg表示信号灯集的操作方式和访问权限
返回值:
成功返回信号灯集的id
失败返回-1,且修改errno的值。
#include
#include
#include
int semctl(int semid, int semnum, int cmd, ...)
参数:
参数1:semid表示所要操控的信号灯集id;
参数2:semnum表示信号灯集中信号灯的序号(0.1.2.3 .......)
参数3:cmd操控请求命令;
IPC_RMID: 删除信号灯
SETVAL:设置信号灯的值,将值存储到共用体semun中
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) */
};
GETVAL:获取信号灯的值。
返回值:
失败返回-1,且修改errno的值。
使用套接字除了可以实现网络间不同主机间的通信外,还可以实现同一主机的不同进程间的通信,且建立的通信是双向的通信。socket进程通信与网络通信使用的是统一套接口,只是地址结构与某些参数不同。
(1)创建socket,类型为AF_LOCAL或AF_UNIX,表示用于进程通信:
创建套接字需要使用 socket 系统调用,其原型如下:
int socket(int domain, int type, int protocol);
其中,domain 参数指定协议族,对于本地套接字来说,其值须被置为 AF_UNIX 枚举值;type 参数指定套接字类型,protocol 参数指定具体协议;type 参数可被设置为 SOCK_STREAM(流式套接字)或 SOCK_DGRAM(数据报式套接字),protocol 字段应被设置为 0;其返回值为生成的套接字描述符。
对于本地套接字来说,流式套接字(SOCK_STREAM)是一个有顺序的、可靠的双向字节流,相当于在本地进程之间建立起一条数据通道;数据报式套接字(SOCK_DGRAM)相当于单纯的发送消息,在进程通信过程中,理论上可能会有信息丢失、复制或者不按先后次序到达的情况,但由于其在本地通信,不通过外界网络,这些情况出现的概率很小。
SOCK_STREAM 式本地套接字的通信双方均需要具有本地地址,其中服务器端的本地地址需要明确指定,指定方法是使用 struct sockaddr_un 类型的变量。
struct sockaddr_un {
sa_family_t sun_family; /* AF_UNIX /
char sun_path[UNIX_PATH_MAX]; / 路径名 */
};
这里面有一个很关键的东西,socket进程通信命名方式有两种。一是普通的命名,socket会根据此命名创建一个同名的socket文件,客户端连接的时候通过读取该socket文件连接到socket服务端。这种方式的弊端是服务端必须对socket文件的路径具备写权限,客户端必须知道socket文件路径,且必须对该路径有读权限。
另外一种命名方式是抽象命名空间,这种方式不需要创建socket文件,只需要命名一个全局名字,即可让客户端根据此名字进行连接。后者的实现过程与前者的差别是,后者在对地址结构成员sun_path数组赋值的时候,必须把第一个字节置0,即sun_path[0] = 0,
其中,offsetof函数在#include 头文件中定义。因第二种方式的首字节置0,
提示:客户端连接服务器的时候,必须与服务端的命名方式相同,即如果服务端是普通命名方式,客户端的地址也必须是普通命名方式;如果服务端是抽象命名方式,客户端的地址也必须是抽象命名方式。
SOCK_STREAM 式本地套接字的通信双方均需要具有本地地址,其中服务器端的本地地址需要明确指定,指定方法是使用 struct sockaddr_un 类型的变量,将相应字段赋值,再将其绑定在创建的服务器套接字上,绑定要使用 bind 系统调用,其原形如下:
int bind(int socket, const struct sockaddr *address, size_t address_len);
其中 socket表示服务器端的套接字描述符,address 表示需要绑定的本地地址,是一个 struct sockaddr_un 类型的变量,address_len 表示该本地地址的字节长度。实现服务器端地址指定功能的代码如下(假设服务器端已经通过上文所述的 socket 系统调用创建了套接字,server_sockfd 为其套接字描述符):
struct sockaddr_un server_address;
server_address.sun_family = AF_UNIX;
strcpy(server_address.sun_path, “Server Socket”);
bind(server_sockfd, (struct sockaddr*)&server_address, sizeof(server_address));
客户端的本地地址不用显式指定,只需能连接到服务器端即可,因此,客户端的 struct sockaddr_un 类型变量需要根据服务器的设置情况来设置,代码如下(假设客户端已经通过上文所述的 socket 系统调用创建了套接字,client_sockfd 为其套接字描述符):
struct sockaddr_un client_address;
client_address.sun_family = AF_UNIX;
strcpy(client_address.sun_path, “Server Socket”);
服务器端套接字创建完毕并赋予本地地址值(名称,本例中为Server Socket)后,需要进行监听,等待客户端连接并处理请求,监听使用 listen 系统调用,接受客户端连接使用accept系统调用,它们的原形如下:
int listen(int socket, int backlog);
int accept(int socket, struct sockaddr *address, size_t address_len);
其中 socket 表示服务器端的套接字描述符;backlog 表示排队连接队列的长度(若有多个客户端同时连接,则需要进行排队);address 表示当前连接客户端的本地地址,该参数为输出参数,是客户端传递过来的关于自身的信息;address_len 表示当前连接客户端本地地址的字节长度,这个参数既是输入参数,又是输出参数。实现监听、接受和处理的代码如下:
#define MAX_CONNECTION_NUMBER 10
int server_client_length, server_client_sockfd;
struct sockaddr_un server_client_address;
listen(server_sockfd, MAX_CONNECTION_NUMBER);
while(1)
{
// … (some process code)
server_client_length = sizeof(server_client_address);
server_client_sockfd = accept(server_sockfd, (struct sockaddr)&server_client_address, &server_client_length);
// … (some process code)
}
这里使用死循环的原因是服务器是一个不断提供服务的实体,它需要不间断的进行监听、接受并处理连接,本例中,每个连接只能进行串行处理,即一个连接处理完后,才能进行后续连接的处理。如果想要多个连接并发处理,则需要创建线程,将每个连接交给相应的线程并发处理。
客户端套接字创建完毕并赋予本地地址值后,需要连接到服务器端进行通信,让服务器端为其提供处理服务。对于 SOCK_STREAM 类型的流式套接字,需要客户端与服务器之间进行连接方可使用。连接要使用 connect 系统调用,其原形为
int connect(int socket, const struct sockaddr *address, size_t address_len);
其中socket为客户端的套接字描述符,address表示当前客户端的本地地址,是一个 struct sockaddr_un 类型的变量,address_len 表示本地地址的字节长度。实现连接的代码如下:
connect(client_sockfd, (struct sockaddr*)&client_address, sizeof(client_address));
无论客户端还是服务器,都要和对方进行数据上的交互,这种交互也正是我们进程通信的主题。一个进程扮演客户端的角色,另外一个进程扮演服务器的角色,两个进程之间相互发送接收数据,这就是基于本地套接字的进程通信。发送和接收数据要使用 write 和 read 系统调用,它们的原形为:
int read(int socket, char *buffer, size_t len);
int write(int socket, char *buffer, size_t len);
其中 socket 为套接字描述符;len 为需要发送或需要接收的数据长度;对于 read 系统调用,buffer 是用来存放接收数据的缓冲区,即接收来的数据存入其中,是一个输出参数;对于 write 系统调用,buffer 用来存放需要发送出去的数据,即 buffer 内的数据被发送出去,是一个输入参数;返回值为已经发送或接收的数据长度。例如客户端要发送一个 “Hello” 字符串给服务器,则代码如下:
char buffer[10] = “Hello”;
write(client_sockfd, buffer, strlen(buffer));
交互完成后,需要将连接断开以节省资源,使用close系统调用,其原形为:
int close(int socket);
socket本地通信不怎么使用,一般在网络编程中使用socket比较多,socket的网络编程看另外一篇文章,每一种进程间通信都有自己的优缺点,根据不同的情况选择不同的方式。