IPC进程间通信及示例代码

一. 什么是进程通信

        进程通信( InterProcess Communication,IPC)就是指进程之间的信息交换。实际上,进程的同步与互斥本质上也是一种进程通信(这也就是待会我们会在进程通信机制中看见信号量和 PV 操作的原因了),只不过它传输的仅仅是信号量,通过修改信号量,使得进程之间建立联系,相互协调和协同工作,但是它缺乏传递数据的能力

        虽然存在某些情况,进程之间交换的信息量很少,比如仅仅交换某个状态信息,这样进程的同步与互斥机制完全可以胜任这项工作。但是大多数情况下,进程之间需要交换大批数据,比如传送一批信息或整个文件,这就需要通过一种新的通信机制来完成,也就是所谓的进程通信。

        再来从操作系统层面直观的看一些进程通信:我们知道,为了保证安全,每个进程的用户地址空间都是独立的,一般而言一个进程不能直接访问另一个进程的地址空间,不过内核空间是每个进程都共享的,所以进程之间想要进行信息交换就必须通过内核

下面就来我们来列举一下 Linux 内核提供的常见的进程通信机制:

  • 管道(也称作共享文件)
  • 消息队列(也称作消息传递)
  • 共享内存(也称作共享存储)
  • 信号量和 PV 操作
  • 信号
  • 套接字(Socket)

二. 管道

1.匿名管道

        管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。如果想实现相互通信(全双工通信),我们需要创建两个管道才行。另外,通过管道符 | 创建的管道是匿名管道,用完了就会被自动销毁。并且,匿名管道只能在具有亲缘关系(父子进程)的进程间使用。也就是说,匿名管道只能用于父子进程之间的通信

在 Linux 的实际编码中,是通过 pipe 函数来创建匿名管道的,若创建成功则返回 0,创建失败就返回 -1:

#include<sys/types.h>
#include<stdio.h>
#include<unistd.h>
#include<errno.h>
#include<stdlib.h>
#include<string.h>
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);
}

编译运行 

cc -o unamepipe unamepipe.c -g
./unamepipe

运行结果 

2.命名管道

        有名管道fifo解决了pipe只能有关系的进程才能通信的问题,实现一个有名管道实际上就是实现一个FIFO文件,有名管道一旦建立,之后它的读,以及关闭操作都与普通管道完全相同。虽然FIFO文件的inode节点在磁盘上,但仅是一个节点而已,文件的数据还是存在内核缓冲页面上,和普通管道相同。

使用 Linux 命令 mkfifo 来创建有名管道:

$ mkfifo myPipe

myPipe 就是这个管道的名称,接下来,我们往 myPipe 这个有名管道中写入数据: 

$ echo "hello" > myPipe

        执行这行命令后,你会发现它就停在这了,这是因为管道里的内容没有被读取,只有当管道里的数据被读完后,命令才可以正常退出。于是,我们执行另外一个命令来读取这个有名管道里的数据: 

$ cat < myPipe
hello

程序源码示例如下:

//读进程
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#define FIFO_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");

	}
	return 0;
}
//写进程
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

#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");

        }
        return 0;
}

编译运行
打开两个终端执行

三. 消息队列

        可以看出,管道这种进程通信方式虽然使用简单,但是效率比较低,不适合进程间频繁地交换数据,并且管道只能传输无格式的字节流。为此,消息传递机制(Linux 中称消息队列)应用而生。比如,A 进程要给 B 进程发送消息,A 进程把数据放在对应的消息队列后就可以正常返回了,B 进程在需要的时候自行去消息队列中读取数据就可以了。同样的,B 进程要给 A 进程发送消息也是如此。

消息队列的本质就是存放在内存中的消息的链表,而消息本质上是用户自定义的数据结构。如果进程从消息队列中读取了某个消息,这个消息就会被从消息队列中删除。对比一下管道机制:

  • 消息队列允许一个或多个进程向它写入或读取消息。
  • 消息队列可以实现消息的随机查询,不一定非要以先进先出的次序读取消息,也可以按消息的类型读取。比有名管道的先进先出原则更有优势。
  • 对于消息队列来说,在某个进程往一个队列写入消息之前,并不需要另一个进程在该消息队列上等待消息的到达。而对于管道来说,除非读进程已存在,否则先有写进程进行写入操作是没有意义的。
  • 消息队列的生命周期随内核,如果没有释放消息队列或者没有关闭操作系统,消息队列就会一直存在。而匿名管道随进程的创建而建立,随进程的结束而销毁。

需要注意的是,消息队列对于交换较少数量的数据很有用,因为无需避免冲突。但是,由于用户进程写入数据到内存中的消息队列时,会发生从用户态拷贝数据到内核态的过程;同样的,另一个用户进程读取内存中的消息数据时,会发生从内核态拷贝数据到用户态的过程。因此,如果数据量较大,使用消息队列就会造成频繁的系统调用,也就是需要消耗更多的时间以便内核介入

相关函数

消息数据格式如下

源程序示例如下:

// 写进程 
#include <stdio.h> 
#include <sys/ipc.h> 
#include <sys/msg.h> 
  
// 消息队列数据结构
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); 
  
    return 0; 
}

 

// 读进程
#include <stdio.h> 
#include <sys/ipc.h> 
#include <sys/msg.h> 
  
// structure for message queue 
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); 
  
    // 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; 
}

运行结果

 

四. 共享内存

        为了避免像消息队列那样频繁的拷贝消息、进行系统调用,共享内存机制出现了。顾名思义,共享内存就是允许不相干的进程将同一段物理内存连接到它们各自的地址空间中,使得这些进程可以访问同一个物理内存,这个物理内存就成为共享内存。如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。

        集合内存管理的内容,我们来深入理解下共享内存的原理。首先,每个进程都有属于自己的进程控制块(PCB)和逻辑地址空间(Addr Space),并且都有一个与之对应的页表,负责将进程的逻辑地址(虚拟地址)与物理地址进行映射,通过内存管理单元(MMU)进行管理。两个不同进程的逻辑地址通过页表映射到物理空间的同一区域,它们所共同指向的这块区域就是共享内存

         不同于消息队列频繁的系统调用,对于共享内存机制来说,仅在建立共享内存区域时需要系统调用,一旦建立共享内存,所有的访问都可作为常规内存访问,无需借助内核。这样,数据就不需要在进程之间来回拷贝,所以这是最快的一种进程通信方式。

相关函数

shmget:申请共享内存
shmat:建立用户进程空间到共享内存的映射
shmdt:解除映射关系
shmctl:回收共享内存空间

程序源码示例

//写进程
#include <sys/ipc.h> 
#include <sys/shm.h> 
#include <stdio.h> 
  
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); 
  
    return 0; 
}
//读进程
#include <sys/ipc.h> 
#include <sys/shm.h> 
#include <stdio.h> 

  
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); 
  
    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; 
}

编译运行

五. 信号量和 PV 操作

        实际上,对具有多 CPU 系统的最新研究表明,在这类系统上,消息传递的性能其实是要优于共享内存的,因为消息队列无需避免冲突,而共享内存机制可能会发生冲突。也就是说如果多个进程同时修改同一个共享内存,先来的那个进程写的内容就会被后来的覆盖。并且,在多道批处理系统中,多个进程是可以并发执行的,但由于系统的资源有限,进程的执行不是一贯到底的, 而是走走停停,以不可预知的速度向前推进(异步性)。但有时候我们又希望多个进程能密切合作,按照某个特定的顺序依次执行,以实现一个共同的任务。

        举个例子,如果有 A、B 两个进程分别负责读和写数据的操作,这两个线程是相互合作、相互依赖的。那么写数据应该发生在读数据之前。而实际上,由于异步性的存在,可能会发生先读后写的情况,而此时由于缓冲区还没有被写入数据,读进程 A 没有数据可读,因此读进程 A 被阻塞。

        因此,为了解决上述这两个问题,保证共享内存在任何时刻只有一个进程在访问(互斥),并且使得进程们能够按照某个特定顺序访问共享内存(同步),我们就可以使用进程的同步与互斥机制,常见的比如信号量与 PV 操作。

        信号量就是具有原子性的计数器,就相当于一把锁,在每个进程要访问临界资源时,必须要向信号量拿个锁”,它才能进去临界资源这个“房间”,并锁上门,不让其他进程进来,此时信号量执行P()操作,锁的数目减少了一个,所以计数器减1,;当它访问完成时,它出来,将锁还给信号量,执行V()操作,计数器加1;

程序源码示例:

让两个进程分别向显示器(linux下一切皆文件,临界资源)打印AA和BB,当没有信号量进行保护时,会出现数据混乱,例如:“AABBABAAAB…”,为了解决这一问题,我们创建信号量进行保护。打印“AA”或“BB”

#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>
#include<sys/sem.h>

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) return 0;
	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);
}

编译运行

源代码示例2,与上述代码几乎相同,只不过变为了父子进程间的信号量机制。

#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>
#include<sys/sem.h>
#include<sys/ipc.h>
#include<sys/types.h>

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) return 0;
    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);
}

编译运行

在这里我们可以看到子进程的pid为父进程的pid+1,注意的是fork创建子进程的返回值为0,和这里的pid是不同的。

六. 信号

注意!信号和信号量是完全不同的两个概念

        信号是进程通信机制中唯一的异步通信机制,它可以在任何时候发送信号给某个进程。通过发送指定信号来通知进程某个异步事件的发送,以迫使进程执行信号处理程序。信号处理完毕后,被中断进程将恢复执行。用户、内核和进程都能生成和发送信号。

        信号事件的来源主要有硬件来源和软件来源。所谓硬件来源就是说我们可以通过键盘输入某些组合键给进程发送信号,比如常见的组合键 Ctrl+C 产生 SIGINT 信号,表示终止该进程;而软件来源就是通过 kill 系列的命令给进程发送信号,比如 kill -9 1111 ,表示给 PID 为 1111 的进程发送 SIGKILL 信号,让其立即结束。

程序源码示例

以模拟闹钟的形式,通过一个进程向另一个进程发送SIGALRM信号来表现进程间的通信。

#include <sys/types.h>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

static int alarm_fired = 0;

/*该函数用来模拟闹钟*/
void ding(int sig)
{
    alarm_fired =1;
}

/*main函数中告诉子进程在等待5秒后发送SIGALRM信号给它的父进程*/
int main()
{
    pid_t pid;
    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);
}

编译运行

七. Socket

        至此,上面介绍的 5 种方法都是用于同一台主机上的进程之间进行通信的,如果想要跨网络与不同主机上的进程进行通信,那该怎么做呢?这就是 Socket 通信做的事情了(当然,Socket 也能完成同主机上的进程通信)。

        socket即套接字是一种通信机制,凭借这种机制,客户/服务器(即要进行通信的进程)系统的开发工作既可以在本地单机上进行,也可以跨网络进行。也就是说它可以让不在同一台计算机但通过网络连接计算机上的进程进行通信。也因为这样,套接字明确地将客户端和服务器区分开来。

 程序源码示例

 //服务器
    #include <unistd.h>  
    #include <sys/types.h>  
    #include <sys/socket.h>  
    #include <netinet/in.h>  
    #include <signal.h>  
    #include <stdio.h>  
    #include <stdlib.h>  
     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, (struct sockaddr*)&server_addr, sizeof(server_addr));  
        //创建套接字队列,监听套接字  
        listen(server_sockfd, 5);  
        //忽略子进程停止或退出信号  
        signal(SIGCHLD, SIG_IGN);  
          
        while(1)  
        {  
            char ch = '\0';  
            client_len = sizeof(client_addr);  
            printf("Server waiting\n");  
            //接受连接,创建新的套接字  
            client_sockfd = accept(server_sockfd, (struct sockaddr*)&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 <unistd.h>  
    #include <sys/types.h>  
    #include <sys/socket.h>  
    #include <netinet/in.h>  
    #include <arpa/inet.h>  
    #include <stdio.h>  
    #include <stdlib.h>  
      
    int main()  
    {  
        int sockfd = -1;  
        int len = 0;  
        struct sockaddr_in address;  
        int result;  
        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, (struct sockaddr*)&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);  
    }  

编译运行

八. 总结

简单总结一下上面六种 Linux 内核提供的进程通信机制:

1)首先,最简单的方式就是管道,管道的本质是存放在内存中的特殊的文件。也就是说,内核在内存中开辟了一个缓冲区,这个缓冲区与管道文件相关联,对管道文件的操作,被内核转换成对这块缓冲区的操作。管道分为匿名管道和有名管道,匿名管道只能在父子进程之间进行通信,而有名管道没有限制。

2)虽然管道使用简单,但是效率比较低,不适合进程间频繁地交换数据,并且管道只能传输无格式的字节流。为此消息队列应用而生。消息队列的本质就是存放在内存中的消息的链表,而消息本质上是用户自定义的数据结构。如果进程从消息队列中读取了某个消息,这个消息就会被从消息队列中删除。

3)消息队列的速度比较慢,因为每次数据的写入和读取都需要经过用户态与内核态之间数据的拷贝过程,共享内存可以解决这个问题。所谓共享内存就是:两个不同进程的逻辑地址通过页表映射到物理空间的同一区域,它们所共同指向的这块区域就是共享内存。如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。

对于共享内存机制来说,仅在建立共享内存区域时需要系统调用,一旦建立共享内存,所有的访问都可作为常规内存访问,无需借助内核。这样,数据就不需要在进程之间来回拷贝,所以这是最快的一种进程通信方式。

4)共享内存速度虽然非常快,但是存在冲突问题,为此,我们可以使用信号量和 PV 操作来实现对共享内存的互斥访问,并且还可以实现进程同步。

5)信号和信号量是完全不同的两个概念!信号是进程通信机制中唯一的异步通信机制,它可以在任何时候发送信号给某个进程。通过发送指定信号来通知进程某个异步事件的发送,以迫使进程执行信号处理程序。信号处理完毕后,被中断进程将恢复执行。用户、内核和进程都能生成和发送信号。

6)上面介绍的 5 种方法都是用于同一台主机上的进程之间进行通信的,如果想要跨网络与不同主机上的进程进行通信,就需要使用 Socket 通信。另外,Socket 也能完成同主机上的进程通信。

 

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: Dart 是 Google 开发的编程语言,可以用来开发移动应用和 web 应用。命名管道是 UNIX 系统中的一种 IPC进程间通信)方式,可以在不同进程之间传递数据。在 Dart 中,可以使用 dart:io 库中的 NamedPipe 类来操作命名管道。 下面是一个简单的 Unix 示例,演示了如何在两个不同的 Dart 程序之间使用命名管道进行通信: 程序1(sender.dart) ```dart import 'dart:io'; main() async { var pipe = await NamedPipeServer.create('/tmp/my_pipe'); var sink = pipe.sink; sink.write('Hello from sender!'); await sink.flush(); await sink.close(); } ``` 程序2(receiver.dart) ```dart import 'dart:io'; main() async { var pipe = await NamedPipeClient.connect('/tmp/my_pipe'); var input = pipe.input; var message = await input.read(); print(message); await input.close(); } ``` 在这个示例中,程序 1 使用 NamedPipeServer.create() 创建了一个名为 /tmp/my_pipe 的命名管道,并向其中写入了一条消息 "Hello from sender!"。程序 2 使用 NamedPipeClient.connect() 连接到了同一个命名管道,并从中读取了消息,最后打印出来。 ### 回答2: Dart是一种跨平台的编程语言,支持在不同的操作系统上进行开发。在Unix系统中,进程间通信是一个非常重要的概念。命名管道是一种用于在不同进程之间进行通信的机制。 在Dart中,可以使用dart:io包来实现使用命名管道进行进程间通信的功能。首先,需要创建一个命名管道。可以使用File类的static方法mkfifo来创建一个命名管道文件。 具体示例代码如下: import 'dart:io'; void main() { String pipePath = './mypipe'; // 命名管道的文件路径 Process.start('mkfifo', [pipePath]).then((Process process) { print('命名管道已创建'); // 接下来可以进行进程间通信的操作 }); } 上面的示例代码中,首先使用Process.start方法调用mkfifo命令来创建一个命名管道文件,然后通过then回调函数来监听创建命名管道的结果,并在命名管道创建成功后进行进一步的处理。 在创建命名管道后,就可以使用命名管道进行进程间通信了。可以使用File类来打开命名管道,并进行读写操作。具体的进程间通信的操作可以根据需求进行自定义。 需要注意的是,使用命名管道进行进程间通信时,需要确保读、写操作的顺序正确,以避免出现阻塞的情况。此外,命名管道也可以用于进程间的同步操作,例如等待另一个进程完成某个指定的任务后再进行下一步操作。 总而言之,Dart可以通过使用dart:io包来实现在Unix系统中使用命名管道进行进程间通信的功能。通过创建命名管道文件,然后使用File类打开该文件进行读写操作,可以实现不同进程之间的通信与同步。具体的操作可以根据需求进行自定义。 ### 回答3: 在Unix系统中,进程间通信是非常重要的,为了实现进程间的数据传递,可以使用命名管道。Dart语言也提供了类似的方法来实现进程间通信。 命名管道是一种特殊的文件,它可以用来实现进程间的读写操作。在Unix示例中,首先我们需要创建一个命名管道,在Dart中可以使用File类的create方法来创建文件。创建命名管道的操作类似于创建普通文件,只是我们需要给文件一个特殊的名称,通常以“/tmp/”开头。 在创建完命名管道之后,我们可以使用File类的open方法来打开管道。在Unix示例中,我们需要使用O_RDONLY或者O_WRONLY标识符来指定管道的读写模式。然后,我们可以使用File类的read方法来读取管道中的数据,使用write方法来写入数据。 在进程间通信时,通常会有一个进程负责写入数据,另一个进程负责读取数据。这两个进程可以通过访问同一个命名管道来实现通信。读取进程会在打开管道之后一直等待数据的到来,直到写入进程向管道中写入数据。然后读取进程会读取数据并进行相应的处理。 需要注意的是,在使用命名管道时,需要保证写入和读取的进程顺序是正确的,否则可能会产生读写阻塞的问题。此外,写入和读取的进程在使用管道时需要进行同步,以免出现数据竞争的情况。 总结起来,Dart语言可以使用命名管道来实现进程间通信,类似于Unix中的示例。使用命名管道可以在进程间传递数据,实现数据的读写操作。但是需要注意保证进程的顺序和同步,以确保通信的正确性和稳定性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

咸鱼弟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值