多进程与多线程

一. 多进程程序的特点 

进程是一个具有独立功能的程序关于某个数据集合的一次可以并发执行的运行活动,是处于活动状态的计算机程序。进程作为构成系统的基本细胞,不仅是系统内部独立运行的实体,而且是独立竞争资源的基本实体。

进程是资源管理的最小单位,线程是程序执行的最小单位。进程管理着资源(比如cpu、内存、文件等等),而将线程分配到某个cpu上执行。在操作系统设计上,从进程演化出线程,最主要的目的就是更好的支持多处理器系统和减小上下文切换开销。

进程的状态 系统为了充分的利用资源,对进程区分了不同的状态.将进程分为新建,运行,阻塞,就绪和完成五个状态. 

新建 表示进程正在被创建,

运行 是进程正在运行,

阻塞 是进程正在等待某一个事件发生,

就绪 是表示系统正在等待CPU来执行命令,

完成 表示进程已经结束了系统正在回收资源. 

    由于UNIX系统是分时多用户系统, CPU按时间片分配给各个用户使用,而在实质上应该说CPU按时间片分配给各个进程使用每个进程都有自己的运行环境以使得在CPU做进程切换时不会"忘记"该进程已计算了一半的"半成品”. DOS的概念来说进程的切换都 是一次"DOS中断"处理过程, 包括三个层次: 

1) 用户数据的保存包括正文段(TEXT), 数据段(DATA,BSS), 栈段(STACK), 共享内存段(SHARED MEMORY)的保存

2) 寄存器数据的保存包括PC(program counter,指向下一条要执行的指令的地址), PSW(processor status word,处理机状态字), SP(stack pointer,栈指针), PCBP(pointer of process control block,进程控制块指针), FP(frame pointer,指向栈中一个函数的local变量的首地址)AP(augument pointer,指向栈中函数调用的实参位置)ISP(interrupt stack pointer,中断栈指针), 以及其他的通用寄存器等

3) 系统层次的保存

包括proc,u,虚拟存储空间管理表格,中断处理栈.以便于该进程再一次得到CPU时间片时能正常运行。 既然系统已经处理好所有这些中断处理的过程我们做程序还有什么要担心 的呢我们尽可以使用系统提供的多进程的特点让几个程序精诚合作简单而又高效地把结果给它搞出来。

另外,UNIX系统本身也是用C语言写的多进程程序,多进程编程是UNIX的特点,当我们熟悉了多进程?将会对UNIX系统机制有一个较深的认识.首先我介绍一下多进程程序的一些突出的特点: 

1.1并行化 

一件复杂的事件是可以分解成若干个简单事件来解决的这在程序员的大脑中早就形成了这种概念首先将问题分解成一个个小问题将小问题再细分最后在一个合适的规模上做成一个函数在软件工程中也是这么说的如果我们以图的方式来思考一些小问题的计算是可以互不干扰的可以同时处理而在关键点则需要统一在一个地方来处理这样程序的运行就是并行的至少从人的时间观念上来说是这样的而每个小问题的计算又是较简单的

1.2简单有序 

这样的程序对程序员来说不亚于管理一班人程序员为每个进程设计好相应的功能并通过一定的通讯机制将它们有机地结合在一起对每个进程的设计是简单的只在总控部分小心应付(其实也是蛮简单的), 就可完成整个程序的施工

1.3.互不干扰 

这个特点是操作系统的特点各个进程是独立的不会串位

1.4.事务化 

比如在一个数据电话查询系统中将程序设计成一个进程只处理一次查询即可即完成一个事务当电话查询开始时产生这样一个进程对付这次查询另一个电话进来时主控程序又产生一个这样的进程对付每个进程完成查询任务后消失这样的编程多简单只要做一次查询的程序就可以了

.常用的多进程编程的系统调用 

2.1.fork()     创建一个新的进程.

功能:创建一个新的进程

语法:

#include <unistd.h> 

#include <sys/types.h> 

pid_t fork(); 

说明:本系统调用产生一个新的进程叫子进程是调用进程的一个复制品调用进程叫父进程子进程继承了父进程的几乎所有的属性。

进程:代码段(程序代码)

堆栈段(局部变量、函数返回地址、函数参数)

数据段(全局变量、常数等)

在Linux系统中,系统调用fork后,内核为完成系统调用fork要进行几步操作:

第一步,为新进程在进程表中分配一个表项。系统对一个普通用户可以同时运行的进程数是有限制的,对超级用户没有该限制,但不能超过进程表的最大表项的数目。

第二步,给子进程一个唯一的进程标识号(PID)。该进程标识号其实就是该表项在进程表中的索引号。

第三步,复制一个父进程的进程表项的副本给子进程。内核初始化子进程的进程表项时,是从父进程处拷贝的。所以子进程拥有与父进程一样的uid、当前目录、当前根、用户文件描述符表等。

第四步,把与父进程相连的文件表和索引节点表的引用数加1。这些文件自动地与该子进程相连。

第五步,内核为子进程创建用户级上下文。内核为子进程的代码段分配内存,并复制父进程的区内容,生成的是进程的静态部分。

第六步,生成进程的动态部分,然后对父进程返回子进程的pid,对子进程返回0。

从父进程拷贝的内容主要有:

●用户标识符,包括实际用户号(real)和有效用户号(effective);

●环境变量

●打开的文件描述符、套接字描述符

●信号处理设置

●堆栈

●目录

●进程组标志(process ID)

●会晤组标志(session ID)

●正文

子进程特有内容:

●进程号

●父进程号

●进程执行时间

●未处理的信号被处理为空

●不继承异步的输入输出操作

简述:fork() 调用成功时,分别返回两个整数,对父进程返回 〉0的整数,对子进程返回 0,

函数执行过程:

① 内核在系统进程表中,创建一个新条目;

② 复制父进程内容(已打开的文件描述符、堆栈、正文等);

③ 修改两者的堆栈,给父进程返回子进程号,给子进程返回0(父进程知道每个子进程的标志号,而子进程可根据需要调用getppid() 来获得父进程的标志号)。

例子:

pid_t fork(void) 

#include <unistd.h>

pid_t pid;

if((pid=fork())==0)

{

  //子进程代码

exit(0);

}

else if(pid>0)

{

//父进程代码

exit(0);

}

else

{

  printf("Error");

  exit(1);

}

 

2.2.system()   子进程执行指定的命令

功能:产生一个新的进程子进程执行指定的命令

语法:

#include <stdio.h> 

#include <stdlib.h> 

int system(string) 

char *string; 

说明:

本调用将参数string传递给一个命令解释器(一般为sh)执行string被解释为一条命令sh执行该命令.若参数string为一个空指针则为检查命令解释器是否存在.

 该命令可以同命令行命令相同形式但由于命令做为一个参数放在系统调用中应注意编译时对特殊意义字符的处理命令的查找是按PATH环境变量的定义的命令所生成的后果一般不会对父进程造成影响

返回值:当参数为空指针时只有当命令解释器有效时返回值为非零若参数不为空指针返回值为该命令的返回状态(waitpid())的返回值命令无效或语法错误则返回非零值,所执行的命令被终止其他情况则返回-1. 

例子:char command[81]; 

int i; 

for (i=1;i<8;i++) { 

sprintf(command,"ps t tty%02i",i); 

system(command); 

2.3.exec()     执行一个文件 

功能:执行一个文件

语法

#include <unistd.h>

int execve(const char* path, char* const* argv,char* const* envp);

int execl(const char* path, char* arg,...); 

int execp(const char* file, char* arg,...); 

int execle(const char* path, const char* argv,...,char* const* envp);

int execv(const char* path, char* const* arg); 

int execvp(const char* file, char* const* arg);

说明:

 exec函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件

其中只有execve是真正意义上的系统调用,其它都是在此基础上经过包装的库函数

与一般情况不同,exec函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程ID等一些表面上的信息仍保持原样,颇有些神似"三十六计"中的"金蝉脱壳"。看上去还是旧的躯壳,却已经注入了新的灵魂。只有调用失败了,它们才会返回一个-1,从原程序的调用点接着往下执行。

fork()和exec()这两个函数,前者用于并行执行,父、子进程执行相同正文中的不同部分;后者用于调用其他进程,父、子进程执行不同的正文,调用前,一般应为子进程创造一个干净的环境。

fork()以后,父、子进程共享代码段,并只重新创建数据有改变的页(段页式管理)

exec()以后,建立新的代码段,用被调用程序的内容填充。

前者的子进程执行后续的公共代码,后者的子进程不执行后续的公共代码。

父、子进程以及各个子进程执行的顺序不定。

例子:printf("now this process will be ps command\n"); 

execl("/bin/ps","ps","-ef",NULL); 

2.4.popen()    初始化从/到一个进程的管道

功能:初始化从/到一个进程的管道

语法:

#include <stdio.h> 

FILE *popen(command,type) 

char *command,type; 

说明:本系统调用在调用进程和被执行命令间创建一个管道

参数command做为被执行的命令行.type做为I/O模式,"r"为从被 

执行命令读,"w"为向被执行命令写.返回一个标准流指针,做为管 

道描述符,向被执行命令读或写数据(做为被执行命令的STDIN或 

STDOUT)该系统调用可以用来在程序中调用系统命令,并取得命令 

的输出信息或者向命令输入信息

返回值:不成功则返回NULL,成功则返回管道的文件指针

2.5.pclose()    关闭到一个进程的管道

功能:关闭到一个进程的管道

语法:

#include <stdio.h> 

int pclose(strm) 

FILE *strm; 

说明:本系统调用用于关闭由popen()打开的管道,并会等待由popen() 

激活的命令执行结束后,关闭管道后读取命令返回码

返回值:若关闭的文件描述符不是由popen()打开的,则返回-1. 

例子:printf("now this process will call popen system call\n"); 

FILE * fd; 

if ((fd=popen("ps -ef","r"))==NULL) { 

printf("call popen failed\n"); 

return; 

else { 

char str[80]; 

while (fgets(str,80,fd)!=NULL) 

printf("%s\n",str); 

pclose(fd); 

2.6.wait()      等待一个子进程返回并修改状态

功能:等待一个子进程返回并修改状态 

语法:

#include <sys/types.h> 

#include <sys/wait.h> 

pid_t wait(stat_loc) 

int *stat_loc; 

说明:允许调用进程取得子进程的状态信息.调用进程将会挂起直到其 

一个子进程终止

返回值:等待到一个子进程返回时,返回值为该子进程号,否则返回值为 

-1.同时stat_loc返回子进程的返回值

例子:/*父进程*/ 

if (fork()>0) { 

wait((int *)0); 

/*父进程等待子进程的返回*/ 

else { 

/*子进程处理过程*/ 

exit(0); 

2.7.waitpid()   等待指定进程号的子进程的返回并修改状态

功能:等待指定进程号的子进程的返回并修改状态 

语法:

#include <sys/types.h> 

#include <sys/wait.h> 

pid_t waitpid(pid,stat_loc,options) 

pid_t pid; 

int *stat_loc,options; 

说明:pid等于-1,options等于0,该系统调用等同于wait().否则该 

系统调用的行为由参数pidoptions决定

pid指定了一组父进程要求知道其状态的子进程

-1:要求知道任何一个子进程的返回状态

>0:要求知道进程号为pid值的子进程的状态

<-1:要求知道进程组号为pid的绝对值的子进程的状态

options参数为以比特方式表示的标志以或运算组成的位图,每个 

标志以字节中某个比特置1表示

WUNTRACED:报告任何未知而又已停止运行的指定进程号的子进 

程的状态.该子进程的状态自停止运行时起就没有被报告 过

WCONTINUED:报告任何继续运行的指定进程号的子进程的状态

该子进程的状态自继续运行起就没有被报告过

WHOHANG:若调用本系统调用时,指定进程号的子进程的状态目 

前并不是立即有效的(即可被立即读取的),调用进程并被 暂停执行

WNOWAIT:保持将其状态设置在stat_loc的进程在可等待状态

该进程将等待直到下次被要求其返回状态值

返回值:等待到一个子进程返回时,返回值为该子进程号,否则返回值为 1. 

同时stat_loc返回子进程的返回值

例子:pid_t pid; 

int stat_loc; /*父进程*/ 

if ((pid="fork())">0) { 

waitpid(pid,&stat_loc,0); 

/*父进程等待进程号为pid的子进程的返回*/ 

else { 

/*子进程的处理过程*/ 

exit(1); 

/*父进程*/ 

printf("stat_loc is [%d]\n",stat_loc); 

/*字符串"stat_loc is [1]"将被打印出来*/ 

2.8.setpgrp()   设置进程组号和会话号

功能:设置进程组号和会话号

语法:

#include <sys/types.h> 

pid_t setpgrp() 

说明:若调用进程不是会话首进程.将进程组号和会话号都设置为与它 

的进程号相等.并释放调用进程的控制终端

返回值:调用成功后,返回新的进程组号

例子:/*父进程处理*/ 

if (fork()>0) { 

/*父进程处理*/ 

else { 

setpgrp(); 

/*子进程的进程组号已修改成与它的进程号相同*/ 

exit(0); 

2.9.exit()      终止进程

功能:终止进程

语法:

#include <stdlib.h> 

void exit(status) 

int status; 

说明:调用进程被该系统调用终止.引起附加的处理在进程被终止前全 

部结束

返回值:无 

2.10.signal()   信号管理功能

功能:信号管理功能 

语法:

#include <signal.h> 

void (*signal(sig,disp))(int) 

int sig; 

void (*disp)(int); 

void (*sigset(sig,disp))(int) 

int sig; 

void (*disp)(int); 

int sighold(sig) 

int sig; 

int sigrelse(sig) 

int sig; 

int sigignore(sig) 

int sig; 

int sigpause(sig) 

int sig; 

说明:这些系统调用提供了应用程序对指定信号的简单的信号处理

signal()sigset()用于修改信号定位.参数sig指定信号(除了 

SIGKILLSIGSTOP,这两种信号由系统处理,用户程序不能捕捉到). 

disp指定新的信号定位,即新的信号处理函数指针.可以为 

SIG_IGN,SIG_DFL或信号句柄地址

若使用signal(),disp是信号句柄地址,sig不能为SIGILL,SIGTRAP 

SIGPWR,收到该信号时,系统首先将重置sig的信号句柄为SIG_DFL, 

然后执行信号句柄

若使用sigset(),disp是信号句柄地址,该信号时,系统首先将该 

信号加入调用进程的信号掩码中,然后执行信号句柄.当信号句柄 

运行结束 

,系统将恢复调用进程的信号掩码为信号收到前的状态.另外

使用sigset(),dispSIG_HOLD,则该信号将会加入调用进程的 

信号掩码中而信号的定位不变

sighold()将信号加入调用进程的信号掩码中

sigrelse()将信号从调用进程的信号掩码中删除

sigignore()将信号的定位设置为SIG_IGN. 

sigpause()将信号从调用进程的信号掩码中删除,同时挂起调用 

进程直到收到信号

若信号SIGCHLD的信号定位为SIG_IGN,则调用进程的子进程在终 

止时不会变成僵死进程.调用进程也不用等待子进程返回并做相 

应处理

返回值:调用成功则signal()返回最近调用signal()设置的disp的值

否则返回SIG_ERR. 

例子一:设置用户自己的信号中断处理函数,SIGINT信号为例

int flag=0; 

void myself() 

flag=1; 

printf("get signal SIGINT\n"); 

/*若要重新设置SIGINT信号中断处理函数为本函数则执行以 

*下步骤*/ 

void (*a)(); 

a=myself; 

signal(SIGINT,a); 

flag=2; 

main() 

while (1) { 

sleep(2000); /*等待中断信号*/ 

if (flag==1) { 

printf("skip system call sleep\n"); 

exit(0); 

if (flag==2) { 

printf("skip system call sleep\n"); 

printf("waiting for next signal\n"); 

2.11.kill()     向一个或一组进程发送一个信号

功能:向一个或一组进程发送一个信号

语法:

#include <sys/types.h> 

#include <signal.h> 

int kill(pid,sig); 

pid_t pid; 

int sig; 

说明:本系统调用向一个或一组进程发送一个信号,该信号由参数sig指 

,为系统给出的信号表中的一个.若为0(空信号)则检查错误但 

实际上并没有发送信号,用于检查pid的有效性

pid指定将要被发送信号的进程或进程组.pid若大于0,则信号将 

被发送到进程号等于pid的进程;pid等于0则信号将被发送到所 

有的与发送信号进程同在一个进程组的进程(系统的特殊进程除 

);pid小于-1,则信号将被发送到所有进程组号与pid绝对值 

相同的进程;pid等于-1,则信号将被发送到所有的进程(特殊系 

统进程除外). 

信号要发送到指定的进程,首先调用进程必须有对该进程发送信 

号的权限.若调用进程有合适的优先级则具备有权限.若调用进程 

的实际或有效的UID等于接收信号的进程的实际UID或用setuid() 

系统调用设置的UID,sig等于SIGCONT同时收发双方进程的会话 

号相同,则调用进程也有发送信号的权限

若进程有发送信号到pid指定的任何一个进程的权限则调用成功

否则调用失败,没有信号发出

返回值:调用成功则返回0,否则返回-1. 

例子:假设前一个例子进程号为324,现向它发一个SIGINT信号,让它做 

信号处理

kill((pid_t)324,SIGINT); 

2.12.alarm()   设置一个进程的超时时钟

功能:设置一个进程的超时时钟

语法:

#include <unistd.h< 

unsigned int alarm(sec) 

unsigned int sec; 

说明:指示调用进程的超时时钟在指定的时间后向调用进程发送一个 

SIGALRM信号.设置超时时钟时时间值不会被放入堆栈中,后一次 

设置会把前一次(还未到超时时间)冲掉

sec0,则取消任何以前设置的超时时钟

fork()会将新进程的超时时钟初始化为0.而当一个进程用exec() 

族系统调用新的执行文件时,调用前设置的超时时钟在调用后仍 

有效

返回值:返回上次设置超时时钟后到调用时还剩余的时间秒数

例子:int flag=0; 

void myself() 

flag=1; 

printf("get signal SIGALRM\n"); 

/*若要重新设置SIGALRM信号中断处理函数为本函数则执行 

*以下步骤*/ 

void (*a)(); 

a=myself; 

signal(SIGALRM,a); 

flag=2; 

main() 

alarm(100); /*100秒后发超时中断信号*/ 

while (1) { 

sleep(2000); /*等待中断信号*/ 

if (flag==1) { 

printf("skip system call sleep\n"); 

exit(0); 

if (flag==2) { 

printf("skip system call sleep\n"); 

printf("waiting for next signal\n"); 

2.13.msgsnd()  发送消息到指定的消息队列中

功能:发送消息到指定的消息队列中

语法:

#include <sys/types.h> 

#include <sys/ipc.h> 

#include <sys/msg.h> 

int msgsnd(msqid,msgp,msgsz,msgflg) 

int msqid; 

void *msgp; 

size_t msgsz; 

int msgflg; 

说明:发送一个消息到由msqid指定消息队列标识号的消息队列

参数msgp指向一个用户定义的缓冲区,并且缓冲区的第一个域应 

为长整型,指定消息类型,其他数据放在缓冲区的消息中其他正文 

区内.下面是消息元素定义

long mtype; 

char mtext[]; 

mtype是一个整数,用于接收进程选择消息类型

mtext是一个长度为msgsz字节的任何正文,参数msgsz可从0到系 

统允许的最大值间变化

msgflg指定操作行为

(msgflg&IPC_NOWAIT)是真的,消息并不是被立即发送而调用 

进程会立即返回

(msgflg&IPC_NOWAIT)不是真的,则调用进程会被挂起直到下 

面情况之一发生

消息被发送出去

消息队列标志被系统删除.系统调用返回-1. 

调用进程接收到一个未被忽略的中断信号,调用进程继续 

执行或被终止

调用成功后,对应指定的消息队列的相关结构做如下动作

消息数(msg_qnum)1. 

消息队列最近发送进程号(msg_lspid)改为调用进程号

消息队列发送时间(msg_stime)改为当前系统时间

以上信息可用命令ipcs -a看到

返回值:成功则返回0,否则返回-1. 

2.14.msgrcv()  从消息队列中取得指定类型的消息

功能:从消息队列中取得指定类型的消息

语法:

#include <sys/types.h> 

#include <sys/ipc.h> 

#include <sys/msg.h> 

int msgrcv(msqid,msgp,msgsz,msgtyp,msgflg) 

int msqid; 

void *msgp; 

int msgsz; 

long msgtyp; 

int msgflg; 

说明:本系统调用从由msqid指定的消息队列中读取一个由msgtyp指定 

类型的消息到由msgp指向的缓冲区中,同样的,该缓冲区的结构如 

前所述,包括消息类型和消息正文.msgsz为可接收的消息正文的 

字节数.若接收到的消息正文的长度大于msgsz,则会被截短到 

msgsz字节为止(当消息标志msgflg&MSG_NOERROR为真时),截掉的 

部份将被丢失,而且不通知消息发送进程

msgtyp指定消息类型

0则接收消息队列中第一个消息

大于0则接收消息队列中第一个类型为msgtyp的消息

小于0则接收消息队列中第一个类型值不小于msgtyp绝对值且 

类型值又最小的消息

msgflg指定操作行为

(msgflg&IPC_NOWAIT)是真的,调用进程会立即返回,若没有 

接收到消息则返回值为-1,errno设置为ENOMSG. 

(msgflg&IPC_NOWAIT)不是真的,则调用进程会被挂起直到下 

面情况之一发生

队列中的消息的类型是有效的

消息队列标志被系统删除.系统调用返回-1. 

调用进程接收到一个未被忽略的中断信号,调用进程继续 

执行或被终止

调用成功后,对应指定的消息队列的相关结构做如下动作

消息数(msg_qnum)1. 

消息队列最近接收进程号(msg_lrpid)改为调用进程号

消息队列接收时间(msg_rtime)改为当前系统时间

以上信息可用命令ipcs -a看到

返回值:调用成功则返回值等于接收到实际消息正文的字节数

不成功则返回-1. 

2.15.msgctl()   消息控制操作

功能:消息控制操作 

语法:

#include <sys/types.h> 

#include <sys/ipc.h> 

#include <sys/msg.h> 

int msgctl(msqid,cmd,buf) 

int msqid,cmd; 

struct msqid_ds *buf; 

说明:本系统调用提供一系列消息控制操作,操作动作由cmd定义,以下 

cmd定义值表明了各操作动作的定义

. IPC_STAT:msqid相关的数据结构中各个元素的当前值放入由 

buf指向的结构中

. IPC_SET:msqid相关的数据结构中的下列元素设置为由buf指 

向的结构中的对应值

msg_perm.uid 

msg_perm.gid 

msg_perm.mode 

msg_qbytes 

本命令只能由有效UID等于msg_perm.cuidmsg_perm.uid的 

进程或有效UID有合适权限的进程操作.只有具有合适权限的 

用户才能增加msg_qbytes的值

. IPC_RMID:删除由msqid指示的消息队列.将它从系统中删除并 

破坏相关的数据结构

本命令只能由有效UID等于msg_perm.cuidmsg_perm.uid的 

进程或有效UID有合适权限的进程操作

返回值:调用成功则返回值为0,否则为-1. 

2.16.msgget()   取得一个消息队列

功能:取得一个消息队列

语法:

#include <sys/types.h> 

#include <sys/ipc.h> 

#include <sys/msg.h> 

int msgget(key,msgflg) 

key_t key; 

int msgflg; 

说明:本系统调用返回与参数key相关的消息队列的标识符

若以下事实成立,则与消息队列相关的标识符和数据结构将被创 

建出来

若参数key等于IPC_PRIVATE. 

若参数key没有一个已存在的消息队列标识符与之相关,同时值 

(msgflg&IPC_CREAT)为真

创建消息队列时,与新的消息队列标识符相关的数据结构将被初 

始化为如下

. msg_perm.cuidmsg_perm.uid设置为调用进程的有效UID. 

. msg_perm.cgidmsg_perm.gid设置为调用进程的有效GID. 

. msg_perm.mode访问权限比特位设置为msgflg访问权限比特位

. msg_qnum,msg_lspid,msg_lrpid,msg_stime,msg_rtime设置为0. 

. msg_ctime设置为当前系统时间

. msg_qbytes设置为系统允许的最大值

返回值:调用成功则返回一非0,称为消息队列标识符;否则返回值为-1. 

例子:本例将包括上述所有消息队列操作的系统调用

#define RKEY 0x9001L /*读消息队列的KEY*/ 

#define WKEY 0x9002L /*写消息队列的KEY*/ 

#define MSGFLG 0666 /*消息队列访问权限*/ 

#define IPC_WAIT 0 /*等待方式在include文件中未定义*/ 

int rmsqid; /*读消息队列标识符*/ 

int wmsqid; /*写消息队列标识符*/ 

struct msgbuf { 

long mtype; 

char mtext[200]; 

} buf; 

/*若读消息队列已存在就取得标识符,否则则创建并取得标识符*/ 

if ((rmsqid=msgget(RKEY,MSGFLG|IPC_CREAT))<0) { 

printf("get read message queue failed\n"); 

exit(1); 

} /*若写消息队列已存在则失败,若不存在则创建并取得标识符*/ 

if ((wmsqid="msgget(WKEY," MSGFLG|IPC_CREAT|IPC_TRUNC))<0) { 

printf("get write message queue failed\n"); 

exit(2); 

} /*接收所有类型的消息*/ 

if (msgrcv(rmsqid,&buf,sizeof(struct msgbuf)-sizeof(long), 0L,IPC_WAIT)>0) 

printf("get %ld type message from queue:%s\n", 

buf.mtype,buf.mtext); 

else { 

printf("get message failed\n"); 

exit(3); 

buf.mtype=3L; 

if (msgsnd(wmsqid,&buf,sizeof(struct msgbuf)-sizeof(long), 

IPC_NOWAIT)>0) 

printf("send message OK\n"); 

else { 

printf("send message failed\n"); 

exit(4); 

msgctl(wmsqid,IPC_RMID,(struct msqid *)NULL); 

2.17.shmat()    联接共享内存的操作

功能:联接共享内存的操作

语法:

#include <sys/types.h> 

#include <sys/ipc.h> 

#include <sys/shm.h> 

void *shmat(shmid,shmaddr,shmflg) 

int shmid; 

void *shmaddr; 

int shmid; 

说明:将由shmid指示的共享内存联接到调用进程的数据段中.被联接的 

段放在地址,该地址由以下准则指定

shmaddr等于(void *)0,则段联接到由系统选择的第一个可 

用的地址上

shmaddr不等于(void *)0同时(shmflg&SHM_RND)值为真,则 

段联接到由(shmaddr-(shmaddr%SHMLBA))给出的地址上

shmaddr不等于(void *)0同时(shmflg&SHM_RND)值为假,则 

段联接到由shmaddr指定的地址上

(shmflg&sSHM_RDONLY)为真并且调用进程有读允许,则被联接 

的段为只读;否则,若值不为真且调用进程有读写权限,则被联接 

的段为可读写的

返回值:若调用成功则返回被联接的共享内存段在数据段上的启始地址

否则返回值为-1. 

2.18.shmdt()    断开共享内存联接的操作

功能:断开共享内存联接的操作

语法:

#include <sys/types.h> 

#include <sys/ipc.h> 

#include <sys/shm.h> 

void *shmdt(shmaddr) 

void *shmaddr; 

说明:本系统调用将由shmaddr指定的共享内存段从调用进程的数据段 

脱离出去

返回值:若调用成功则返回值为0,否则返回值为-1. 

2.19.shmget()   取得共享内存段

功能:取得共享内存段 

语法:

#include <sys/types.h> 

#include <sys/ipc.h> 

#include <sys/shm.h> 

int shmget(key,size,shmflg) 

key_t key; 

int size,shmflg; 

说明:本系统调用返回key相关的共享内存标识符

共享内存标识符和相关数据结构及至少size字节的共享内存段能 

正常创建,要求以下事实成立

参数key等于IPC_PRIVATE. 

参数key没有相关的共享内存标识符,同时(shmflg&IPC_CREAT) 

值为真

共享内存创建时,新生成的共享内存标识相关的数据结构被初始 

化如下

. shm_perm.cuidshm_perm.uid设置为调用进程的有效UID. 

. shm_perm.cgidshm_perm.gid设置为调用进程的有效GID. 

. shm_perm.mode访问权限比特位设置为shmflg访问权限比特位

. shm_lpid,shm_nattch,shm_atime,shm_dtime设置为0. 

. shm_ctime设置为当前系统时间

. shm_segsz设置为0. 

返回值:若调用成功则返回一个非0,称为共享内存标识符,否则返回 

值为-1. 

2.20.shmctl()   共享内存控制操作

功能:共享内存控制操作

语法:

#include <sys/types.h> 

#include <sys/ipc.h> 

#include <sys/shm.h> 

int shmctl(shmid,cmd,buf) 

int shmid,cmd; 

struct shmid_ds *buf; 

说明:本系统调用提供一系列共享内存控制操作.操作行为由cmd指定

以下为cmd的有效值

. IPC_STAT:shmid相关的数据结构中各个元素的当前值放入由 

buf指向的结构中

. IPC_SET:shmid相关的数据结构中的下列元素设置为由buf指 

向的结构中的对应值

shm_perm.uid 

shm_perm.gid 

shm_perm.mode 

本命令只能由有效UID等于shm_perm.cuidshm_perm.uid的 

进程或有效UID有合适权限的进程操作

. IPC_RMID:删除由shmid指示的共享内存.将它从系统中删除并 

破坏相关的数据结构

本命令只能由有效UID等于shm_perm.cuidshm_perm.uid的 

进程或有效UID有合适权限的进程操作

返回值:若调用成功则返回0,否则返回-1. 

例子:本例包括上述所有共享内存操作系统调用

#include <sys/types.h> 

#include <sys/ipc.h> 

#include <sys/shm.h> 

#define SHMKEY 74 

#define K 1024 

int shmid; 

cleanup() 

shmctl(shmid,IPC_RMID,0); 

exit(0); 

main() 

int *pint; 

char *addr1,*addr2; 

extern char *shmat(); 

extern cleanup(); 

for (i=0;i<20;i++) signal(i,cleanup); 

shmid=shmget(SHMKEY,128*K,0777|IPC_CREAT); 

addr1=shmat(shmid,0,0); 

addr2=shmat(shmid,0,0); 

printf("addr1 0x%x addr2 0x%x\n",addr1,addr2); 

pint=(int*)addr1; 

for (i=0;i<256;i++) *pint++=i; 

pint=(int*)addr1; 

*pint=256; 

pint=(int*)addr2; 

for (i=0;i<256;i++) 

printf("index %d\tvalue%d\n",i,*pint++); 

shmdt(addr1); 

shmdt(addr2); 

pause(); 

2.21.semctl()   信号量控制操作

功能:信号量控制操作

语法:

#include <sys/types.h> 

#include <sys/ipc.h> 

#include <sys/sem.h> 

int semctl(semid,memnum,cmd,arg) 

int semid,semnum,cmd; 

union semun { 

int val; 

struct semid_ds *buf; 

ushort *array; 

}arg; 

说明:本系统调用提供了一个信号量控制操作,操作行为由cmd定义,这 

些命令是对由semidsemnum指定的信号量做操作的.每个命令都 

要求有相应的权限级别

. GETVAL:返回semval的值,要求有读权限

. SETVAL:设置semval的值到arg.val.此命令成功执行后

semadj的值对应的所有进程的信号量全部被清除,要求有修 

改权限

. GETPID:返回sempid的值,要求有读权限

. GETNCNT:返回semncnt的值,要求有读权限

. GETZCNT:返回semzcnt的值,要求有读权限

以下命令在一组信号量中的各个semval上操作

. GETALL:返回每个semval的值,同时将各个值放入由arg.array 

指向的数组中.当此命令成功执行后,semadj的值对应的所有 

进程的信号量全部被清除,要求有修改权限

. SETALL:根据由arg.array指向的数组设置各个semval.当此 

命令成功执行后,semadj的值对应的所有进程的信号量全部 

被清除,要求有修改权限

以下命令在任何情况下都是有效的

. IPC_STAT:将与semid相关的数据结构的各个成员的值放入由 

arg.buf指向的结构中.要求有读权限

. IPC_SET:设置semid相关数据结构的如下成员,设置数据从 

arg.buf指向的结构中读取

sem_perm.uid 

sem_perm.gid 

sem_perm.mode 

本命令只能由有效UID等于sem_perm.cuidsem_perm.uid的 

进程或有效UID有合适权限的进程操作

. IPC_RMID:删除由semid指定的信号量标识符和相关的一组信号 

量及数据结构.本命令只能由有效UID等于sem_perm.cuid或 

sem_perm.uid的进程或有效UID有合适权限的进程操作

返回值:若调用成功,则根据cmd返回以下值

GETVAL:semval的值

GETPID:sempid的值

GETNCNT:semncnt的值

GETZCNT:semzcnt的值

其他:0. 

若调用失败则返回-1. 

2.22.semget()   取得一组信号量

功能:取得一组信号量

语法:

#include <sys/types.h> 

#include <sys/ipc.h> 

#include <sys/sem.h> 

int semget(key,nsems,semflg) 

key_t key; 

int nsems,semflg; 

说明:返回和key相关的信号量标识符

若以下事实成立,则与信号量标识符,与之相关的semid_ds数据结 

构及一组nsems信号量将被创建

. key等于IPC_PRIVATE. 

系统内还没有与key相关的信号量,同时(semflg&IPC_CREAT) 

为真

创建时新的信号量相关的semid_ds数据结构被初始化如下

在操作权限结构,sem_perm.cuidsem_perm.uid设置等于调用 

进程的有效UID. 

在操作权限结构,sem_perm.cgidsem_perm.gid设置等于调用 

进程的有效GID. 

访问权限比特位sem_perm.mode设置等于semflg的访问权限比 

特位

. sem_otime设置等于0,sem_ctime设置等于当前系统时间

返回值:若调用成功,则返回一非0,称为信号量标识符;否则返回-1. 

2.23.semop()   信号量操作

功能:信号量操作

语法:

#include <sys/types.h> 

#include <sys/ipc.h> 

#include <sys/sem.h> 

int semop(semid,sops,nsops) 

int semid; 

struct sembuf *sops; 

unsigned nsops; 

说明:本系统调用用于执行用户定义的在一组信号量上操作的行为集合

该组信号量与semid相关

参数sops为一个用户定义的信号量操作结构数组指针

参数nsops为该数组的元素个数

数组的每个元素结构包括如下成员

sem_num; /* 信号量数 */ 

sem_op; /* 信号量操作 */ 

sem_flg; /* 操作标志 */ 

由本系统调用定义的每个信号量操作是针对由semidsem_num指 

定的信号量的.变量sem_op指定三种信号量操作的一种

sem_op为一负数并且调用进程具有修改权限,则下列情况之 

一将会发生

semval不小于sem_op的绝对值,sem_op的绝对值被减去 

semval的值.(semflg&SEM_UNDO)为真则sem_op的绝对值加 

上调用进程指定的信号量的semadj

semval小于sem_op的绝对值同时(semflg&IPC_NOWAIT)为 

,则本调用立即返回

semval小于sem_op的绝对值同时(semflg&IPC_NOWAIT)为 

,则本系统调用将增加指定信号量相关的semncnt(加一), 

将调用进程挂起直到下列条件之一被满足

(1).semval值变成不小于sem_op的绝对值.当这种情况发 

生时,指定的信号量相关的semncnt减一,若 

(semflg&SEM_UNDO)为真则sem_op的绝对值加上调用 

进程指定信号量的semadj

(2).调用进程等待的semid已被系统删除

(3).调用进程捕俘到信号,此时,指定信号量的semncnt值 

减一,调用进程执行中断服务程序

sem_op为一正值,同时调用进程具有修改权限,sem_op的值加 

semval的值,(semflg&SEM_UNDO)为真,sem_op减去调用 

进程指定信号量的semadj

sem_op0,同时调用进程具有读权限,下列情况之一将会发 

semval0,本系统调用立即返回

semval不等于0(semflg&IPC_NOWAIT)为真,本系统调用 

立即返回

semval不等于0(semflg&IPC_NOWAIT)为假,本系统调用 

将把指定信号量的 

semzcnt值加一,将调用进程挂起直到下列情况之一发生

(1).semval值变为0,指定信号量的semzcnt值减一

(2).调用进程等待的semid已被系统删除

(3).调用进程捕俘到信号,此时,指定信号量的semncnt值 

减一,调用进程执行中断服务程序

返回值:调用成功则返回0,否则返回-1. 

例子:本例将包括上述信号量操作的所有系统调用

#include <sys/types.h> 

#include <sys/ipc.h> 

#include <sys/sem.h> 

#define SEMKEY 75 

int semid; 

unsigned int count; 

/*在文件sys/sem.h中定义的sembuf结构 

* struct sembuf { 

* unsigned short sem_num; 

* short sem_op; 

* short sem_flg; 

* }*/ 

struct sembuf psembuf,vsembuf; /*PV操作*/ 

cleanup() 

semctl(semid,2,IPC_RMID,0); 

exit(0); 

main(argc,argv) 

int argc; 

char *argv[]; 

int i,first,second; 

short initarray[2],outarray[2]; 

extern cleanup(); 

if (argc==1) { 

for (i=0;i<20;i++) 

signal(i,clearup); 

semid=semget(SEMKEY,2,0777|IPC_CREAT); 

initarray[0]=initarray[1]=1; 

semctl(semid,2,SETALL,initarray); 

semctl(semid,2,GETALL,outarray); 

printf("sem init vals %d%d \n", outarray[0],outarray[1]); 

pause(); /*睡眠到被一软件中断信号唤醒*/ 

} else if (argv[1][0]="='a') 

first=0; 

second=1; 

} else { 

first=1; 

second=0; 

semid=semget(SEMKEY,2,0777); 

psembuf.sem_op=-1; 

psembuf.sem_flg=SEM_UNDO; 

vsembuf.sem_op=1; 

vsembuf.sem_flg=SEM_UNDO; 

for (count=0;;xcount++) 

psembuf.sem_num=first; 

semop(semid,&psembuf,1); 

psembuf.sem_num=second; 

semop(semid,&psembuf,1); 

printf("proc %d count %d\n",getpid(),count); 

vsembuf.sem_num=second; 

semop(semid,&vsembuf,1); 

vsembuf.sem_num=first; 

semop(semid,&vsembuf,1); 

2.24.sdenter()  共享数据段同步访问,加锁

功能:共享数据段同步访问,加锁

语法:

#include <sys/sd.h< 

int sdenter(addr,flags) 

char *addr; 

int flags; 

说明:用于指示调用进程即将可以访问共享数据段中的内容

参数addr为将一个sdget()调用的有效返回码

所执行的动作取决于flags的值

. SD_NOWAIT:若另一个进程已对指定的段调用本系统调用且还没 

有调用sdleave(),并且该段并非用SD_UNLOCK标志创建,则调 

用进程不是等待该段空闲而是立即返回错误码

. SD_WRITE:指示调用进程希望向共享数据段写数据.此时,另一 

个进程用SD_RDONLY标志联接该共享数据段则不被允许

返回值:调用成功则返回0,否则返回-1. 

2.25.sdleave()  共享数据段同步访问,解锁

功能:共享数据段同步访问,解锁

语法:

#include <sys/sd.h&dt; 

int sdleave(addr,flags) 

char *addr; 

说明:用于指示调用进程已完成修改共享数据段中的内容

返回值:调用成功则返回0,否则返回-1. 

2.26.sdget()    联接共享数据段到调用进程的数据空间中

功能:联接共享数据段到调用进程的数据空间中

语法:

#include <sys/sd.h> 

char *sdget(path,flags,size.mode) 

char *path; 

int flags; 

long size; 

int mode; 

说明:本系统调用将共享数据段联接到调用进程的数据段中,具体动作 

flags的值定义

. SD_RDONLY:联接的段为只读的

. SD_WRITE:联接的段为可读写的

. SD_CREAT:若由path命名的段存在且不在使用中,本标志的作用 

同早先创建一个段相同,否则,该段根据sizemode的值进程 

创建.对段的读写访问权限的授予基于mode给的权限,功能与 

一般文件的相同.段被初始化为全0. 

. SD_UNLOCK:若用此标志创建该段,则允许有多个进程同时访问 

(在读写中)该段

返回值:若调用成功则返回联接的段地址.否则返回-1. 

2.27.sdfree()   将共享数据段从调用进程的数据空间中断开联接

功能:将共享数据段从调用进程的数据空间中断开联接

语法:

#include <sys/sd.h> 

int sdfree(addr) 

char *addr; 

说明:本系统调用将共享数据段从调用进程的数据段的指定地址中分离

若调用进程已完成sdenter()的调用,还未调用sdleave()就调用 

本系统调用,sdleave()被自动调用,然后才做本调用的工作

返回值:若调用成功则返回联接的段地址.否则返回-1. 

2.28.sdgetv()   同步共享数据访问

功能:同步共享数据访问

语法:

#include <sys/sd.h> 

int sdgetv(addr) 

char *addr; 

说明:用于同步协调正在使用共享数据段的进程.返回值为共享数据段 

的版本号.当有进程对该段做sdleave()操作时,版本号会被修改

返回值:若调用成功,则返回指定共享数据段的版本号,否则返回-1. 

2.29.sdwaitv()  同步共享数据访问

功能:同步共享数据访问

语法:

#include <sys/sd.h> 

int sdwaitv(addr,vnum) 

char *addr; 

int vnum; 

说明:用于同步协调正在使用共享数据段的进程.返回值为共享数据段 

的版本号.调用进程会睡眠直到指定段的版本号不再等于vnum; 

返回值:若调用成功,则返回指定共享数据段的版本号,否则返回-1. 

2.30.sbrk()     修改数据段空间分配

功能:修改数据段空间分配

语法:

char *sbrk(incr) 

int incr; 

说明:用于动态修改调用进程数据段的空间分配.进程将重置进程的分 

段值并分配一个合适大小的空间.分段值为数据段外第一次分配 

的地址.要分配的空间的增加量等于分段值的增加量.新分配的空 

间设置为0.若相同的内存空间重新分配给同一个进程,则空间的 

内容不确定

返回值:若成功调用则返回值为0,否则返回-1. 

例子:本例将包括上述共享数据空间操作的所有系统调用

char * area1; 

char buf[21]; 

int v; 

/*取得或创建一个共享数据空间(系统特殊文件),名字为 

/tmp/area1,长度为640,用户访问权限为0777*/ 

area1=sdget("/tmp/area1",SD_WRITE|SD_CREAT,640,0777); 

if ((int)area1==-1) { 

printf("get share data segment area1 failed\n"); 

exit(1); 

/*取得共享数据段area1的版本号*/ 

v=sdgetv(area1); 

/*申请访问共享数据段area1,若已有进程在访问该段则本进程挂 

*,否则进入访问并将该数据段加写锁*/ 

sdenter(area1,SD_WRITE); 

/*对共享数据段访问,10a*/ 

strcpy(area1,"aaaaaaaaaa"); 

/*申请解除访问权限,若已有进程申请访问则激活该进程*/ 

sdleave(area1); 

/*进程处理过程*/ 

/*等待取共享数据段area1的版本号*/ 

sdwaitv(area1,v); 

/*重新申请访问共享数据段area1*/ 

sdenter(area1,SD_WRITE); 

/*读取共享数据段中的数据*/ 

memcpy(buf,area1,20); 

/*申请解除访问权限,若已有进程申请访问则激活该进程*/ 

sdleave(area1); 

printf("the data now in area1 is [%s]\n",buf); 

2.31.getenv()   取得指定环境变量值

功能:取得指定环境变量值

语法:

#include <unistd.h>

#include <stdlib.h>

char *getenv(name) 

char *name; 

说明:本系统调用检查环境字符串(格式如name="value),并在找到有指定名字的环境值后,返回指向value字符串的指针.否则返回空指 针

返回值:如前述

例子

char * value; 

value=getenv("HOME"); 

printf("HOME="[%s]\n",value); /*将打印出HOME环境变量的值*/ 

2.32.putenv()   修改或增加环境值

功能:修改或增加环境值

语法:

#include <stdlib.h> 

int putenv(string) 

char *string; 

说明:参数string指向一个字符串,格式如下

name=value 

本系统调用将环境变量name等于值value,修改或增加一个环境变 

,字符串string成为环境的一部分

返回值:putenv()不能取得合适的内存空间则返回非0,否则返回0. 

例子:/*父进程处理*/ 

putenv("HOME=/home/abcdef"); 

putenv("PATH=/bin"); 

if (fork()>0) 

exit(0); /*父进程退出运行*/ 

/*子进程处理*/ 

setpgrp(); 

/*父进程设置的环境变量已传到子进程*/ 

char * value1; 

value1=getenv("HOME"); 

value2=getenv("PATH"); 

printf("HOME=[%s],PATH=[%s]\n",value1,value2); 

/*将打印出"HOME=/home/abcdef""PATH=/bin"*/ 

.多进程编程技巧 

3.1.主要程序结构 

3.1.1事件主控方式 

若是应用程序属于事务处理方式,则在主函数中设计为监控事件发生当事件发生时,可以生成一个新的进程来处理该事务,事务处理完成后就 可以让子进程退出系统.这种处理方式一般不要消息传递

3.1.2信息协调方式 

若是应用程序需要由多个进程协调处理完成,则可以生成这些进程通过消息在进程间的传递,使各个进程能相互协调,共同完成事务.这种处理方式一般是用fork()生成几个进程后,exec()调用其它程序文件,使得不同的程序同时在系统内运行.然后通过IPC机制传送消息,使各个程序能协调运行

3.2.选择主体分叉点 

3.2.1事件初始产生 

对应于事件主控方式的程序结构.关键点在于以何种方式选择事件的初始发生点,如网络程序给出的建链信息.主控程序在收到该消息后就认为是一个事件开始,则可以产生一个子进程处理后面的事务:接收交易信息,事务处理,发送返回交易信息,关闭链接等,完成后将子进程退出系统

3.2.2主程序自主产生 

对应于信息协调方式的程序结构.主控程序只负责生成几个子进程,各个子进程分别调用exec()将不同的执行文件调入内存运行,主控程序在生成所有的子进程后即可退出系统,将子进程留在内存中运行

3.3.进程间关系处理 

3.3.1父子进程关系 

进程组处理 

进程组的概念是这样的,当系统启动时,第一个进程是init,其进程 

组号等于进程号,由它产生的所有子进程的进程组号也相同,子进程 

的子进程也继承该进程组号,这样,init所生成的所有子进程都属 

于同一个进程组.但是,同一个进程组的父子进程可能在信号上有相 

互通讯,若父进程先于子进程退出系统,则子进程会成为一个孤儿进 

,可能变成僵死进程.从而使该子进程在其不"愿意"的情况下退出 

运行.为解决这个问题,子进程可以自己组成一个新的进程组,即调 

setpgrp()与原进程组脱离关系,产生一个新的进程组,进程组号 

与它的进程号相同.这样,父进程退出运行后就不会影响子进程的当 

前运行

子进程信号处理 

但是,单做上述处理还不能解决另一个困难,即子进程在退出运行 

,找不到其父进程(父进程已退出,子进程的父进程号改为1).发送 

子进程退出信号后没有父进程做出响应处理,该子进程就不可能完 

全退出运行,可能进入僵死状态.所以父进程在产生子进程前最好屏 

蔽子进程返回信号的处理,生成子进程,在父进程退出运行后,子进 

程返回则其进程返回信号的处理会由系统给出缺省处理,子进程就 

可以正常退出

3.3.2兄弟进程关系 

交换进程号 

对于信息协调方式的程序来说,各兄弟进程间十分需要相互了解进 

程号,以便于信号处理机制.比较合理的方法是父进程生成一个共享 

内存的空间,每个子进程都在启动时在共享内存中设置自己的进程 

.这样,当一个子进程要向另一个子进程发送信号或是因为其他原 

因需要知道另一个子进程号时,就可以在共享内存中访问得到所需 

要的进程号

3.3.3僵尸进程及如何处理子进程死亡 

3.3.3.1僵尸进程

僵尸(Zombie)进程是非常特殊的一种,它已经放弃了几乎所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集,除此之外,僵尸进程不再占有任何内存空间。

当一个进程已退出,但其父进程还没有调用系统调用wait(稍后介绍)对其进行收集之前的这段时间里,它会一直保持僵尸状态。

僵尸进程的概念是从UNIX上继承来的,而UNIX的先驱们设计这个东西并非是因为闲来无聊想烦烦其他的程序员。僵尸进程中保存着很多对程序员和系统管理员非常重要的信息,首先,这个进程是怎么死亡的?是正常退出呢,还是出现了错误,还是被其它进程强迫退出的?其次,这个进程占用的总系统CPU时间和总用户CPU时间分别是多少?发生页错误的数目和收到信号的数目。这些信息都被存储在僵尸进程中,试想如果没有僵尸进程,进程一退出,所有与之相关的信息都立刻归于无形,而此时程序员或系统管理员需要用到,就只好干瞪眼了。

收集这些信息,并终结这些僵尸进程靠waitpid调用和wait调用等方法完成。

僵尸进程虽然对其他进程几乎没有什么影响,不占用CPU时间,消耗的内存也几乎可以忽略不计,但有它在那里呆着,还是让人觉得心里很不舒服,同时Linux系统中进程数目是有限制的,在一些特殊的情况下,如果存在太多的僵尸进程,也会影响到新进程的产生。

for(int i=0;i<10;i++)

  if( fork==0)

        exit(0);

   

3.3.3.2处理子进程死亡的四种方法

3.3.3.2.1  忽略SIGCHLD信号.(只在Linux使用);

struct sigaction act,oldact;

act.sa_handler=SIG_IGN;

sigemptyset(&act.sa_mask);

act.sa_flags=0;

if(sigaction(SIGCHLD,&act,&oldact)<0)

exit(1);

内核负责清除进程表项。(Linux only)

3.3.3.2.2  调用wait()或waitpid();

  pid_t wait(int* statloc);

pid_t waitpid(pid_t pid,int* statloc,int option);

前者等待任意一个子进程结束,后者等待特定子进程结束;

函数返回子进程号,statloc返回exit的参数。

Wait:  进程一旦调用了wait,就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。

参数statloc用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。如果我们对这个子进程是如何死掉的毫不在意,只想把这个僵尸进程消灭掉,(绝大多数情况下如此),我们就可以设定这个参数为NULL,即:

pidx = wait(NULL)

如果成功,wait会返回被收集的子进程的进程ID,如果调用进程没有子进程,调用就会失败,此时wait返回-1,同时errno被置为ECHILD。

Waitpid: 从本质上讲,系统调用waitpid和wait的作用是完全相同的,但waitpid多出了两个可由用户控制的参数pid和options,从而为我们编程提供了另一种更灵活的方式。

Pid:是一个进程ID。当pid取不同的值时,有不同的意义:

pid>0时,只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去。 

pid=-1时,等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用一模一样。 

pid=0时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬。 

pid<-1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。 

Options:目前在Linux中只支持WNOHANG和WUNTRACED两个选项,可以用"|"运算符把它们连接起来使用,如:

ret=waitpid(-1,NULL,WNOHANG | WUNTRACED);

WNOHANG,表示即使没有子进程退出,它也会立即返回,不会像wait那样永远等下去。

WUNTRACED,与跟踪调试有关,极少用到。

我们也可以把options设为0,如:

ret=waitpid(-1,NULL,0);

waitpid的返回值比wait稍微复杂一些,一共有3种情况:

1) 正常返回的时候,waitpid返回收集到的子进程的进程ID; 

2) 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;

3) 调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在。例如:当pid所指示的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid就会出错返回,这时errno被设置为ECHILD;

3.3.3.2.3  捕获SIGCHLD信号,在处理函数中调用wait()或waitpid().

void sigchld_handler(int signo)

{

  pid_t pid;

  int stat;

  while( (pid=waitpid(-1,&stat,WNOHANG))>0 )

{}

return;

}

void main()

{

struct sigaction act,oldact;

act.sa_handler=sigchld_handler;

sigemptyset(&act.sa_mask);

act.sa_flags=0;

if(sigaction(SIGCHLD,&act,&oldact)<0)

{

  ……

}

}

3.3.3.2.4  两次调用fork().

第一次调用fork()产生的子进程的主进程,调用exit(0), 第二次调用fork()产生的子进程成为孤儿(orphaned process)进程,交给init管理,孤儿进程退出时.系统会把它清理干净。

int main()

{

      int i;

      pid_t pid;

      pid=fork();

      if(pid==0)

      {

     for(i=0;i<5;i++)

     {

        if(fork(0)==0)

        {

       sleep(1);

       exit(0);

        }

     }

     exit(0);

      }

      for(;;){}

}

总结:进程的一生

随着一句fork,一个新进程呱呱落地,但它这时只是老进程的一个克隆。

然后随着exec,新进程脱胎换骨,离家独立,开始了为人民服务的职业生涯。

人有生老病死,进程也一样,它可以是自然死亡,即运行到main函数的最后一个"}",从容地离我们而去;也可以是自杀,自杀有2种方式,一种是调用exit函数,一种是在main函数内使用return,无论哪一种方式,它都可以留下遗书,放在返回值里保留下来;它还甚至能可被谋杀,被其它进程通过另外一些方式结束他的生命。

进程死掉以后,会留下一具僵尸,wait和waitpid充当了殓尸工,把僵尸推去火化,使其最终归于无形。

3.3.4守护进程 

Linux有三种进程:核心进程、守护进程、用户进程。

守护进程在后台运行,如:打印管理程序、http服务器

Linux的大多数服务器就是用守护进程实现的。

守护进程的编程要点 
    不同Unix环境下守护进程的编程规则并不一致,但守护进程的编程原则其实都一样,区别在于具体的实现细节不同。这个原则就是要满足守护进程的特性。同时,Linux是基于Syetem V的SVR4并遵循Posix标准,实现起来与BSD4相比更方便。编程要点如下; 
1. 在后台运行 

   在进程中调用fork后,使父进程终止,让Daemon在子进程中后台执行。 

   

if(pid=fork()) 

    exit(0);//是父进程,结束父进程,子进程继续 

2. 脱离控制终端,登录会话和进程组 
    进程与控制终端,登录会话和进程组之间的关系:

进程属于一个进程组,进程组号(GID)就是进程组长的进程号(PID)。

登录会话可以包含多个进程组。这些进程组共享一个控制终端。这个控制终端通常是创建进程的登录终端。会话过程对控制终端具有独占性

由于控制终端,登录会话和进程组通常是从父进程继承下来的。我们的目的就是要摆脱它们,使之不受它们的影响。方法是在第1点的基础上,调用setsid()使进程成为会话组长:     setsid();

说明:当进程是会话组长时setsid()调用失败。

第一点已经保证进程不是会话组长。

setsid()调用成功后,进程成为新的会话组长和新的进程组长,并与原来的登录会话和进程组脱离,同时与控制终端脱离。 

1. 忽略SIGHUP,再次调用fork(),然后父进程退出。

目的:禁止进程重新打开控制终端 

现在,进程已经成为无终端的会话组长。但它可以重新申请打开一个控制终端。可以通过使进程不再成为会话组长来禁止进程重新打开控制终端: 

if(pid=fork()) 

exit(0);//结束第一子进程,第二子进程继续(第二子进程不再是会话组长) 

2. 关闭打开的文件描述符 

进程从创建它的父进程那里继承了打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下,以及引起无法预料的错误。按如下方法关闭它们: 

for(i=0;i 关闭打开的文件描述符close(i);)

3. 改变当前工作目录 

进程活动时,其工作目录所在的文件系统不能卸下。一般需要将工作目录改变到根目录。对于需要转储核心,写运行日志的进程将工作目录改变到特定目录如/tmpchdir("/") 

4. 重设文件创建掩模 

进程从创建它的父进程那里继承了文件创建掩模。它可能修改守护进程所创建的文件的存取位。为防止这一点,将文件创建掩模清除:umask(0),使得进程具有完全的写权限; 

5. 处理SIGCHLD信号 

处理SIGCHLD信号并不是必须的。但对于某些进程,特别是服务器进程往往在请求到来时生成子进程处理请求。如果父进程不等待子进程结束,子进程将成为僵尸进程(zombie)从而占用系统资源。如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。在Linux下可以简单地将SIGCHLD信号的操作设为SIG_IGN。 

signal(SIGCHLD,SIG_IGN); 

这样,内核在子进程结束时不会产生僵尸进程。

守护进程实例 

这个守护进程实例包括两部分:主程序test.c和初始化程序init.c。主程序每隔一分钟向/tmp目录中的日志test.log报告运行状态。

初始化程序中的init_daemon函数负责生成守护进程。可以利用init_daemon函数生成自己的守护进程。 

1. init.c清单 

#include < unistd.h > 
#include < signal.h > 
#include < sys/param.h > 
#include < sys/types.h > 
#include < sys/stat.h > 
void init_daemon(void) 

  int pid; 
  int i; 
  if(pid=fork()) 
      exit(0);//是父进程,结束父进程 
  else if(pid< 0) 
      exit(1);//fork失败,退出 
  

//第一子进程,在后台继续执行 
setsid();  //第一子进程成为新的会话组长和进程组长 
           //并与控制终端分离 
if(pid=fork()) 
    exit(0);    //是第一子进程,结束第一子进程 
else if(pid< 0) 
    exit(1);    //fork失败,退出 

//第二子进程,继续 
//第二子进程不再是会话组长 

for(i=0;i< NOFILE;++i)//关闭打开的文件描述符 
close(i); 

chdir("/tmp");//改变工作目录到/tmp 

umask(0);//重设文件创建掩模 

return; 

}

2. test.c清单 
#include < stdio.h > 
#include < time.h > 

void init_daemon(void);//守护进程初始化函数 

main() 

  FILE *fp; 
  time_t t; 
  init_daemon();//初始化为Daemon 

while(1)//每隔一分钟向test.log报告运行状态 

  sleep(60);//睡眠一分钟 
  if((fp=fopen("test.log","a")) >=0) 
  { 
    t=time(0); 
    fprintf(fp,"Im here at %sn",asctime(localtime(&t)) ); 
    fclose(fp); 
  } 


编译:gcc -g -o test init.c test.c 
执行:./test 

查看进程:ps -ef 
从输出可以发现test守护进程的各种特性满足上面的要求。 

3.4.进程间通讯处理 

3.4.1共享内存需要锁机制 

由于共享内存在设计时没有处理锁机制,故当有多个进程在访问共享 

内存时就会产生问题.:一个进程修改一个共享内存单元,另一个进程在 

读该共享内存单元时可能有第三个进程立即修改该单元,从而会影响程序 

的正确性.同时还有分时系统对各进程是分时间片处理的,可能会引起不 

同的正确性问题.按操作系统的运作方式,则有读锁和写锁来保证数据的 

一致性.所以没有锁机制的共享内存,必须和信号量一起使用,才能保证共 

享内存的正确操作

3.4.2消息队列需要关键值 

消息队列的操作在进程取得消息队列的访问权限后就必须通过关键 

值来读消息队列中的相同关键值的消息,写消息时带入消息关键值.这样 

可以通过不同的关键值区分不同的交易,使得在同一个消息队列可以供多 

种消息同时使用而不冲突.若读消息队列使用关键值0则读取消息队列中 

第一个消息,不论其关键值如何

3.4.3信号需要信号处理函数设置和再设置 

在用户进程需要对某个中断做自己定义的处理时,可以自己定义中断 

处理函数,并设置中断处理函数与该中断相关联.这样,用户进程在收到该 

中断后,即调用用户定义的函数,处理完成后用户进程从被中断处继续运 

(若用户定义的中断函数没有长跳函数或退出运行等会改变运行指令地 

址的系统调用).在中断信号被处理后,该中断的处理函数会恢复成上次缺 

省处理函数而不是保持用户定义函数,故在用户定义的中断处理函数中一 

般都再定义该中断和函数自己的关联

3.4.4IPC的权限设置 

在消息队列,共享内存和信号量的访问时有用户访问权限设置,类同 

于文件的访问权限的设置如(777表示rwxrwxrwx),用命令ipcs即可看到在 

系统中生成的消息队列,共享内存和信号量的访问权限.其意义也类似于 

文件访问权限.只是执行位无效

在有名管道和文件方式共享内存中以系统文件的方式定义了用户的 

访问权限.用命令ls -l可以看到它们以系统文件方式存在并具有访问权 

限值,并可以看到有名管道的文件类型为p,文件方式共享内存的文件类型 

s. 

3.4.5信号中断对系统调用一级有效 

系统在设计系统调用时就考虑了中断处理问题.当进程运行到一个系 

统调用时发生了中断,则进程进入该中断处理,处理完成后,进程会跳过该 

系统调用而进入下一条程序指令

应该注意的是中断发生在系统调用一级而不是子程序或函数一级.比 

如一个程序在一个子程序被调用前设置了超时中断,并在子程序中收到超 

时中断,系统在处理完超时中断后接着处理该子程序被中断的系统调用之 

后的指令,而不是从调用该子程序名指令的后一条指令继续处理

3.4.6各种IPC方式的特点 

消息队列

通过消息队列key值定义和生成消息队列

任何进程只要有访问权限并知道key即可访问消息队列

消息队列为内存块方式数据段

消息队列中的消息元素长度可为系统参数限制内的任何长度

消息元素由消息类型分类,其访问方式为按类型访问

在一次读写操作前都必须取得消息队列标识符,即访问权.访问后即 

脱离访问关系

消息队列中的某条消息被读后即从队列中删除

消息队列的访问具备锁机制处理,即一个进程在访问时另一个进程 

不能访问

操作时要注意系统资源和效率

在权限允许时,消息队列的信息传递是双向的

共享内存 

通过共享内存key值定义和生成共享内存

任何进程只要有访问权限并知道key即可访问共享内存

共享内存为内存块方式的数据段

共享内存中的数据长度可为系统参数限制内的任何长度

共享内存的访问同数组的访问方式相同

在取得共享内存标识符将共享内存与进程数据段联接后即可开始对 

之进行读写操作,在所有操作完成之后再做共享内存和进程数据 

段脱离操作,才完成全部共享内存访问过程

共享内存中的数据不会因数据被进程读取后消失

共享内存的访问不具备锁机制处理,即多个进程可能同时访问同一 

个共享内存的同一个数据单元

共享内存的使用最好和信号量一起操作,以具备锁机制,保证数据的 

一致

在权限允许时,共享内存的信息传递是双向的

信号量 

用于生成锁机制,避免发生数据不一致

没有其他的数据信息

不需要有父子关系或兄弟关系

信号 

信号由系统进行定义

信号的发送只要有权限即可进行

信号是一个事件发生的信息标志,不带有其它信息

信号不具备数据块

信号的处理可由用户自己定义

信号可能由用户进程,操作系统(软件或硬件原因)等发出

有一些信号是不可被屏蔽的

信号中断的是系统调用级的函数

信号的信息传递是单向的

管道 

做为系统的特殊设备文件,可以是内存方式的,也可以是外存方式的

管道的传输一般是单向的,即一个管道一向,若两个进程要做双向传 

输则需要2个管道.管道生成时即有两端,一端为读,一端为写,两个 

进程要协调好,一个进程从读方读,另一个进程向写方写

管道的读写使用流设备的读写函数,:read(),write. 

管道的传输方式为FIFO,流方式的.不象消息队列可以按类型读取

有名管道 

一般为系统特殊文件方式,使用的进程之间不一定要有父子关系 

或兄弟关系

无名管道 

一般为内存方式,使用的进程之间一定要有父子关系或兄弟关系

文件 

文件是最简单的进程间通讯方式,使用外部存贮器为中介

操作麻烦,定位困难

保密程度低

容易出现数据不一致问题

占用硬盘空间

只要有权限并知道文件名,任何进程都可对之操作

特殊处理 

为避免出现保密问题,在打开文件,取得文件描述符后,调用 

unlink()将硬盘上的文件路径名删除,则硬盘上就没有文件拷贝 

.但在进程中该文件描述符是打开的,由该进程生成的子进程中 

该文件描述符也是打开的,就可以利用系统提供的文件缓冲区做 

进程间通讯,代价是进程间必须有父子关系或兄弟关系

环境变量 

信息的传送一般是单向的,即由父进程向子进程传送

保密性较好

双方必须约定环境变量名

只占用本进程和子进程的环境变量区

共享数据段 

操作比较复杂

占用硬盘空间,生成系统特殊文件

其他性质与共享内存相类似

流 

文件描述符的操作方式

进程间不一定要有父子关系或兄弟关系

双向传送信息

进程各自生成socket,bind()联接

其他性质与管道相类似

流编程为TCP/IP网络编程范围,在本文中暂不阐述

传递参数 

信息的传送一般是单向的即由父进程向子进程传送

保密性较差,用进程列表即可显示出来

双方必须约定参数位置

只占用子进程的参数区.  

.进程通信与同步

4.1. 概述 

  Linux内核主要由五个子系统组成:进程调度,内存管理,虚拟文件系统,网络接口,进程间通信。

    Linux提供了多种进程间的通信机制,其中,信号和管道是最基本的两种,也提供 System V的进程间通信机制,包括消息队列、信号灯及共享内存等。

 System V IPC对象权限包含在ipc_perm数据结构中,位于include/linux/ipc.h。

System V的消息是在ipc/msg.c中实现、共享内存在ipc/shm.c中实现、信号灯在ipc/sem.c中,管道在/ipc/pipe.c中实现。  

与Windows相比,在进程间通信机制上,Linux提供了标准的UNIX IPC机制,接近于IPC原语,比较底层,提供了最大的灵活性,也可以在此基础上建立更加复杂的高级IPC机制;Windows则在基本IPC机的基础上,提供了许多直接面向应用程序的高级IPC机制。

linux下进程间通信的几种主要手段简介:

1. 管道(Pipe)及有名管道(named pipe):管道可用于具有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信; 

2. 信号(Signal):信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又能够统一对外接口,用sigaction函数重新实现了signal函数); 

3. 消息(Message)队列:消息队列是消息的链接表,包括Posix消息队列system V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。 

4. 共享内存:使得多个进程可以访问同一块内存空间,是单机最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。 

5. 信号灯(semaphore):主要作为进程间以及同一进程不同线程之间的同步手段。 

6. 套接口(Socket)和UINX域套接字:更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支开发出来的,但现在一般可以移植到其它类Unix系统上:Linux和System V的变种都支持套接字。

进程同步就是要协调好2个以上的进程,使之以安排好地次序依次执行。有时候,父进程要求子进程的运算结果进行下一步的运算,或者子进程的功能是为父进程提供了下一步执行的先决条件(如:子进程建立文件,而父进程写入数据),此时父进程就必须在某一个位置停下来,等待子进程运行结束,而如果父进程不等待而直接执行下去的话,会出现极大的混乱。

解决进程同步问题可用信号、管道、套接字、共享内存等多种方法。简单情况下也可以用wait系统调用简单的予以解决。请看下面这段程序:

#include <sys/types.h>

#include <sys/wait.h>

main()

{

pid_t pc, pr;

int status;

pc=fork();

if(pc<0)

printf("Error occured on forking.\n");

else if(pc==0)

{

/* 子进程的工作 */

exit(0);

}

else

{

/* 父进程的工作 */

pr=wait(&status);

/* 利用子进程的结果 */

}

}

当fork调用成功后,父子进程各做各的事情,但当父进程的工作告一段落,需要用到子进程的结果时,它就停下来调用wait,一直等到子进程运行结束,然后利用子进程的结果继续执行。

4.2. 管道

系统调用pipe ( ) 创建管道。管道是进程间通信最古老的方式,它包括无名管道和有名管道两种,前者用于父进程和子进程间的通信,后者用于运行于同一台机器上的任意两个进程间的通信。

(无名)管道特点:

· 管道是一个单向信道,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道; 

· 只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程); 

· 单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在与内存中。 

· 数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,且每次都是从缓冲区的头部读出数据。(先进先出)

Ex:

#include <unistd.h>

int pipe(int fileds[2]); 

fileds[0] 用于读,fileds[1] 用于写。 读/写 —— 0/1

#include <stdio.h>

#include <stdlib.h>

#include <sys/types.h>

#include <unistd.h>

int main()

{

  int pfds[2];

  char buf[30];

  pipe(pfds);

  if(fork()==0)

  {

close(pfds[0]);

sleep(2);

write(pfds[1],"abcdef",7);

exit(0);

  }

  else

  {

   close(pfds[1]);

read(pfds[0],buf,30);

wait(NULL);

exit(0);

  }

}

note:

1.向管道中写入数据时,linux将不保证写入的原子性,管道缓冲区一有空闲区域,写进程就会试图向管道写入数据。如果读进程不读走管道缓冲区中的数据,那么写操作将一直阻塞。 

2. 写端对读端存在依赖性。只有在管道的读端存在时,向管道中写入数据才有意义。否则,向管道中写入数据的进程将收到内核传来的SIGPIPE信号,应用程序可以处理该信号,也可以忽略(默认动作则是应用程序终止)。 

3.严格遵循先进先出(first in first out),不支持诸如lseek()等文件定位操作。

有名管道(named pipe或FIFO), 与管道不同之处在于,它与一个路径名关联,以FIFO的文件形式存在于文件系统中。这样,即使与FIFO的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过FIFO相互通信。

系统调用mkfifo ( ) 创建有名管道

int mkfifo(const char * pathname, mode_t mode)

该函数的第一个参数是一个普通的路径名,也就是创建后FIFO的名字。第二个参数与打开普通文件的open()函数中的mode 参数相同。如果mkfifo的第一个参数是一个已经存在的路径名时,会返回EEXIST错误,所以一般典型的调用代码首先会检查是否返回该错误,如果确实返回该错误,那么只要调用打开FIFO的函数就可以了。生成了有名管道后,就可以使用一般的文件I/O函数如open、close、read、write等来对它进行操作。

Ex:

写:

#include <sys/types.h>

#include <sys/stat.h>

#include <errno.h>

#include <fcntl.h>

#define FIFO_SERVER "/tmp/fifoserver"

main(int argc,char** argv)

//参数为即将写入的字节数

{

int fd;

char w_buf[4096*2];

int real_wnum;

memset(w_buf,0,4096*2);

if((mkfifo(FIFO_SERVER,O_CREAT|O_EXCL)<0)&&(errno!=EEXIST))

printf("cannot create fifoserver\n");

if(fd==-1)

if(errno==ENXIO)

printf("open error; no reading process\n");

   //设置非阻塞标志

fd=open(FIFO_SERVER,O_WRONLY|O_NONBLOCK,0);

       //设置阻塞标志 fd=open(FIFO_SERVER,O_WRONLY,0);

real_wnum=write(fd,w_buf,2048);

if(real_wnum==-1)

{

if(errno==EAGAIN)

printf("write to fifo error; try later\n");

}

else 

printf("real write num is %d\n",real_wnum);

}

读: 

#include <sys/types.h>

#include <sys/stat.h>

#include <errno.h>

#include <fcntl.h>

#define FIFO_SERVER "/tmp/fifoserver"

main(int argc,char** argv)

{

char r_buf[4096*2];

int  fd;

int  r_size;

int  ret_size;

r_size=atoi(argv[1]);

printf("requred real read bytes %d\n",r_size);

memset(r_buf,0,sizeof(r_buf));

fd=open(FIFO_SERVER,O_RDONLY|O_NONBLOCK,0);

//fd=open(FIFO_SERVER,O_RDONLY,0);

//在此处可以把读程序编译成两个不同版本:阻塞版本及非阻塞版本

if(fd==-1)

{

printf("open %s for read error\n");

exit();

}

while(1)

{

memset(r_buf,0,sizeof(r_buf));

ret_size=read(fd,r_buf,r_size);

if(ret_size==-1)

if(errno==EAGAIN)

printf("no data avlaible\n");

printf("real read bytes %d\n",ret_size);

   sleep(1);

}

pause();

unlink(FIFO_SERVER);

}

note:

  管道是最古老的方式,具有通用性。但Pipe_buf有限,通常几百到几千字节。

4.3. 消息队列

系统 V IPC引入了三种进程间通信机制:消息、信号灯、共享内存。内核为每种机制维护一个表,在表中存储所有相关实例,每个表项由一个关键字(用户选择的名字)来标志。

将一个路径名和项目标识 转换为一个关键字:

#include <sys/types.h>

#include <sys/ipc.h>

key_t ftok(char* pathname,char proj);

Linux系统维护着一个msgque消息队列链表,其中每个元素指向一个描述消息队列的msqid_ds结构。当创建新的消息队列时,系统将从系统内存中分配一个msqid_ds结构,同时将其插入到数组中。 

每个msqid_ds结构包含一个ipc_perm结构和指向已经进入此队列消息的指针,以及有关队列修改时间信息,如上次系统向队列中写入的时间等。

struct kern_ipc_perm

{   

     key_t   key;    //该键值则唯一对应一个消息队列

   uid_t   uid;

     gid_t   gid;

uid_t   cuid;

gid_t   cgid;

mode_t  mode;

unsigned long seq;

}

msqid_ds包含两个等待队列:一个为队列写入进程使用而另一个由队列读取进程使用。  

每次进程试图向写入队列写入消息时,系统将把其有效用户和组标志符与此队列的ipc_perm结构中的模式进行比较。如果允许写入操作,则把此消息从此进程的地址空间拷贝到msg数据结构中,并放置到此消息队列尾部。由于 Linux严格限制可写入消息的个数和长度,队列中可能容纳不下这个消息。此时,此写入进程将被添加到这个消息队列的等待队列中,同时调用调度管理器选择新进程运行。当有消息从此队列中释放时,该进程将被唤醒。  

从队列中读的过程与之类似。进程对这个写入队列的访问权限将被再次检验。读取进程将选择队列中第一个消息(不管是什么类型)或者第一个某特定类型的消息。如果没有消息可以满足此要求,读取进程将被添加 到消息队列的读取等待队列中,然后系统运行调度管理器。当有新消息写入队列时,进程将被唤醒继续执行。 

    #include <sys/types.h>

#include <sys/ipc.h>

#include <sys/msg.h>

int msgget(key_t key, int msgflg);

int msgsnd(int msqid,  struct msgbuf* msgp,  int msgsz,  int msgflg);

int msgrcv(int msqid,  struct msgbuf* msgp,  int msgsz,  long msgtyp,  int msgflg);

int msgctl(int msqid,  int cmd,  struct msqid_ds* buf);

struct mymsgbuf 


long mtype; //消息类型,正整数 
char mtext[80]; 

}; 
void send_message(int qid, struct mymsgbuf *qbuf, long type, char *text); 
void read_message(int qid, struct mymsgbuf *qbuf, long type); 
void remove_queue(int qid); 
 
int main(int argc, char *argv[]) 

  key_t key; 
  int msgqueue_id; 
  struct mymsgbuf qbuf; 

/* Create unique key via call to ftok() */ 
  key = ftok("/home/beej/somefile", 'w');  /* key = 123456  
  

/* Open the queue - create if necessary */ 
if((msgqueue_id = msgget(key, IPC_CREAT | 0666)) = = -1)  //创建一个消息队列 

{                                                  //一般在服务器创建

perror("msgget");                                //客户端仅输入权限

exit(1); 

}

。。。。。。。。

return(0);  
   } 

void send_message(int qid, struct mymsgbuf *qbuf, long type, char *text) 

  qbuf->mtype = type; 
  strcpy(qbuf->mtext, text); 
  if((msgsnd(qid, (struct msgbuf *)qbuf, strlen(qbuf->mtext)+1, 0)) = =-1) 
  { 
    perror("msgsnd"); 
    exit(1); 
  } 


void read_message(int qid, struct mymsgbuf *qbuf, long type) 

  qbuf->mtype = type; 
  msgrcv(qid, (struct msgbuf *)qbuf, 80, type, 0); 


void remove_queue(int qid) 

  msgctl(qid, IPC_RMID, 0); 

Note: 已逐渐淘汰。

4.4. 信号灯(一个计数器)

信号灯主要提供对进程间共享资源访问控制机制。相当于内存中的标志,进程可以根据它判定是否能够访问某些共享资源,同时,进程也可以修改该标志。除了用于访问控制外,还可用于进程同步。信号灯有以下两种类型:

二值信号灯:最简单的信号灯形式,信号灯的值只能取0或1,类似于互斥锁。
计算信号灯:信号灯的值可以取任意非负值(当然受内核本身的约束)。 

对信号灯的操作有下面三种类型:

打开或创建信号灯、 信号灯值操作、 获得或设置信号灯属性

信号灯最简单的形式是某个可以被多个进程检验和设置(test&set)的内存单元。这个检验与设置操作对每个进程而言是不可中断或者说是一个原子性操作;一旦启动谁也终止不了。检验与设置操作的结果是信号灯当前值加1, 这个值可以是正数也可以是负数。根据这个操作的结果,进程可能可以一直睡眠到此信号灯的值被另一个进程更改为止。信号灯可用来实现临界区(critical region):某一时刻在此区域内的代码只能被一个进程执行。  

如果你有多个协作进程从一个数据文件中读取与写入记录。有时你可能需要这些文件访问遵循严格的访问次序。 那么可在文件操作代码上使用一个初始值为1的信号灯,它带有两个信号灯操作,一个检验并对信号灯值减1,而另一个检验并加1。第一个访问文件的进程将试图将信号灯值减1,如果获得成功则信号灯值变成了 0。此进程于是开始使用这个数据文件,但是此时如果另一进程也想将信号灯值减1,则信号灯值将为-1,这次操作将会失败。它将挂起执行直到第一个进程完成对此数据文件的使用。此时这个等待进程将被唤醒,这次它对信号灯的操作将成功。

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/sem.h>

int semget(key_t key, int nsems, int semflg);  //

int semctl(int semid, int semnum, int cmd, union semnu arg);

int semop(int semid, struct sembuf* sops, unsigned nsops);//数组、个数

struct sembuf

{

  unsigned short    sem_num; //序号

  short             sem_op; //操作,+1-1

  short             sem_flg; //IPC_NOWAIT, SEM_UNDO

};

Field          Description

   sem_num        semaphore number

   sem_op          semaphore operation

   sem_flg          semaphore flags

union semun{  //一些参数

  int val;

  struct semid_ds *buf;

  ushort *array;

}

删除信号灯:

  union semun dummy;

  int semid;

…..

semctl(semid,0,IPC_RMID,dummy);

Ex:

#include <string.h>

#include <sys/socket.h>

#include <sys/type.h>

#include <netinet/in.h>

#include <sys/ipc.h>

#include <sys/sem.h>

#include <stdio.h>

#include <signal.h>

#define  SEM_NAME "/tmp/sem_name.tmp"

void sigint_handler(int);

int semid;

unsigned int count;

struct sembuf psembuf,vsembuf;

int main(int argc,char*argv[])

{

int i,first,second;

key_t key;

short initarray[2],outarray[2];

  

key=ftok(SEM_NAME,'a');

if(argc==1)

{

signal(SIGINT,sigint_handler);

semid=semget(key,2,0777|IPC_CREAT);

initarray[0]=initarray[1]=1;

semctl(semid,2,SETALL,initarray);//1,1

semctl(semid,2,GETALL,outarray);

printf("sem init vals %d %d\n",outarray[0],outarray[1]);

pause();

}

else if(argv[1][0]==0)

{

first=0;

second=1;

}

else

{

first=1;

second=0;

}

semid=semget(key,2,0777);

psembuf.sem_op=-1;

psembuf.sem_flg=SEM_UNDO;

vsembuf.sem_op=1;

vsembuf.sem_flg=SEM_UNDO;

  

for(count=0;;count++)

{

psembuf.sem_num=first;

semop(semid,&psembuf,1); //1—操作个数

psembuf.sem_num=second;

semop(semid,&psembuf,1);

  

semctl(semid,2,GETALL,outarray);

printf("proc %d count %d sem value %d %d\n",getpid(),

count,outarray[0],outarray[1]);

  

vsembuf.sem_num=second;

semop(semid,&vsembuf,1);

vsembuf.sem_num=first;

semop(semid,&vsembuf,1);

}

 

}

void sigint_handler(int sig)

{

semctl(semid,2,IPC_RMID,0);

exit(0);

}

4.5. 共享内存

共享内存允许一个或多个进程通过同时出现在它们虚拟地址空间中的内存来通讯。此虚拟内存的页面出现在每个共享进程页表中。但此页面并不一定位于所有共享进程虚拟内存的相同位置。和其它系统V IPC对象的使用方法一样,对共享内存区域的访问是通过键和访问权限检验来控制的。一旦内存被共享,则再不会检验进程对对象的使用方式。它依赖于其它机制,如系统V信号灯,来同步对共享内存的访问。 共享内存是最有用的进程间通信方式,也是最快的IPC形式。由于多个进程共享同一块内存区域,必然需要某种同步机制,互斥锁和信号量都可以。

采用共享内存通信的一个显而易见的好处是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝。

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/shm.h>

int shmget(key_t key, int size, int shmflg);

char* shmat(int shmid, char* shmaddr, int shmflg);  //将共享区附接于shmaddr

int shmdt(char* shmaddr); // //将共享区断开

int shmctl(int semid, int cmd, struct shmid_ds* buf);

在/proc/sys/kernel/目录下,记录着系统V共享内存的限制,如一个共享内存区的最大字节数shmmax,系统范围内最大共享内存区标识符数shmmni等。

Ex:

写:

#include <sys/ipc.h>

#include <sys/shm.h>

#include <sys/types.h>

#include <unistd.h>

typedef struct{

char name[4];

int age;

} people;

main(int argc, char** argv)

{

int shm_id,i;

key_t key;

char temp;

people *p_map;

char* name = "/dev/shm/myshm2";

key = ftok(name,0);

if(key==-1)

perror("ftok error");

shm_id=shmget(key,4096,0777|IPC_CREAT);

if(shm_id==-1)

{

perror("shmget error");

return;

}

p_map=(people*)shmat(shm_id,0,0);

temp='a';

for(i = 0;i<10;i++)

{

temp+=1;

memcpy((*(p_map+i)).name,&temp,1);

(*(p_map+i)).age=20+i;

}

if(shmdt(p_map)==-1)

perror(" detach error ");

}

读:

#include <sys/ipc.h>

#include <sys/shm.h>

#include <sys/types.h>

#include <unistd.h>

typedef struct{

char name[4];

int age;

} people;

main(int argc, char** argv)

{

int shm_id,i;

key_t key;

people *p_map;

char* name = "/dev/shm/myshm2";

key = ftok(name,0);

if(key == -1)

perror("ftok error");

shm_id = shmget(key,4096,0777);

if(shm_id == -1)

{

perror("shmget error");

return;

}

p_map = (people*)shmat(shm_id,0,0);

for(i = 0;i<10;i++)

{

  printf( "name:%s\n",(*(p_map+i)).name );

  printf( "age %d\n",(*(p_map+i)).age );

}

if(shmdt(p_map) == -1)

perror(" detach error ");

}

4.6. UNIX域套接字

    Unix域套接字,只能与同一台机器上的套接字相连,是面向连接的;每一个到套接字的新连接都产生一个新的通信管道。Unix域套接字经常被用来代替命名管道实现很多重要服务中的IPC;也可以用socketpair()来得到非命名Unix域套接字,与非命名管道类似。(管套)

1.命名Unix域套接字

int socket(AF_UNIX, SOCK_STREAM, 0)

int socket(AF_UNIX, SOCK_DGRAM, 0)

struct sockaddr_un

{

  short int sun_family;

  char sun_path[104];

}

需要有独有的套接字地址。然后执行bind( ), listen( ), accept( ), 

note:

  i. 客户机必需拥有打开文件的权限;

  ii. 客户机调用connect( )时,若倾听套接字的队列已满,立即返回ECONNREFUSED.

2.非命名Unix域套接字

int socketpair(AF_UNIX, SOCK_STREAM, 0, int sockfd[2])

int socketpair(AF_UNIX, SOCK_DGRAM, 0,int sockfd[2])

创建后,父、子进程分别关掉一个多余的套接字,然后进行全双工通信。

int main()

{

  int sv[2];

  char buf;

  if(socketpair(AF_UNIX,SOCK_STREAM,0,sv)<0)

   exit(1);

  

  if(fork()==0)

  {

close(sv[0]);   

read(sv[1],&buf,1);

buf=toupper(buf);

write(sv[1],&buf,1);

exit(0);

  }

  else

  {

close(sv[1]);

sleep(1);

write(sv[0],"b",1);

read(sv[0],&buf,1);

exit(0);

  }

}

.线程

5.1. 概述 

    

5.2. C++线程同步 

5.2.1概述  

   现在流行的进程线程同步互斥的控制机制,其实是由最原始最基本的4种方法实现的。由这4种方法组合优化就有了.NetJava下灵活多变的,编程简便的线程进程控制手段。  

  这4种方法具体定义如下 在《操作系统教程》ISBN 7-5053-6193-7 一书中可以找到更加详细的解释  

  1临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。  

  2互斥量:为协调共同对一个共享资源的单独访问而设计的。  

  3信号量:为控制一个具有有限数量用户资源而设计。  

4事 件:用来通知线程有一些事件已发生,从而启动后继任务的开始。

5.2.2临界区(Critical Section)

5.2.3互斥量

5.2.4信号量

5.2.5事件

5.3. 概述 

 临界区(Critical Section)  

  保证在某一时刻只有一个线程能访问数据的简便办法。在任意时刻只允许一个线程对共享资源进行访问。如果有多个线程试图同时访问临界区,那么在有一个线程进入后其他所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操作共享资源的目的。  

  临界区包含两个操作原语: 

 EnterCriticalSection() 进入临界区

 LeaveCriticalSection() 离开临界区  

  EnterCriticalSection()语句执行后代码将进入临界区以后无论发生什么,必须确保与之匹配的LeaveCriticalSection()都能够被执行到。否则临界区保护的共享资源将永远不会被释放。虽然临界区同步速度很快,但却只能用来同步本进程内的线程,而不可用来同步多个进程中的线程。  

  MFC提供了很多功能完备的类,我用MFC实现了临界区。MFC为临界区提供有一个CCriticalSection类,使用该类进行线程同步处理是非常简单的。只需在线程函数中用CCriticalSection类成员函数Lock()和UnLock()标定出被保护代码片段即可。Lock()后代码用到的资源自动被视为临界区内的资源被保护。UnLock后别的线程才能访问这些资源。 

   

  //CriticalSection 

  CCriticalSection global_CriticalSection; 

   

  // 共享资源 

  char global_Array[256]; 

   

  //初始化共享资源 

  void InitializeArray() 

  { 

   for(int i = 0;i<256;i++) 

   { 

   global_Array[i]=I; 

   } 

  } 

   

  //写线程 

  UINT Global_ThreadWrite(LPVOID pParam) 

  { 

   CEdit *ptr=(CEdit *)pParam; 

   ptr->SetWindowText(""); 

   //进入临界区 

  global_CriticalSection.Lock(); 

   for(int i = 0;i<256;i++) 

   { 

   global_Array[i]=W; 

   ptr->SetWindowText(global_Array); 

   Sleep(10); 

   }  

 //离开临界区 

   global_CriticalSection.Unlock(); 

   return 0; 

  } 

   

  //删除线程 

  UINT Global_ThreadDelete(LPVOID pParam) 

  { 

   CEdit *ptr=(CEdit *)pParam; 

   ptr->SetWindowText(""); 

   //进入临界区 

   global_CriticalSection.Lock(); 

   for(int i = 0;i<256;i++) 

   { 

   global_Array[i]=D; 

   ptr->SetWindowText(global_Array); 

   Sleep(10); 

   } 

   

 //离开临界区 

   global_CriticalSection.Unlock(); 

   return 0; 

  } 

   

  //创建线程并启动线程 

  void CCriticalSectionsDlg::OnBnClickedButtonLock() 

  { 

 

 

  

 作者: 61.51.111.*2006-8-24 14:09   回复此发言    

 

--------------------------------------------------------------------------------

 

四种进程或线程同步互斥的控制方法  

    //Start the first Thread 

   CWinThread *ptrWrite = AfxBeginThread(Global_ThreadWrite, 

   &m_Write, 

   THREAD_PRIORITY_NORMAL, 

   0, 

   CREATE_SUSPENDED); 

   ptrWrite->ResumeThread(); 

     

   //Start the second Thread 

   CWinThread *ptrDelete = AfxBeginThread(Global_ThreadDelete, 

   &m_Delete, 

   THREAD_PRIORITY_NORMAL, 

   0, 

   CREATE_SUSPENDED); 

   ptrDelete->ResumeThread(); 

  }  

    

  在测试程序中,Lock UnLock两个按钮分别实现,在有临界区保护共享资源的执行状态,和没有临界区保护共享资源的执行状态。  

  程序运行结果 

     

  

  互斥量(Mutex)  

    

  互斥量跟临界区很相似,只有拥有互斥对象的线程才具有访问资源的权限,由于互斥对象只有一个,因此就决定了任何情况下此共享资源都不会同时被多个线程所访问。当前占据资源的线程在任务处理完后应将拥有的互斥对象交出,以便其他线程在获得后得以访问资源。互斥量比临界区复杂。 因为使用互斥不仅仅能够在同一应用程序不同线程中实现资源的安全共享,而且可以在不同应用程序的线程之间实现对资源的安全共享。

   

    互斥量包含的几个操作原语: 

    CreateMutex() 创建一个互斥量 

    OpenMutex() 打开一个互斥量 

    ReleaseMutex() 释放互斥量 

    WaitForMultipleObjects() 等待互斥量对象  

    

  同样MFC为互斥量提供有一个CMutex类。使用CMutex类实现互斥量操作非常简单,但是要特别注意对CMutex的构造函数的调用  

   

  CMutex( BOOL bInitiallyOwn = FALSE, LPCTSTR lpszName = NULL, LPSECURITY_ATTRIBUTES lpsaAttribute = NULL)  

  不用的参数不能乱填,乱填会出现一些意想不到的运行结果。 

   

  //创建互斥量 

  CMutex global_Mutex(0,0,0); 

   

  // 共享资源 

  char global_Array[256]; 

   

  void InitializeArray() 

  { 

   for(int i = 0;i<256;i++) 

   { 

   global_Array[i]=I; 

   } 

  } 

  UINT Global_ThreadWrite(LPVOID pParam) 

  { 

   CEdit *ptr=(CEdit *)pParam; 

   ptr->SetWindowText(""); 

   global_Mutex.Lock(); 

   for(int i = 0;i<256;i++) 

   { 

   global_Array[i]=W; 

   ptr->SetWindowText(global_Array); 

   Sleep(10); 

   } 

   global_Mutex.Unlock(); 

   return 0; 

  } 

   

  UINT Global_ThreadDelete(LPVOID pParam) 

  { 

   CEdit *ptr=(CEdit *)pParam; 

   ptr->SetWindowText(""); 

   global_Mutex.Lock(); 

   for(int i = 0;i<256;i++) 

   { 

   global_Array[i]=D; 

   ptr->SetWindowText(global_Array); 

   Sleep(10); 

   } 

   global_Mutex.Unlock(); 

   return 0; 

  }  

  同样在测试程序中,Lock UnLock两个按钮分别实现,在有互斥量保护共享资源的执行状态,和没有互斥量保护共享资源的执行状态。  

  程序运行结果 

     

  

    

  信号量(Semaphores)  

  信号量对象对线程的同步方式与前面几种方法不同,信号允许多个线程同时使用共享资源,这与操作系统中的PV操作相同。它指出了同时访问共享资源的线程最大数目。它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。在用CreateSemaphore()创建信号量时即要同时指出允许的最大资源计数和当前可用资源计数。一般是将当前可用资源计数设置为最大资源计数,每增加一个线程对共享资源的访问,当前可用资源计数就会减1,只要当前可用资源计数是大于0的,就可以发出信号量信号。但是当前可用计数减小到0时则说明当前占用资源的线程数已经达到了所允许的最大数目,不能在允许其他线程的进入,此时的信号量信号将无法发出。线程在处理完共享资源后,应在离开的同时通过ReleaseSemaphore()函数将当前可用资源计数加1。在任何时候当前可用资源计数决不可能大于最大资源计数。  

 

 

  

 作者: 61.51.111.*2006-8-24 14:09   回复此发言    

 

--------------------------------------------------------------------------------

 

四种进程或线程同步互斥的控制方法  

 

  PV操作及信号量的概念都是由荷兰科学家E.W.Dijkstra提出的。信号量S是一个整数,S大于等于零时代表可供并发进程使用的资源实体数,但S小于零时则表示正在等待使用共享资源的进程数。  

   P操作申请资源: 

    (1S1; 

    (2)若S1后仍大于等于零,则进程继续执行; 

    (3)若S1后小于零,则该进程被阻塞后进入与该信号相对应的队列中,然后转入进程调度。  

   

  V操作 释放资源: 

    (1S1; 

    (2)若相加结果大于零,则进程继续执行; 

    (3)若相加结果小于等于零,则从该信号的等待队列中唤醒一个等待进程,然后再返回原进程继续执行或转入进程调度。 

   

    信号量包含的几个操作原语: 

    CreateSemaphore() 创建一个信号量 

    OpenSemaphore() 打开一个信号量 

    ReleaseSemaphore() 释放信号量 

    WaitForSingleObject() 等待信号量 

   

  //信号量句柄 

  HANDLE global_Semephore; 

   

  // 共享资源 

  char global_Array[256]; 

  void InitializeArray() 

  { 

   for(int i = 0;i<256;i++) 

   { 

   global_Array[i]=I; 

   } 

  } 

   

 //线程

  UINT Global_ThreadOne(LPVOID pParam) 

  { 

   CEdit *ptr=(CEdit *)pParam; 

   ptr->SetWindowText(""); 

   //等待对共享资源请求被通过 等于 P操作 

  WaitForSingleObject(global_Semephore, INFINITE); 

   for(int i = 0;i<256;i++) 

   { 

   global_Array[i]=O; 

   ptr->SetWindowText(global_Array); 

   Sleep(10); 

   } 

  

  //释放共享资源 等于 V操作 

   ReleaseSemaphore(global_Semephore, 1, NULL); 

   return 0; 

  } 

   

  UINT Global_ThreadTwo(LPVOID pParam) 

  { 

   CEdit *ptr=(CEdit *)pParam; 

   ptr->SetWindowText(""); 

   WaitForSingleObject(global_Semephore, INFINITE); 

   for(int i = 0;i<256;i++) 

   { 

   global_Array[i]=T; 

   ptr->SetWindowText(global_Array); 

   Sleep(10); 

   } 

   ReleaseSemaphore(global_Semephore, 1, NULL); 

   return 0; 

  } 

   

  UINT Global_ThreadThree(LPVOID pParam) 

  { 

   CEdit *ptr=(CEdit *)pParam; 

   ptr->SetWindowText(""); 

   WaitForSingleObject(global_Semephore, INFINITE); 

   for(int i = 0;i<256;i++) 

   { 

   global_Array[i]=H; 

   ptr->SetWindowText(global_Array); 

   Sleep(10); 

   } 

   ReleaseSemaphore(global_Semephore, 1, NULL); 

   return 0; 

  } 

   

  void CSemaphoreDlg::OnBnClickedButtonOne() 

  { 

   //设置信号量 个资源 1同时只可以有一个线程访问 

   global_Semephore= CreateSemaphore(NULL, 1, 1, NULL); 

   this->StartThread(); 

  

  // TODO: Add your control notification handler code here 

  } 

   

  void CSemaphoreDlg::OnBnClickedButtonTwo() 

  { 

   //设置信号量 个资源 同时只可以有两个线程访问 

   global_Semephore= CreateSemaphore(NULL, 2, 2, NULL); 

   this->StartThread(); 

   // TODO: Add your control notification handler code here 

  } 

   

  void CSemaphoreDlg::OnBnClickedButtonThree() 

  { 

  //设置信号量 个资源 同时只可以有三个线程访问 

   global_Semephore= CreateSemaphore(NULL, 3, 3, NULL); 

   this->StartThread();  

  

  // TODO: Add your control notification handler code here 

  }  

  信号量的使用特点使其更适用于对Socket(套接字)程序中线程的同步。例如,网络上的HTTP服务器要对同一时间内访问同一页面的用户数加以限制,这时可以为每一个用户对服务器的页面请求设置一个线程,而页面则是待保护的共享资源,通过使用信号量对线程的同步作用可以确保在任一时刻无论有多少用户对某一页面进行访问,只有不大于设定的最大用户数目的线程能够进行访问,而其他的访问企图则被挂起,只有在有用户退出对此页面的访问后才有可能进入。  

 

 

  

 作者: 61.51.111.*2006-8-24 14:09   回复此发言    

 

--------------------------------------------------------------------------------

 

四种进程或线程同步互斥的控制方法  

   

  程序运行结果 

     

  

    

  事件(Event)  

    

  事件对象也可以通过通知操作的方式来保持线程的同步。并且可以实现不同进程中的线程同步操作。  

  信号量包含的几个操作原语: 

    CreateEvent() 创建一个信号量 

    OpenEvent() 打开一个事件 

    SetEvent() 回置事件 

    WaitForSingleObject() 等待一个事件 

    WaitForMultipleObjects() 等待多个事件  

  WaitForMultipleObjects 函数原型: 

    WaitForMultipleObjects( 

    IN DWORD nCount, // 等待句柄数 

    IN CONST HANDLE *lpHandles, //指向句柄数组 

    IN BOOL bWaitAll, //是否完全等待标志 

    IN DWORD dwMilliseconds //等待时间 

    )  

  

  参数nCount指定了要等待的内核对象的数目,存放这些内核对象的数组由lpHandles来指向。fWaitAll对指定的这nCount个内核对象的两种等待方式进行了指定,为TRUE时当所有对象都被通知时函数才会返回,为FALSE则只要其中任何一个得到通知就可以返回。dwMilliseconds在这里的作用与在WaitForSingleObject()中的作用是完全一致的。如果等待超时,函数将返回WAIT_TIMEOUT。 

   

  //事件数组 

  HANDLE global_Events[2]; 

   

  // 共享资源 

  char global_Array[256]; 

   

  void InitializeArray() 

  { 

   for(int i = 0;i<256;i++) 

   { 

   global_Array[i]=I; 

   } 

  } 

   

  UINT Global_ThreadOne(LPVOID pParam) 

  { 

   CEdit *ptr=(CEdit *)pParam; 

   ptr->SetWindowText(""); 

   for(int i = 0;i<256;i++) 

   { 

   global_Array[i]=O; 

   ptr->SetWindowText(global_Array); 

   Sleep(10); 

   } 

  

  //回置事件 

   SetEvent(global_Events[0]); 

   return 0; 

  } 

   

  UINT Global_ThreadTwo(LPVOID pParam) 

  { 

   CEdit *ptr=(CEdit *)pParam; 

   ptr->SetWindowText(""); 

   for(int i = 0;i<256;i++) 

   { 

   global_Array[i]=T; 

   ptr->SetWindowText(global_Array); 

   Sleep(10); 

   } 

  

  //回置事件 

   SetEvent(global_Events[1]); 

   return 0; 

  } 

   

  UINT Global_ThreadThree(LPVOID pParam) 

  { 

   CEdit *ptr=(CEdit *)pParam; 

   ptr->SetWindowText(""); 

  

  //等待两个事件都被回置 

   WaitForMultipleObjects(2, global_Events, true, INFINITE); 

   for(int i = 0;i<256;i++) 

   { 

   global_Array[i]=H; 

   ptr->SetWindowText(global_Array); 

   Sleep(10); 

   } 

   return 0; 

  } 

  void CEventDlg::OnBnClickedButtonStart() 

  { 

   for (int i = 0; i < 2; i++) 

   { 

  

  //实例化事件 

   global_Events[i]=CreateEvent(NULL,false,false,NULL); 

   } 

   CWinThread *ptrOne = AfxBeginThread(Global_ThreadOne, 

   &m_One, 

   THREAD_PRIORITY_NORMAL, 

   0, 

   CREATE_SUSPENDED); 

   ptrOne->ResumeThread(); 

   

   //Start the second Thread 

   CWinThread *ptrTwo = AfxBeginThread(Global_ThreadTwo, 

   &m_Two, 

   THREAD_PRIORITY_NORMAL, 

   0, 

   CREATE_SUSPENDED); 

   ptrTwo->ResumeThread(); 

   

   //Start the Third Thread 

   CWinThread *ptrThree = AfxBeginThread(Global_ThreadThree, 

   &m_Three, 

   THREAD_PRIORITY_NORMAL, 

   0, 

   CREATE_SUSPENDED); 

   ptrThree->ResumeThread(); 

   // TODO: Add your control notification handler code here 

  }  

  

  事件可以实现不同进程中的线程同步操作,并且可以方便的实现多个线程的优先比较等待操作,例如写多个WaitForSingleObject来代替WaitForMultipleObjects从而使编程更加灵活。  

  程序运行结果 

     

  

  总结:  

  1. 互斥量与临界区的作用非常相似,但互斥量是可以命名的,也就是说它可以跨越进程使用。所以创建互斥量需要的资源更多,所以如果只为了在进程内部是用的话使用临界区会带来速度上的优势并能够减少资源占用量。因为互斥量是跨进程的互斥量一旦被创建,就可以通过名字打开它。  

  2. 互斥量(Mutex),信号灯(Semaphore),事件(Event)都可以被跨越进程使用来进行同步数据操作,而其他的对象与数据同步操作无关,但对于进程和线程来讲,如果进程和线程在运行状态则为无信号状态,在退出后为有信号状态。所以可以使用WaitForSingleObject来等待进程和线程退出。  

  3. 通过互斥量可以指定资源被独占的方式使用,但如果有下面一种情况通过互斥量就无法处理,比如现在一位用户购买了一份三个并发访问许可的数据库系统,可以根据用户购买的访问许可数量来决定有多少个线程/进程能同时进行数据库操作,这时候如果利用互斥量就没有办法完成这个要求,信号灯对象可以说是一种资源计数器。

 

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值