进程间通信(IPC)以及同步

时间:2021-11-27 19:05:56

临界区的概念: 把涉及对共享内存访问的程序片段称为临界区。临界区的提出是为了防止多个进程同时处于其中,避免竞争条件。


http://www.cnblogs.com/skyofbitbit/p/3651750.html

http://www.cnblogs.com/applebunny/archive/2012/07/11/2586483.html

http://jingyan.baidu.com/article/3a2f7c2e17e12b26afd611cb.html

1.共享内存

      共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式

2. 信号量 

         信号量又称为信号灯,它是用来协调不同进程间的数据对象的,而最主要的应用是共享内存方式的进程间通信。本质上,信号量是一个计数器,它用来记录对某个资源(如共享内存)的存取状况它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。一般说来,为了获得共享资源,进程需要执行下列操作: 
   (1) 测试控制该资源的信号量。 
   (2) 若此信号量的值为正,则允许进程使用该资源。进程将信号量减1。 
   (3) 若此信号量为0,则该资源目前不可用,进程进入睡眠状态,直至信号量值大于0,进程被唤醒,转入步骤(1)。 
   (4) 当进程不再使用一个信号量控制的资源时,信号量值加1。如果此时有进程正在睡眠等待此信号量,则唤醒此进程。 

互斥量是信号量的简化版本,没有信号量的计数功能,只能为0/1,是一个二元信号灯,本质上说就是一把锁,提供对资源的独占访问,指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。

互斥量用于线程的互斥,信号量用于线程的同步,限制访问者对资源的访问顺序。

2.1 管程

   管程是一个由过程,变量以及数据结构组成的集合,结构上相当于一个类。它是一种进程同步的机制。管程的互斥机制是由编译器负责,实现临界区互斥自动化,无需程序员考虑,我们只考虑如何将临界区转换成管程即可。任一时刻只能有一个进程进入管程中。管程中引入条件变量以及wait和signal两个操作,实现自身进程的阻塞以及将等在管程之外的另一个进程调入其中。

3.管道

      管道是进程间通信中最古老的方式,一种半双工的通信方式,数据只能单向流动,它包括无名管道有名管道两种,前者用于父进程和子进程间的通信,后者用于运行于同一台机器上的任意两个进程间的通信。 

无名管道由pipe()函数创建: 
   #include <unistd.h> 
   int pipe(int filedis[2]); 
   参数filedis返回两个文件描述符:filedes[0]为读而打开,filedes[1]为写而打开。filedes[1]的输出是filedes[0]的输入。下面的例子示范了如何在父进程和子进程间实现通信。 

#define INPUT 0 
#define OUTPUT 1 

void main() { 
int file_descriptors[2]; 
/*定义子进程号 */ 
pid_t pid; 
char buf[256]; 
int returned_count; 
/*创建无名管道*/ 
pipe(file_descriptors); 
/*创建子进程*/ 
if((pid = fork()) == -1) { 
printf("Error in fork/n"); 
exit(1); 
} 
/*执行子进程*/ 
if(pid == 0) { 
printf("in the spawned (child) process.../n"); 
/*子进程向父进程写数据,关闭管道的读端*/ 
close(file_descriptors[INPUT]); 
write(file_descriptors[OUTPUT], "test data", strlen("test data")); 
exit(0); 
} else { 
/*执行父进程*/ 
printf("in the spawning (parent) process.../n"); 
/*父进程从管道读取子进程写的数据,关闭管道的写端*/ 
close(file_descriptors[OUTPUT]); 
returned_count = read(file_descriptors[INPUT], buf, sizeof(buf)); 
printf("%d bytes of data received from spawned process: %s/n", 
returned_count, buf); 
} 
} 
   在Linux系统下,有名管道可由两种方式创建:命令行方式mknod系统调用和函数mkfifo。下面的两种途径都在当前目录下生成了一个名为myfifo的有名管道: 
     方式一:mkfifo("myfifo","rw"); 
     方式二:mknod myfifo p 
   生成了有名管道后,就可以使用一般的文件I/O函数如open、close、read、write等来对它进行操作。下面即是一个简单的例子,假设我们已经创建了一个名为myfifo的有名管道。 
  /* 进程一:读有名管道*/ 
#include <stdio.h> 
#include <unistd.h> 
void main() { 
FILE * in_file; 
int count = 1; 
char buf[80]; 
in_file = fopen("mypipe", "r"); 
if (in_file == NULL) { 
printf("Error in fdopen./n"); 
exit(1); 
} 
while ((count = fread(buf, 1, 80, in_file)) > 0) 
printf("received from pipe: %s/n", buf); 
fclose(in_file); 
} 
  /* 进程二:写有名管道*/ 
#include <stdio.h> 
#include <unistd.h> 
void main() { 
FILE * out_file; 
int count = 1; 
char buf[80]; 
out_file = fopen("mypipe", "w"); 
if (out_file == NULL) { 
printf("Error opening pipe."); 
exit(1); 
} 
sprintf(buf,"this is test data for the named pipe example/n"); 
fwrite(buf, 1, 80, out_file); 
fclose(out_file); 
} 

4.套接字( socket ) 

    除了在异地的计算机进程间以外,套接口同样适用于本地同一台计算机内部的进程间通信。

5.消息传递系统

6.消息队列

7.消息/信号

总结:

剪切板和匿名管道只能实现同一机器两个进程间的通信,而信号量、消息、消息队列和共享内存也只能在同一台机器上的多个进程间通信;但是命名管道、邮件槽和套接字不仅可以实现同一机器上的两个进程的通信,还可以实现跨网络的进程间通信;另外,油槽和套接字可以实现一对多的通信,而命名管道只能是点对点的单一通信,但油槽的缺点是数据量较小,通常都是在424字节以下,如果数据量较大,则可以采用命名管道和套接字的方式来完成。综上:在跨网络通信中套接字无疑是最优秀的通信方式。


经典ipc问题:http://3y.uu456.com/bp-6f4c0d07eff9aef8941e0679-1.html

1.哲学家就餐问题

描述:

五位哲学家围坐一圆桌,各有一盘特别滑溜的意大利面条,面条是那样地滑溜,以致于哲学家不得不用两把叉子才能吃着,在盘与盘之间仅配备一把叉。哲学家的生活由吃与想交织而成的。当一位哲学家饥饿时,他要抓住他的左右叉,一次一把,不管先后。如果他抓到两把叉,那就吃一会儿,然后放回原处,继续想。问题的焦点集中在:你能否为每一哲学家编制一道程序,去做理当做的事情而不卡壳。

思路:一个哲学家只有在左右两个邻居都没有进餐时才能进入就餐状态。 需用一个数组记录跟踪每个哲学家的状态,是在进餐,思考,还是饥饿状态(处于正在试图拿叉子的状态)。每个哲学家状态的变化是处于临界区中的。当放下叉子时,哲学家需检查左右邻居,是否处于饥饿状态,合适条件下解除其阻塞。

#define N 5

#define LEFT (i-1)%N

#define RIGHT (i+1)%N

#define THINKING 0

#define HUNGRY 1

#define EATING 2

int state[N];//记录每个人状态的数组

semaphore mutex=1;//临界区互斥

semaphore s[N]={0};//每个哲学家一个信号量,表示能否得到两只叉子

void philosopher(int i)

{

while(1){

think();

take_forks(i);

eat();

put_forks(i);

}

}

void take_forks(int i)

{

donw(&mutex);

state[i]=HUNGRY;

test(i);//试图得到两只叉子

up(&mutex);

down(&s[i]);//如果得不到叉子就阻塞

}

void put_forks(int i)

{

down(&mutex);

stata[i]=THINKING;

test(LEFT);//看下左邻居现在是否能进餐

test(RIGHT);//看下右邻居现在是否能进餐

up(&mutex);

}

void test(int i)

{

if(state[i]==HUNGRY && state[LEFT]!=EATING && state[RIGHT]!=EATING){ state[i]=EATING;

up(&s[i]);

}

}


2.读者-写者问题

描述:读者和写者问题模拟对数据库的访问。设想一个像飞机订票系统那样的一个大数据库,有许多希望读写它的进程在竟争。让多个进程同时读数据库是可接受的;然而,如果一个进程在写(即改变)数据库,那么,任何别的进程都不得访问该数据库,即使读也不行。你如何对读者和写者进行程序设计?

思路:解法分为读者优先、写者优先、公平竞争三种。

读者优先:只要有一个读者在使用数据库,则允许后续更大的读者进入数据库。这时若有写者来了,只能挂起,直到没有读者为止。缺点:若有一个稳定的读者流,则写者就没有进入数据库的机会。

semaphore mutex=1;//控制对rc的访问

semaphore db=1;//控制对数据库的访问

int rc=1;//正在读或想要读的进程数

void reader(void)

{

while(1){

down(&mutex);

rc++;

if(rc==1)

down(&db);

up(&mutex);

read_database();

down(&mutex);

rc--;

if(rc==0)

up(&db);

up(&mutex);

use_data_read();

}

}

void writer(void)

{

while(1){

think_up_data();

down(&db);

write_database();

up(&db);

}

}


写者优先:当有一个写者被挂起时,这时有一个读者来了,读者不被立刻允许进入。这样,当一个写者到达时,不用等待后续到达的读者,只要当当前的读者完成时就可进入数据库。一旦有写者来,则后续读者必须等待,唤醒时优先考虑写者

semaphore read=1;//用于阻塞读者

semaphore rmutex=1,wmutex=1;//用于控制对rcount和wcount的访问

semaphore db=1;//控制对数据库的访问

int rcount=wcount=0;

void reader(void)

{

p(read); //只要有写者等待,读者就会阻塞

p(rmutex);

rcount++;

if(rcount==1)

p(db);

v(rmutex);

v(read);//及时释放,多个读者可以同时读

read_database();

p(rmutex);

rcount--;

if(rcount==0)

v(db);

v(rmutex);

}

void writer(void)

{

p(wmutex);

wcount++;

if(wcount==1)

p(read);

v(wmutex);

p(db);

write_database();

v(db);

p(wmutex);

wcount--;

if(wcount==0)

v(read);

v(wmutex);

}