c语言多进程多线程编程

更新时间:2024-02-03 18:24:01 阅读量: 教育文库 文档下载

说明:文章内容仅供预览,部分内容可能不全。下载后的文档,内容与下面显示的完全一致。下载之前请确认下面内容是否您想要的,是否完整无缺。

C语言多进程编程

一.多进程程序的特点

进程是一个具有独立功能的程序关于某个数据集合的一次可以并发执行的运行活动,是处

于活动状态的计算机程序。进程作为构成系统的基本细胞,不仅是系统内部独立运行的实体,而且是独立竞争资源的基本实体。

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

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

新建 表示进程正在被创建, 运行 是进程正在运行,

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

就绪 是表示系统正在等待CPU来执行命令, 完成 表示进程已经结束了系统正在回收资源.

由于UNIX系统是分时多用户系统, CPU按时间片分配给各个用户使用,而在实质上应该说CPU按时间片分配给各个进程使用, 每个进程都有自己的运行环境以使得在CPU做进程切换时不会\忘记\该进程已计算了一半的\半成品”. 以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 #include 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 pid_t pid;

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

//子进程代码 exit(0); }

else if(pid>0) {

//父进程代码 exit(0);

父进程 子进程1 子进程2 应用程序 fork ( ) } else

{

printf(\ exit(1); }

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

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

#include #include 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,\ system(command);

}

2.3.exec() 执行一个文件

功能:执行一个文件 语法

#include

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(\ execl(\

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

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

#include

FILE *popen(command,type) char *command,type;

说明:本系统调用在调用进程和被执行命令间创建一个管道. 参数command做为被执行的命令行.type做为I/O模式,\为从被 执行命令读,\为向被执行命令写.返回一个标准流指针,做为管

道描述符,向被执行命令读或写数据(做为被执行命令的STDIN或 STDOUT)该系统调用可以用来在程序中调用系统命令,并取得命令 的输出信息或者向命令输入信息.

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

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

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

#include int pclose(strm) FILE *strm;

说明:本系统调用用于关闭由popen()打开的管道,并会等待由popen() 激活的命令执行结束后,关闭管道后读取命令返回码.

返回值:若关闭的文件描述符不是由popen()打开的,则返回-1. 例子:printf(\ FILE * fd;

if ((fd=popen(\ printf(\ return; }

else {

char str[80];

while (fgets(str,80,fd)!=NULL) printf(\ } pclose(fd);

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

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

#include #include pid_t wait(stat_loc) int *stat_loc;

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

返回值:等待到一个子进程返回时,返回值为该子进程号,否则返回值为 -1.同时stat_loc返回子进程的返回值. 例子:/*父进程*/ if (fork()>0) { wait((int *)0);

/*父进程等待子进程的返回*/ } else {

/*子进程处理过程*/ exit(0); }

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

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

#include #include

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=\

waitpid(pid,&stat_loc,0);

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

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

exit(1); }

/*父进程*/

printf(\

/*字符串\将被打印出来*/

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

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

#include pid_t setpgrp()

说明:若调用进程不是会话首进程.将进程组号和会话号都设置为与它 的进程号相等.并释放调用进程的控制终端. 返回值:调用成功后,返回新的进程组号. 例子:/*父进程处理*/ if (fork()>0) { /*父进程处理*/ } else {

setpgrp();

/*子进程的进程组号已修改成与它的进程号相同*/ exit(0); }

2.9.exit() 终止进程

功能:终止进程. 语法:

#include void exit(status) int status;

说明:调用进程被该系统调用终止.引起附加的处理在进程被终止前全 部结束. 返回值:无

2.10.signal() 信号管理功能

功能:信号管理功能 语法:

#include

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(\

/*若要重新设置SIGINT信号中断处理函数为本函数则执行以 *下步骤*/ void (*a)(); a=myself;

signal(SIGINT,a); flag=2; }

main()

{

while (1) {

sleep(2000); /*等待中断信号*/ if (flag==1) {

printf(\ exit(0); }

if (flag==2) {

printf(\ printf(\ } } }

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

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

#include #include 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

unsigned int alarm(sec) unsigned int sec;

说明:指示调用进程的超时时钟在指定的时间后向调用进程发送一个 SIGALRM信号.设置超时时钟时时间值不会被放入堆栈中,后一次 设置会把前一次(还未到超时时间)冲掉.

若sec为0,则取消任何以前设置的超时时钟.

fork()会将新进程的超时时钟初始化为0.而当一个进程用exec() 族系统调用新的执行文件时,调用前设置的超时时钟在调用后仍 有效.

返回值:返回上次设置超时时钟后到调用时还剩余的时间秒数. 例子:int flag=0; void myself() { flag=1;

printf(\

/*若要重新设置SIGALRM信号中断处理函数为本函数则执行 *以下步骤*/ void (*a)(); a=myself;

signal(SIGALRM,a); flag=2; } main()

{

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

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

if (flag==1) {

printf(\ exit(0); }

if (flag==2) {

printf(\ printf(\ } } }

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

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

#include #include #include

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 #include #include

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 #include #include

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 #include #include 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(\

exit(1);

} /*若写消息队列已存在则失败,若不存在则创建并取得标识符*/ if ((wmsqid=\,\T|IPC_TRUNC))<0) { printf(\ exit(2);

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

if (msgrcv(rmsqid,&buf,sizeof(struct msgbuf)-sizeof(long), 0L,IPC_WAIT)>0) printf(\ buf.mtype,buf.mtext); else {

printf(\ exit(3); }

buf.mtype=3L;

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

printf(\ else {

printf(\

exit(4);

}

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

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

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

#include #include #include

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 #include #include void *shmdt(shmaddr) void *shmaddr;

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

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

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

功能:取得共享内存段 语法:

#include #include #include

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 #include #include

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 #include #include #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(\ 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(\ shmdt(addr1); shmdt(addr2); pause(); }

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

功能:信号量控制操作. 语法:

#include #include #include

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 #include #include

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 #include #include

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 #include #include #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(\ pause(); /*睡眠到被一软件中断信号唤醒*/ } else if (argv[1][0]=\ { 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(\ vsembuf.sem_num=second; semop(semid,&vsembuf,1); vsembuf.sem_num=first; semop(semid,&vsembuf,1); } }

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

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

#include

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

int sdleave(addr,flags) char *addr;

说明:用于指示调用进程已完成修改共享数据段中的内容. 返回值:调用成功则返回0,否则返回-1.

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

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

#include

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 int sdfree(addr) char *addr;

说明:本系统调用将共享数据段从调用进程的数据段的指定地址中分离. 若调用进程已完成sdenter()的调用,还未调用sdleave()就调用 本系统调用,则sdleave()被自动调用,然后才做本调用的工作. 返回值:若调用成功则返回联接的段地址.否则返回-1.

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

功能:同步共享数据访问. 语法:

#include int sdgetv(addr) char *addr;

说明:用于同步协调正在使用共享数据段的进程.返回值为共享数据段 的版本号.当有进程对该段做sdleave()操作时,版本号会被修改. 返回值:若调用成功,则返回指定共享数据段的版本号,否则返回-1.

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

功能:同步共享数据访问. 语法:

#include 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(\T,640,0777);

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

printf(\ exit(1); }

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

v=sdgetv(area1);

/*申请访问共享数据段area1,若已有进程在访问该段则本进程挂 *起,否则进入访问并将该数据段加写锁*/ sdenter(area1,SD_WRITE);

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

strcpy(area1,\

/*申请解除访问权限,若已有进程申请访问则激活该进程*/ sdleave(area1); /*进程处理过程*/

/*等待取共享数据段area1的版本号*/ sdwaitv(area1,v);

/*重新申请访问共享数据段area1*/ sdenter(area1,SD_WRITE); /*读取共享数据段中的数据*/

memcpy(buf,area1,20);

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

printf(\

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

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

#include #include char *getenv(name) char *name;

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

char * value;

value=getenv(\

printf(\将打印出HOME环境变量的值*/

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

功能:修改或增加环境值.

语法:

#include int putenv(string) char *string;

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

name=value

本系统调用将环境变量name等于值value,修改或增加一个环境变 量,字符串string成为环境的一部分.

返回值:若putenv()不能取得合适的内存空间则返回非0值,否则返回0. 例子:/*父进程处理*/

putenv(\ putenv(\TH=/bin\ if (fork()>0)

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

setpgrp();

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

value1=getenv(\ value2=getenv(\TH\

printf(\TH=[%s]\\n\ /*将打印出\和\TH=/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 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(\ {

t=time(0);

fprintf(fp,\ 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 #include

main() {

pid_t pc, pr; int status;

pc=fork(); if(pc<0)

printf(\else if(pc==0) {

/* 子进程的工作 */ exit(0); } else {

/* 父进程的工作 */ pr=wait(&status);

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

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

4.2. 管道

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

pipewrite(无名)管道特点:

?

read

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

? ?

只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程); 单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在与内存中。

?

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

Ex:

#include int pipe(int fileds[2]);

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

#include #include #include #include

int main() {

int pfds[2]; char buf[30]; pipe(pfds); if(fork()==0) {

close(pfds[0]); sleep(2);

write(pfds[1],\ 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 #include #include #include

#define FIFO_SERVER \

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(\

if(fd==-1)

if(errno==ENXIO)

printf(\

//设置非阻塞标志

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(\ }

else

printf(\ }

读:

#include #include #include #include

#define FIFO_SERVER \

main(int argc,char** argv) {

char r_buf[4096*2]; int fd;

int r_size; int ret_size;

r_size=atoi(argv[1]);

printf(\ 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(\ 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(\

printf(\

sleep(1); }

pause();

unlink(FIFO_SERVER); } note:

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

4.3. 消息队列

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

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

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严格限制可写入消息的个数和长度,队列中可能容纳不下这个消息。此时,此写入进程将被添加到这个消息队列的等待队列中,同时调用调度管理器选择新进程运行。当有消息从此队列中释放时,该进程将被唤醒。

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

醒继续执行。

数据区队列首部Key13212#include #include #include

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(\/* key = 123456

/* Open the queue - create if necessary */

if((msgqueue_id = msgget(key, IPC_CREAT | 0666)) = = -1) //创建一个消息队列

{ //一般在服务器创建

perror(\ //客户端仅输

入权限

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(\ 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,这次操作将会失败。它将挂起执行直到第一个进程完成对此数据文件的使用。此时这个等待进程将被唤醒,这次它对信号灯的操作将成功。

信号灯表Key1Key2Key30信号灯数组132345001323

#include

#include #include

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

#include #include #include #include #include #include #include

#define SEM_NAME \

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(\ 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(\

本文来源:https://www.bwwdw.com/article/av5w.html

Top