目录
1.匿名管道
2.命名管道
3.消息队列
4.共享内存
5.信号
6.信号量
概述
进程间的7种通信方式如下:
管道pipe: 管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
命名管道FIFO: 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
消息队列MessageQueue: 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
共享存储SharedMemory: 共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
信号量Semaphore: 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
套接字Socket: 套接口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
详细说明
1.匿名管道
从一个进程连接数据流到另一个进程时,就使用管道,通常是把一个进程的输出通过管道连接到另一个进程的输入。
函数
该函数的原型为int pipe(int file_descritor[2]),其参数是一个由两个整数类型的文件描述符组成的数组的指针,该函数在数组中填上两个新的文件描述符后返回0,如果失败返回-1.两个文件描述符以一种特殊的方式连接起来,写到file_descriptor[1]的数据都可以从file_descriptor[0]读回来。数据基于先进先出的原则进行处理。
2.读写规则
管道两端可分别用描述字fd[0]以及fd[1]来描述,需要注意的是,管道的两端是固定了任务的。即一端只能用于读,由描述字fd[0]表示,称其为管道 读端;另一端则只能用于写,由描述字fd[1]来表示,称其为管道写端。如果试图从管道写端读取数据,或者向管道读端写入数据都将导致错误发生。一般文件 的I/O函数都可以用于管道,如close、read、write等等。
2.1从管道中读取数据
如果管道的写端不存在,则认为已经读到了数据的末尾,读函数返回的读出字节数为0; 当管道的写端存在时,如果请求的字节数目大于PIPE_BUF,则返回管道中现有的数据字节数,如果请求的字节数目不大于PIPE_BUF,则返回管道中现 有数据字节数(此时,管道中数据量小于请求的数据量);或者返回请求的字节数(此时,管道中数据量不小于请求的数据量)。
2.2向管道中写入数据
向管道中写入数据时,linux将不保证写入的原子性,管道缓冲区一有空闲区域,写进程就会试图向管道写入数据。如果读进程不读走管道缓冲区中的数据,那么写操作将一直阻塞。
注:只有在管道的读端存在时,向管道中写入数据才有意义。
2.3read
read(fd,buf,nbyte)
功能:从fd所指示的文件中读出nbyte个字节的数据,并将它们送至由指针buf所指示的缓冲区中。如该文件被加锁,等待,直到锁打开为止。
2.4write
write(fd,buf,nbyte)
功能:把nbyte个字节的数据,从buf所指向的缓冲区写到由fd所指向的文件中。如文件加锁,暂停写入,直至开锁。
sprintf(str, format )
功能:根据参数format 字符串来转换并格式化数据,然后将结果复制到参数str所指的字符串数组,直到出现字符结束(‘\0’)为止。
2.6.程序源码
#include<sys/>
#include<>
#include<>
#include<>
#include<>
#include<>
int main(void)
{
pid_t pid1;
int fields[2];
char buffer[80];
char s[100];
char ss[100];
if(pipe(fields)!=0){
fprintf(stderr,"Createpipe error:%s\n\a",strerror(errno));
exit(1);
}
if((pid1=fork())<0)printf("fork child error!\n");
/* 子进程写入数据 */
if(pid1==0){
printf("fork child,child is sending a message !\n");
char s[]="hello!\n";
write(fields[1],s,sizeof(s));
exit(0)
}
/* 父进程读取数据 */
else
{
printf("parent read start !\n");
read(fields[0],buffer,80);
printf("parent receive the message:%s",buffer);
}
exit (0);
}
2.命名管道
特点:
- 有名管道fifo解决了pipe只能有关系的进程才能通信的问题
- 实现一个有名管道实际上就是实现一个FIFO文件,有名管道一旦建立,之后它的读,以及关闭操作都与普通管道完全相同。虽然FIFO文件的inode节点在磁盘上,但仅是一个节点而已,文件的数据还是存在内核缓冲页面上,和普通管道相同。
- 有名管道的文件仅仅是作为传输数据的通道,它并不存放传输的数据。
可以通过命令行mkfifo
的形式创建匿名管道:mkfifo myPipe
echo “hello ” > myPipe
cat < myPipe
使用函数创建:int mkfifo(const char * pathname,mode_t mode);
程序源码//读进程
#include<sys/>
#include<sys/>
#include<>
#include<>
#include<>
#include<>
#include<>
#include<>
#defineFIFO_PATH "myfifofile"
int main(){
int fd;
char cont_r[255];
#创建命名管道
if(mkfifo(FIFO_PATH,0666)<0 && errno!=EEXIST)
{
perror("create fifo failed");
return-1;
}
else {
printf("create fifo success\n");
#打开文件进行读操作
fd =open(FIFO_PATH,O_CREAT|O_RDONLY,0666);
if(fd>0)
{
while(1){
read(fd,cont_r,255);
printf("read:%s\n",cont_r);
}
close(fd);
}else
perror("open failed");
}
return0;
}
//写进程
#include<sys/>
#include<sys/>
#include<>
#include<>
#include<>
#include<>
#include<>
#include<>
#define FIFO_PATH "myfifofile"
int main(){
int fd;
char cont_w[] = "hello sundy";
if(mkfifo(FIFO_PATH,0666)<0&& errno!=EEXIST)
{
perror("create fifo failed");
return-1;
}
else
{
printf("create fifo success\n");
fd =open(FIFO_PATH,O_CREAT|O_WRONLY,0666);
if(fd>0)
{
while(1){
write(fd,cont_w,strlen(cont_w));
printf("write success\n");
sleep(2);
}
close(fd);
}else
perror("open failed");
}
return0;
}
3.消息队列
特点
- 由于管道不适合进程间频繁地交换数据,消息队列则可以解决这个问题。A进程要给B进程发送消息,A进程把数据放到对应的消息队列之后就可以正常返回了,B进程需要的时候再去读取数据就可以。
- 如果没有释放消息队列或者关闭操作系统,消息队列会一直存在,而匿名管道是随进程的创建而建立,随进程的结束而销毁。
- 消息队列本质上是位于内核空间的链表,链表的每个节点都是一条消息。每一条消息都有自己的消息类型,消息类型用整数来表示,而且必须大于 0。每种类型的消息都被对应的链表所维护。
消息队列不适合比较大的数据的传输。 - 相关函数
//创建和获取IPC内核对象int msgget(key_t key,int flags);
//将消息发送到消息队列int msgsnd(int msqid,const void *msgp,size_t msgsz,int msgflg);
//接收ssize_t msgrcv(int msqid,void *msgp,size_t msgsz,int msgflg);
//查看,设置,删除IPC内核对象int msgctl(int msqid,int cmd,struct msqid_ds *buf);
程序源码
// 写进程
#include<>
#include<sys/>
#include<sys/>
// 消息队列数据结构
typedef struct mesg_buffer{
long mesg_type;
char mesg_text[100];
} message;
int main()
{
key_t key;
int msgid;
// ftok to generate unique key
key = ftok("progfile", 65);
// msgget creates a message queue
// and returns identifier
msgid = msgget(key, 0666| IPC_CREAT);
message.mesg_type = 1;
printf("Write Data : ");
gets(message.mesg_text);
// msgsnd to send message
msgsnd(msgid, &message, sizeof(message), 0);
// display the message
printf("Data send is : %s \n", message.mesg_text);
return0;
}
// 读进程
#include<>
#include<sys/>
#include<sys/>
// structure for message queue
typedef struct mesg_buffer{
long mesg_type;
char mesg_text[100];
} message;
intmain()
{
key_t key;
int msgid;
// ftok to generate unique key
key = ftok("progfile", 65);
// msgget creates a message queue // and returns identifier
msgid = msgget(key, 0666| IPC_CREAT);
// msgrcv to receive message
msgrcv(msgid, &message, sizeof(message), 1, 0);
// display the message
printf("Data Received is : %s \n", message.mesg_text);
// to destroy the message queue
msgctl(msgid, IPC_RMID, NULL);
return 0;
}
- 注意: 读进程会先运行会发生阻塞,等待写进程发送数据
4.共享内存
特点:
- 消息队列的读取和写入的过程,都会发生用户态与内核态之间的消息拷贝过程。共享内存很好的解决了这一问题。
- 每个进程都会维护一个从内存地址到虚拟内存页面之间的映射关系。尽管每个进程都有自己的内存地址,不同的进程可以同时将同一个内存页面映射到自己的地址空间中,从而达到共享内存的目的。
- 所有进程共享同一块内存,共享内存在各种进程间通信方式中具有最高的效率。访问共享内存区域和访问进程独有的内存区域一样快,并不需要通过系统调用或者其它需要切入内核的过程来完成。同时它也避免了对数据的各种不必要的复制。
- 共享内存的几乎可以认为没有上限,它也是不局限与父子进程,采用跟消息队列类似的定位方式,因为内存是共享的,不存在任何单向的限制,最大的问题就是需要应用程序自己做互斥。
- 相关函数
shmget:申请共享内存
shmat:建立用户进程空间到共享内存的映射
shmdt:解除映射关系
shmctl:回收共享内存空间
- 程序源码
//写进程
#include<sys/>
#include<sys/>
#include<>
int main()
{
// ftok to generate unique key
key_t key = ftok("shmfile",65);
// shmget returns an identifier in shmid
int shmid = shmget(key,1024,0666|IPC_CREAT);
// shmat to attach to shared memory
char *str = (char*) shmat(shmid,(void*)0,0);
gets(str);
printf("Data written in memory: %s\n",str);
//detach from shared memory
shmdt(str);
return0;
}
//读进程
#include<sys/>
#include<sys/>
#include<>
intmain()
{
// ftok to generate unique key
key_t key = ftok("shmfile",65);
// shmget returns an identifier in shmid
int shmid = shmget(key,1024,0666|IPC_CREAT);
// shmat to attach to shared memory
char *str = (char*) shmat(shmid,(void*)0,0);
printf("Data read from memory: %s\n",str);
//detach from shared memory
shmdt(str);
// destroy the shared memory
shmctl(shmid,IPC_RMID,NULL);
return 0;
}
- 注意:先读的话读进程并不会发生阻塞,等待写进程。
5.信号
信号是linux系统响应某些条件而产生的一个事件,接收到该信号的进程会相应的采取一些行动。可作为进程间传递消息的一种方式,信号可以被生成、捕获、响应或忽略。信号在中定义,信号的名称都以SIG开头如:SIGALRM 超时警告;SIGINT:终端中断。 如果进程接收到这些信号中的一个,但是事先没有安排捕获它,进程将会立刻终止。
- 发送信号
进程通过调用kill函数向其他进程发送一个信号,成功时返回0,失败时返回-1
其定义为int kill(pid_t pid, int sig),kill函数把sig给定的信号发送给参数pid给出的进程号所指定的进程。 - 函数
该函数的定义为void (*signal(int sig, void (*func)(int)))(int),带有sig和func两个参数,准备捕获的信号为sig参数,接收到指定的信号后将要调用的函数由参数func指定,信号处理函数必须有一个int类型的参数(即接受到的信号代码)并且返回类型为void. - 3.程序源码
以模拟闹钟的形式,通过一个进程向另一个进程发送SIGALRM信号来表现进程间的通信。
#include<sys/>
#include<>
#include<>
#include<>
#include<>
static int alarm_fired = 0;
/*该函数用来模拟闹钟*
void ding(intsig){
alarm_fired =1;
}
/*main函数中告诉子进程在等待5秒后发送SIGALRM信号给它的父进程*/
int main(){
pid_tpid;
printf("alarm start\n");
pid = fork(); /*创建子进程*/
switch(pid)
{
case -1:
perror("fork failed");
exit(1);
case 0:
sleep(5); /*子进程休眠5秒*/
kill(getppid(), SIGALRM); /*子进程在5秒后将SIGALRM信号传递给父进程*/
exit(0);
}
/*父进程通过一个signal调用捕获SIGALRM信号的工作,等待该信号的到来*/
printf("waitting for alarm to go on\n");
(void) signal(SIGALRM, ding);
pause();
if(alarm_fired)
printf("ding!\n");
printf("done\n");
exit(0);
}
6.信号量
- 函数:创建信号量
该函数用来创建一个新信号量,其定义为:int semget(key_t key, int num, int sem_flags)
Key是整数值,程序对所有信号量的访问都是间接的,先提供一个键,再由系统生成一个信号量标识符。num_sem参数指定需要的信号量数目,一般取1;sem_flags参数是一组标志。
semget函数在成功时返回一个正数,也就是其他信号量函数用到的信号量标识符,失败时返回-1. - 函数:改变信号量的值
定义为:int semop(int sem_id, struct sembuf *sem_ops, size_t num_sem_ops)
sem_id 表示信号量标识符,semops指向一个结构数组的指针,每个数组元素至少包含以下几个成员:
struct sembuf {
short sem_num; //信号量编号,一般取0
short sem_op;//信号量需要改变的值,-1/+1
short sem_flg;//设置为SEM_UNDO
} - 函数:控制信号量的信息
定义为:int semctl(int sem_id, int sem_num, int command,…)
sem_id:表示信号量标识符,sem_num表示信号量编号一般取0,command参数是将要采取的行动,如:SETVAL:用来把信号量初始化为一个已知的值,作用就是在信号量第一次使用前对它进行设置。IPC_RMID用于删除一个不再继续使用的信号量标识符。如果还有第四个参数,它是一个union semun结构(该联合结构可能需自己定义,可通过查阅semctl的手册查看是否给出了该定义)
union semun{
int val;
struct semid_ds *buf;
unsigned short *array;
}
- 4.程序源码
让两个进程分别向显示器(linux下一切皆文件,临界资源)打印AA和BB,当没有信号量进行保护时,会出现数据混乱,例如:“AABBABAAAB…”,为了解决这一问题,我们创建信号量进行保护。打印“AA”或“BB”
#include <>
#include <>
#include <>
#include <sys/>
union semun
{
int val;
struct semid_ds *buf;
unsigned short *array;
// struct seminfo *buff;
};
static int set_semvalue(void);
static void del_semvalue(void);
static int semaphore_p(void);
static int semaphore_v(void);
static int sem_id;
int main(int argc, char *argv[])
{
int i;
int pause_time;
char op_char = 'O';
srand((unsigned int)getpid());
sem_id = semget((key_t)1234, 1, 0666 | IPC_CREAT);
/* 如果程序第一个被调用,也就是调用时含有一个参数,使得argc>1,此时就调用set_semvalue初始化信号量,并将op_char设置为x*/
if (argc > 1)
{
if (!set_semvalue())
{
fprintf(stderr, "Failed to initialize semaphore\n");
exit(EXIT_FAILURE);
}
op_char = 'X';
sleep(2);
}
/*进入和离开临界区10次,每次循环开始的时候首先调用semaphore_p函数,它在程序将进入临界区域时设置信号量以等待进入*/
for (i = 0; i < 10; i++)
{
if (!semaphore_p())
exit(EXIT_FAILURE);
printf("%c", op_char);
fflush(stdout);
pause_time = rand() % 3;
sleep(pause_time);
printf("%c", op_char);
fflush(stdout);
/*进入临界区域后,调用semaphore_v将信号量设置为可用,然后等待一段随机的时间,再进入下一次循环*/
if (!semaphore_v())
exit(EXIT_FAILURE);
pause_time = rand() % 2;
sleep(pause_time);
}
printf("\n%d - finished\n", getpid());
if (argc > 1)
{
sleep(10);
del_semvalue();
}
exit(EXIT_SUCCESS);
}
/*该函数用来将semctl调用的command参数设置为SETVAL来初始化信号量*/
static int set_semvalue(void)
{
union semun sem_union;
sem_union.val = 1;
if (semctl(sem_id, 0, SETVAL, sem_union) == -1)
return0;
return (1);
}
/*通过调用semctl调用的command设置为IPC_RMID来删除信号量ID*/
static void del_semvalue(void)
{
union semun sem_union;
if (semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
fprintf(stderr, "Failed to delete semaphore");
}
/*对信号量执行减1操作*/
static int semaphore_p(void)
{
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = -1;
sem_b.sem_flg = SEM_UNDO;
if (semop(sem_id, &sem_b, 1) == -1)
{
fprintf(stderr, "semaphore_p failed\n");
return (0);
}
return (1);
}
/*对信号量执行加1操作*/
static int semaphore_v(void)
{
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = 1;
sem_b.sem_flg = SEM_UNDO;
if (semop(sem_id, &sem_b, 1) == -1)
{
fprintf(stderr, "semaphore_v failed\n");
return (0);
}
return (1);
}
- 5.附代码2:父子进程间的信号量
与上述代码几乎相同,只不过变为了父子进程间的信号量机制。
#include <>
#include <>
#include <>
#include <sys/>
#include <sys/>
#include <sys/>
static int set_semvalue(void);
static void del_semvalue(void);
static int semaphore_p(void);
static int semaphore_v(void);
static int sem_id;
union semun
{
int val;
struct semid_ds *buf;
unsigned short *array;
// struct seminfo *buff;
};
int main(int argc, char *argv[])
{
int i;
int pause_time;
char op_char = 'O';
srand((unsigned int)getpid());
sem_id = semget((key_t)1234, 1, 0666 | IPC_CREAT);
int id = fork();
if (id < 0)
{
perror("fork failed\n");
return -1;
}
else if (id > 0)
{
if (!set_semvalue())
{
fprintf(stderr, "Failed to initialize semaphore\n");
exit(EXIT_FAILURE);
}
op_char = 'X';
sleep(2);
}
for (i = 0; i < 10; i++)
{
if (!semaphore_p())
exit(EXIT_FAILURE);
printf("%c", op_char);
fflush(stdout);
pause_time = rand() % 3;
sleep(pause_time);
printf("%c", op_char);
fflush(stdout);
if (!semaphore_v())
exit(EXIT_FAILURE);
pause_time = rand() % 2;
sleep(pause_time);
}
printf("\n%d - finished\n", getpid());
if (id > 0)
{
sleep(10);
del_semvalue();
}
exit(EXIT_SUCCESS);
}
static int set_semvalue(void)
{
union semun sem_union;
sem_union.val = 1;
if (semctl(sem_id, 0, SETVAL, sem_union) == -1)
return0;
return (1);
}
static void del_semvalue(void)
{
union semun sem_union;
if (semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
fprintf(stderr, "Failed to delete semaphore");
}
static int semaphore_p(void)
{
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = -1;
sem_b.sem_flg = SEM_UNDO;
if (semop(sem_id, &sem_b, 1) == -1)
{
fprintf(stderr, "semaphore_p failed\n");
return (0);
}
return (1);
}
static int semaphore_v(void)
{
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = 1;
sem_b.sem_flg = SEM_UNDO;
if (semop(sem_id, &sem_b, 1) == -1)
{
fprintf(stderr, "semaphore_v failed\n");
return (0);
}
return (1);
}
-
socket即套接字是一种通信机制,凭借这种机制,客户/服务器(即要进行通信的进程)系统的开发工作既可以在本地单机上进行,也可以跨网络进行。也就是说它可以让不在同一台计算机但通过网络连接计算机上的进程进行通信。也因为这样,套接字明确地将客户端和服务器区分开来。
-
程序源码
//服务器
#include<>
#include<sys/>
#include<sys/>
#include<netinet/>
#include<>
#include<>
#include<>
int main()
{
int server_sockfd = -1;
int client_sockfd = -1;
int client_len = 0;
struct sockaddr_in server_addr;
struct sockaddr_in client_addr;
//创建流套接字
server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
//设置服务器接收的连接地址和监听的端口
server_addr.sin_family = AF_INET;
//指定网络套接字
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
//接受所有IP地址的连接
server_addr.sin_port = htons(9736);//绑定到9736端口
//绑定(命名)套接字
bind(server_sockfd, (structsockaddr*)&server_addr, sizeof(server_addr));
//创建套接字队列,监听套接字
listen(server_sockfd, 5);
//忽略子进程停止或退出信号
signal(SIGCHLD, SIG_IGN);
while(1)
{
charch = ‘\0’;
client_len = sizeof(client_addr);
printf(“Server waiting\n”);
//接受连接,创建新的套接字
client_sockfd = accept(server_sockfd, (structsockaddr*)&client_addr, &client_len);
if(fork() == 0)
{
//子进程中,读取客户端发过来的信息,处理信息,再发送给客户端
read(client_sockfd, &ch, 1);
sleep(5);
ch++;
write(client_sockfd, &ch, 1);
close(client_sockfd);
exit(0);
}
else
{
//父进程中,关闭套接字
close(client_sockfd);
}
}
}
//客户端
#include<>
#include<sys/>
#include<sys/>
#include<netinet/>
#include<arpa/>
#include<>
#include<>
int main()
{
int sockfd = -1;
int len = 0;
struct sockaddr_in address;
in tresult;
char ch = 'A';
//创建流套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
//设置要连接的服务器的信息
address.sin_family = AF_INET;
//使用网络套接字
address.sin_addr.s_addr = inet_addr("127.0.0.1");
//服务器地址
address.sin_port = htons(9736);
//服务器所监听的端口
len = sizeof(address);
//连接到服务器
result = connect(sockfd, (structsockaddr*)&address, len);
if(result == -1)
{
perror("ops:client\n");
exit(1);
}
//发送请求给服务器 write(sockfd, &ch, 1);
//从服务器获取数据 read(sockfd, &ch, 1);
printf("char form server = %c\n", ch);
close(sockfd);
exit(0);
}