精华内容
下载资源
问答
  • 进程与多线程异同及适用...1.单进程线程一个人在一张桌子上吃饭。 2.单进程线程多个人在同一张桌子上一起吃饭。 3.多进程线程多个人每个人在自己的桌子上吃饭。 多线程的问题是多个人同时吃一道菜的...

    “进程——资源分配的最小单位,线程——程序执行的最小单位”

    举个例子

    1.单进程单线程:一个人在一张桌子上吃饭。
    2.单进程多线程:多个人在同一张桌子上一起吃饭。
    3.多进程单线程:多个人每个人在自己的桌子上吃饭。

    • 多线程的问题是多个人同时吃一道菜的时候容易发生争抢,例如两个人同时夹一个菜(共享资源),一个人刚伸出筷子,结果发现已经有另一人在夹菜了,此时他就要等另一个人夹完菜之后才可以夹菜,也就是说在同一张桌子上有多个人吃饭就不可避免会发生争抢。

    • 多进程每个人都有自己的一道菜,就不会同时争抢一道菜。但是另一个问题随之而来。每个人开一张桌子消耗就会较大。开桌子(创建进程)的消耗也因饭店(操作系统)的不同有所区别: 对于Windows来说,开桌子的开销是很大的,因此Windows鼓励大家在一张桌子上吃饭。Windows多线程学习的重点是面对资源争抢与同步方面的问题。对于Linux来说,开桌子的开销很小,因此Linux鼓励大家尽量每个人都开一张自己的桌子吃饭。这带来新的问题,坐在两张不同的桌子上,说话不方便,因此Linux下的学习重点是进程间通讯的方式。(PS:两者的开销相差一百倍左右)

    多线程多进程优缺点对比

    多进程优点

    1. 每个进程相互独立,不影响主程序的稳定性,子进程崩溃没有关系。
    2. 通过增加CPU,就可以容易扩充性能。
    3. 可以尽量减少线程加锁/解锁的影响,极大提高性能。就算是线程运行的模块算法效率低也没有关系。
    4. 每个子进程都有2GB地址空间和资源,总体能够达到性能上限非常高。

    多进程的缺点

    1. 逻辑控制复杂,需要个主程序交互。
    2. 需要跨越边界,如果有大数据量传送就不太好,适合小数据量传送、密集运算。多进程调度开销较大。

    多线程的优点

    1. 无需跨越进程边界。
    2. 程序逻辑和控制方式简单。
    3. 所有线程可以直接共享内存和变量等。
    4. 线程方式消耗的总资源比进程方式少。

    多线程的缺点

    1. 每个线程与主程序共用地址空间,受限于2GB地址空间。
    2. 线程之间同步与加锁控制比较麻烦。
    3. 一个线程的崩溃可能影响到整个程序的稳定性。
    4. 到达一定的线程数后,即是再增加CPU也不能提高性能。
    5. 线程能够提高总的性能是有限的,而且线程多了之后本身的调度也是一个麻烦事儿,需要消耗较多的CPU资源。

    多进程和多线程的对比

    我们按照多个不同的维度,来看看多进程和多线程的对比(注:都是相对的,不是说一个好得不得了,另一个差的无法忍受)

    维度多进程多线程总结
    数据共享同步共享复杂需要IPC;同步简单共享简单;同步复杂各有优势
    内存、CPU占用内存多,切换复杂,CPU利用率低占用内存极少,切换简单,CPU利用率高线程占优
    创建、销毁和切换创建销毁、切换复杂、速度慢创建销毁、切换简单、速度快线程占优
    编程调试编程调试简单编程调试复杂进程占优
    可靠性进程间不会相互影响一个线程挂掉将导致整个进程挂掉进程占优
    分布式适用于多核、多级分布;如果一台机器不够,扩展到多台机器比较简单适用于多核分布进程占优

    应用场景

    1. 需要频繁创建销毁的优先使用线程。例如:web服务器,来一个建立一个线程,断了就销毁线程。要是用进程,创建和校会的代价是很难承受的。
    2. 需要进行大量的计算优先使用线程。所谓大计算量,就是消耗CPU很多,切换频繁,这种情况用线程最合适。例如:图像处理,算法处理。
    3. 强相关处理用线程,弱相关处理用进程。一般的server需要完成的任务如下:消息接发和消息处理。接发消息和消息处理就是弱相关任务,而消息处理里面又能分为消息解码,业务处理,这两个任务相对来说相关性强很多,因此,消息接发和消息处理可以分进程设计,消息解码和业务处理可以分线程设计。
    4. 可能扩展到多机分布的用多进程,多核分布用多线程。
    5. 都满足需求的情况下,哪种方式拿手用哪种方式。

    参考资料

    1. 多线程和多进程的区别与联系
    2. 多进程和多线程的优缺点分析
    3. 面试总结,多进程和多线程的区别
    4. 多进程多线程适用场景
    展开全文
  • 多进程多线程

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

    一. 多进程程序的特点 

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

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

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

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

    运行 是进程正在运行,

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

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

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

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

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

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

    3) 系统层次的保存

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

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

    1.1并行化 

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

    1.2简单有序 

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

    1.3.互不干扰 

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

    1.4.事务化 

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

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

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

    功能:创建一个新的进程

    语法:

    #include <unistd.h> 

    #include <sys/types.h> 

    pid_t fork(); 

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

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

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

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

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

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

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

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

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

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

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

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

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

    ●环境变量

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

    ●信号处理设置

    ●堆栈

    ●目录

    ●进程组标志(process ID)

    ●会晤组标志(session ID)

    ●正文

    子进程特有内容:

    ●进程号

    ●父进程号

    ●进程执行时间

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

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

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

    函数执行过程:

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

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

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

    例子:

    pid_t fork(void) 

    #include <unistd.h>

    pid_t pid;

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

    {

      //子进程代码

    exit(0);

    }

    else if(pid>0)

    {

    //父进程代码

    exit(0);

    }

    else

    {

      printf("Error");

      exit(1);

    }

     

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

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

    语法:

    #include <stdio.h> 

    #include <stdlib.h> 

    int system(string) 

    char *string; 

    说明:

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

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

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

    例子:char command[81]; 

    int i; 

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

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

    system(command); 

    2.3.exec()     执行一个文件 

    功能:执行一个文件

    语法

    #include <unistd.h>

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

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

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

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

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

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

    说明:

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

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

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

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

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

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

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

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

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

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

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

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

    语法:

    #include <stdio.h> 

    FILE *popen(command,type) 

    char *command,type; 

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

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

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

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

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

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

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

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

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

    语法:

    #include <stdio.h> 

    int pclose(strm) 

    FILE *strm; 

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

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

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

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

    FILE * fd; 

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

    printf("call popen failed\n"); 

    return; 

    else { 

    char str[80]; 

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

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

    pclose(fd); 

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

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

    语法:

    #include <sys/types.h> 

    #include <sys/wait.h> 

    pid_t wait(stat_loc) 

    int *stat_loc; 

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

    一个子进程终止

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

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

    例子:/*父进程*/ 

    if (fork()>0) { 

    wait((int *)0); 

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

    else { 

    /*子进程处理过程*/ 

    exit(0); 

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

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

    语法:

    #include <sys/types.h> 

    #include <sys/wait.h> 

    pid_t waitpid(pid,stat_loc,options) 

    pid_t pid; 

    int *stat_loc,options; 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    例子:pid_t pid; 

    int stat_loc; /*父进程*/ 

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

    waitpid(pid,&stat_loc,0); 

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

    else { 

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

    exit(1); 

    /*父进程*/ 

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

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

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

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

    语法:

    #include <sys/types.h> 

    pid_t setpgrp() 

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

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

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

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

    if (fork()>0) { 

    /*父进程处理*/ 

    else { 

    setpgrp(); 

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

    exit(0); 

    2.9.exit()      终止进程

    功能:终止进程

    语法:

    #include <stdlib.h> 

    void exit(status) 

    int status; 

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

    部结束

    返回值:无 

    2.10.signal()   信号管理功能

    功能:信号管理功能 

    语法:

    #include <signal.h> 

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

    int sig; 

    void (*disp)(int); 

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

    int sig; 

    void (*disp)(int); 

    int sighold(sig) 

    int sig; 

    int sigrelse(sig) 

    int sig; 

    int sigignore(sig) 

    int sig; 

    int sigpause(sig) 

    int sig; 

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

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

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

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

    SIG_IGN,SIG_DFL或信号句柄地址

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

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

    然后执行信号句柄

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

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

    运行结束 

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

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

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

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

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

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

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

    进程直到收到信号

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

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

    应处理

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

    否则返回SIG_ERR. 

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

    int flag=0; 

    void myself() 

    flag=1; 

    printf("get signal SIGINT\n"); 

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

    *下步骤*/ 

    void (*a)(); 

    a=myself; 

    signal(SIGINT,a); 

    flag=2; 

    main() 

    while (1) { 

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

    if (flag==1) { 

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

    exit(0); 

    if (flag==2) { 

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

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

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

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

    语法:

    #include <sys/types.h> 

    #include <signal.h> 

    int kill(pid,sig); 

    pid_t pid; 

    int sig; 

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

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

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

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

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

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

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

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

    统进程除外). 

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

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

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

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

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

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

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

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

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

    信号处理

    kill((pid_t)324,SIGINT); 

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

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

    语法:

    #include <unistd.h< 

    unsigned int alarm(sec) 

    unsigned int sec; 

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

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

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

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

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

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

    有效

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

    例子:int flag=0; 

    void myself() 

    flag=1; 

    printf("get signal SIGALRM\n"); 

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

    *以下步骤*/ 

    void (*a)(); 

    a=myself; 

    signal(SIGALRM,a); 

    flag=2; 

    main() 

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

    while (1) { 

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

    if (flag==1) { 

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

    exit(0); 

    if (flag==2) { 

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

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

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

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

    语法:

    #include <sys/types.h> 

    #include <sys/ipc.h> 

    #include <sys/msg.h> 

    int msgsnd(msqid,msgp,msgsz,msgflg) 

    int msqid; 

    void *msgp; 

    size_t msgsz; 

    int msgflg; 

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

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

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

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

    long mtype; 

    char mtext[]; 

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

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

    统允许的最大值间变化

    msgflg指定操作行为

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

    进程会立即返回

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

    面情况之一发生

    消息被发送出去

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

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

    执行或被终止

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

    消息数(msg_qnum)1. 

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

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

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

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

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

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

    语法:

    #include <sys/types.h> 

    #include <sys/ipc.h> 

    #include <sys/msg.h> 

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

    int msqid; 

    void *msgp; 

    int msgsz; 

    long msgtyp; 

    int msgflg; 

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

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

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

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

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

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

    msgtyp指定消息类型

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

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

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

    类型值又最小的消息

    msgflg指定操作行为

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

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

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

    面情况之一发生

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

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

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

    执行或被终止

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

    消息数(msg_qnum)1. 

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

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

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

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

    不成功则返回-1. 

    2.15.msgctl()   消息控制操作

    功能:消息控制操作 

    语法:

    #include <sys/types.h> 

    #include <sys/ipc.h> 

    #include <sys/msg.h> 

    int msgctl(msqid,cmd,buf) 

    int msqid,cmd; 

    struct msqid_ds *buf; 

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

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

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

    buf指向的结构中

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

    向的结构中的对应值

    msg_perm.uid 

    msg_perm.gid 

    msg_perm.mode 

    msg_qbytes 

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

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

    用户才能增加msg_qbytes的值

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

    破坏相关的数据结构

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

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

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

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

    功能:取得一个消息队列

    语法:

    #include <sys/types.h> 

    #include <sys/ipc.h> 

    #include <sys/msg.h> 

    int msgget(key,msgflg) 

    key_t key; 

    int msgflg; 

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

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

    建出来

    若参数key等于IPC_PRIVATE. 

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

    (msgflg&IPC_CREAT)为真

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

    始化为如下

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

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

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

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

    . msg_ctime设置为当前系统时间

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

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

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

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

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

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

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

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

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

    struct msgbuf { 

    long mtype; 

    char mtext[200]; 

    } buf; 

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

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

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

    exit(1); 

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

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

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

    exit(2); 

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

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

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

    buf.mtype,buf.mtext); 

    else { 

    printf("get message failed\n"); 

    exit(3); 

    buf.mtype=3L; 

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

    IPC_NOWAIT)>0) 

    printf("send message OK\n"); 

    else { 

    printf("send message failed\n"); 

    exit(4); 

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

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

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

    语法:

    #include <sys/types.h> 

    #include <sys/ipc.h> 

    #include <sys/shm.h> 

    void *shmat(shmid,shmaddr,shmflg) 

    int shmid; 

    void *shmaddr; 

    int shmid; 

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

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

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

    用的地址上

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

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

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

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

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

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

    的段为可读写的

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

    否则返回值为-1. 

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

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

    语法:

    #include <sys/types.h> 

    #include <sys/ipc.h> 

    #include <sys/shm.h> 

    void *shmdt(shmaddr) 

    void *shmaddr; 

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

    脱离出去

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

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

    功能:取得共享内存段 

    语法:

    #include <sys/types.h> 

    #include <sys/ipc.h> 

    #include <sys/shm.h> 

    int shmget(key,size,shmflg) 

    key_t key; 

    int size,shmflg; 

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

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

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

    参数key等于IPC_PRIVATE. 

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

    值为真

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

    化如下

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

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

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

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

    . shm_ctime设置为当前系统时间

    . shm_segsz设置为0. 

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

    值为-1. 

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

    功能:共享内存控制操作

    语法:

    #include <sys/types.h> 

    #include <sys/ipc.h> 

    #include <sys/shm.h> 

    int shmctl(shmid,cmd,buf) 

    int shmid,cmd; 

    struct shmid_ds *buf; 

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

    以下为cmd的有效值

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

    buf指向的结构中

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

    向的结构中的对应值

    shm_perm.uid 

    shm_perm.gid 

    shm_perm.mode 

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

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

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

    破坏相关的数据结构

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

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

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

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

    #include <sys/types.h> 

    #include <sys/ipc.h> 

    #include <sys/shm.h> 

    #define SHMKEY 74 

    #define K 1024 

    int shmid; 

    cleanup() 

    shmctl(shmid,IPC_RMID,0); 

    exit(0); 

    main() 

    int *pint; 

    char *addr1,*addr2; 

    extern char *shmat(); 

    extern cleanup(); 

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

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

    addr1=shmat(shmid,0,0); 

    addr2=shmat(shmid,0,0); 

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

    pint=(int*)addr1; 

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

    pint=(int*)addr1; 

    *pint=256; 

    pint=(int*)addr2; 

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

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

    shmdt(addr1); 

    shmdt(addr2); 

    pause(); 

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

    功能:信号量控制操作

    语法:

    #include <sys/types.h> 

    #include <sys/ipc.h> 

    #include <sys/sem.h> 

    int semctl(semid,memnum,cmd,arg) 

    int semid,semnum,cmd; 

    union semun { 

    int val; 

    struct semid_ds *buf; 

    ushort *array; 

    }arg; 

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

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

    要求有相应的权限级别

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

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

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

    改权限

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

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

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

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

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

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

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

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

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

    被清除,要求有修改权限

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

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

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

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

    arg.buf指向的结构中读取

    sem_perm.uid 

    sem_perm.gid 

    sem_perm.mode 

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

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

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

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

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

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

    GETVAL:semval的值

    GETPID:sempid的值

    GETNCNT:semncnt的值

    GETZCNT:semzcnt的值

    其他:0. 

    若调用失败则返回-1. 

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

    功能:取得一组信号量

    语法:

    #include <sys/types.h> 

    #include <sys/ipc.h> 

    #include <sys/sem.h> 

    int semget(key,nsems,semflg) 

    key_t key; 

    int nsems,semflg; 

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

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

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

    . key等于IPC_PRIVATE. 

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

    为真

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

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

    进程的有效UID. 

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

    进程的有效GID. 

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

    特位

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

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

    2.23.semop()   信号量操作

    功能:信号量操作

    语法:

    #include <sys/types.h> 

    #include <sys/ipc.h> 

    #include <sys/sem.h> 

    int semop(semid,sops,nsops) 

    int semid; 

    struct sembuf *sops; 

    unsigned nsops; 

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

    该组信号量与semid相关

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

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

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

    sem_num; /* 信号量数 */ 

    sem_op; /* 信号量操作 */ 

    sem_flg; /* 操作标志 */ 

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

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

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

    一将会发生

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

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

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

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

    ,则本调用立即返回

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

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

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

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

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

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

    进程指定信号量的semadj

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

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

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

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

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

    进程指定信号量的semadj

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

    semval0,本系统调用立即返回

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

    立即返回

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

    将把指定信号量的 

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

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

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

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

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

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

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

    #include <sys/types.h> 

    #include <sys/ipc.h> 

    #include <sys/sem.h> 

    #define SEMKEY 75 

    int semid; 

    unsigned int count; 

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

    * struct sembuf { 

    * unsigned short sem_num; 

    * short sem_op; 

    * short sem_flg; 

    * }*/ 

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

    cleanup() 

    semctl(semid,2,IPC_RMID,0); 

    exit(0); 

    main(argc,argv) 

    int argc; 

    char *argv[]; 

    int i,first,second; 

    short initarray[2],outarray[2]; 

    extern cleanup(); 

    if (argc==1) { 

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

    signal(i,clearup); 

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

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

    semctl(semid,2,SETALL,initarray); 

    semctl(semid,2,GETALL,outarray); 

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

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

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

    first=0; 

    second=1; 

    } else { 

    first=1; 

    second=0; 

    semid=semget(SEMKEY,2,0777); 

    psembuf.sem_op=-1; 

    psembuf.sem_flg=SEM_UNDO; 

    vsembuf.sem_op=1; 

    vsembuf.sem_flg=SEM_UNDO; 

    for (count=0;;xcount++) 

    psembuf.sem_num=first; 

    semop(semid,&psembuf,1); 

    psembuf.sem_num=second; 

    semop(semid,&psembuf,1); 

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

    vsembuf.sem_num=second; 

    semop(semid,&vsembuf,1); 

    vsembuf.sem_num=first; 

    semop(semid,&vsembuf,1); 

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

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

    语法:

    #include <sys/sd.h< 

    int sdenter(addr,flags) 

    char *addr; 

    int flags; 

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

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

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

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

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

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

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

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

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

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

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

    语法:

    #include <sys/sd.h&dt; 

    int sdleave(addr,flags) 

    char *addr; 

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

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

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

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

    语法:

    #include <sys/sd.h> 

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

    char *path; 

    int flags; 

    long size; 

    int mode; 

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

    flags的值定义

    . SD_RDONLY:联接的段为只读的

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

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

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

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

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

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

    (在读写中)该段

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

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

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

    语法:

    #include <sys/sd.h> 

    int sdfree(addr) 

    char *addr; 

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

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

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

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

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

    功能:同步共享数据访问

    语法:

    #include <sys/sd.h> 

    int sdgetv(addr) 

    char *addr; 

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

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

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

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

    功能:同步共享数据访问

    语法:

    #include <sys/sd.h> 

    int sdwaitv(addr,vnum) 

    char *addr; 

    int vnum; 

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

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

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

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

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

    语法:

    char *sbrk(incr) 

    int incr; 

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

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

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

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

    内容不确定

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

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

    char * area1; 

    char buf[21]; 

    int v; 

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

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

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

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

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

    exit(1); 

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

    v=sdgetv(area1); 

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

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

    sdenter(area1,SD_WRITE); 

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

    strcpy(area1,"aaaaaaaaaa"); 

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

    sdleave(area1); 

    /*进程处理过程*/ 

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

    sdwaitv(area1,v); 

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

    sdenter(area1,SD_WRITE); 

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

    memcpy(buf,area1,20); 

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

    sdleave(area1); 

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

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

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

    语法:

    #include <unistd.h>

    #include <stdlib.h>

    char *getenv(name) 

    char *name; 

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

    返回值:如前述

    例子

    char * value; 

    value=getenv("HOME"); 

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

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

    功能:修改或增加环境值

    语法:

    #include <stdlib.h> 

    int putenv(string) 

    char *string; 

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

    name=value 

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

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

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

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

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

    putenv("PATH=/bin"); 

    if (fork()>0) 

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

    /*子进程处理*/ 

    setpgrp(); 

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

    char * value1; 

    value1=getenv("HOME"); 

    value2=getenv("PATH"); 

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

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

    .多进程编程技巧 

    3.1.主要程序结构 

    3.1.1事件主控方式 

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

    3.1.2信息协调方式 

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

    3.2.选择主体分叉点 

    3.2.1事件初始产生 

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

    3.2.2主程序自主产生 

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

    3.3.进程间关系处理 

    3.3.1父子进程关系 

    进程组处理 

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

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

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

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

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

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

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

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

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

    前运行

    子进程信号处理 

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

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

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

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

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

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

    可以正常退出

    3.3.2兄弟进程关系 

    交换进程号 

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

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

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

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

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

    要的进程号

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

    3.3.3.1僵尸进程

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

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

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

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

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

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

      if( fork==0)

            exit(0);

       

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

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

    struct sigaction act,oldact;

    act.sa_handler=SIG_IGN;

    sigemptyset(&act.sa_mask);

    act.sa_flags=0;

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

    exit(1);

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

    3.3.3.2.2  调用wait()或waitpid();

      pid_t wait(int* statloc);

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

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

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

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

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

    pidx = wait(NULL)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    void sigchld_handler(int signo)

    {

      pid_t pid;

      int stat;

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

    {}

    return;

    }

    void main()

    {

    struct sigaction act,oldact;

    act.sa_handler=sigchld_handler;

    sigemptyset(&act.sa_mask);

    act.sa_flags=0;

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

    {

      ……

    }

    }

    3.3.3.2.4  两次调用fork().

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

    int main()

    {

          int i;

          pid_t pid;

          pid=fork();

          if(pid==0)

          {

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

         {

            if(fork(0)==0)

            {

           sleep(1);

           exit(0);

            }

         }

         exit(0);

          }

          for(;;){}

    }

    总结:进程的一生

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

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

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

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

    3.3.4守护进程 

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

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

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

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

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

       

    if(pid=fork()) 

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

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

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

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

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

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

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

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

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

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

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

    if(pid=fork()) 

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

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

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

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

    3. 改变当前工作目录 

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

    4. 重设文件创建掩模 

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

    5. 处理SIGCHLD信号 

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

    signal(SIGCHLD,SIG_IGN); 

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

    守护进程实例 

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

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

    1. init.c清单 

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

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

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

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

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

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

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

    return; 

    }

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

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

    main() 

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

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

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


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

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

    3.4.进程间通讯处理 

    3.4.1共享内存需要锁机制 

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

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

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

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

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

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

    享内存的正确操作

    3.4.2消息队列需要关键值 

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

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

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

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

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

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

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

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

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

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

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

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

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

    3.4.4IPC的权限设置 

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

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

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

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

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

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

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

    s. 

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

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

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

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

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

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

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

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

    3.4.6各种IPC方式的特点 

    消息队列

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

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

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

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

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

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

    脱离访问关系

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

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

    不能访问

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

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

    共享内存 

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

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

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

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

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

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

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

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

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

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

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

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

    一致

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

    信号量 

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

    没有其他的数据信息

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

    信号 

    信号由系统进行定义

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

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

    信号不具备数据块

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

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

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

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

    信号的信息传递是单向的

    管道 

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

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

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

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

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

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

    有名管道 

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

    或兄弟关系

    无名管道 

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

    文件 

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

    操作麻烦,定位困难

    保密程度低

    容易出现数据不一致问题

    占用硬盘空间

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

    特殊处理 

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

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

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

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

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

    环境变量 

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

    保密性较好

    双方必须约定环境变量名

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

    共享数据段 

    操作比较复杂

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

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

    流 

    文件描述符的操作方式

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

    双向传送信息

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

    其他性质与管道相类似

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

    传递参数 

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

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

    双方必须约定参数位置

    只占用子进程的参数区.  

    .进程通信与同步

    4.1. 概述 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    #include <sys/types.h>

    #include <sys/wait.h>

    main()

    {

    pid_t pc, pr;

    int status;

    pc=fork();

    if(pc<0)

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

    else if(pc==0)

    {

    /* 子进程的工作 */

    exit(0);

    }

    else

    {

    /* 父进程的工作 */

    pr=wait(&status);

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

    }

    }

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

    4.2. 管道

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

    (无名)管道特点:

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

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

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

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

    Ex:

    #include <unistd.h>

    int pipe(int fileds[2]); 

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

    #include <stdio.h>

    #include <stdlib.h>

    #include <sys/types.h>

    #include <unistd.h>

    int main()

    {

      int pfds[2];

      char buf[30];

      pipe(pfds);

      if(fork()==0)

      {

    close(pfds[0]);

    sleep(2);

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

    exit(0);

      }

      else

      {

       close(pfds[1]);

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

    wait(NULL);

    exit(0);

      }

    }

    note:

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

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

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

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

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

    int mkfifo(const char * pathname, mode_t mode)

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

    Ex:

    写:

    #include <sys/types.h>

    #include <sys/stat.h>

    #include <errno.h>

    #include <fcntl.h>

    #define FIFO_SERVER "/tmp/fifoserver"

    main(int argc,char** argv)

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

    {

    int fd;

    char w_buf[4096*2];

    int real_wnum;

    memset(w_buf,0,4096*2);

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

    printf("cannot create fifoserver\n");

    if(fd==-1)

    if(errno==ENXIO)

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

       //设置非阻塞标志

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

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

    real_wnum=write(fd,w_buf,2048);

    if(real_wnum==-1)

    {

    if(errno==EAGAIN)

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

    }

    else 

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

    }

    读: 

    #include <sys/types.h>

    #include <sys/stat.h>

    #include <errno.h>

    #include <fcntl.h>

    #define FIFO_SERVER "/tmp/fifoserver"

    main(int argc,char** argv)

    {

    char r_buf[4096*2];

    int  fd;

    int  r_size;

    int  ret_size;

    r_size=atoi(argv[1]);

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

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

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

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

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

    if(fd==-1)

    {

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

    exit();

    }

    while(1)

    {

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

    ret_size=read(fd,r_buf,r_size);

    if(ret_size==-1)

    if(errno==EAGAIN)

    printf("no data avlaible\n");

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

       sleep(1);

    }

    pause();

    unlink(FIFO_SERVER);

    }

    note:

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

    4.3. 消息队列

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

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

    #include <sys/types.h>

    #include <sys/ipc.h>

    key_t ftok(char* pathname,char proj);

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

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

    struct kern_ipc_perm

    {   

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

       uid_t   uid;

         gid_t   gid;

    uid_t   cuid;

    gid_t   cgid;

    mode_t  mode;

    unsigned long seq;

    }

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

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

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

        #include <sys/types.h>

    #include <sys/ipc.h>

    #include <sys/msg.h>

    int msgget(key_t key, int msgflg);

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

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

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

    struct mymsgbuf 


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

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

      key_t key; 
      int msgqueue_id; 
      struct mymsgbuf qbuf; 

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

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

    {                                                  //一般在服务器创建

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

    exit(1); 

    }

    。。。。。。。。

    return(0);  
       } 

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

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


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

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


    void remove_queue(int qid) 

      msgctl(qid, IPC_RMID, 0); 

    Note: 已逐渐淘汰。

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

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

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

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

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

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

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

    #include <sys/types.h>

    #include <sys/ipc.h>

    #include <sys/sem.h>

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

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

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

    struct sembuf

    {

      unsigned short    sem_num; //序号

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

      short             sem_flg; //IPC_NOWAIT, SEM_UNDO

    };

    Field          Description

       sem_num        semaphore number

       sem_op          semaphore operation

       sem_flg          semaphore flags

    union semun{  //一些参数

      int val;

      struct semid_ds *buf;

      ushort *array;

    }

    删除信号灯:

      union semun dummy;

      int semid;

    …..

    semctl(semid,0,IPC_RMID,dummy);

    Ex:

    #include <string.h>

    #include <sys/socket.h>

    #include <sys/type.h>

    #include <netinet/in.h>

    #include <sys/ipc.h>

    #include <sys/sem.h>

    #include <stdio.h>

    #include <signal.h>

    #define  SEM_NAME "/tmp/sem_name.tmp"

    void sigint_handler(int);

    int semid;

    unsigned int count;

    struct sembuf psembuf,vsembuf;

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

    {

    int i,first,second;

    key_t key;

    short initarray[2],outarray[2];

      

    key=ftok(SEM_NAME,'a');

    if(argc==1)

    {

    signal(SIGINT,sigint_handler);

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

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

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

    semctl(semid,2,GETALL,outarray);

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

    pause();

    }

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

    {

    first=0;

    second=1;

    }

    else

    {

    first=1;

    second=0;

    }

    semid=semget(key,2,0777);

    psembuf.sem_op=-1;

    psembuf.sem_flg=SEM_UNDO;

    vsembuf.sem_op=1;

    vsembuf.sem_flg=SEM_UNDO;

      

    for(count=0;;count++)

    {

    psembuf.sem_num=first;

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

    psembuf.sem_num=second;

    semop(semid,&psembuf,1);

      

    semctl(semid,2,GETALL,outarray);

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

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

      

    vsembuf.sem_num=second;

    semop(semid,&vsembuf,1);

    vsembuf.sem_num=first;

    semop(semid,&vsembuf,1);

    }

     

    }

    void sigint_handler(int sig)

    {

    semctl(semid,2,IPC_RMID,0);

    exit(0);

    }

    4.5. 共享内存

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

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

    #include <sys/types.h>

    #include <sys/ipc.h>

    #include <sys/shm.h>

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

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

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

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

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

    Ex:

    写:

    #include <sys/ipc.h>

    #include <sys/shm.h>

    #include <sys/types.h>

    #include <unistd.h>

    typedef struct{

    char name[4];

    int age;

    } people;

    main(int argc, char** argv)

    {

    int shm_id,i;

    key_t key;

    char temp;

    people *p_map;

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

    key = ftok(name,0);

    if(key==-1)

    perror("ftok error");

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

    if(shm_id==-1)

    {

    perror("shmget error");

    return;

    }

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

    temp='a';

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

    {

    temp+=1;

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

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

    }

    if(shmdt(p_map)==-1)

    perror(" detach error ");

    }

    读:

    #include <sys/ipc.h>

    #include <sys/shm.h>

    #include <sys/types.h>

    #include <unistd.h>

    typedef struct{

    char name[4];

    int age;

    } people;

    main(int argc, char** argv)

    {

    int shm_id,i;

    key_t key;

    people *p_map;

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

    key = ftok(name,0);

    if(key == -1)

    perror("ftok error");

    shm_id = shmget(key,4096,0777);

    if(shm_id == -1)

    {

    perror("shmget error");

    return;

    }

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

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

    {

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

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

    }

    if(shmdt(p_map) == -1)

    perror(" detach error ");

    }

    4.6. UNIX域套接字

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

    1.命名Unix域套接字

    int socket(AF_UNIX, SOCK_STREAM, 0)

    int socket(AF_UNIX, SOCK_DGRAM, 0)

    struct sockaddr_un

    {

      short int sun_family;

      char sun_path[104];

    }

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

    note:

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

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

    2.非命名Unix域套接字

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

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

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

    int main()

    {

      int sv[2];

      char buf;

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

       exit(1);

      

      if(fork()==0)

      {

    close(sv[0]);   

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

    buf=toupper(buf);

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

    exit(0);

      }

      else

      {

    close(sv[1]);

    sleep(1);

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

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

    exit(0);

      }

    }

    .线程

    5.1. 概述 

        

    5.2. C++线程同步 

    5.2.1概述  

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

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

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

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

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

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

    5.2.2临界区(Critical Section)

    5.2.3互斥量

    5.2.4信号量

    5.2.5事件

    5.3. 概述 

     临界区(Critical Section)  

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

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

     EnterCriticalSection() 进入临界区

     LeaveCriticalSection() 离开临界区  

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

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

       

      //CriticalSection 

      CCriticalSection global_CriticalSection; 

       

      // 共享资源 

      char global_Array[256]; 

       

      //初始化共享资源 

      void InitializeArray() 

      { 

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

       { 

       global_Array[i]=I; 

       } 

      } 

       

      //写线程 

      UINT Global_ThreadWrite(LPVOID pParam) 

      { 

       CEdit *ptr=(CEdit *)pParam; 

       ptr->SetWindowText(""); 

       //进入临界区 

      global_CriticalSection.Lock(); 

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

       { 

       global_Array[i]=W; 

       ptr->SetWindowText(global_Array); 

       Sleep(10); 

       }  

     //离开临界区 

       global_CriticalSection.Unlock(); 

       return 0; 

      } 

       

      //删除线程 

      UINT Global_ThreadDelete(LPVOID pParam) 

      { 

       CEdit *ptr=(CEdit *)pParam; 

       ptr->SetWindowText(""); 

       //进入临界区 

       global_CriticalSection.Lock(); 

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

       { 

       global_Array[i]=D; 

       ptr->SetWindowText(global_Array); 

       Sleep(10); 

       } 

       

     //离开临界区 

       global_CriticalSection.Unlock(); 

       return 0; 

      } 

       

      //创建线程并启动线程 

      void CCriticalSectionsDlg::OnBnClickedButtonLock() 

      { 

     

     

      

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

     

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

     

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

        //Start the first Thread 

       CWinThread *ptrWrite = AfxBeginThread(Global_ThreadWrite, 

       &m_Write, 

       THREAD_PRIORITY_NORMAL, 

       0, 

       CREATE_SUSPENDED); 

       ptrWrite->ResumeThread(); 

         

       //Start the second Thread 

       CWinThread *ptrDelete = AfxBeginThread(Global_ThreadDelete, 

       &m_Delete, 

       THREAD_PRIORITY_NORMAL, 

       0, 

       CREATE_SUSPENDED); 

       ptrDelete->ResumeThread(); 

      }  

        

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

      程序运行结果 

         

      

      互斥量(Mutex)  

        

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

       

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

        CreateMutex() 创建一个互斥量 

        OpenMutex() 打开一个互斥量 

        ReleaseMutex() 释放互斥量 

        WaitForMultipleObjects() 等待互斥量对象  

        

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

       

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

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

       

      //创建互斥量 

      CMutex global_Mutex(0,0,0); 

       

      // 共享资源 

      char global_Array[256]; 

       

      void InitializeArray() 

      { 

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

       { 

       global_Array[i]=I; 

       } 

      } 

      UINT Global_ThreadWrite(LPVOID pParam) 

      { 

       CEdit *ptr=(CEdit *)pParam; 

       ptr->SetWindowText(""); 

       global_Mutex.Lock(); 

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

       { 

       global_Array[i]=W; 

       ptr->SetWindowText(global_Array); 

       Sleep(10); 

       } 

       global_Mutex.Unlock(); 

       return 0; 

      } 

       

      UINT Global_ThreadDelete(LPVOID pParam) 

      { 

       CEdit *ptr=(CEdit *)pParam; 

       ptr->SetWindowText(""); 

       global_Mutex.Lock(); 

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

       { 

       global_Array[i]=D; 

       ptr->SetWindowText(global_Array); 

       Sleep(10); 

       } 

       global_Mutex.Unlock(); 

       return 0; 

      }  

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

      程序运行结果 

         

      

        

      信号量(Semaphores)  

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

     

     

      

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

     

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

     

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

     

      PV操作及信号量的概念都是由荷兰科学家E.W.Dijkstra提出的。信号量S是一个整数,S大于等于零时代表可供并发进程使用的资源实体数,但S小于零时则表示正在等待使用共享资源的进程数。  

       P操作申请资源: 

        (1S1; 

        (2)若S1后仍大于等于零,则进程继续执行; 

        (3)若S1后小于零,则该进程被阻塞后进入与该信号相对应的队列中,然后转入进程调度。  

       

      V操作 释放资源: 

        (1S1; 

        (2)若相加结果大于零,则进程继续执行; 

        (3)若相加结果小于等于零,则从该信号的等待队列中唤醒一个等待进程,然后再返回原进程继续执行或转入进程调度。 

       

        信号量包含的几个操作原语: 

        CreateSemaphore() 创建一个信号量 

        OpenSemaphore() 打开一个信号量 

        ReleaseSemaphore() 释放信号量 

        WaitForSingleObject() 等待信号量 

       

      //信号量句柄 

      HANDLE global_Semephore; 

       

      // 共享资源 

      char global_Array[256]; 

      void InitializeArray() 

      { 

       for(int i = 0;i<256;i++) 

       { 

       global_Array[i]=I; 

       } 

      } 

       

     //线程

      UINT Global_ThreadOne(LPVOID pParam) 

      { 

       CEdit *ptr=(CEdit *)pParam; 

       ptr->SetWindowText(""); 

       //等待对共享资源请求被通过 等于 P操作 

      WaitForSingleObject(global_Semephore, INFINITE); 

       for(int i = 0;i<256;i++) 

       { 

       global_Array[i]=O; 

       ptr->SetWindowText(global_Array); 

       Sleep(10); 

       } 

      

      //释放共享资源 等于 V操作 

       ReleaseSemaphore(global_Semephore, 1, NULL); 

       return 0; 

      } 

       

      UINT Global_ThreadTwo(LPVOID pParam) 

      { 

       CEdit *ptr=(CEdit *)pParam; 

       ptr->SetWindowText(""); 

       WaitForSingleObject(global_Semephore, INFINITE); 

       for(int i = 0;i<256;i++) 

       { 

       global_Array[i]=T; 

       ptr->SetWindowText(global_Array); 

       Sleep(10); 

       } 

       ReleaseSemaphore(global_Semephore, 1, NULL); 

       return 0; 

      } 

       

      UINT Global_ThreadThree(LPVOID pParam) 

      { 

       CEdit *ptr=(CEdit *)pParam; 

       ptr->SetWindowText(""); 

       WaitForSingleObject(global_Semephore, INFINITE); 

       for(int i = 0;i<256;i++) 

       { 

       global_Array[i]=H; 

       ptr->SetWindowText(global_Array); 

       Sleep(10); 

       } 

       ReleaseSemaphore(global_Semephore, 1, NULL); 

       return 0; 

      } 

       

      void CSemaphoreDlg::OnBnClickedButtonOne() 

      { 

       //设置信号量 个资源 1同时只可以有一个线程访问 

       global_Semephore= CreateSemaphore(NULL, 1, 1, NULL); 

       this->StartThread(); 

      

      // TODO: Add your control notification handler code here 

      } 

       

      void CSemaphoreDlg::OnBnClickedButtonTwo() 

      { 

       //设置信号量 个资源 同时只可以有两个线程访问 

       global_Semephore= CreateSemaphore(NULL, 2, 2, NULL); 

       this->StartThread(); 

       // TODO: Add your control notification handler code here 

      } 

       

      void CSemaphoreDlg::OnBnClickedButtonThree() 

      { 

      //设置信号量 个资源 同时只可以有三个线程访问 

       global_Semephore= CreateSemaphore(NULL, 3, 3, NULL); 

       this->StartThread();  

      

      // TODO: Add your control notification handler code here 

      }  

      信号量的使用特点使其更适用于对Socket(套接字)程序中线程的同步。例如,网络上的HTTP服务器要对同一时间内访问同一页面的用户数加以限制,这时可以为每一个用户对服务器的页面请求设置一个线程,而页面则是待保护的共享资源,通过使用信号量对线程的同步作用可以确保在任一时刻无论有多少用户对某一页面进行访问,只有不大于设定的最大用户数目的线程能够进行访问,而其他的访问企图则被挂起,只有在有用户退出对此页面的访问后才有可能进入。  

     

     

      

     作者: 61.51.111.*2006-8-24 14:09   回复此发言    

     

    --------------------------------------------------------------------------------

     

    四种进程或线程同步互斥的控制方法  

       

      程序运行结果 

         

      

        

      事件(Event)  

        

      事件对象也可以通过通知操作的方式来保持线程的同步。并且可以实现不同进程中的线程同步操作。  

      信号量包含的几个操作原语: 

        CreateEvent() 创建一个信号量 

        OpenEvent() 打开一个事件 

        SetEvent() 回置事件 

        WaitForSingleObject() 等待一个事件 

        WaitForMultipleObjects() 等待多个事件  

      WaitForMultipleObjects 函数原型: 

        WaitForMultipleObjects( 

        IN DWORD nCount, // 等待句柄数 

        IN CONST HANDLE *lpHandles, //指向句柄数组 

        IN BOOL bWaitAll, //是否完全等待标志 

        IN DWORD dwMilliseconds //等待时间 

        )  

      

      参数nCount指定了要等待的内核对象的数目,存放这些内核对象的数组由lpHandles来指向。fWaitAll对指定的这nCount个内核对象的两种等待方式进行了指定,为TRUE时当所有对象都被通知时函数才会返回,为FALSE则只要其中任何一个得到通知就可以返回。dwMilliseconds在这里的作用与在WaitForSingleObject()中的作用是完全一致的。如果等待超时,函数将返回WAIT_TIMEOUT。 

       

      //事件数组 

      HANDLE global_Events[2]; 

       

      // 共享资源 

      char global_Array[256]; 

       

      void InitializeArray() 

      { 

       for(int i = 0;i<256;i++) 

       { 

       global_Array[i]=I; 

       } 

      } 

       

      UINT Global_ThreadOne(LPVOID pParam) 

      { 

       CEdit *ptr=(CEdit *)pParam; 

       ptr->SetWindowText(""); 

       for(int i = 0;i<256;i++) 

       { 

       global_Array[i]=O; 

       ptr->SetWindowText(global_Array); 

       Sleep(10); 

       } 

      

      //回置事件 

       SetEvent(global_Events[0]); 

       return 0; 

      } 

       

      UINT Global_ThreadTwo(LPVOID pParam) 

      { 

       CEdit *ptr=(CEdit *)pParam; 

       ptr->SetWindowText(""); 

       for(int i = 0;i<256;i++) 

       { 

       global_Array[i]=T; 

       ptr->SetWindowText(global_Array); 

       Sleep(10); 

       } 

      

      //回置事件 

       SetEvent(global_Events[1]); 

       return 0; 

      } 

       

      UINT Global_ThreadThree(LPVOID pParam) 

      { 

       CEdit *ptr=(CEdit *)pParam; 

       ptr->SetWindowText(""); 

      

      //等待两个事件都被回置 

       WaitForMultipleObjects(2, global_Events, true, INFINITE); 

       for(int i = 0;i<256;i++) 

       { 

       global_Array[i]=H; 

       ptr->SetWindowText(global_Array); 

       Sleep(10); 

       } 

       return 0; 

      } 

      void CEventDlg::OnBnClickedButtonStart() 

      { 

       for (int i = 0; i < 2; i++) 

       { 

      

      //实例化事件 

       global_Events[i]=CreateEvent(NULL,false,false,NULL); 

       } 

       CWinThread *ptrOne = AfxBeginThread(Global_ThreadOne, 

       &m_One, 

       THREAD_PRIORITY_NORMAL, 

       0, 

       CREATE_SUSPENDED); 

       ptrOne->ResumeThread(); 

       

       //Start the second Thread 

       CWinThread *ptrTwo = AfxBeginThread(Global_ThreadTwo, 

       &m_Two, 

       THREAD_PRIORITY_NORMAL, 

       0, 

       CREATE_SUSPENDED); 

       ptrTwo->ResumeThread(); 

       

       //Start the Third Thread 

       CWinThread *ptrThree = AfxBeginThread(Global_ThreadThree, 

       &m_Three, 

       THREAD_PRIORITY_NORMAL, 

       0, 

       CREATE_SUSPENDED); 

       ptrThree->ResumeThread(); 

       // TODO: Add your control notification handler code here 

      }  

      

      事件可以实现不同进程中的线程同步操作,并且可以方便的实现多个线程的优先比较等待操作,例如写多个WaitForSingleObject来代替WaitForMultipleObjects从而使编程更加灵活。  

      程序运行结果 

         

      

      总结:  

      1. 互斥量与临界区的作用非常相似,但互斥量是可以命名的,也就是说它可以跨越进程使用。所以创建互斥量需要的资源更多,所以如果只为了在进程内部是用的话使用临界区会带来速度上的优势并能够减少资源占用量。因为互斥量是跨进程的互斥量一旦被创建,就可以通过名字打开它。  

      2. 互斥量(Mutex),信号灯(Semaphore),事件(Event)都可以被跨越进程使用来进行同步数据操作,而其他的对象与数据同步操作无关,但对于进程和线程来讲,如果进程和线程在运行状态则为无信号状态,在退出后为有信号状态。所以可以使用WaitForSingleObject来等待进程和线程退出。  

      3. 通过互斥量可以指定资源被独占的方式使用,但如果有下面一种情况通过互斥量就无法处理,比如现在一位用户购买了一份三个并发访问许可的数据库系统,可以根据用户购买的访问许可数量来决定有多少个线程/进程能同时进行数据库操作,这时候如果利用互斥量就没有办法完成这个要求,信号灯对象可以说是一种资源计数器。

     

    展开全文
  • 多线程多进程

    以操作系统的角度述说线程与进程


    进程

    进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。

    进程一般由程序,数据集合和进程控制块三部分组成。程序用于描述进程要完成的功能,是控制进程执行的指令集;数据集合是程序在执行时所需要的数据和工作区;程序控制块(Program Control Block,简称PCB),包含进程的描述信息和控制信息,是进程存在的唯一标志。

    进程具有以下特征:
    动态性:进程是程序的一次执行过程,是临时的,是有生命期的,是动态产生,动态消亡的;
    并发性:任何进程都可以同其他进程一起并发执行;
    独立性:进程是系统进行资源分配和调度的一个独立单位;
    结构性:进程由程序、数据和进程控制块三部分组成


    线程

    在早期的操作系统中并没有线程的概念进程是能拥有资源和独立运行的最小单位,也是程序执行的最小单位。任务调度采用的是时间片轮转的抢占式调度方式,而进程是任务调度的最小单位,每个进程有各自独立的一块内存,使得各个进程之间内存地址相互隔离。
    后来,随着计算机的发展,对CPU的要求越来越高,进程之间的切换开销较大,已经无法满足越来越复杂的程序的要求了。于是就发明了线程,线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。
    一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间)。
    一个标准的线程由线程ID、当前指令指针(PC)、寄存器和堆栈组成。 而进程由内存空间(代码、数据、进程空间、打开的文件)和一个或多个线程组成。


    进程与线程的区别

    • 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;

    • 一个进程可以由一个或多个线程组成,线程是一个进程中代码的不同执行路线;

    • 进程之间相互独立,但同一个进程下的各个线程之间共享程序的内存空间(代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号),某进程内的线程在其他进程不可见;

    • 调度和切换:线程上下文切换比进程上下文切换要快得多

    进程与线程的资源共享关系
    进程与线程的资源共享关系

    单线程与多线程的关系
    单线程与多线程的关系

    总之,线程和进程都是一种抽象的概念,线程是一种比进程更小的抽象,线程和进程都可用于实现并发。

    在早期的操作系统中并没有线程的概念,进程是能拥有资源和独立运行的最小单位,也是程序执行的最小单位。它相当于一个进程里只有一个线程,进程本身就是线程。所以线程有时被称为轻量级进程(Lightweight Process,LWP)。

    多线程与多核

    同一个时间点只有一个任务在执行?不准确,不全面

    多核(心)处理器是指在一个处理器上集成多个运算核心从而提高计算能力,也就是有多个真正并行计算的处理核心,每一个处理核心对应一个内核线程。内核线程(Kernel Thread, KLT)就是直接由操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操作调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。一般一个处理核心对应一个内核线程,比如单核处理器对应一个内核线程,双核处理器对应两个内核线程,四核处理器对应四个内核线程。

    现在的电脑一般是双核四线程、四核八线程,是采用超线程技术将一个物理处理核心模拟成两个逻辑处理核心,对应两个内核线程,所以在操作系统中看到的CPU数量是实际物理CPU数量的两倍,如你的电脑是双核四线程,打开“任务管理器\性能”可以看到4个CPU的监视器,四核八线程可以看到8个CPU的监视器。

    超线程技术就是利用特殊的硬件指令,把一个物理芯片模拟成两个逻辑处理核心,让单个处理器都能使用线程级并行计算,进而兼容多线程操作系统和软件,减少了CPU的闲置时间,提高的CPU的运行效率。这种超线程技术(如双核四线程)由处理器硬件的决定,同时也需要操作系统的支持才能在计算机中表现出来。

    程序一般不会直接去使用内核线程,而是去使用内核线程的一种高级接口——轻量级进程(Light Weight Process,LWP),轻量级进程就是我们通常意义上所讲的线程(我们在这称它为用户线程),由于每个轻量级进程都由一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程。用户线程与内核线程的对应关系有三种模型:一对一模型、多对一模型、多对多模型,在这以4个内核线程、3个用户线程为例对三种模型进行说明。

    • 一对一模型:一个用户线程对应一个内核线程

    对于一对一模型来说,一个用户线程就唯一地对应一个内核线程(反过来不一定成立,一个内核线程不一定有对应的用户线程)。这样,如果CPU没有采用超线程技术(如四核四线程的计算机),一个用户线程就唯一地映射到一个物理CPU的线程,线程之间的并发是真正的并发。一对一模型使用户线程具有与内核线程一样的优点,一个线程因某种原因阻塞时其他线程的执行不受影响;此处,一对一模型也可以让多线程程序在多处理器的系统上有更好的表现。
    但一对一模型也有两个缺点:1.许多操作系统限制了内核线程的数量,因此一对一模型会使用户线程的数量受到限制;2.许多操作系统内核线程调度时,上下文切换的开销较大,导致用户线程的执行效率下降。

    一对一模型

                     一对一模型
    
    • 多对一模型:多个用户线程映射到一个内核线程上

    多对一模型将多个用户线程映射到一个内核线程上,线程之间的切换由用户态的代码来进行,因此相对一对一模型,多对一模型的线程切换速度要快许多;此外,多对一模型对用户线程的数量几乎无限制。但多对一模型也有两个缺点:1.如果其中一个用户线程阻塞,那么其它所有线程都将无法执行,因为此时内核线程也随之阻塞了;2.在多处理器系统上,处理器数量的增加对多对一模型的线程性能不会有明显的增加,因为所有的用户线程都映射到一个处理器上了。

    多对一模型

                    多对一模型
    
    • 多对多模型:多个用户线程映射到多个内核线程上

    多对多模型结合了一对一模型和多对一模型的优点,将多个用户线程映射到多个内核线程上。多对多模型的优点有:1.一个用户线程的阻塞不会导致所有线程的阻塞,因为此时还有别的内核线程被调度来执行;2.多对多模型对用户线程的数量没有限制;3.在多处理器的操作系统中,多对多模型的线程也能得到一定的性能提升,但提升的幅度不如一对一模型的高。
    在现在流行的操作系统中,大都采用多对多的模型。

    多对多模型

                    多对多模型
    

    线程的生命周期

    当线程的数量小于处理器的数量 时,线程的并发是真正的并发,不同的线程运行在不同的处理器上。但当线程的数量大于处理器的数量时,线程的并发会受到一些阻碍,此时并不是真正的并发,因为此时至少有一个处理器会运行多个线程。

    在单个处理器运行多个线程时,并发是一种模拟出来的状态。 操作系统采用时间片轮转的方式轮流执行每一个线程。 现在,几乎所有的现代操作系统采用的都是时间片轮转的抢占式调度方式,如我们熟悉的Unix、Linux、Windows及Mac OS X等流行的操作系统。

    我们知道线程是程序执行的最小单位,也是任务执行的最小单位。在早期只有进程的操作系统中,进程有五种状态,创建、就绪、运行、阻塞(等待)、退出。早期的进程相当于现在的只有单个线程的进程,那么现在的多线程也有五种状态,现在的多线程的生命周期与早期进程的生命周期类似。

    早期进程的生命周期

                早期进程的生命周期
    

    进程在运行过程有三种状态:就绪、运行、阻塞,创建和退出状态描述的是进程的创建过程和退出过程。

    创建:进程正在创建,还不能运行。操作系统在创建进程时要进行的工作包括分配和建立进程控制块表项、建立资源表格并分配资源、加载程序并建立地址空间;

    就绪:时间片已用完,此线程被强制暂停,等待下一个属于他的时间片到来;
    运行:此线程正在执行,正在占用时间片;
    阻塞:也叫等待状态,等待某一事件(如IO或另一个线程)执行完;

    退出:进程已结束,所以也称结束状态,释放操作系统分配的资源。

    线程的生命周期

                    线程的生命周期
    

    创建:一个新的线程被创建,等待该线程被调用执行;

    就绪:时间片已用完,此线程被强制暂停,等待下一个属于他的时间片到来;
    运行:此线程正在执行,正在占用时间片;
    阻塞:也叫等待状态,等待某一事件(如IO或另一个线程)执行完;

    退出:一个线程完成任务或者其他终止条件发生,该线程终止进入退出状态,退出状态释放该线程所分配的资源。


    线程优先级与线程安全


    线程优先级

    现在主流操作系统(如Windows、Linux、Mac OS X)的任务调度除了具有前面提到的时间片轮转的特点外,还有优先级调度(Priority Schedule)的特点。优先级调度决定了线程按照什么顺序轮流执行,在具有优先级调度的系统中,线程拥有各自的线程优先级(Thread Priority)。具有高优先级的线程会更早地执行,而低优先级的线程通常要等没有更高优先级的可执行线程时才会被执行。

    线程的优先级可以由用户手动设置,此外系统也会根据不同情形调整优先级。通常情况下,频繁地进入等待状态(进入等待状态会放弃之前仍可占用的时间份额)的线程(如IO线程),比频繁进行大量计算以至于每次都把所有时间片全部用尽的线程更受操作系统的欢迎。因为频繁进入等待的线程只会占用很少的时间,这样操作系统可以处理更多的任务。我们把频繁等待的线程称之为IO密集型线程(IO Bound Thread),而把很少等待的线程称之为CPU密集型线程(CPU Bound Thread)IO密集型线程总是比CPU密集型线程更容易得到优先级的提升。

    线程饿死:

    在优先级调度下,容易出现一种线程饿死的现象。一个线程饿死是说它的优先级较低,在它执行之前总是有比它优先级更高的线程等待执行,因此这个低优先级的线程始终得不到执行。当CPU密集型的线程优先级较高时,其它低优先级的线程就很可能出现饿死的情况;当IO密集型线程优先级较高时,其它线程相对不容易造成饿死的情况,因为IO线程有大量的等待时间。为了避免线程饿死,调度系统通常会逐步提升那些等待了很久而得不到执行的线程的优先级。这样,一个线程只要它等待了足够长的时间,其优先级总会被提升到可以让它执行的程度,也就是说这种情况下线程始终会得到执行,只是时间的问题。

    在优先级调度环境下,线程优先级的改变有三种方式:
    1. 用户指定优先级;
    2. 根据进入等待状态的频繁程度提升或降低优先级(由操作系统完成);
    3. 长时间得不到执行而被提升优先级。

    线程安全与锁

    在多个线程并发执行访问同一个数据时,如果不采取相应的措施,将会是非常危险的。假设你在工行有一个银行账户,两张银联卡(自己手里一张,女朋友手里一张),里面有100万。假设取钱就两个过程:1.检查账户余额,2.取出现金(如果要取出的金额 < 账户余额,则取现成功,否则取现失败)。有一天你要买房想把钱取出来,而此时你女朋友也想买一辆车(假设你们事先没有商量)。两个人都在取钱,你在A号ATM机取100万,女朋友在B号ATM机取80万。这时A号ATM检查账户余额发现有100万,可以取出;而与此同时,同一时刻B号ATM也在检查账户余额发现有100万,可以取出;这样,A、B都把钱取出来了。

    100万的存款取出180万,银行就亏大发了(当然你就笑呵呵了……)!这就是线程并发的不安全性。为避免这种情况发生,我们要将多个线程对同一数据的访问同步,确保线程安全。

    所谓同步(synchronization)就是指一个线程访问数据时,其它线程不得对同一个数据进行访问,即同一时刻只能有一个线程访问该数据,当这一线程访问结束时其它线程才能对这它进行访问。同步最常见的方式就是使用锁(Lock),也称为线程锁。锁是一种非强制机制,每一个线程在访问数据或资源之前,首先试图获取(Acquire)锁,并在访问结束之后释放(Release)锁。在锁被占用时试图获取锁,线程会进入等待状态,直到锁被释放再次变为可用。

    二元信号量

    二元信号量(Binary Semaphore)是一种最简单的锁,它有两种状态:占用和非占用。 它适合只能被唯一一个线程独占访问的资源。当二元信号量处于非占用状态时,第一个试图获取该二元信号量锁的线程会获得该锁,并将二元信号量锁置为占用状态,之后其它试图获取该二元信号量的线程会进入等待状态,直到该锁被释放。

    信号量

    多元信号量允许多个线程访问同一个资源,多元信号量简称信号量(Semaphore)。它负责协调各个线程,以保证它们能够正确、合理的使用公共资源。对于允许多个线程并发访问的资源,这是一个很好的选择。一个初始值为N的信号量允许N个线程并发访问。
    线程访问资源时首先获取信号量锁,进行如下操作(P操作):
    1. 将信号量的值减1;
    2. 如果信号量的值小于0,则进入等待状态,否则继续执行;
    访问资源结束之后,线程释放信号量锁,进行如下操作(V操作):
    1. 将信号量的值加1;
    2. 如果信号量的值小于1(等于0),唤醒一个等待中的线程;

    信号量机制(pv操作)及三个经典同步问题
    http://blog.csdn.net/speedme/article/details/17597373

    信号量可以分为几类:
    1. 二进制信号量:只允许信号量取0或1值,其同时只能被一个线程获取;
    2. 整型信号量:信号量取值是整数,它可以被多个线程同时获得,直到信号量的值变为0;
    3. 记录型信号量:每个信号量s除了一个整数值value(计数)外,还有一个等待队列List,其中是阻塞在该信号量的各个线程的标识。当信号量被释放一个,值被加一后,系统自动从等待队列中唤醒一个等待中的线程,让其获得信号量,同时信号量再减一。

    互斥量

    互斥量(Mutex)和二元信号量类似,资源仅允许一个线程访问。与二元信号量不同的是,信号量在整个系统中可以被任意线程获取和释放,也就是说,同一个信号量可以由一个线程获取而由另一线程释放。而互斥量则要求哪个线程获取了该互斥量锁就由哪个线程释放,其它线程越俎代庖释放互斥量是无效的。

    使用互斥不仅仅能够在同一应用程序不同线程中实现资源的安全共享,而且可以在不同应用程序的线程之间实现对资源的安全共享。

    互斥量与信号量之间的区别

    1. 互斥量用于线程的互斥,信号线用于线程的同步

    这是互斥量和信号量的根本区别,也就是互斥和同步之间的区别

    互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。

    同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。

    1. 互斥量值只能为0/1,信号量值可以为非负整数

    也就是说,一个互斥量只能用于一个资源的互斥访问,它不能实现多个资源的多线程互斥问题。信号量可以实现多个同类资源的多线程互斥和同步。当信号量为单值信号量是,也可以完成一个资源的互斥访问。

    1. 互斥量的加锁和解锁必须由同一线程分别对应使用,信号量可以由一个线程释放,另一个线程得到

    临界区

    临界区(Critical Section)是一种比互斥量更加严格的同步手段。互斥量和信号量在系统的任何进程都是可见的,也就是说一个进程创建了一个互斥量或信号量,另一进程试图获取该锁是合法的。而临界区的作用范围仅限于本进程,其它的进程无法获取该锁。除此之处,临界区与互斥量的性质相同。

    读写锁

    读写锁(Read-Write Lock)允许多个线程同时对同一个数据进行读操作,而只允许一个线程进行写操作。这是因为读操作不会改变数据的内容,是安全的;而写操作会改变数据的内容,是不安全的。对同一个读写锁,有两种获取方式:共享的(Shared)和独占的(Exclusive)。当锁处于自由状态时,试图以任何一种方式获取锁都能成功,并将锁置为对应的状态;如果锁处于共享状态,其它线程以共享方式获取该锁,仍然能成功,此时该锁分配给了多个线程;如果其它线程试图以独占的方式获取处于共享状态的锁,它必须等待所有线程释放该锁;处于独占状态的锁阻止任何线程获取该锁,不论它们以何种方式。

    获取读写锁的方式总结如下:

    读写锁的状态以共享方式获取以独占方式获取
    自由成功成功
    共享成功等待
    独占等待等待

    总结

    1. 互斥量与临界区的作用非常相似,但互斥量是可以命名的,也就是说它可以跨越进程使用。所以创建互斥量需要的资源更多,所以如果只为了在进程内部是用的话使用临界区会带来速度上的优势并能够减少资源占用量 。因为互斥量是跨进程的互斥量一旦被创建,就可以通过名字打开它。

    2. 互斥量(Mutex),信号灯(Semaphore),事件(Event)都可以被跨越进程使用来进行同步数据操作,而其他的对象与数据同步操作无关,但对于进程和线程来讲,如果进程和线程在运行状态则为无信号状态,在退出后为有信号状态。所以可以使用WaitForSingleObject来等待进程和 线程退出。

    3. 通过互斥量可以指定资源被独占的方式使用,但如果有下面一种情况通过互斥量就无法处理,比如现在一位用户购买了一份三个并发访问许可的数据库系统,可以根据用户购买的访问许可数量来决定有多少个线程/进程能同时进行数据库操作,这时候如果利用互斥量就没有办法完成这个要求,信号灯对象可以说是一种资源计数器

    C++中的多线程
    http://blog.csdn.net/luoweifu/article/details/46835437

    转载整理自:
    原文:
    http://blog.csdn.net/luoweifu/article/details/46595285
    作者:luoweifu

    展开全文
  • 进程线程同步 进程通信

    千次阅读 2011-04-30 16:45:00
    在任意时刻只允许一个线程对共享资源进行访问,如果有多个线程试图访问公共资源,那么在有一个线程进入后,其他试图访问公共资源的线程将被挂起,并一直等到进入临界区的线程离开,临界区在被释

     

    一、进程/线程间同步机制。

    临界区、互斥区、事件、信号量四种方式
    临界区(Critical Section)、互斥量(Mutex)、信号量(Semaphore)、事件(Event)的区别
    1
    、临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。在任意时刻只允许一个线程对共享资源进行访问,如果有多个线程试图访问公共资源,那么在有一个线程进入后,其他试图访问公共资源的线程将被挂起,并一直等到进入临界区的线程离开,临界区在被释放后,其他线程才可以抢占。
    2
    、互斥量:采用互斥对象机制。只有拥有互斥对象的线程才有访问公共资源的权限,因为互斥对象只有一个,所以能保证公共资源不会同时被多个线程访问。互斥不仅能实现同一应用程序的公共资源安全共享,还能实现不同应用程序的公共资源安全共享 .互斥量比临界区复杂。因为使用互斥不仅仅能够在同一应用程序不同线程中实现资源的安全共享,而且可以在不同应用程序的线程之间实现对资源的安全共享。
    3
    、信号量:它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目 .信号量对象对线程的同步方式与前面几种方法不同,信号允许多个线程同时使用共享资源,这与操作系统中的PV操作相同。它指出了同时访问共享资源的线程最大数目。它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。

    PV操作及信号量的概念都是由荷兰科学家E.W.Dijkstra提出的。信号量S是一个整数,S大于等于零时代表可供并发进程使用的资源实体数,但S小于零时则表示正在等待使用共享资源的进程数。
       P操作申请资源:
      (1S1
      (2)若S1后仍大于等于零,则进程继续执行;
      (3)若S1后小于零,则该进程被阻塞后进入与该信号相对应的队列中,然后转入进程调度。
      
      V操作 释放资源:
      (1S1
      (2)若相加结果大于零,则进程继续执行;
      (3)若相加结果小于等于零,则从该信号的等待队列中唤醒一个等待进程,然后再返回原进程继续执行或转入进程调度。
    4
    、事 件: 通过通知操作的方式来保持线程的同步,还可以方便实现对多个线程的优先级比较的操作 .

    总结:
      1.互斥量与临界区的作用非常相似,但互斥量是可以命名的,也就是说它可以跨越进程使用。所以创建互斥量需要的资源更多,所以如果只为了在进程内部是用的话使用临界区会带来速度上的优势并能够减少资源占用量。因为互斥量是跨进程的互斥量一旦被创建,就可以通过名字打开它。
      2.互斥量(Mutex),信号灯(Semaphore),事件(Event)都可以被跨越进程使用来进行同步数据操作,而其他的对象与数据同步操作无关,但对于进程和线程来讲,如果进程和线程在运行状态则为无信号状态,在退出后为有信号状态。所以可以使用WaitForSingleObject来等待进程和线程退出。
      3.通过互斥量可以指定资源被独占的方式使用,但如果有下面一种情况通过互斥量就无法处理,比如现在一位用户购买了一份三个并发访问许可的数据库系统,可以根据用户购买的访问许可数量来决定有多少个线程/进程能同时进行数据库操作,这时候如果利用互斥量就没有办法完成这个要求,信号灯对象可以说是一种资源计数器。

    ___________________________________________________________________________

    线程之间通信的两个基本问题是互斥和同步。

      线程同步是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。

      线程互斥是指对于共享的操作系统资源(指的是广义的"资源",而不是Windows.res文件,譬如全局变量就是一种共享资源),在各线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。

      线程互斥是一种特殊的线程同步。

      实际上,互斥和同步对应着线程间通信发生的两种情况:

      (1)当有多个线程访问共享资源而不使资源被破坏时;

      (2)当一个线程需要将某个任务已经完成的情况通知另外一个或多个线程时。

      在WIN32中,同步机制主要有以下几种:

      (1)事件(Event);

      (2)信号量(semaphore);

      (3)互斥量(mutex);

      (4)临界区(Critical section)

      全局变量

      因为进程中的所有线程均可以访问所有的全局变量,因而全局变量成为Win32多线程通信的最简单方式。例如:

    int var; //全局变量
    UINT ThreadFunction(LPVOIDpParam)
    {
     var = 0;
     while (var < MaxValue)
     {
      //线程处理
      ::InterlockedIncrement(long*) &var);
     }
     return 0;
    }
    请看下列程序:
    int globalFlag = false;
    DWORD WINAPI ThreadFunc(LPVOID n)
    {
     Sleep(2000);
     globalFlag = true;

     return 0;
    }

    int main()
    {
     HANDLE hThrd;
     DWORD threadId;

     hThrd = CreateThread(NULL, 0, ThreadFunc, NULL, 0, &threadId);
     if (hThrd)
     {
      printf("Thread launched/n");
      CloseHandle(hThrd);
     }

     while (!globalFlag)
     ;
     printf("exit/n");
    }


      上述程序中使用全局变量和while循环查询进行线程间同步,实际上,这是一种应该避免的方法,因为:

      (1)当主线程必须使自己与ThreadFunc函数的完成运行实现同步时,它并没有使自己进入睡眠状态。由于主线程没有进入睡眠状态,因此操作系统继续为它调度C P U时间,这就要占用其他线程的宝贵时间周期;

      (2)当主线程的优先级高于执行ThreadFunc函数的线程时,就会发生globalFlag永远不能被赋值为true的情况。因为在这种情况下,系统决不会将任何时间片分配给ThreadFunc线程。

      事件

      事件(Event)WIN32提供的最灵活的线程间同步方式,事件可以处于激发状态(signaled or true)或未激发状态(unsignal or false)。根据状态变迁方式的不同,事件可分为两类:

      (1)手动设置:这种对象只可能用程序手动设置,在需要该事件或者事件发生时,采用SetEventResetEvent来进行设置。

      (2)自动恢复:一旦事件发生并被处理后,自动恢复到没有事件状态,不需要再次设置。

      创建事件的函数原型为:

    HANDLE CreateEvent(
     LPSECURITY_ATTRIBUTES lpEventAttributes,
     // SECURITY_ATTRIBUTES结构指针,可为NULL
     BOOL bManualReset,
     // 手动/自动
     // TRUE:在WaitForSingleObject后必须手动调用ResetEvent清除信号
     // FALSE:在WaitForSingleObject后,系统自动清除事件信号
     BOOL bInitialState, //初始状态
     LPCTSTR lpName //事件的名称
    );


      使用"事件"机制应注意以下事项:

      (1)如果跨进程访问事件,必须对事件命名,在对事件命名的时候,要注意不要与系统命名空间中的其它全局命名对象冲突;

      (2)事件是否要自动恢复;

      (3)事件的初始状态设置。

      由于event对象属于内核对象,故进程B可以调用OpenEvent函数通过对象的名字获得进程Aevent对象的句柄,然后将这个句柄用于ResetEventSetEventWaitForMultipleObjects等函数中。此法可以实现一个进程的线程控制另一进程中线程的运行,例如:

    HANDLE hEvent=OpenEvent(EVENT_ALL_ACCESS,true,"MyEvent");
    ResetEvent(hEvent);

    临界区

      定义临界区变量

    CRITICAL_SECTION gCriticalSection;


      通常情况下,CRITICAL_SECTION结构体应该被定义为全局变量,以便于进程中的所有线程方便地按照变量名来引用该结构体。

      初始化临界区

    VOID WINAPI InitializeCriticalSection(
     LPCRITICAL_SECTION lpCriticalSection
     //指向程序员定义的CRITICAL_SECTION变量
    );


      该函数用于对pcs所指的CRITICAL_SECTION结构体进行初始化。该函数只是设置了一些成员变量,它的运行一般不会失败,因此它采用了VOID类型的返回值。该函数必须在任何线程调用EnterCriticalSection函数之前被调用,如果一个线程试图进入一个未初始化的CRTICAL_SECTION,那么结果将是很难预计的。

      删除临界区

    VOID WINAPI DeleteCriticalSection(
     LPCRITICAL_SECTION lpCriticalSection
     //指向一个不再需要的CRITICAL_SECTION变量
    );


      进入临界区

    VOID WINAPI EnterCriticalSection(
     LPCRITICAL_SECTION lpCriticalSection
     //指向一个你即将锁定的CRITICAL_SECTION变量
    );


      离开临界区

    VOID WINAPI LeaveCriticalSection(
     LPCRITICAL_SECTION lpCriticalSection
     //指向一个你即将离开的CRITICAL_SECTION变量
    );


      使用临界区编程的一般方法是:

    void UpdateData()
    {
     EnterCriticalSection(&gCriticalSection);
     ...//do something
     LeaveCriticalSection(&gCriticalSection);
    }


      关于临界区的使用,有下列注意点:

      (1)每个共享资源使用一个CRITICAL_SECTION变量;

      (2)不要长时间运行关键代码段,当一个关键代码段长时间运行时,其他线程就会进入等待状态,这会降低应用程序的运行性能;

      (3)如果需要同时访问多个资源,则可能连续调用EnterCriticalSection

      (4Critical Section不是OS核心对象,如果进入临界区的线程""了,将无法释放临界资源。这个缺点在Mutex中得到了弥补。

      互斥

      互斥量的作用是保证每次只能有一个线程获得互斥量而得以继续执行,使用CreateMutex函数创建:

    HANDLE CreateMutex(
     LPSECURITY_ATTRIBUTES lpMutexAttributes,
     // 安全属性结构指针,可为NULL
     BOOL bInitialOwner,
     //是否占有该互斥量,TRUE:占有,FALSE:不占有
     LPCTSTR lpName
     //信号量的名称
    );


      Mutex是核心对象,可以跨进程访问,下面的代码给出了从另一进程访问命名Mutex的例子:

    HANDLE hMutex;
    hMutex = OpenMutex(MUTEX_ALL_ACCESS, FALSE, L"mutexName");
    if (hMutex){
     

    else{
     
    }


      相关API

    BOOL WINAPI ReleaseMutex(
     HANDLE hMutex
    );


      使用互斥编程的一般方法是:

    void UpdateResource()
    {
     WaitForSingleObject(hMutex,…);
     ...//do something
     ReleaseMutex(hMutex);
    }


      互斥(mutex)内核对象能够确保线程拥有对单个资源的互斥访问权。互斥对象的行为特性与临界区相同,但是互斥对象属于内核对象,而临界区则属于用户方式对象,因此这导致mutexCritical Section的如下不同:

      (1) 互斥对象的运行速度比关键代码段要慢;

      (2) 不同进程中的多个线程能够访问单个互斥对象;

      (3) 线程在等待访问资源时可以设定一个超时值。

      

    信号量

      信号量是维护0到指定最大值之间的同步对象。信号量状态在其计数大于0时是有信号的,而其计数是0时是无信号的。信号量对象在控制上可以支持有限数量共享资源的访问。

      信号量的特点和用途可用下列几句话定义:

      (1)如果当前资源的数量大于0,则信号量有效;

      (2)如果当前资源数量是0,则信号量无效;

      (3)系统决不允许当前资源的数量为负值;

      (4)当前资源数量决不能大于最大资源数量。

      创建信号量

    HANDLE CreateSemaphore (
     PSECURITY_ATTRIBUTE psa,
     LONG lInitialCount, //开始时可供使用的资源数
     LONG lMaximumCount, //最大资源数
    PCTSTR pszName);


      释放信号量

      通过调用ReleaseSemaphore函数,线程就能够对信标的当前资源数量进行递增,该函数原型为:

    BOOL WINAPI ReleaseSemaphore(
     HANDLE hSemaphore,
     LONG lReleaseCount, //信号量的当前资源数增加lReleaseCount
     LPLONG lpPreviousCount
    );


      打开信号量

      和其他核心对象一样,信号量也可以通过名字跨进程访问,打开信号量的API为:

    HANDLE OpenSemaphore (
     DWORD fdwAccess,
     BOOL bInherithandle,
     PCTSTR pszName
    );


      互锁访问

      当必须以原子操作方式来修改单个值时,互锁访问函数是相当有用的。所谓原子访问,是指线程在访问资源时能够确保所有其他线程都不在同一时间内访问相同的资源。

      请看下列代码:

    int globalVar = 0;

    DWORD WINAPI ThreadFunc1(LPVOID n)
    {
     globalVar++;
     return 0;
    }
    DWORD WINAPI ThreadFunc2(LPVOID n)
    {
     globalVar++;
     return 0;
    }


      运行ThreadFunc1ThreadFunc2线程,结果是不可预料的,因为globalVar++并不对应着一条机器指令,我们看看globalVar++的反汇编代码:

    00401038 mov eax,[globalVar (0042d3f0)]
    0040103D add eax,1
    00401040 mov [globalVar (0042d3f0)],eax


      在"mov eax,[globalVar (0042d3f0)]" 指令与"add eax,1" 指令以及"add eax,1" 指令与"mov [globalVar (0042d3f0)],eax"指令之间都可能发生线程切换,使得程序的执行后globalVar的结果不能确定。我们可以使用InterlockedExchangeAdd函数解决这个问题:

    int globalVar = 0;

    DWORD WINAPI ThreadFunc1(LPVOID n)
    {
     InterlockedExchangeAdd(&globalVar,1);
     return 0;
    }
    DWORD WINAPI ThreadFunc2(LPVOID n)
    {
     InterlockedExchangeAdd(&globalVar,1);
     return 0;
    }


      InterlockedExchangeAdd保证对变量globalVar的访问具有"原子性"。互锁访问的控制速度非常快,调用一个互锁函数的CPU周期通常小于50,不需要进行用户方式与内核方式的切换(该切换通常需要运行1000CPU周期)。

      互锁访问函数的缺点在于其只能对单一变量进行原子访问,如果要访问的资源比较复杂,仍要使用临界区或互斥。

      可等待定时器

      可等待定时器是在某个时间或按规定的间隔时间发出自己的信号通知的内核对象。它们通常用来在某个时间执行某个操作。

      创建可等待定时器

    HANDLE CreateWaitableTimer(
     PSECURITY_ATTRISUTES psa,
     BOOL fManualReset,//人工重置或自动重置定时器
    PCTSTR pszName);


      设置可等待定时器

      可等待定时器对象在非激活状态下被创建,程序员应调用 SetWaitableTimer函数来界定定时器在何时被激活:

    BOOL SetWaitableTimer(
     HANDLE hTimer, //要设置的定时器
     const LARGE_INTEGER *pDueTime, //指明定时器第一次激活的时间
     LONG lPeriod, //指明此后定时器应该间隔多长时间激活一次
     PTIMERAPCROUTINE pfnCompletionRoutine,
     PVOID PvArgToCompletionRoutine,
    BOOL fResume);


      取消可等待定时器

    BOOl Cancel WaitableTimer(
     HANDLE hTimer //要取消的定时器
    );


      打开可等待定时器

      作为一种内核对象,WaitableTimer也可以被其他进程以名字打开:

    HANDLE OpenWaitableTimer (
     DWORD fdwAccess,
     BOOL bInherithandle,
     PCTSTR pszName
    );


      实例

      下面给出的一个程序可能发生死锁现象:

    #include <windows.h>
    #include <stdio.h>
    CRITICAL_SECTION cs1, cs2;
    long WINAPI ThreadFn(long);
    main()
    {
     long iThreadID;
     InitializeCriticalSection(&cs1);
     InitializeCriticalSection(&cs2);
     CloseHandle(CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadFn, NULL, 0,&iThreadID));
     while (TRUE)
     {
      EnterCriticalSection(&cs1);
      printf("/n线程1占用临界区1");
      EnterCriticalSection(&cs2);
      printf("/n线程1占用临界区2");

      printf("/n线程1占用两个临界区");

      LeaveCriticalSection(&cs2);
      LeaveCriticalSection(&cs1);

      printf("/n线程1释放两个临界区");
      Sleep(20);
     };
     return (0);
    }

    long WINAPI ThreadFn(long lParam)
    {
     while (TRUE)
     {
      EnterCriticalSection(&cs2);
      printf("/n线程2占用临界区2");
      EnterCriticalSection(&cs1);
      printf("/n线程2占用临界区1");

      printf("/n线程2占用两个临界区");

      LeaveCriticalSection(&cs1);
      LeaveCriticalSection(&cs2);

      printf("/n线程2释放两个临界区");
      Sleep(20);
     };
    }


      运行这个程序,在中途一旦发生这样的输出:

      线程1占用临界区1

      线程2占用临界区2

      或

      线程2占用临界区2

      线程1占用临界区1

      或

      线程1占用临界区2

      线程2占用临界区1

      或

      线程2占用临界区1

      线程1占用临界区2

      程序就""掉了,再也运行不下去。因为这样的输出,意味着两个线程相互等待对方释放临界区,也即出现了死锁。

      如果我们将线程2的控制函数改为:

    long WINAPI ThreadFn(long lParam)
    {
     while (TRUE)
     {
      EnterCriticalSection(&cs1);
      printf("/n线程2占用临界区1");
      EnterCriticalSection(&cs2);
      printf("/n线程2占用临界区2");

      printf("/n线程2占用两个临界区");

      LeaveCriticalSection(&cs1);
      LeaveCriticalSection(&cs2);

      printf("/n线程2释放两个临界区");
      Sleep(20);
     };
    }


      再次运行程序,死锁被消除,程序不再挡掉。这是因为我们改变了线程2中获得临界区12的顺序,消除了线程12相互等待资源的可能性。

      由此我们得出结论,在使用线程间的同步机制时,要特别留心死锁的发生。

    ____________________________________________________________

    二、进程间通信方式

    线程间通信:由于多线程共享地址空间和数据空间,所以多个线程间的通信是一个线程的数据可以直接提供给其他线程使用,而不必通过操作系统(也就是内核的调度)。

             进程间的通信则不同,它的数据空间的独立性决定了它的通信相对比较复杂,需要通过操作系统。以前进程间的通信只能是单机版的,现在操作系统都继承了基于套 接字(socket)的进程间的通信机制。这样进程间的通信就不局限于单台计算机了,实现了网络通信。

            进程的通信机制主要有:管道、有名管道、消息队列、信号量、共享空间、信号、套接字。

            管道: 它传递数据是单向性的,只能从一方流向另一方,也就是一种半双工的通信方式;只用于有亲缘关系的进程间的通信,亲缘关系也就是父子进程或兄弟进程;没有名 字并且大小受限,传输的是无格式的流,所以两进程通信时必须约定好数据通信的格式。管道它就像一个特殊的文件,但这个文件之存在于内存中,在创建管道时, 系统为管道分配了一个页面作为数据缓冲区,进程对这个数据缓冲区进行读写,以此来完成通信。其中一个进程只能读一个只能写,所以叫半双工通信,为什么一个 只能读一个只能写呢?因为写进程是在缓冲区的末尾写入,读进程是在缓冲区的头部读取,他们各自 的数据结构不同,所以功能不同。

            有名管道: 看见这个名字就能知道个大概了,它于管道的不同的是它有名字了。这就不同与管道只能在具有亲缘关系的进程间通信了。它提供了一个路径名与之关联,有了自己 的传输格式。有名管道和管道的不同之处还有一点是,有名管道是个设备文件,存储在文件系统中,没有亲缘关系的进程也可以访问,但是它要按照先进先出的原则 读取数据。同样也是单双工的。

            消息队列:是存放在内核中的消息链表,每个消息队列由消息队列标识符标识,于管道不同的是,消息队列存放在内核中,只有在内核重启时才能删除一个消息队列,内核重启也就是系统重启,同样消息队列的大小也是受限制的。

            信号量: 也可以说是一个计数器,常用来处理进程或线程同步的问题,特别是对临界资源的访问同步问题。临界资源:为某一时刻只能由一个进程或线程操作的资源,当信号 量的值大于或等于0时,表示可以供并发进程访问的临界资源数,当小于0时,表示正在等待使用临界资源的进程数。更重要的是,信号量的值仅能由PV操作来改 变。

            共享内存: 就是分配一块能被其他进程访问的内存。共享内存可以说是最有用的进程间通信方式,也是最快的IPC形式。首先说下在使用共享内存区前,必须通过系统函数将 其附加到进程的地址空间或说为映射到进程空间。两个不同进程A、B共享内存的意思是,同一块物理内存被映射到 进程A、B各自的进程地址空间。进程A可以即时看到进程B对共享内存中数据的更新,反之亦然。由于多个进程共享同一块内存区域,必然需要某种同步机制,互 斥锁和信号量都可以。采用共享内存通信的一个显而易 见的好处是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝。对于像管道和消息队列等通信方式,则需要在内核和用户空间进行四次的数据拷贝,而 共享内存则只拷贝两次数据[1]:一次从输入文件到共享内存区,另一次从共享内存区到输出文件。实际上,进程之间在共享内存时,并不总是读写少量数据后就 解除映射,有新的通信时,再重新建立共享内存区域。而是保持共享区域,直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存 中的内容往往是在解除映射时才写回文件的。因此,采用共享内存的通信方式效率是非常高的。

             信号: 信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是异步的,一个进程不必通过任何 操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。信号是进程间通信机制中唯一的异步通信机制,可以看作是异步通知,通知接收信号的进程 有哪些事情发生了。信号机制经过POSIX实时扩展后,功能更加强大,除了基本通知功能外,还可以传递附加信息。信号事件的发生有两个来源:硬件来源(比 如我们按下了键盘或者其它硬件故障);软件来源。信号分为可靠信号和不可靠信号,实时信号和非实时信号。进程有三种方式响应信号1.忽略信号2.捕捉信号 3.执行缺省操作。

            套接字:这一块在网络编程那一块讲的 很多,在此就不在说拉。

    _____________________________________________________

    三、进程/线程同步机制与进程间通信机制比较

    很明显2者有类似,但是差别很大

    同步主要是临界区、互斥、信号量、事件

    进程间通信是管道、内存共享、消息队列、信号量、socket

    共通之处是,信号量和消息(事件)

     

     

    展开全文
  • 作为开发,日志组件是软件构成里面必不可少的一个模块,不管软件大小,日志都可以作为发现和解决问题的一个手段,很重要。在.Net 系里面log4Net是其中的一个不错的日志组件。 log4Net的优点有不少,例如配置简单灵活...
  • 进程的同步与通信,进程线程同步的区别,进程线程通信的区别 进程同步与互斥的区别? 进程的同步方式有哪些? 进程的通信方式有哪些? 进程同步与通信的区别是什么? 线程的同步/通信与进程的同步/通信有...
  • 进程线程

    2017-03-07 15:25:05
    在我们开始讨论线程进程,时间片和所有其他精彩的“调度概念”之前, 让我们建立一个类比。 我想先做的是说明线程进程如何工作。我能想到的最好的方法(缺乏对实时系统的设计的挖掘)是想象我们的线程进程在...
  • 使用场所:目标子动能交互少,如果资源和性能许可,可以设计由多个子应用程序来组合完成目的。 2. 多线程  优点:提高系统的并行性,并且开销小。数据共享方便(不需要进程间的通信) 缺点
  • 进程线程的定义及区别, 进程的概念进程是在道程序系统出现以后,为了描述系统内部各作业的活动规律而引进的概念。由 于多道程序系统所带来的复杂环境,程序本身有了并行性【为了充分利用资源,在主存中同时...
  • 该书中第11章是写web服务器的搭建,无奈对web还比较陌生。还没有搞明白。  这些所谓的并发,其实都是操作系统做的事情,比如,...用这种方法,每个逻辑控制流都是一个进程,由内核来调度和维护。因为进程有独立的
  • 进程是正在运行的程序的实例,其实就是系统中正在运行的一个应用程序,线程进程一个实体。进程包含线程线程共用进程的资源。 进程线程的选择取决以下几点: 1、需要频繁创建销毁的优先使用线程;因为对...
  • Win32多线程之为什么不使用多进程

    千次阅读 2013-11-03 12:10:22
    人们一旦接触到线程,他们最常问的问题就是:有什么是线程能够给我而进程所不能给我的?... 如果使用多重进程,最困难的问题大概是:如何把窗口的handle交给另一个进程,在Win32中,handle只在其诞生地
  • 多进程通信方式 文件映射:本地之间 共享内存:本地之间 匿名管道:本地之间 命名管道:跨服务器 邮件槽:的传输数据,通常通过网络向台Windows机器传输 剪切板:本地之间 socket:跨服务器 ...
  • 进程线程相关

    2013-04-07 14:26:00
    1 定义及区别 线程的划分尺度小于进程,因此线程的并发性高。进程在执行中拥有独立的内存单元,而同一个进程线程共享内存,从而极大的...多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但是操
  • 线程进程通信

    千次阅读 2012-09-20 15:50:22
    进程/线程间同步机制。 ...临界区、互斥区、事件、信号量四种方式 ...临界区(Critical Section)、互斥量(Mutex)、信号量(Semaphore)...1、临界区:通过对多线程的串行化来访问公共资源或段代码,速度快,
  • 进程的出现: (复习功课嘛,就把相关的内容都看看了,哎,都忘的差不多了) ...并行性是指两个或者多个事件在同一时刻发生,这是一个具有微观意义的概念,即在物理上这些事件是同时发生的;而并发性是指两个
  • 四种进程线程同步机制

    千次阅读 2011-10-06 00:15:32
    现在流行的进程线程同步互斥的控制机制,其实是由最原始最基本的4种方法实现的: 1.临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。 2.互斥量:为协调共同对一个共享资源的...
  • 一、进程/线程间同步机制。 临界区、互斥区、事件、信号量四种方式 ...在任意时刻只允许一个线程对共享资源进行访问,如果有多个线程试图访问公共资源,那么在有一个线程进入后,其他试图访问公共资源的
  • 进程线程、应用程序域关系

    千次阅读 2011-12-22 23:31:30
    个进程从主线程的执行开始进而创建一个多个附加线程,就是所谓基于多线程的多任务。  那进程与线程的区别到底是什么?进程是执行程序的实例。例如,当你运行记事本程序(Nodepad)时,你就创建了
  • dpdk多线程模型

    千次阅读 2019-09-07 15:16:52
    一个cpu上可以运行多个线程, 由linux内核来调度各个线程的执行。内核在调度线程时,会进行上下文切换,保存线程的堆栈等信息, 以便这个线程下次再被调度执行时,继续从指定的位置开始执行。然而上下文切换是需要...
  • 进程线程关系

    2015-05-07 21:40:04
    一、 进程的概念  进程是在道程序系统出现以后,为了描述系统内部各作业的活动规律而引进的概念。 由 于多道程序系统所带来的复杂环境,...一个是通过中间媒介——资源发生的间接制约关系,一个是各并行程序
  • 管道,信号量,共享内存...阿门,看来我是第一个吃螃蟹的人。  由于线程又称轻量级别的进程,属于广义进程范围。最显著的特征是线程可以通过所属的线程共享资源和全局变量;进程间不能共享全局变量。  进程线程
  • 1.在Windows编程中互斥量与临界区比较类似,请分析一下二者的主要区别。... 互斥量是内核对象,因此还可以用于不同进程中子线程对资源的互斥访问。  互斥量可以很好的解决由于线程意外终止资源无法释放的问题。
  • 进程间通信与线程间通信

    千次阅读 2017-10-27 17:15:56
    序 今天被问及进程间通信的问题,发现自己了解的并不够,所以,对此好好总结一番~ 操作系统的主要任务是管理计算机的软件、硬件资源。...因此多进程多线程间为了完成一定的任务,就需要进行一定

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 19,691
精华内容 7,876
关键字:

一个线程可以跨越多个进程