【Linux】Linux 下多进程编程详解
一.多进程程序的特点
进程是一个具有独立功能的程序关于某个数据集合的一次可以并发执行的运行活动,是处
于活动状态的计算机程序。
进程作为构成系统的基本细胞,
不仅是系统内部独立运行的实体,
而且是独立竞争资源的基本实体。
进程是资源管理的最小单位,线程是程序执行的最小单位。进程管理着资源(比
如 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 时
间片时能正常运行。 既然系统已经处理好所有这些中断处理的过程, 我们做程序
还有什么要担心 的呢? 我们尽可以使用系统提供的多进程的特点, 让几个程序精
诚合作, 简单而又高效地把结果给它搞出来。
多进程程序的
一些突出的特点:
并行化
简单有序
互不干扰
事务化
二.常用的多进程编程的系统调用
1)功能:创建一个新的进程.
语法:
#include <unistd.h>
#include <sys/types.h>
pid_t fork();
说明:本系统调用产生一个新的进程, 叫子进程, 是调用进程的一个复制品. 调用进程叫父进
程, 子进程继承了父进程的几乎所有的属性。
简述:fork() 调用成功时,分别返回两个整数,对父进程返回 〉0 的整数,对子进程返回
0,
进程:代码段(程序代码)
堆栈段(局部变量、函数返回地址、函数参数)
数据段(全局变量、常数等)
在 Linux 系统中,系统调用 fork 后,内核为完成系统调用 fork 要进行几步操作:
第一步,为新进程在进程表中分配一个表项。系统对一个普通用户可以同时运行的进
程数是有限制的,对超级用户没有该限制,但不能超过进程表的最大表项的
数目。
第二步,给子进程一个唯一的进程标识号(PID)
第三步,复制一个父进程的进程表项的副本给子进程。内核初始化子进程的进程表项
时,是从父进程处拷贝的。所以子进程拥有与父进程一样的 uid、当前目录、
当前根、用户文件描述符表等。
第四步,
父进程相连的文件表和索引节点表的引用数加 1。
这些文件自动地与该子
进程相连。
第五步,内核为子进程创建用户级上下文。内核为子进程的代码段分配内存,并复制
父进程的区内容,生成的是进程的静态部分。
第六步,生成进程的动态部分,然后对父进程返回子进程的 pid,对子进程返回 0。
从父进程拷贝的内容主要有:
用户标识符,包括实际用户号(real)和有效用户号(effective)
环境变量
打开的文件描述符、套接字描述符
信号处理设置
堆栈
目录
进程组标志(process ID)
会晤组标志(session ID)
正文
子进程特有内容:
进程号
父进程号
进程执行时间
未处理的信号被处理为空
不继承异步的输入输出操作
函数执行过程:
1 内核在系统进程表中,创建一个新条目;
2 复制父进程内容(已打开的文件描述符、堆栈、正文等)
3 修改两者的堆栈,给父进程返回子进程号,给子进程返回 0(父进程知道每个子进程
的标志号,而子进程可根据需要调用 getppid() 来获得父进程的标志号)
例子:
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc,char *argv[])
{
pid_t pid;
if((pid=fork())==0)//对子进程返回==0的整数
{
printf("This is Child fork\n");//子进程代码
printf("Child pid= %d\n",pid);//子进程代码
//exit(0);
}
else if(pid>0) //对父进程返回 〉0 的整数
{
printf("This is Father fork\n");//父进程代码
printf("Father pid= %d\n",pid);//子进程代码
//exit(0);
}
else
{
printf("Error");
exit(1);
}
}
$gcc -o fork fork.c
$./fork
This is Father fork
Father pid= 6250
This is Child fork
Child pid= 0
2)system()
应用程序
fork ( )
父进程
子进程 1
子进程 2
子进程执行指定的命令
功能:产生一个新的进程, 子进程执行指定的命令.
语法:
#include <stdio.h>
#include <stdlib.h>
int system(string)
char *string;
说明:
本调用将参数 string 传递给一个命令解释器(一般为 sh)执行, 即 string 被解释为一条命
令, 由 sh 执行该命令.若参数 string 为一个空指针则为检查命令解释器是否存在.
该命令可以同命令行命令相同形式, 但由于命令做为一个参数放在系统调用中, 应注意
编译时对特殊意义字符的处理. 命令的查找是按 PATH 环境变量的定义的. 命令所生成的后
果一般不会对父进程造成影响.
返回值:当参数为空指针时, 只有当命令解释器有效时返回值为非零. 若参数不为空指
针, 返回值为该命令的返回状态(同 waitpid())的返回值. 命令无效或语法错误则返回非零值,
所执行的命令被终止. 其他情况则返回-1.
例子:
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc,char *argv[])
{
char command[81];
sprintf(command,"ps aux");
system(command);
}
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);
4)popen()
功能:初始化从/到一个进程的管道.
语法:
#include <stdio.h>
FILE *popen(command,type)
char *command,type;
说明:本系统调用在调用进程和被执行命令间创建一个管道.
参数 command 做为被执行的命令行.type 做为 I/O 模式,"r"为从被
执行命令读,"w"为向被执行命令写.返回一个标准流指针,做为管
道描述符,向被执行命令读或写数据(做为被执行命令的 STDIN 或
STDOUT)该系统调用可以用来在程序中调用系统命令,并取得命令
的输出信息或者向命令输入信息.
返回值:不成功则返回 NULL,成功则返回管道的文件指针.
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);
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);
}
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().否则该
系统调用的行为由参数 pid 和 options 决定.
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 指定信号(除了
SIGKILL 和 SIGSTOP,这两种信号由系统处理,用户程序不能捕捉到).
disp 指定新的信号定位,即新的信号处理函数指针.可以为
SIG_IGN,SIG_DFL 或信号句柄地址.
若使用 signal(),disp 是信号句柄地址,sig 不能为 SIGILL,SIGTRAP
或 SIGPWR,收到该信号时,系统首先将重置 sig 的信号句柄为 SIG_DFL,
然后执行信号句柄.
若使用 sigset(),disp 是信号句柄地址,该信号时,系统首先将该
信号加入调用进程的信号掩码中,然后执行信号句柄.当信号句柄
运行结束
后,系统将恢复调用进程的信号掩码为信号收到前的状态.另外,
使用 sigset()时,disp 为 SIG_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 信号.设置超时时钟时时间值不会被放入堆栈中,后一次
设置会把前一次(还未到超时时间)冲掉.
若 sec 为 0,则取消任何以前设置的超时时钟.
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.cuid 或 msg_perm.uid 的
进程或有效 UID 有合适权限的进程操作.只有具有合适权限的
用户才能增加 msg_qbytes 的值.
. IPC_RMID:删除由 msqid 指示的消息队列.将它从系统中删除并
破坏相关的数据结构.
本命令只能由有效 UID 等于 msg_perm.cuid 或 msg_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.cuid 和 msg_perm.uid 设置为调用进程的有效 UID.
. msg_perm.cgid 和 msg_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.cuid 和 shm_perm.uid 设置为调用进程的有效 UID.
. shm_perm.cgid 和 shm_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.cuid 或 shm_perm.uid 的
进程或有效 UID 有合适权限的进程操作.
. IPC_RMID:删除由 shmid 指示的共享内存.将它从系统中删除并
破坏相关的数据结构.
本命令只能由有效 UID 等于 shm_perm.cuid 或 shm_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 定义,这
些命令是对由 semid 和 semnum 指定的信号量做操作的.每个命令都
要求有相应的权限级别:
. 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.cuid 或 sem_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.cuid 和 sem_perm.uid 设置等于调用
进程的有效 UID.
. 在操作权限结构,sem_perm.cgid 和 sem_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; /* 操作标志 */
由本系统调用定义的每个信号量操作是针对由 semid 和 sem_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_op 为 0,同时调用进程具有读权限,下列情况之一将会发
生:
* 若 semval 为 0,本系统调用立即返回.
* 若 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; /*P 和 V 操作*/
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 命名的段存在且不在使用中,本标志的作用
同早先创建一个段相同,否则,该段根据 size 和 mode 的值进程
创建.对段的读写访问权限的授予基于 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);
/*对共享数据段访问,写 10 个 a*/
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 ( ) 创建管道。管道是进程间通信最古老的方式,它包括无
名管道和有名管道两种,前者用于父进程和子进程间的通信,后者用于运行于同
一台机器上的任意两个进程间的通信。
pipe
write
read
(无名)管道特点:
??
管道是一个单向信道,数据只能向一个方向流动;需要双方通信时,需要
建立起两个管道;
?? 只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程)
;
?? 单独构成一种独立的文件系统:
管道对于管道两端的进程而言,
就是一个
文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单
独构成一种文件系统,并且只存在与内存中。
??
数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读
出。写入的内容每次都添加在管道缓冲区的末尾,且每次都是从缓冲区的
头部读出数据。
(先进先出)
Ex:
#include <unistd.h>
int pipe(int fileds[2]);
fileds[0] 用于读,fileds[1] 用于写。 读/写 —— 0/1
#include
#include
#include
#include
<stdio.h>
<stdlib.h>
<sys/types.h>
<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
严格限制可写入消息的个数和长度,队列中可能容纳不下这个消息。此时,此写
入进程将被添加到这个消息队列的等待队列中,
同时调用调度管理器选择新进程
运行。当有消息从此队列中释放时,该进程将被唤醒。
从队列中读的过程与之类似。进程对这个写入队列的访问权限将被再次检
验。读取进程将选择队列中第一个消息(不管是什么类型)或者第一个某特定类
型的消息。如果没有消息可以满足此要求,读取进程将被添加 到消息队列的读
取等待队列中,然后系统运行调度管理器。当有新消息写入队列时,进程将被唤
醒继续执行。
数据区
队列首部
Key1
3
2
1
2
#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,
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
int
/* 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,这次操作将会失败。它将挂起执行直到第
一个进程完成对此数据文件的使用。此时这个等待进程将被唤醒,这次它对信号
灯的操作将成功。
信号灯数组
信号灯表
Key1
Key2
Key3
0
1 2 3
3
1 2
3
4
5
3
0
0
#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
short
short
};
Field
sem_num;
sem_op;
sem_flg;
//序号
//操作,+1,-1
//IPC_NOWAIT, SEM_UNDO 等
Description
sem_num
sem_op
sem_flg
semaphore number
semaphore operation
semaphore flags
union semun{ //一些参数
int val;
struct semid_ds *buf;
ushort *array;
}
删除信号灯:
union semun dummy;
int semid;
.....
semctl(semid,0,IPC_RMID,dummy);
Ex: <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>
#include
#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 种方法组合优化就有了.Net 和 Java 下灵活多变的,编程简便的线程进程控制手段。
这 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
回复此发言
--------------------------------------------------------------------------------
2 四种进程或线程同步互斥的控制方法
//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,
LPSECURITY_ATTRIBUTES lpsaAttribute = NULL)
LPCTSTR
不用的参数不能乱填,乱填会出现一些意想不到的运行结果。
//创建互斥量
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)
lpszName
=
NULL,
{
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
回复此发言
--------------------------------------------------------------------------------
3 四种进程或线程同步互斥的控制方法
PV 操作及信号量的概念都是由荷兰科学家 E.W.Dijkstra 提出的。
信号量 S 是一个整数,
S 大于等于零时代表可供并发进程使用的资源实体数, S 小于零时则表示正在等待使用共
但
享资源的进程数。
P 操作申请资源:
(1)S 减 1;
(2)若 S 减 1 后仍大于等于零,则进程继续执行;
(3)若 S 减 1 后小于零,则该进程被阻塞后进入与该信号相对应的队列中,然后转
入进程调度。
V 操作 释放资源:
(1)S 加 1;
(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;
}
}
//线程 1
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 个资源 1 同时只可以有一个线程访问
global_Semephore= CreateSemaphore(NULL, 1, 1, NULL);
this->StartThread();
// TODO: Add your control notification handler code here
}
void CSemaphoreDlg::OnBnClickedButtonTwo()
{
//设置信号量 2 个资源 2 同时只可以有两个线程访问
global_Semephore= CreateSemaphore(NULL, 2, 2, NULL);
this->StartThread();
// TODO: Add your control notification handler code here
}
void CSemaphoreDlg::OnBnClickedButtonThree()
{
//设置信号量 3 个资源 3 同时只可以有三个线程访问
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
回复此发言
--------------------------------------------------------------------------------
4 四种进程或线程同步互斥的控制方法
程序运行结果
事件(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)