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