精华内容
下载资源
问答
  • 多线程私有资源和共享资源

    千次阅读 2014-03-18 20:34:08
    一、线程概念  线程是操作系统能够进行运算调度的最小单位。...在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多内核线程(kernel thread),而把用户线程(use
    一、线程概念
            线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix  System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。

    为什么对于多数合作性任务,多线程比多个独立的进程更优越呢?
             线程共享相同的内存空间。不同的线程可以存取内存中的同一个变量。所以,程序中的所有线程都可以读或写声明过的全局变量。如果曾用fork()编写过重要代码,就会认识到这个工具的重要性。为什么呢?虽然fork()允许创建多个进程,但它还会带来以下通信问题,如何让多个进程相互通信,这里每个进程都有自己独立的内存空间。对这个问题没有一个简单的答案。虽然有许多不通种类的本地IPC(进程间通信),但他们都遇到两个重要的障碍:

    1.加强了某种形式的额外内核开销,从而降低性能。
    2.对于大多数情形,IPC不是对于代码的“自然”扩展,通常极大地增加了程序的复杂性。
    •  线程的共享资源和私有资源:

    线程共享的环境包括:进程代码段、进程的公有数据(利用这些共享的数据,线程很容易的实现相互之间的通讯)、进程打开的文件描述符、信号的处理器、进程的当前目录和进程用户ID与进程组ID。     进程拥有这许多共性的同时,还拥有自己的个性。有了这些个性,线程才能实现并发性。这些个性包括:

    1.线程ID

    每个线程都有自己的线程ID,这个ID在本进程中是唯一的。进程用此来标   识线程。   

    2.寄存器组的值

    由于线程间是并发运行的,每个线程有自己不同的运行线索,当从一个线   程切换到另一个线程上时,必须将原有的线程的寄存器集合的状态保存,以便   将来该线程在被重新切换到时能得以恢复。   

               3. 线程的堆栈

    堆栈是保证线程独立运行所必须的。

    线程函数可以调用函数,而被调用函数中又是可以层层嵌套的,所以线程     必须拥有自己的函数堆栈,使得函数调用可以正常执行,不受其他线程的影     响。

    4.错误返回码

    由于同一个进程中有很多个线程在同时运行,可能某个线程进行系统调用   后设置了errno值,而在该线程还没有处理这个错误,另外一个线程就在此时   被调度器投入运行,这样错误值就有可能被修改。

    所以,不同的线程应该拥有自己的错误返回码变量。

    5.线程的信号屏蔽码

    由于每个线程所感兴趣的信号不同,所以线程的信号屏蔽码应该由线程自   己管理。但所有的线程都共享同样的信号处理器。

    6.线程的优先级

    由于线程需要像进程那样能够被调度,那么就必须要有可供调度使用的参   数,这个参数就是线程的优先级。

    1、引入pthread_equal的原因:

           在线程中,线程ID的类型是pthread_t类型,由于在Linux下线程采用POSIX标准,所以,在不同的系统下,pthread_t的类型是不同的,比如在ubuntn下,是unsigned long类型,而在solaris系统中,是unsigned int类型。而在FreeBSD上才用的是结构题指针。 所以不能直接使用==判读,而应该使用pthread_equal来判断。

     

    2、引入pthread_self的原因:

            在使用pthread_create(pthread_t *thread_id,NULL,void* (*fun) (void *),void * args);虽然第一个参数中已经保存了线程ID,但是,前提是主线程首先执行时,才能实现的,而如果不是,那么thread指向一个未出划的变量。那么才子线程想使用时,应该使用pthread_self();


    二、线程的实行和创建

           Linux线程的实现包括在用户级实现和核心级实现。在用户级实现线程时,没有核心支持的多线程进程。因此,核心只有单线程进程的概念,而多线程进程由与应用程序连接的过程库实现。核心不知道线程的存在也就不能独立的调度这些线程了。线程的调度由一个线程运行库组织。如果一个线程调用了一个阻塞的系统调用,进程可能被阻塞,当然其中的所有线程也同时被阻塞,所以UNIX所有了异步I/O工具。这种机制的最大缺点是不能发挥多处理器的优势。它的优点是系统的消耗小,可以修改以适应特殊应用场合。而在核心级实现线程时,允许不同进程里的线程按照同一相对优先方法凋度,这适合于发挥多处理器的并发优点。目前Linux众多的线程库中大部分实现的是用户级的线程,只有一些用于研究的线程库才尝试实现核心级进程

           LinuxThread线程库采用称为1-1模型:每个线程实际上在核心是一个个单独的进程,核心的调度程序负责线程的调度,就像调度普通进程。线程是用系统调用clone()创建的,clone()系统调用是fork()的普通形式,它允许新进程共享父进程的存储空间,文件描述符和软中断处理程序。这种模型的优点是最小限度地依赖CPU级多处理技术(每个CPU一个线程),以及最小限度地使用I/O操作。

         系统创建线程是一个复杂的工作,当一个进程启动后,它会自动创建一个线程即主线程(main thread)或者初始化线程(initial thread),然后就利用Pthread_initialize() 初始化系统管理线程并且启动线程机制,这时完成的工作是建立管理线程堆栈以及建立管理线程通信的管道。线程机制启动后,要创建线程必须让Pthread_creat()向管理线程发送请求。管理线程接到请求后首先检查是否需要调整调度策略,如果需要,判断是否能够做到。接着为线程找出一个空段,并且根据需要再分配堆栈。接下来,分配新线程的标识符,处始化新线程描述符,确定线程的调度参数,并根据需要调整管理线程的优先级。最后便创建线程。  新线程创建以后,管理线程会将其插入系统活动线程的双向链表中。

           Linux支持若干线程库,它们有些遵循Posix标准,有些不遵循。最常用的线程库是LinuxThreads,它是一个面向Linux的Posix 1003.1c pthread标准接口。此线程库实现时使用了两个信号SIGUSR1和SIGUSR2,因此用户不能使用它们。此线程库将线程分配在高端存储空间,在初始化进程的堆栈下2M处。她采用按需增长的策略,所以初始化时不会使用很多虚拟空间(现在是4k),如果需要可以增长到2M。为每个线程保留这么大的地址空间意味着,在32为体系结构下,不能有超过大约1000个线程共存,因为这是合理的,每个线程使用核心的进程表的一项,而它通常限制为512项


    三、函数原型

    就像每个进程有一个进程ID一样,每个线程也有一个线程ID。进程ID在整个系统中是唯一的,但线程ID不同,线程ID只在它所属的进程环境中有效。

     进程ID用pid_t数据类型来表示,是一个非负整数。线程ID    则用pthread_t数据类型来表示,实现的时候可以用一个结构来代表pthread数据类型,所以可移植性的操作系统不能把它作为整数处理。因此必须使用函数来对两个线程ID进行比较。

    pthread_t pthread_self(void)

    功能:获得本线程ID

    返回值:返回本线程的标识符。此函数总是成功


    int pthread_equal(pthread_t thread1, pthread_t thread2)

    功能:判断两个线程描述符是否相等,在LinuxThreads中,线程ID相同的线程必然是同一个线程

    参数: thread1:线程ID1

                thread2:线程ID2

    返回值:如果相等,则返回非0值,否则返回0


    (1)线程的创建和终止:

    int pthread_create(pthread_t  *thread, const pthread_attr_t  *attr,void *(*start_routine) (void *), void  *arg);

             功能:创建一个新的线程

              参数:

                       thread  : 指向的内存单元存放新创建线程的线程ID
            attr:用于定制各种不同的线程属性。(详细见线程属性)
            start_routine:新建的 线程从此函数地址开始运行。
            arg:作为start_routine函数的参数。 如果需要向   start_routine   函数传递的参数不止一个,那么需要把这些参数放到一个结构中,然后把这个结构的地址作为arg的参数传入

            返回值:若成功则返回0,否则返回出错编号

    注:另外,在编译时注意加上-lpthread参数,以调用链接库。因为pthread并非Linux系统的默认库,而是posix线程库,在Linux中将其作为一个库来使用,因此加上 -lpthread(或-pthread)以显示的链接该库。函数在执行错误时的错误信息将作为返回值返回,并不修改系统全局变量errno,当然也无法使用perror()打印错误信息。



    void  pthread_exit(void  *retval)

    功能:使用函数pthread_exit退出线程,这是线程的主动行为;由于一个进程中的多个线程是共享数据段的,因此通常在线程退出之后,退出线程所占用的资源并不会随着线程的终止而得到释放,但是可以用pthread_join()函数来同步并释放资源。

              参数:retval:pthread_exit()调用线程的返回值,可由其他函数如pthread_join来获取。

              注:exit是退出整个进程,而pthread_exit是退出当前线程


     int pthread_join(pthread_t thread, void **retval);

    功能:以阻塞的方式等待thread指定的线程结束。当函数返回时,被等待线程的资源被收回。如果进程已经结束,那么该函数会立即返回。并且thread指定的线程必须是joinable(可结合)的。

    参数:

    thread: 线程标识符,即线程ID,标识唯一线程。
              retval: 用户定义的指针,用来存储被等待线程的返回值。
    返回值 : 0代表成功。 失败,返回的则是错误号。 对分离状态的线程进行 pthread_join的调用会产生失败。返回EINVAL。

    注:线程的分离状态: 

         在任何一个时间点上,线程是可结合的(joinable),或者是分离的(detached)。一个可结合的线程能够被其他线程收回其资源和杀死;在被其他线程回收之前,它的存储器资源(如栈)是不释放的。相反,一个分离的线程是不能被其他线程回收或杀死的,它的存储器资源在它终止时由系统自动释放。

         线程的分离状态决定一个线程以什么样的方式来终止自己。在默认情况下线程是非分离状态的,这种情况下,原有的线程等待创建的线程结束。只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。而分离线程不是这样子的,它没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。程序员应该根据自己的需要,选择适当的分离状态。所以如果我们在创建线程时就知道不需要了解线程的终止状态,则可以pthread_attr_t结构中的detachstate线程属性,让线程以分离状态启动。


     int pthread_detach(pthread_t thread);

         功能:使线程进入分离状态

         参数: thread:要分离的线程ID

         返回值:  若成功则返回0,若出错则为非零。


    (2)线程的取消


    int pthread_cancel(pthread_t thread)

            功能:请求取消统一进程中的其他线程

            参数:thread:要取消的线程ID

            返回值:如果成功则返回0,否则返回错误编号

    注:线程取消的方法是向目标线程发Cancel信号,但如何处理Cancel信号则由目标线程自己决定,或者忽略、或者立即终止、或者继续运行至Cancelation-point(取消点),由不同的Cancelation状态决定。

    pthread_cancle只是向目标线程发送一个取消请求,之后函数立即返回。而目标线程在取消请求发出以后还是继续运行,直到线程到达某个取消点。取消点是线程检查自身是否被取消,并按照可取消状态进行动作的一个位置。

    下表是一些取消点


    线程有两个属性并没有包含在pthread_attr_t结构中,它们是可取消状态和可取消类型。这两个属性影响着线程在响应pthread_cancel函数调用时所呈现的行为。

    可取消状态:线程启动时的默认可取消状态是PTHREAD_CANCEL_ENABLE。 PTHREAD_CANCEL_ENABLE:表示允许取消。PTHREAD_CANCEL_DISABLE:即不允许取消(收到的取消请求将被忽略)

    int pthread_setcancelstate(int state, int *oldstate)

             功能:设置取消状态,即设置本线程对Cancel信号的反应

             参数:state: PTHREAD_CANCEL_ENABLE或者PTHREAD_CANCEL_DISABLE

                          oldstate:如果不为 NULL则存入原来的Cancel状态以便恢复。

             返回值:成功返回0,错误返回错误编号


    可取消类型:分为两种,PTHREAD_CANCEL_DEFFERED:即延迟取消,在线程到达真正的取消点之前,是不会出现真正的取消。PTHREAD_CANCEL_ASYCHRONOUS:立即执行取消动作(退出)。仅当可取消状态状态为Enable时有效。

    int pthread_setcanceltype(int type, int *oldtype)

              功能:设置取消类型。

              参数:PTHREAD_CANCEL_DEFFERED或PTHREAD_CANCEL_ASYCHRONOUS

              返回值:成功返回0,错误返回错误编号


    如果应用程序在很长一段时间内都不会调用到取消点的函数。则可以使用 pthread_testcancel在程序中自己添加取消点。

    void pthread_testcancel(void)

    功能:调用此函数时,如果有某个取消请求正处于未决状态(即取消请求还未被响应),而且可取消状态并没有被设置为无效,那么线程就会被取消。但是如果可取消状态被设置为无效,则此函数调用没有任何效果。


    展开全文
  • 线程间到底共享哪些进程资源

    千次阅读 2020-12-18 14:10:00
    到底线程之间共享了哪些进程资源,共享资源意味着什么?共享资源这种机制是如何实现的?对此如果你没有答案的话,那么这意味着你几乎很难写出能正确工作的多线程程序,同时也意味着这篇文章就是为你准备的。 逆向...

    进程和线程这两个话题是程序员绕不开的,操作系统提供的这两个抽象概念实在是太重要了。

    关于进程和线程有一个极其经典的问题,那就是进程和线程的区别是什么?相信很多同学对答案似懂非懂。

    记住了不一定真懂

    关于这个问题有的同学可能已经“背得”滚瓜烂熟了:“进程是操作系统分配资源的单位,线程是调度的基本单位,线程之间共享进程资源”。

    可是你真的理解了上面最后一句话吗?到底线程之间共享了哪些进程资源,共享资源意味着什么?共享资源这种机制是如何实现的?对此如果你没有答案的话,那么这意味着你几乎很难写出能正确工作的多线程程序,同时也意味着这篇文章就是为你准备的。

    逆向思考

    查理芒格经常说这样一句话:“反过来想,总是反过来想”,如果你对线程之间共享了哪些进程资源这个问题想不清楚的话那么也可以反过来思考,那就是有哪些资源是线程私有的

     

    线程私有资源

    线程运行的本质其实就是函数的执行,函数的执行总会有一个源头,这个源头就是所谓的入口函数,CPU从入口函数开始执行从而形成一个执行流,只不过我们人为的给执行流起一个名字,这个名字就叫线程。

    既然线程运行的本质就是函数的执行,那么函数执行都有哪些信息呢?

    在《函数运行时在内存中是什么样子》这篇文章中我们说过,函数运行时的信息保存在栈帧中,栈帧中保存了函数的返回值、调用其它函数的参数、该函数使用的局部变量以及该函数使用的寄存器信息,如图所示,假设函数A调用函数B:

    此外,CPU执行指令的信息保存在一个叫做程序计数器的寄存器中,通过这个寄存器我们就知道接下来要执行哪一条指令。由于操作系统随时可以暂停线程的运行,因此我们保存以及恢复程序计数器中的值就能知道线程是从哪里暂停的以及该从哪里继续运行了。

    由于线程运行的本质就是函数运行,函数运行时信息是保存在栈帧中的,因此每个线程都有自己独立的、私有的栈区。

    同时函数运行时需要额外的寄存器来保存一些信息,像部分局部变量之类,这些寄存器也是线程私有的,一个线程不可能访问到另一个线程的这类寄存器信息

    从上面的讨论中我们知道,到目前为止,所属线程的栈区、程序计数器、栈指针以及函数运行使用的寄存器是线程私有的。

    以上这些信息有一个统一的名字,就是线程上下文,thread context。

    我们也说过操作系统调度线程需要随时中断线程的运行并且需要线程被暂停后可以继续运行,操作系统之所以能实现这一点,依靠的就是线程上下文信息。

    现在你应该知道哪些是线程私有的了吧。

    除此之外,剩下的都是线程间共享资源。

    那么剩下的还有什么呢?还有图中的这些。

    这其实就是进程地址空间的样子,也就是说线程共享进程地址空间中除线程上下文信息中的所有内容,意思就是说线程可以直接读取这些内容。

    接下来我们分别来看一下这些区域。

     

    代码区

    进程地址空间中的代码区,这里保存的是什么呢?从名字中有的同学可能已经猜到了,没错,这里保存的就是我们写的代码,更准确的是编译后的可执行机器指令

    那么这些机器指令又是从哪里来的呢?答案是从可执行文件中加载到内存的,可执行程序中的代码区就是用来初始化进程地址空间中的代码区的。

    线程之间共享代码区,这就意味着程序中的任何一个函数都可以放到线程中去执行,不存在某个函数只能被特定线程执行的情况

     

     数据区

    进程地址空间中的数据区,这里存放的就是所谓的全局变量。

    什么是全局变量?所谓全局变量就是那些你定义在函数之外的变量,在C语言中就像这样:

    char c; // 全局变量
    void func() {    }
    

    其中字符c就是全局变量,存放在进程地址空间中的数据区。

    在程序员运行期间,也就是run time,数据区中的全局变量有且仅有一个实例,所有的线程都可以访问到该全局变量

    值得注意的是,在C语言中还有一类特殊的“全局变量”,那就是用static关键词修饰过的变量,就像这样:

    void func(){    static int a = 10;}
    

    注意到,虽然变量a定义在函数内部,但变量a依然具有全局变量的特性,也就是说变量a放在了进程地址空间的数据区域,即使函数执行完后该变量依然存在,而普通的局部变量随着函数调用结束和函数栈帧一起被回收掉了,但这里的变量a不会被回收,因为其被放到了数据区。

    这样的变量对每个线程来说也是可见的,也就是说每个线程都可以访问到该变量。

     

    堆区

    堆区是程序员比较熟悉的,我们在C/C++中用malloc或者new出来的数据就存放在这个区域,很显然,只要知道变量的地址,也就是指针,任何一个线程都可以访问指针指向的数据,因此堆区也是线程共享的属于进程的资源。

     

    栈区

    唉,等等!刚不是说栈区是线程私有资源吗,怎么这会儿又说起栈区了?

    确实,从线程这个抽象的概念上来说,栈区是线程私有的,然而从实际的实现上看,栈区属于线程私有这一规则并没有严格遵守,这句话是什么意思?

    通常来说,注意这里的用词是通常,通常来说栈区是线程私有,既然有通常就有不通常的时候。

    不通常是因为不像进程地址空间之间的严格隔离,线程的栈区没有严格的隔离机制来保护,因此如果一个线程能拿到来自另一个线程栈帧上的指针,那么该线程就可以改变另一个线程的栈区,也就是说这些线程可以任意修改本属于另一个线程栈区中的变量。

    这从某种程度上给了程序员极大的便利,但同时,这也会导致极其难以排查到的bug。

    试想一下你的程序运行的好好的,结果某个时刻突然出问题,定位到出问题代码行后根本就排查不到原因,你当然是排查不到问题原因的,因为你的程序本来就没有任何问题,是别人的问题导致你的函数栈帧数据被写坏从而产生bug,这样的问题通常很难排查到原因,需要对整体的项目代码非常熟悉,常用的一些debug工具这时可能已经没有多大作用了。

    说了这么多,那么同学可能会问,一个线程是怎样修改本属于其它线程的数据呢?

    接下来我们用一个代码示例讲解一下。

     

    修改线程私有数据

    不要担心,以下代码足够简单:

    void thread(void* var) {
        int* p = (int*)var;
        *p = 2;
    }
    
    
    int main() {
        int a = 1;
        pthread_t tid;
        
        pthread_create(&tid, NULL, thread, (void*)&a);
        return 0;
    }
    

    这段代码是什么意思呢?

    首先我们在主线程的栈区定义了一个局部变量,也就是 int a= 1这行代码,现在我们已经知道了,局部变量a属于主线程私有数据,但是,接下来我们创建了另外一个线程。

    在新创建的这个线程中,我们将变量a的地址以参数的形式传给了新创建的线程,然后我来看一下thread函数。

    在新创建的线程中,我们获取到了变量a的指针,然后将其修改为了2,也就是这行代码,我们在新创建的线程中修改了本属于主线程的私有数据。

    现在你应该看明白了吧,尽管栈区是线程的私有数据,但由于栈区没有添加任何保护机制,一个线程的栈区对其它线程是可以见的,也就是说我们可以修改属于任何一个线程的栈区。

    就像我们上文说得到的,这给程序员带来了极大便利的同时也带来了无尽的麻烦,试想上面这段代码,如果确实是项目需要那么这样写代码无可厚非,但如果上述新创建线程是因bug修改了属于其它线程的私有数据的话,那么产生问题就很难定位了,因为bug可能距离问题暴露的这行代码已经很远了,这样的问题通常难以排查。

     

    动态链接库

    进程地址空间中除了以上讨论的这些实际上还有其它内容,还有什么呢?

    这就要从可执行程序说起了。

    什么是可执行程序呢?在Windows中就是我们熟悉的exe文件,在Linux世界中就是ELF文件,这些可以被操作系统直接运行的程序就是我们所说的可执行程序。

    那么可执行程序是怎么来的呢?

    有的同学可能会说,废话,不就是编译器生成的吗?

    实际上这个答案只答对了一半。

    假设我们的项目比较简单只有几个源码文件,编译器是怎么把这几个源代码文件转换为最终的一个可执行程序呢?

    原来,编译器在将可执行程序翻译成机器指令后,接下来还有一个重要的步骤,这就是链接,链接完成后生成的才是可执行程序。

    完成链接这一过程的就是链接器。

    其中链接器可以有两种链接方式,这就是静态链接动态链接

    静态链接的意思是说把所有的机器指令一股脑全部打包到可执行程序中,动态链接的意思是我们不把动态链接的部分打包到可执行程序,而是在可执行程序运行起来后去内存中找动态链接的那部分代码,这就是所谓的静态链接和动态链接。

    动态链接一个显而易见的好处就是可执行程序的大小会很小,就像我们在Windows下看一个exe文件可能很小,那么该exe很可能是动态链接的方式生成的

    而动态链接的部分生成的库就是我们熟悉的动态链接库,在Windows下是以DLL结尾的文件,在Linux下是以so结尾的文件。

    说了这么多,这和线程共享资源有什么关系呢?

    原来如果一个程序是动态链接生成的,那么其地址空间中有一部分包含的就是动态链接库,否则程序就运行不起来了,这一部分的地址空间也是被所有线程所共享的。

    也就是说进程中的所有线程都可以使用动态链接库中的代码。

    以上其实是关于链接这一主题的极简介绍,关于链接这一话题的详细讨论可以参考《彻底理解链接器》系列文章。

     

    文件

    最后,如果程序在运行过程中打开了一些文件,那么进程地址空间中还保存有打开的文件信息,进程打开的文件也可以被所有的线程使用,这也属于线程间的共享资源。

     

     One More Thing:TLS

    本文就这些了吗?

    实际上关于线程私有数据还有一项没有详细讲解,因为再讲下去本篇就撑爆了,而且本篇已经讲解的部分足够用了,剩下的这一点仅仅作为补充,也就是选学部分,如果你对此不感兴趣的话完全可以跳过,没有问题

    关于线程私有数据还有一项技术,那就是线程局部存储,Thread Local Storage,TLS。

    这是什么意思呢?

    其实从名字上也可以看出,所谓线程局部存储,是指存放在该区域中的变量有两个含义:

    • 存放在该区域中的变量是全局变量,所有线程都可以访问

    • 虽然看上去所有线程访问的都是同一个变量,但该全局变量独属于一个线程,一个线程对此变量的修改对其他线程不可见。

    说了这么多还是没懂有没有?没关系,接下来看完这两段代码还不懂你来打我。

    我们先来看第一段代码,不用担心,这段代码非常非常的简单:

    int a = 1; // 全局变量
    
    
    void print_a() {
        cout<<a<<endl;
    }
    
    
    void run() {
        ++a;
        print_a();
    }
    
    
    void main() {
        thread t1(run);
        t1.join();
    
    
        thread t2(run);
        t2.join();
    }
    

    怎么样,这段代码足够简单吧,上述代码是用C++11写的,我来讲解下这段代码是什么意思。

    • 首先我们创建了一个全局变量a,初始值为1

    • 其次我们创建了两个线程,每个线程对变量a加1

    • 线程的join函数表示该线程运行完毕后才继续运行接下来的代码

    那么这段代码的运行起来会打印什么呢?

    全局变量a的初始值为1,第一个线程加1后a变为2,因此会打印2;第二个线程再次加1后a变为3,因此会打印3,让我们来看一下运行结果:

    2
    3
    

    看来我们分析的没错,全局变量在两个线程分别加1后最终变为3。

    接下来我们对变量a的定义稍作修改,其它代码不做改动:

    __thread int a = 1; // 线程局部存储
    

    我们看到全局变量a前面加了一个__thread关键词用来修饰,也就是说我们告诉编译器把变量a放在线程局部存储中,那这会对程序带来哪些改变呢?

    简单运行一下就知道了:

    2
    2
    

    和你想的一样吗?有的同学可能会大吃一惊,为什么我们明明对变量a加了两次,但第二次运行为什么还是打印2而不是3呢?

    想一想这是为什么。

    原来,这就是线程局部存储的作用所在,线程t1对变量a的修改不会影响到线程t2,线程t1在将变量a加到1后变为2,但对于线程t2来说此时变量a依然是1,因此加1后依然是2。

    因此,线程局部存储可以让你使用一个独属于线程的全局变量。也就是说,虽然该变量可以被所有线程访问,但该变量在每个线程中都有一个副本,一个线程对改变量的修改不会影响到其它线程。

     

    总结

    怎么样,没想到教科书上一句简单的“线程共享进程资源”背后竟然会有这么多的知识点吧,教科书上的知识看似容易,但,并不简单

    希望本篇能对大家理解进程、线程能有多帮助。

    展开全文
  • 共享内存的常用函数原理剖析

    千次阅读 2012-12-10 19:45:12
    共享内存区域是被多个进程共享的一部分物理内存。如果多个进程都把该内存区域映射到自己的虚拟地址空间,则这些进程就都可以直接访问该共享内存区域,从而可以通过该区域进行通信。共享内存是进程间共享数据的一种最...
    共享内存区域是被多个进程共享的一部分物理内存。如果多个进程都把该内存区域映射到自己的虚拟地址空间,则这些进程就都可以直接访问该共享内存区域,从而可以通过该区域进行通信。共享内存是进程间共享数据的一种最快的方法,一个进程向共享内存区域写入了数据,共享这个内存区域的所有进程就可以立刻看到其中的内容。这块共享虚拟内存的页面,出现在每一个共享该页面的进程的页表中。但是它不需要在所有进程的虚拟内存中都有相同的虚拟地址。 
    
    
                                                     

             象所有的 System V IPC对象一样,对于共享内存对象的获取是由key控制。内存共享之后,对进程如何使用这块内存就不再做检查。它们必须依赖于其它机制,比如System V的信号灯来同步对于共享内存区域的访问(信号灯如何控制对临界代码的访问另起一篇说话)。
     
            每一个新创建的共享内存对象都用一个shmid_kernel数据结构来表达。系统中所有的shmid_kernel数据结构都保存在shm_segs向量表中,该向量表的每一个元素都是一个指向shmid_kernel数据结构的指针。
    shm_segs向量表的定义如下:
    struct shmid_kernel *shm_segs[SHMMNI];

     
        SHMMNI为128,表示系统中最多可以有128个共享内存对象。
     
       数据结构shmid_kernel的定义如下:
        struct shmid_kernel
           
            struct shmid_ds u;        
            unsigned long shm_npages; 
            unsigned long *shm_pages;   
            struct vm_area_struct *attaches; 
        };

     
        其中:
        shm_pages代表该共享内存对象的所占据的内存页面数组,数组里面的每个元素当然是每个内存页面的起始地址.
        shm_npages则是该共享内存对象占用内存页面的个数,以页为单位。这个数量当然涵盖了申请空间的最小整数倍.
        (A new shared memory segment,  with size  equal to the value of size rounded up to a multiple of PAGE_SIZE)
        shmid_ds是一个数据结构,它描述了这个共享内存区的认证信息,字节大小,最后一次粘附时间、分离时间、改变时间,创建该共享区域的进程,最后一次对它操作的进程,当前有多少个进程在使用它等信息。
        其定义如下:
        struct shmid_ds {
            struct ipc_perm shm_perm;  
            int shm_segsz;             
            __kernel_time_t shm_atime; 
            __kernel_time_t shm_dtime; 
            __kernel_time_t shm_ctime; 
            __kernel_ipc_pid_t shm_cpid;
            __kernel_ipc_pid_t shm_lpid;
            unsigned short shm_nattch;  
            unsigned short shm_unused;  
            void *shm_unused2;          
            void *shm_unused3;          
        };

     
            attaches描述被共享的物理内存对象所映射的各进程的虚拟内存区域。每一个希望共享这块内存的进程都必须通过系统调用将其关联(attach)到它的虚拟内存中。这一过程将为该进程创建了一个新的描述这块共享内存的vm_area_struct数据结构。创建时可以指定共享内存在它的虚拟地址空间的位置,也可以让Linux自己为它选择一块足够的空闲区域。
     
            这个新的vm_area_struct结构是维系共享内存和使用它的进程之间的关系的,所以除了要关联进程信息外,还要指明这个共享内存数据结构shmid_kernel所在位置; 另外,便于管理这些经常变化的vm_area_struct,所以采取了链表形式组织这些数据结构,链表由attaches指向,同时 vm_area_struct数据结构中专门提供了两个指针:vm_next_shared和 vm_prev_shared,用于连接该共享区域在使用它的各进程中所对应的vm_area_struct数据结构。 
                 Linux为共享内存提供了四种操作。
            1. 共享内存对象的创建或获得。与其它两种IPC机制一样,进程在使用共享内存区域以前,必须通过系统调用sys_ipc (call值为SHMGET)创建一个键值为key的共享内存对象,或获得已经存在的键值为key的某共享内存对象的引用标识符。以后对共享内存对象的访问都通过该引用标识符进行。对共享内存对象的创建或获得由函数sys_shmget完成,其定义如下:
    int sys_shmget (key_t key, int size, int shmflg)
     
        这里key是表示该共享内存对象的键值,size是该共享内存区域的大小(以字节为单位),shmflg是标志(对该共享内存对象的特殊要求)。
     
        它所做的工作如下:
        1) 如果key == IPC_PRIVATE,则总是会创建一个新的共享内存对象。
     但是  (The name choice IPC_PRIVATE was perhaps unfortunate, IPC_NEW would more clearly show its function)
        * 算出size要占用的页数,检查其合法性。
        * 申请一块内存用于建立shmid_kernel数据结构,注意这里申请的内存区域大小不包括真正的共享内存区,实际上,要等到第一个进程试图访问它的时候才真正创建共享内存区。
        * 根据该共享内存区所占用的页数,为其申请一块空间用于建立页表(每页4个字节),将页表清0。
        * 搜索向量表shm_segs,为新创建的共享内存对象找一个空位置。
        * 填写shmid_kernel数据结构,将其加入到向量表shm_segs中为其找到的空位置。
        * 返回该共享内存对象的引用标识符。
     
        2) 在向量表shm_segs中查找键值为key的共享内存对象,结果有三:
        * 如果没有找到,而且在操作标志shmflg中没有指明要创建新共享内存,则错误返回,否则创建一个新的共享内存对象。
        * 如果找到了,但该次操作要求必须创建一个键值为key的新对象,那么错误返回。
        * 否则,合法性、认证检查,如有错,则错误返回;否则,返回该内存对象的引用标识符。
     
        共享内存对象的创建者可以控制对于这块内存的访问权限和它的key是公开还是私有。如果有足够的权限,它也可以把共享内存锁定在物理内存中。
        参见include/linux/shm.h
     
        2. 关联。在创建或获得某个共享内存区域的引用标识符后,还必须将共享内存区域映射(粘附)到进程的虚拟地址空间,然后才能使用该共享内存区域。系统调用 sys_ipc(call值为SHMAT)用于共享内存区到进程虚拟地址空间的映射,而真正完成粘附动作的是函数sys_shmat,
     
    其定义如下:   

           #include <sys/types.h>
           #include <sys/shm.h>

           void *shmat(int shmid, const void *shmaddr, int shmflg);


     
        其中:
         shmid是shmget返回的共享内存对象的引用标识符;
        shmaddr用来指定该共享内存区域在进程的虚拟地址空间对应的虚拟地址;
        shmflg是映射标志;
        返回的是在进程中的虚拟地址
     
        该函数所做的工作如下:
        1) 根据shmid找到共享内存对象。
        2) 如果shmaddr为0,即用户没有指定该共享内存区域在它的虚拟空间中的位置,则由系统在进程的虚拟地址空间中为其找一块区域(从1G开始);否则,就用shmaddr作为映射的虚拟地址。
      (If  shmaddr  is NULL, the system chooses a suitable (unused) address a他 which to attach the segment)
        3) 检查虚拟地址的合法性(不能超过进程的最大虚拟空间大小—3G,不能太接近堆栈栈顶)。
        4) 认证检查。
        5) 申请一块内存用于建立数据结构vm_area_struct,填写该结构。
        6) 检查该内存区域,将其加入到进程的mm结构和该共享内存对象的vm_area_struct队列中。
     
        共享内存的粘附只是创建一个vm_area_struct数据结构,并将其加入到相应的队列中,此时并没有创建真正的共享内存页。
     
        当进程第一次访问共享虚拟内存的某页时,因为所有的共享内存页还都没有分配,所以会发生一个page fault异常。当Linux处理这个page fault的时候,它找到发生异常的虚拟地址所在的vm_area_struct数据结构。在该数据结构中包含有这类共享虚拟内存的一组处理程序,其中的 nopage操作用来处理虚拟页对应的物理页不存在的情况。对共享内存,该操作是shm_nopage(定义在ipc/shm.c中)。该操作在描述这个共享内存的shmid_kernel数据结构的页表shm_pages中查找发生page fault异常的虚拟地址所对应的页表条目,看共享页是否存在(页表条目为0,表示共享页是第一次使用)。如果不存在,它就分配一个物理页,并为它创建一个页表条目。这个条目不但进入当前进程的页表,同时也存到shmid_kernel数据结构的页表shm_pages中。
     
        当下一个进程试图访问这块内存并得到一个page fault的时候,经过同样的路径,也会走到函数shm_nopage。此时,该函数查看shmid_kernel数据结构的页表shm_pages时,发现共享页已经存在,它只需把这里的页表项填到进程页表的相应位置即可,而不需要重新创建物理页。所以,是第一个访问共享内存页的进程使得这一页被创建,而随后访问它的其它进程仅把此页加到它们的虚拟地址空间。
     
        3. 分离。当进程不再需要共享虚拟内存的时候,它们与之分离(detach)。只要仍旧有其它进程在使用这块内存,这种分离就只会影响当前的进程,而不会影响其它进程。当前进程的vm_area_struct数据结构被从shmid_ds中删除,并被释放。当前进程的页表也被更新,共享内存对应的虚拟内存页被标记为无效。当共享这块内存的最后一个进程与之分离时,共享内存页被释放,同时,这块共享内存的shmid_kernel数据结构也被释放。
     
      系统调用sys_ipc (call值为SHMDT) 用于共享内存区与进程虚拟地址空间的分离,而真正完成分离动作的是函数    

        sys_shmdt,其定义如下:
        int sys_shmdt (char *shmaddr)
     
        其中shmaddr是进程要分离的共享页的开始虚拟地址。
     
        该函数搜索进程的内存结构中的所有vm_area_struct数据结构,找到地址shmaddr对应的一个,调用函数do_munmap将其释放。
     
        在函数do_munmap中,将要释放的vm_area_struct数据结构从进程的虚拟内存中摘下,清除它在进程页表中对应的页表项(可能占多个页表项). 
      
        如果共享的虚拟内存没有被锁定在物理内存中,分离会更加复杂。因为在这种情况下,共享内存的页可能在系统大量使用内存的时候被交换到系统的交换磁盘。为了避免这种情况,可以通过下面的控制操作,将某共享内存页锁定在物理内存不允许向外交换。共享内存的换出和换入,已在第3章中讨论。
     
        4. 控制。Linux在共享内存上实现的第四种操作是共享内存的控制(call值为SHMCTL的sys_ipc调用),它由函数sys_shmctl实现。控制操作包括获得共享内存对象的状态,设置共享内存对象的参数(如uid、gid、mode、ctime等),将共享内存对象在内存中锁定和释放(在对象的mode上增加或去除SHM_LOCKED标志),释放共享内存对象资源等。
     
        共享内存提供了一种快速灵活的机制,它允许进程之间直接共享大量的数据,而无须使用拷贝或系统调用。共享内存的主要局限性是它不能提供同步,如果两个进程企图修改相同的共享内存区域,由于内核不能串行化这些动作,因此写的数据可能任意地互相混合。所以使用共享内存的进程必须设计它们自己的同步协议,如用信号灯等。

    以下是使用共享内存机制进行进程间通信的基本操作:

    需要包含的头文件:

    #include <sys/types.h>

    #include <sys/ipc.h>

    #include <sys/shm.h>

    1.创建共享内存:

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

    参数说明:

    key:用来表示新建或者已经存在的共享内存去的关键字。

    size:创建共享内存的大小。

    shmflg:可以指定的特殊标志。IPC_CREATE,IPC_EXCL以及低九位的权限。

    eg:

    int shmid;

    shmid=shmget(IPC_PRIVATE,4096,IPC_CREATE|IPC_EXCL|0660);

    if(shmid==-1)

    perror("shmget()");

     

    2.连接共享内存

    char *shmat(int shmid,char *shmaddr,int shmflg);

    参数说明

    shmid:共享内存的关键字

    shmaddr:指定共享内存出现在进程内存地址的什么位置,通常我们让内核自己决定一个合适的地址位置,用的时候设为0。

    shmflg:制定特殊的标志位。

    eg:

    int shmid;

    char *shmp;

    shmp=shmat(shmid,0,0);

    if(shmp==(char *)(-1))

    perror("shmat()\n");

    3.使用共享内存

    在使用共享内存是需要注意的是,为防止内存访问冲突,我们一般与信号量结合使用。

    4.分离共享内存:当程序不再需要共享内后,我们需要将共享内存分离以便对其进行释放,分离共享内存的函数原形如下:

    int shmdt(char *shmaddr);

     

    5. 释放共享内存

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

    Posix有名信号灯

    1.posix有名信号灯函数

       函数sem_open创建一个新的有名信号灯或打开一个已存在的有名信号灯。有名信号灯总是既可用于线程间的同步,又能用于进程间的同步。

    1. sem_open

    名称::
    sem_open
    功能:
    创建并初始化有名信号灯
    头文件:
    #include 
    函数原形:
    sem_t *sem_open(const char *name,int oflag,/*mode_t mode,unsigned int value*/);
    参数:
    name   信号灯的外部名字
    oflag   选择创建或打开一个现有的信号灯
    mode 权限位
    value 信号灯初始值
    返回值:
    成功时返回指向信号灯的指针,出错时为SEM_FAILED
          
    oflag参数能是0、O_CREAT(创建一个信号灯)或O_CREAT|O_EXCL(如果没有指定的信号灯就创建),如果指定了O_CREAT,那么第三个和第四个参数是需要的;其中mode参数指定权限位,value参数指定信号灯的初始值,通常用来指定共享资源的书面。该初始不能超过 SEM_VALUE_MAX,这个常值必须低于为32767。二值信号灯的初始值通常为1,计数信号灯的初始值则往往大于1。
       如果指定了O_CREAT(而没有指定O_EXCL),那么只有所需的信号灯尚未存在时才初始化他。所需信号灯已存在条件下指定O_CREAT不是个错误。该标志的意思仅仅是“如果所需信号灯尚未存在,那就创建并初始化他”。不过所需信号灯等已存在条件下指定O_CREAT|O_EXCL却是个错误。
       sem_open返回指向sem_t信号灯的指针,该结构里记录着当前共享资源的数目。

    /* semopen.c */
    1. #include <fcntl.h> /* For O_* constants */
    2. #include <sys/stat.h> /* For mode constants */
    3. #include <semaphore.h>

    4. int main(int argc, char **argv)
    5. {
    6.     sem_t *sem;
    7.     if (argc != 2)
    8.     {
    9.         printf("please input a file name!\n");
    10.         exit(1);
    11.     }
    12.     sem = sem_open(argv[1], O_CREAT, 0644, 1);
    13.     exit(0);
    14. }
    2. sem_close

    名称::
    sem_close
    功能:
    关闭有名信号灯
    头文件:
    #include 
    函数原形:
    int sem_close(sem_t *sem);
    参数:
    sem 指向信号灯的指针
    返回值:
    若成功则返回0,否则返回-1。

       一个进程终止时,内核还对其上仍然打开着的所有有名信号灯自动执行这样的信号灯关闭操作。不论该进程是自愿终止的还是非自愿终止的,这种自动关闭都会发生。
       但应注意的是关闭一个信号灯并没有将他从系统中删除。这就是说,Posix有名信号灯至少是随内核持续的:即使当前没有进程打开着某个信号灯,他的值仍然保持。

    3. sem_unlink

    名称::
    sem_unlink
    功能:
    从系统中删除信号灯
    头文件:
    #include <semaphore.h>

    函数原形:
    int sem_unlink(count char *name);
    参数:
    name   信号灯的外部名字
    返回值:
    若成功则返回0,否则返回-1。

    有名信号灯使用sem_unlink从系统中删除。
    每个信号灯有一个引用计数器记录当前的打开次数,sem_unlink必须等待这个数为0时才能把name所指的信号灯从文件系统中删除。也就是要等待最后一个sem_close发生。

    <!--

    Code highlighting produced by Actipro CodeHighlighter (freeware)
    http://www.CodeHighlighter.com/

    --> /* semunlink.c */
    1. int main(int argc, char **argv)
    2. {
    3.     sem_t *sem;
    4.     int val;
    5.     if (argc != 2)
    6.     {
    7.         printf("please input a file name!\n");
    8.         exit(1);
    9.     }
    10.     if ((sem_unlink(argv[1])) != 0)
    11.         perror("sem_unlink");
    12.     else
    13.         printf("success");
    14.     exit(0);
    15. }


    4. sem_getvalue

    名称::
    sem_getvalue
    功能:
    测试信号灯
    头文件:
    #include 
    函数原形:
    int sem_getvalue(sem_t *sem,int *valp);
    参数:
    sem 指向信号灯的指针
    返回值:
    若成功则返回0,否则返回-1。
    sem_getvalue在由valp指向的正数中返回所指定信号灯的当前值。如果该信号灯当前已上锁,那么返回值或为0,或为某个负数,其绝对值就是等待该信号灯解锁的线程数。

    代码
    <!--

    Code highlighting produced by Actipro CodeHighlighter (freeware)
    http://www.CodeHighlighter.com/

    --> /* semgetvalue.c */
    1. int main(int argc, char **argv)
    2. {
    3.     sem_t *sem;
    4.     int val;
    5.     if (argc != 2)
    6.     {
    7.         printf("please input a file name!\n");
    8.         exit(1);
    9.     }
    10.     sem = sem_open(argv[1], 0);
    11.     sem_getvalue(sem, &val);
    12.     printf("getvalue: value = %d\n", val);
    13.     exit(0)
    14. }

    5. sem_wait/sem_trywait

    名称::
    sem_wait/sem_trywait
    功能:
    等待共享资源
    头文件:
    #include 
    函数原形:
    int sem_wait(sem_t *sem); 
    int sem_trywait(sem_t *sem);
    参数:
    sem 指向信号灯的指针
    返回值:
    若成功则返回0,否则返回-1。

    我们能用sem_wait来申请共享资源,sem_wait函数能测试所指定信号灯的值,如果该值大于0,那就将他减1并即时返回。我们就能使用申请来的共享资源了。如果该值等于0,调用线程就被进入睡眠状态,直到该值变为大于0,这时再将他减1,函数随后返回。sem_wait操作必须是原子的。 sem_wait和sem_trywait的差别是:当所指定信号灯的值已是0时,后者并不将调用线程投入睡眠。相反,他返回一个EAGAIN错误。

    下面的程式我们先不去运行,稍后再运行。

    代码
    <!--

    Code highlighting produced by Actipro CodeHighlighter (freeware)
    http://www.CodeHighlighter.com/

    --> /* semwait.c */
    1. int main(int argc, char **argv)
    2. {
    3.     sem_t *sem;
    4.     int val;
    5.     if (argc != 2)
    6.     {
    7.         printf("please input a file name!\n");
    8.         exit(1);
    9.     }
    10.     sem = sem_open(argv[1], 0);
    11.     sem_wait(sem);
    12.     sem_getvalue(sem, &val);
    13.     printf("pid % ld has semaphore, value = %d\n", (long)getpid(), val);
    14.     pause();
    15.     exit(0);
    16. }

    6. sem_post

    名称::
    sem_post
    功能:
    挂出共享资源
    头文件:
    #include 
    函数原形:
    int sem_post(sem_t *sem);
    int sem_getvalue(sem_t *sem,int *valp);
    参数:
    sem 指向信号灯的指针
    返回值:
    若成功则返回0,否则返回-1。

    当一个线程使用完某个信号灯时,他应该调用sem_post来告诉系统申请的资源已用完。本函数和sem_wait函数的功能正好相反,他把所指定的信号灯的值加1,然后唤醒正在等待该信号灯值变为正数的任意线程。

    下面的程式我们先不去运行,稍后再运行。

    代码
    <!--

    Code highlighting produced by Actipro CodeHighlighter (freeware)
    http://www.CodeHighlighter.com/

    --> /* sempost.c */
    1. int main(int argc, char **argv)
    2. {
    3.     sem_t *sem;
    4.     int val;
    5.     if (argc != 2)
    6.     {
    7.         printf("please input a file name!\n");
    8.         exit(1);
    9.     }
    10.     sem = sem_open(argv[1], 0);
    11.     sem_post(sem);
    12.     sem_getvalue(sem, &val);
    13.     printf("value = %d\n", val);
    14.     exit(0);
    15. }

     
    二. 关于posix有名信号灯使用的几点注意

    我们要注意以下几点:
    1.Posix有名信号灯的值是随内核持续的。也就是说,一个进程创建了一个信号灯,这个进程结束后,这个信号灯还存在,并且信号灯的值也不会改动。
    下面我们利用上面的几个程式来证实这点
    #./semopen test
    #./semgetvalue test
    value=1   信号的值仍然是1

    2。当持有某个信号灯锁的进程没有释放他就终止时,内核并不给该信号灯解锁。
    #./semopen test
    #./semwait test&
    pid 1834 has semaphore,value=0 
    #./semgetvalue test 
    value=0 信号量的值变为0了

    3.posix有名信号灯应用于多线程

    1. #include <stdio.h>
    2. #include <pthread.h>
    3. #include <fcntl.h> /* For O_* constants */
    4. #include <sys/stat.h> /* For mode constants */
    5. #include <semaphore.h>
    6. #include <stdlib.h>


    7. void *thread_function(void *arg); /* 线程入口函数 */
    8. void print(); /* 共享资源函数 */
    9. sem_t *sem; /* 定义Posix有名信号灯 */
    10. int val; /* 定义信号灯当前值 */

    11. int main(int argc, char *argv[])
    12. {
    13.     int n = 5;
    14.     pthread_t a_thread;

    15.     if (argc != 2)
    16.     {
    17.         printf("please input a file name!\n");
    18.         exit(1);
    19.     }
    20.     sem = sem_open(argv[1], O_CREAT, 0644, 3); /* 打开一个信号灯 */

    21.     sem_getvalue(sem, &val);
    22.     printf("The total sem is %d\n", val);

    23.     while(n--) /*循环创建5个子线程,使他们同步运行*/
    24.     {
    25.         if ((pthread_create(&a_thread, NULL, thread_function, &n) != 0))
    26.         {
    27.             perror("Thread creation failed");
    28.             exit(1);
    29.         }
    30.     }
    31.     pthread_join(a_thread, NULL);
    32.     sem_getvalue(sem, &val);
    33.     printf("In the end, sem is %d\n", val);

    34.     sem_close(sem);
    35.     sem_unlink(argv[1]);
    36. }

    37. void *thread_function(void *arg)
    38. {
    39.     int *ttid = arg;

    40.     sem_wait(sem); /* 申请信号灯 */
    41.     printf("the thread is %d\n", *ttid);
    42.     print(); /* 调用共享代码段 */
    43.     sleep(1);
    44.     sem_post(sem); /* 释放信号灯 */
    45.     printf("I'm finished, my tid is %d\n\n", pthread_self());
    46. }

    47. void print()
    48. {
    49.     printf("I get it, my tid is %d\n", pthread_self());
    50.     sem_getvalue(sem, &val);
    51.     printf("Now the value have %d\n", val);
    52. }


    程式用循环的方法建立5个线程,然后让他们调用同一个线程处理函数thread_function,在函数里我们利用信号量来限制访问共享资源的线程数。共享资源我们用print函数来代表,在真正编程中他有能是个终端设备(如打印机)或是一段有实际意义的代码。

    运行结果为:
    #gcc sem1.c -o sem1 -lrt -lpthread

    #.I get it,my tid is 1082330304
    Now the value have 2
    Iget it,my pid is 1894
    Now the value have 1
    Iget it,my pid is 1895
    Now the value have 0
    I’m finished,my pid is 1893
    I’m finished,my pid is 1894
    I’m finished,my pid is 1895
    I get it,my pid is 1896
    Now the value have 2
    I get it,mypid is 1897
    Now the value have 1
    I’m finished,my pid is 1896
    I’m finished,my pid is 1897

    4.posix有名信号灯应用于多进程
    下面就是应用Posix有名信号灯的一个小程序。用它来限制访问共享代码的进程数目。
    #include 
    #include 
    #include 
    #include 

    void print(pid_t);
    sem_t *sem; /*定义Posix有名信号灯*/
    int val; /*定义信号灯当前值*/

    int main(int argc,char *argv[])
    {
    int n=0;

    if(argc!=2)
    {
    printf(“please input a file name!\n”);
    exit(1);
    }
    sem=sem_open(argv[1],O_CREAT,0644,2); /*打开一个信号灯, 初值设为2*/

    while(n++循环创建5个子进程,使它们同步运行*/
    {
    if(fork()==0) 
    {
           sem_wait(sem); /*申请信号灯*/
           print(getpid()); /*调用共享代码段*/
           sleep(1); 
           sem_post(sem); /*释放信号灯*/
           printf(“I’m finished,my pid is %d\n”,getpid());
           return 0; 
    }
    }
    wait(); /*等待所有子进程结束*/
    sem_close(sem);
    sem_unlink(argv[1]);
    exit(0);
    }

    void print(pid_t pid)
    {
    printf(“I get it,my pid is %d\n”,pid);
    sem_getvalue(sem,&val);
    printf(“Now the value have %d\n”,val);
    }

    程序编译后运行会得到如下结果:
    #./8_2 8_2.c
    I get it,my tid is 1082330304
    Now the value have 1
    I get it,my tid is 1090718784
    Now the value have 0
    I finished,my pid is 1082330304
    I finished,my pid is 1090718784
    I get it,my tid is 1099107264
    Now the value have 1
    I get it,my tid is 1116841120
    Now the value have 0
    I finished,my pid is 1099107264
    I finished,my pid is 1116841120
    I get it,my tid is 1125329600
    Now the value have 1
    I finished,my pid is 1125329600

     

    三、基于内存的信号灯

       前面已经介绍了Posix有名信号灯。这些信号灯由一个name参数标识,它通常指代文件系统中的某个文件。然而Posix也提供基于内存的信号灯,它们由应用程序分配信号灯的内存空间,然后由系统初始化它们的值。

    7.
    名称::
    sem_init/sem_destroy
    功能:
    初始化/关闭信号等
    头文件:
    #include 
    函数原形:
    int sem_init(sem_t *sem,int shared,unsigned int value);
    int sem_getvalue(sem_t *sem);
    参数:
    sem 指向信号灯的指针
    shared   作用范围
    value 信号灯初始值
    返回值:
    若成功则返回0,否则返回-1。

    基于内存的信号灯是由sem_init初始化的。sem参数指向必须由应用程序分配的sem_t变量。如果shared为0,那么待初始化的信号灯是在同一进程的各个线程共享的,否则该信号灯是在进程间共享的。当shared为零时,该信号灯必须存放在即将使用它的所有进程都能访问的某种类型的共享内存中。跟sem_open一样,value参数是该信号灯的初始值。
       使用完一个基于内存的信号灯后,我们调用sem_destroy关闭它。
    除了sem_open和sem_close外,其它的poisx有名信号灯函数都可以用于基于内存的信号灯。

    注意:posix基于内存的信号灯和posix有名信号灯有一些区别,我们必须注意到这些。
    1.sem_open不需要类型与shared的参数,有名信号灯总是可以在不同进程间共享的。
    2.sem_init不使用任何类似于O_CREAT标志的东西,也就是说,sem_init总是初始化信号灯的值。因此,对于一个给定的信号灯,我们必须小心保证只调用一次sem_init。
    3.sem_open返回一个指向某个sem_t变量的指针,该变量由函数本身分配并初始化。但sem_init的第一个参数是一个指向某个sem_t变量的指针,该变量由调用者分配,然后由sem_init函数初始化。
    4.posix有名信号灯是通过内核持续的,一个进程创建一个信号灯,另外的进程可以通过该信号灯的外部名(创建信号灯使用的文件名)来访问它。 posix基于内存的信号灯的持续性却是不定的,如果基于内存的信号灯是由单个进程内的各个线程共享的,那么该信号灯就是随进程持续的,当该进程终止时它也会消失。如果某个基于内存的信号灯是在不同进程间同步的,该信号灯必须存放在共享内存区中,这要只要该共享内存区存在,该信号灯就存在。
    5.基于内存的信号灯应用于线程很麻烦(待会你会知道为什么),而有名信号灯却很方便,基于内存的信号灯比较适合应用于一个进程的多个线程。

    下面是posix基于内存的信号灯实现一个进程的各个线程间的互次。
    #include 
    #include 
    #include 
    #include 
    #include 
    #incude 

    void *thread_function(void *arg); /*线程入口函数*/
    void print(void); /*共享资源函数*/

    sem_t bin_sem; /*定义信号灯*/
    int value; /*定义信号量的灯*/

    int main()
    {
    int n=0;
    pthread_t a_thread; 

    if((sem_init(&bin_sem,0,2))!=0) /*初始化信号灯,初始值为2*/
    {
    perror(“sem_init”);
    exit(1);
    }
    while(n++循环创建5个线程*/
    {
    if((pthread_create(&a_thread,NULL,thread_function,NULL))!=0) 
    {
    perror(“Thread creation failed”);
    exit(1);
    }
    }
    pthread_join(a_thread,NULL);/*等待子线程返回*/
    }

    void *thread_function(void *arg)
    {
    sem_wait(&bin_sem); /*等待信号灯*/
    print();
    sleep(1);
    sem_post(&bin_sem); /*挂出信号灯*/
    printf(“I finished,my pid is %d\n”,pthread_self());
    pthread_exit(arg);
    }

    void print()
    {
    printf(“I get it,my tid is %d\n”,pthread_self());
    sem_getvalue(&bin_sem,&value); /*获取信号灯的值*/
    printf(“Now the value have %d\n”,value);
    }

       posix基于内存的信号灯和有名信号灯基本是一样的,上面的几点区别就可以了。 
    下面是运行结果:
    #gcc –lpthread –o seminitthread seminitthread.c
    #./seminitthread 
    I get it,my tid is 1082330304
    Now the value have 1
    I get it,my tid is 1090718784
    Now the value have 0
    I finished,my pid is 1082330304
    I finished,my pid is 1090718784
    I get it,my tid is 1099107264
    Now the value have 1
    I get it,my tid is 1116841120
    Now the value have 0
    I finished,my pid is 1099107264
    I finished,my pid is 1116841120
    I get it,my tid is 1125329600
    Now the value have 1
    I finished,my pid is 1125329600

    下面的程序并不能得到我们想要的结果。
    #include 
    #include 
    #include 
    #include 

    void print(pid_t);
    sem_t *sem; /*定义Posix有名信号灯*/
    int val; /*定义信号灯当前值*/

    int main(int argc,char *argv[])
    {
    int n=0;

    sem=sem_open(argv[1],O_CREAT,0644,3); /*打开一个信号灯*/
    sem_getvalue(sem,&val); /*查看信号灯的值*/
    printf(“The value have %d\n”,val);

    while(n++循环创建5个子进程,使它们同步运行*/
    {
    if(fork()==0) 
    {
           sem_wait(sem); /*申请信号灯*/
           print(getpid()); /*调用共享代码段*/
           sleep(1); 
           sem_post(sem); /*释放信号灯*/
           printf(“I’m finished,my pid is %d\n”,getpid());
           return 0; 
    }
    wait(); /*等待所有子进程结束*/
    return 0;
    }

    void print(pid_t pid)
    {
    printf(“I get it,my pid is %d\n”,pid);
    sem_getvalue(sem,&val);
    printf(“Now the value have %d\n”,val);
    }

    下面是运行结果:
    #cc –lpthread –o sem sem.c
    #./sem 
    The value have 3
    I get it,my pid is 2236
    Now the value have 2
    I get it,my pid is 2237
    Now the value have 2
    I get it,my pid is 2238
    Now the value have 2
    I get it,my pid is 2239
    Now the value have 2
    Iget it,my pid is 2240
    Now the value have 2
    I’m finished,my pid is 2236
    I’m finished,my pid is 2237
    I’m finished,my pid is 2238
    I’m finished,my pid is 2239
    I’m finished,my pid is 2240

    问题在于sem信号灯不在共享内存区中。fork出来的子进程通常不共享父进程的内存空间。子进程是在父进程内存空间的拷贝上启动的,它跟共享内存不是一回事。

    if ($ != jQuery) { $ = jQuery.noConflict(); } var isLogined = false; var cb_blogId = 73254; var cb_entryId = 1938552; var cb_blogApp = "BloodAndBone"; var cb_blogUserGuid = "b0469f6d-1f92-df11-ba8f-001cf0cd104b"; var cb_entryCreatedDate = '2011/1/18 18:05:00';

    展开全文
  • 共享内存的常用函数详解shmget shmat

    千次阅读 2015-03-31 07:16:28
    共享内存区域是被多个进程共享的一部分物理内存。如果多个进程都把该内存区域映射到自己的虚拟地址空间,则这些进程就都可以直接访问该共享内存区域,从而可以通过该区域进行通信。共享内存是进程间共享数据的一种最...
    共享内存区域是被多个进程共享的一部分物理内存。如果多个进程都把该内存区域映射到自己的虚拟地址空间,则这些进程就都可以直接访问该共享内存区域,从而可以通过该区域进行通信。共享内存是进程间共享数据的一种最快的方法,一个进程向共享内存区域写入了数据,共享这个内存区域的所有进程就可以立刻看到其中的内容。这块共享虚拟内存的页面,出现在每一个共享该页面的进程的页表中。但是它不需要在所有进程的虚拟内存中都有相同的虚拟地址。  
    

                                                     

             象所有的 System V IPC对象一样,对于共享内存对象的获取是由key控制。内存共享之后,对进程如何使用这块内存就不再做检查。它们必须依赖于其它机制,比如System V的信号灯来同步对于共享内存区域的访问(信号灯如何控制对临界代码的访问另起一篇说话)。
     
            每一个新创建的共享内存对象都用一个shmid_kernel数据结构来表达。系统中所有的shmid_kernel数据结构都保存在shm_segs向量表中,该向量表的每一个元素都是一个指向shmid_kernel数据结构的指针。
    shm_segs向量表的定义如下:
    struct shmid_kernel *shm_segs[SHMMNI];

     
        SHMMNI为128,表示系统中最多可以有128个共享内存对象。
     
       数据结构shmid_kernel的定义如下:
        struct shmid_kernel
           
            struct shmid_ds u;        
            unsigned long shm_npages; 
            unsigned long *shm_pages;   
            struct vm_area_struct *attaches; 
        };

     
        其中:
        shm_pages代表该共享内存对象的所占据的内存页面数组,数组里面的每个元素当然是每个内存页面的起始地址.
        shm_npages则是该共享内存对象占用内存页面的个数,以页为单位。这个数量当然涵盖了申请空间的最小整数倍.
        (A new shared memory segment,  with size  equal to the value of size rounded up to a multiple of PAGE_SIZE)
        shmid_ds是一个数据结构,它描述了这个共享内存区的认证信息,字节大小,最后一次粘附时间、分离时间、改变时间,创建该共享区域的进程,最后一次对它操作的进程,当前有多少个进程在使用它等信息。
        其定义如下:
        struct shmid_ds {
            struct ipc_perm shm_perm;  
            int shm_segsz;             
            __kernel_time_t shm_atime; 
            __kernel_time_t shm_dtime; 
            __kernel_time_t shm_ctime; 
            __kernel_ipc_pid_t shm_cpid;
            __kernel_ipc_pid_t shm_lpid;
            unsigned short shm_nattch;  
            unsigned short shm_unused;  
            void *shm_unused2;          
            void *shm_unused3;          
        };

     
            attaches描述被共享的物理内存对象所映射的各进程的虚拟内存区域。每一个希望共享这块内存的进程都必须通过系统调用将其关联(attach)到它的虚拟内存中。这一过程将为该进程创建了一个新的描述这块共享内存的vm_area_struct数据结构。创建时可以指定共享内存在它的虚拟地址空间的位置,也可以让Linux自己为它选择一块足够的空闲区域。
     
            这个新的vm_area_struct结构是维系共享内存和使用它的进程之间的关系的,所以除了要关联进程信息外,还要指明这个共享内存数据结构shmid_kernel所在位置; 另外,便于管理这些经常变化的vm_area_struct,所以采取了链表形式组织这些数据结构,链表由attaches指向,同时 vm_area_struct数据结构中专门提供了两个指针:vm_next_shared和 vm_prev_shared,用于连接该共享区域在使用它的各进程中所对应的vm_area_struct数据结构。 
                 Linux为共享内存提供了四种操作。
            1. 共享内存对象的创建或获得。与其它两种IPC机制一样,进程在使用共享内存区域以前,必须通过系统调用sys_ipc (call值为SHMGET)创建一个键值为key的共享内存对象,或获得已经存在的键值为key的某共享内存对象的引用标识符。以后对共享内存对象的访问都通过该引用标识符进行。对共享内存对象的创建或获得由函数sys_shmget完成,其定义如下:
    int sys_shmget (key_t key, int size, int shmflg)
     
        这里key是表示该共享内存对象的键值,size是该共享内存区域的大小(以字节为单位),shmflg是标志(对该共享内存对象的特殊要求)。
     
        它所做的工作如下:
        1) 如果key == IPC_PRIVATE,则总是会创建一个新的共享内存对象。
     但是  (The name choice IPC_PRIVATE was perhaps unfortunate, IPC_NEW would more clearly show its function)
        * 算出size要占用的页数,检查其合法性。
        * 申请一块内存用于建立shmid_kernel数据结构,注意这里申请的内存区域大小不包括真正的共享内存区,实际上,要等到第一个进程试图访问它的时候才真正创建共享内存区。
        * 根据该共享内存区所占用的页数,为其申请一块空间用于建立页表(每页4个字节),将页表清0。
        * 搜索向量表shm_segs,为新创建的共享内存对象找一个空位置。
        * 填写shmid_kernel数据结构,将其加入到向量表shm_segs中为其找到的空位置。
        * 返回该共享内存对象的引用标识符。
     
        2) 在向量表shm_segs中查找键值为key的共享内存对象,结果有三:
        * 如果没有找到,而且在操作标志shmflg中没有指明要创建新共享内存,则错误返回,否则创建一个新的共享内存对象。
        * 如果找到了,但该次操作要求必须创建一个键值为key的新对象,那么错误返回。
        * 否则,合法性、认证检查,如有错,则错误返回;否则,返回该内存对象的引用标识符。
     
        共享内存对象的创建者可以控制对于这块内存的访问权限和它的key是公开还是私有。如果有足够的权限,它也可以把共享内存锁定在物理内存中。
        参见include/linux/shm.h
     
        2. 关联。在创建或获得某个共享内存区域的引用标识符后,还必须将共享内存区域映射(粘附)到进程的虚拟地址空间,然后才能使用该共享内存区域。系统调用 sys_ipc(call值为SHMAT)用于共享内存区到进程虚拟地址空间的映射,而真正完成粘附动作的是函数sys_shmat,
     
    其定义如下:   

           #include <sys/types.h>
           #include <sys/shm.h>

           void *shmat(int shmid, const void *shmaddr, int shmflg);


     
        其中:
         shmid是shmget返回的共享内存对象的引用标识符;
        shmaddr用来指定该共享内存区域在进程的虚拟地址空间对应的虚拟地址;
        shmflg是映射标志;
        返回的是在进程中的虚拟地址
     
        该函数所做的工作如下:
        1) 根据shmid找到共享内存对象。
        2) 如果shmaddr为0,即用户没有指定该共享内存区域在它的虚拟空间中的位置,则由系统在进程的虚拟地址空间中为其找一块区域(从1G开始);否则,就用shmaddr作为映射的虚拟地址。
      (If  shmaddr  is NULL, the system chooses a suitable (unused) address a他 which to attach the segment)
        3) 检查虚拟地址的合法性(不能超过进程的最大虚拟空间大小—3G,不能太接近堆栈栈顶)。
        4) 认证检查。
        5) 申请一块内存用于建立数据结构vm_area_struct,填写该结构。
        6) 检查该内存区域,将其加入到进程的mm结构和该共享内存对象的vm_area_struct队列中。
     
        共享内存的粘附只是创建一个vm_area_struct数据结构,并将其加入到相应的队列中,此时并没有创建真正的共享内存页。
     
        当进程第一次访问共享虚拟内存的某页时,因为所有的共享内存页还都没有分配,所以会发生一个page fault异常。当Linux处理这个page fault的时候,它找到发生异常的虚拟地址所在的vm_area_struct数据结构。在该数据结构中包含有这类共享虚拟内存的一组处理程序,其中的 nopage操作用来处理虚拟页对应的物理页不存在的情况。对共享内存,该操作是shm_nopage(定义在ipc/shm.c中)。该操作在描述这个共享内存的shmid_kernel数据结构的页表shm_pages中查找发生page fault异常的虚拟地址所对应的页表条目,看共享页是否存在(页表条目为0,表示共享页是第一次使用)。如果不存在,它就分配一个物理页,并为它创建一个页表条目。这个条目不但进入当前进程的页表,同时也存到shmid_kernel数据结构的页表shm_pages中。
     
        当下一个进程试图访问这块内存并得到一个page fault的时候,经过同样的路径,也会走到函数shm_nopage。此时,该函数查看shmid_kernel数据结构的页表shm_pages时,发现共享页已经存在,它只需把这里的页表项填到进程页表的相应位置即可,而不需要重新创建物理页。所以,是第一个访问共享内存页的进程使得这一页被创建,而随后访问它的其它进程仅把此页加到它们的虚拟地址空间。
     
        3. 分离。当进程不再需要共享虚拟内存的时候,它们与之分离(detach)。只要仍旧有其它进程在使用这块内存,这种分离就只会影响当前的进程,而不会影响其它进程。当前进程的vm_area_struct数据结构被从shmid_ds中删除,并被释放。当前进程的页表也被更新,共享内存对应的虚拟内存页被标记为无效。当共享这块内存的最后一个进程与之分离时,共享内存页被释放,同时,这块共享内存的shmid_kernel数据结构也被释放。
     
      系统调用sys_ipc (call值为SHMDT) 用于共享内存区与进程虚拟地址空间的分离,而真正完成分离动作的是函数    

        sys_shmdt,其定义如下:
        int sys_shmdt (char *shmaddr)
     
        其中shmaddr是进程要分离的共享页的开始虚拟地址。
     
        该函数搜索进程的内存结构中的所有vm_area_struct数据结构,找到地址shmaddr对应的一个,调用函数do_munmap将其释放。
     
        在函数do_munmap中,将要释放的vm_area_struct数据结构从进程的虚拟内存中摘下,清除它在进程页表中对应的页表项(可能占多个页表项). 
      
        如果共享的虚拟内存没有被锁定在物理内存中,分离会更加复杂。因为在这种情况下,共享内存的页可能在系统大量使用内存的时候被交换到系统的交换磁盘。为了避免这种情况,可以通过下面的控制操作,将某共享内存页锁定在物理内存不允许向外交换。共享内存的换出和换入,已在第3章中讨论。
     
        4. 控制。Linux在共享内存上实现的第四种操作是共享内存的控制(call值为SHMCTL的sys_ipc调用),它由函数sys_shmctl实现。控制操作包括获得共享内存对象的状态,设置共享内存对象的参数(如uid、gid、mode、ctime等),将共享内存对象在内存中锁定和释放(在对象的mode上增加或去除SHM_LOCKED标志),释放共享内存对象资源等。
     
        共享内存提供了一种快速灵活的机制,它允许进程之间直接共享大量的数据,而无须使用拷贝或系统调用。共享内存的主要局限性是它不能提供同步,如果两个进程企图修改相同的共享内存区域,由于内核不能串行化这些动作,因此写的数据可能任意地互相混合。所以使用共享内存的进程必须设计它们自己的同步协议,如用信号灯等。

    以下是使用共享内存机制进行进程间通信的基本操作:

    需要包含的头文件:

    #include <sys/types.h>

    #include <sys/ipc.h>

    #include <sys/shm.h>

    1.创建共享内存:

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

    参数说明:

    key:用来表示新建或者已经存在的共享内存去的关键字。

    size:创建共享内存的大小。

    shmflg:可以指定的特殊标志。IPC_CREATE,IPC_EXCL以及低九位的权限。

    eg:

    int shmid;

    shmid=shmget(IPC_PRIVATE,4096,IPC_CREATE|IPC_EXCL|0660);

    if(shmid==-1)

    perror("shmget()");

     

    2.连接共享内存

    char *shmat(int shmid,char *shmaddr,int shmflg);

    参数说明

    shmid:共享内存的关键字

    shmaddr:指定共享内存出现在进程内存地址的什么位置,通常我们让内核自己决定一个合适的地址位置,用的时候设为0。

    shmflg:制定特殊的标志位。

    eg:

    int shmid;

    char *shmp;

    shmp=shmat(shmid,0,0);

    if(shmp==(char *)(-1))

    perror("shmat()\n");

    3.使用共享内存

    在使用共享内存是需要注意的是,为防止内存访问冲突,我们一般与信号量结合使用。

    4.分离共享内存:当程序不再需要共享内后,我们需要将共享内存分离以便对其进行释放,分离共享内存的函数原形如下:

    int shmdt(char *shmaddr);

     

    5. 释放共享内存

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

    Posix有名信号灯

    1.posix有名信号灯函数

       函数sem_open创建一个新的有名信号灯或打开一个已存在的有名信号灯。有名信号灯总是既可用于线程间的同步,又能用于进程间的同步。

    1. sem_open

    名称::
    sem_open
    功能:
    创建并初始化有名信号灯
    头文件:
    #include
    函数原形:
    sem_t *sem_open(const char *name,int oflag,/*mode_t mode,unsigned int value*/);
    参数:
    name   信号灯的外部名字
    oflag   选择创建或打开一个现有的信号灯
    mode 权限位
    value 信号灯初始值
    返回值:
    成功时返回指向信号灯的指针,出错时为SEM_FAILED
          
    oflag参数能是0、O_CREAT(创建一个信号灯)或O_CREAT|O_EXCL(如果没有指定的信号灯就创建),如果指定了O_CREAT,那么第三个和第四个参数是需要的;其中mode参数指定权限位,value参数指定信号灯的初始值,通常用来指定共享资源的书面。该初始不能超过 SEM_VALUE_MAX,这个常值必须低于为32767。二值信号灯的初始值通常为1,计数信号灯的初始值则往往大于1。
       如果指定了O_CREAT(而没有指定O_EXCL),那么只有所需的信号灯尚未存在时才初始化他。所需信号灯已存在条件下指定O_CREAT不是个错误。该标志的意思仅仅是“如果所需信号灯尚未存在,那就创建并初始化他”。不过所需信号灯等已存在条件下指定O_CREAT|O_EXCL却是个错误。
       sem_open返回指向sem_t信号灯的指针,该结构里记录着当前共享资源的数目。

    /* semopen.c */
    1. #include <fcntl.h>/* For O_* constants */
    2. #include <sys/stat.h>/* For mode constants */
    3. #include <semaphore.h>

    4. int main(int argc, char**argv)
    5. {
    6.     sem_t *sem;
    7.     if (argc!= 2)
    8.     {
    9.         printf("please input a file name!\n");
    10.         exit(1);
    11.     }
    12.     sem = sem_open(argv[1], O_CREAT, 0644, 1);
    13.     exit(0);
    14. }
    2. sem_close

    名称::
    sem_close
    功能:
    关闭有名信号灯
    头文件:
    #include
    函数原形:
    int sem_close(sem_t *sem);
    参数:
    sem 指向信号灯的指针
    返回值:
    若成功则返回0,否则返回-1。

       一个进程终止时,内核还对其上仍然打开着的所有有名信号灯自动执行这样的信号灯关闭操作。不论该进程是自愿终止的还是非自愿终止的,这种自动关闭都会发生。
       但应注意的是关闭一个信号灯并没有将他从系统中删除。这就是说,Posix有名信号灯至少是随内核持续的:即使当前没有进程打开着某个信号灯,他的值仍然保持。

    3. sem_unlink

    名称::
    sem_unlink
    功能:
    从系统中删除信号灯
    头文件:
    #include <semaphore.h>

    函数原形:
    int sem_unlink(count char *name);
    参数:
    name   信号灯的外部名字
    返回值:
    若成功则返回0,否则返回-1。

    有名信号灯使用sem_unlink从系统中删除。
    每个信号灯有一个引用计数器记录当前的打开次数,sem_unlink必须等待这个数为0时才能把name所指的信号灯从文件系统中删除。也就是要等待最后一个sem_close发生。

    <!--

    Code highlighting produced by Actipro CodeHighlighter (freeware)
    http://www.CodeHighlighter.com/

    --> /* semunlink.c */
    1. int main(int argc, char**argv)
    2. {
    3.     sem_t *sem;
    4.     int val;
    5.     if (argc!= 2)
    6.     {
    7.         printf("please input a file name!\n");
    8.         exit(1);
    9.     }
    10.     if ((sem_unlink(argv[1]))!= 0)
    11.         perror("sem_unlink");
    12.     else
    13.         printf("success");
    14.     exit(0);
    15. }


    4. sem_getvalue

    名称::
    sem_getvalue
    功能:
    测试信号灯
    头文件:
    #include
    函数原形:
    int sem_getvalue(sem_t *sem,int *valp);
    参数:
    sem 指向信号灯的指针
    返回值:
    若成功则返回0,否则返回-1。
    sem_getvalue在由valp指向的正数中返回所指定信号灯的当前值。如果该信号灯当前已上锁,那么返回值或为0,或为某个负数,其绝对值就是等待该信号灯解锁的线程数。

    代码
    <!--

    Code highlighting produced by Actipro CodeHighlighter (freeware)
    http://www.CodeHighlighter.com/

    --> /* semgetvalue.c */
    1. int main(int argc, char**argv)
    2. {
    3.     sem_t *sem;
    4.     int val;
    5.     if (argc!= 2)
    6.     {
    7.         printf("please input a file name!\n");
    8.         exit(1);
    9.     }
    10.     sem = sem_open(argv[1], 0);
    11.     sem_getvalue(sem,&val);
    12.     printf("getvalue: value = %d\n", val);
    13.     exit(0)
    14. }

    5. sem_wait/sem_trywait

    名称::
    sem_wait/sem_trywait
    功能:
    等待共享资源
    头文件:
    #include
    函数原形:
    int sem_wait(sem_t *sem);
    int sem_trywait(sem_t *sem);
    参数:
    sem 指向信号灯的指针
    返回值:
    若成功则返回0,否则返回-1。

    我们能用sem_wait来申请共享资源,sem_wait函数能测试所指定信号灯的值,如果该值大于0,那就将他减1并即时返回。我们就能使用申请来的共享资源了。如果该值等于0,调用线程就被进入睡眠状态,直到该值变为大于0,这时再将他减1,函数随后返回。sem_wait操作必须是原子的。 sem_wait和sem_trywait的差别是:当所指定信号灯的值已是0时,后者并不将调用线程投入睡眠。相反,他返回一个EAGAIN错误。

    下面的程式我们先不去运行,稍后再运行。

    代码
    <!--

    Code highlighting produced by Actipro CodeHighlighter (freeware)
    http://www.CodeHighlighter.com/

    --> /* semwait.c */
    1. int main(int argc, char**argv)
    2. {
    3.     sem_t *sem;
    4.     int val;
    5.     if (argc!= 2)
    6.     {
    7.         printf("please input a file name!\n");
    8.         exit(1);
    9.     }
    10.     sem = sem_open(argv[1], 0);
    11.     sem_wait(sem);
    12.     sem_getvalue(sem,&val);
    13.     printf("pid % ld has semaphore, value = %d\n",(long)getpid(), val);
    14.     pause();
    15.     exit(0);
    16. }

    6. sem_post

    名称::
    sem_post
    功能:
    挂出共享资源
    头文件:
    #include
    函数原形:
    int sem_post(sem_t *sem);
    int sem_getvalue(sem_t *sem,int *valp);
    参数:
    sem 指向信号灯的指针
    返回值:
    若成功则返回0,否则返回-1。

    当一个线程使用完某个信号灯时,他应该调用sem_post来告诉系统申请的资源已用完。本函数和sem_wait函数的功能正好相反,他把所指定的信号灯的值加1,然后唤醒正在等待该信号灯值变为正数的任意线程。

    下面的程式我们先不去运行,稍后再运行。

    代码
    <!--

    Code highlighting produced by Actipro CodeHighlighter (freeware)
    http://www.CodeHighlighter.com/

    --> /* sempost.c */
    1. int main(int argc, char**argv)
    2. {
    3.     sem_t *sem;
    4.     int val;
    5.     if (argc!= 2)
    6.     {
    7.         printf("please input a file name!\n");
    8.         exit(1);
    9.     }
    10.     sem = sem_open(argv[1], 0);
    11.     sem_post(sem);
    12.     sem_getvalue(sem,&val);
    13.     printf("value = %d\n", val);
    14.     exit(0);
    15. }

     
    二. 关于posix有名信号灯使用的几点注意

    我们要注意以下几点:
    1.Posix有名信号灯的值是随内核持续的。也就是说,一个进程创建了一个信号灯,这个进程结束后,这个信号灯还存在,并且信号灯的值也不会改动。
    下面我们利用上面的几个程式来证实这点
    #./semopen test
    #./semgetvalue test
    value=1   信号的值仍然是1

    2。当持有某个信号灯锁的进程没有释放他就终止时,内核并不给该信号灯解锁。
    #./semopen test
    #./semwait test&
    pid 1834 has semaphore,value=0
    #./semgetvalue test
    value=0 信号量的值变为0了

    3.posix有名信号灯应用于多线程

    1. #include <stdio.h>
    2. #include <pthread.h>
    3. #include <fcntl.h>/* For O_* constants */
    4. #include <sys/stat.h>/* For mode constants */
    5. #include <semaphore.h>
    6. #include <stdlib.h>


    7. void *thread_function(void*arg);/* 线程入口函数 */
    8. void print();/* 共享资源函数 */
    9. sem_t *sem;/* 定义Posix有名信号灯*/
    10. int val;/* 定义信号灯当前值*/

    11. int main(int argc, char*argv[])
    12. {
    13.     int n = 5;
    14.     pthread_t a_thread;

    15.     if (argc!= 2)
    16.     {
    17.         printf("please input a file name!\n");
    18.         exit(1);
    19.     }
    20.     sem = sem_open(argv[1], O_CREAT, 0644, 3); /* 打开一个信号灯 */

    21.     sem_getvalue(sem,&val);
    22.     printf("The total sem is %d\n", val);

    23.     while(n--)/*循环创建5个子线程,使他们同步运行*/
    24.     {
    25.         if ((pthread_create(&a_thread,NULL, thread_function,&n) != 0))
    26.         {
    27.             perror("Thread creation failed");
    28.             exit(1);
    29.         }
    30.     }
    31.     pthread_join(a_thread,NULL);
    32.     sem_getvalue(sem,&val);
    33.     printf("In the end, sem is %d\n", val);

    34.     sem_close(sem);
    35.     sem_unlink(argv[1]);
    36. }

    37. void *thread_function(void*arg)
    38. {
    39.     int *ttid= arg;

    40.     sem_wait(sem);/* 申请信号灯 */
    41.     printf("the thread is %d\n",*ttid);
    42.     print();/* 调用共享代码段 */
    43.     sleep(1);
    44.     sem_post(sem);/* 释放信号灯 */
    45.     printf("I'm finished, my tid is %d\n\n", pthread_self());
    46. }

    47. void print()
    48. {
    49.     printf("I get it, my tid is %d\n", pthread_self());
    50.     sem_getvalue(sem,&val);
    51.     printf("Now the value have %d\n", val);
    52. }


    程式用循环的方法建立5个线程,然后让他们调用同一个线程处理函数thread_function,在函数里我们利用信号量来限制访问共享资源的线程数。共享资源我们用print函数来代表,在真正编程中他有能是个终端设备(如打印机)或是一段有实际意义的代码。

    运行结果为:
    #gcc sem1.c -o sem1 -lrt -lpthread

    #.I get it,my tid is 1082330304
    Now the value have 2
    Iget it,my pid is 1894
    Now the value have 1
    Iget it,my pid is 1895
    Now the value have 0
    I’m finished,my pid is 1893
    I’m finished,my pid is 1894
    I’m finished,my pid is 1895
    I get it,my pid is 1896
    Now the value have 2
    I get it,mypid is 1897
    Now the value have 1
    I’m finished,my pid is 1896
    I’m finished,my pid is 1897

    4.posix有名信号灯应用于多进程
    下面就是应用Posix有名信号灯的一个小程序。用它来限制访问共享代码的进程数目。
    #include
    #include
    #include
    #include

    void print(pid_t);
    sem_t *sem; /*定义Posix有名信号灯*/
    int val; /*定义信号灯当前值*/

    int main(int argc,char *argv[])
    {
    int n=0;

    if(argc!=2)
    {
    printf(“please input a file name!\n”);
    exit(1);
    }
    sem=sem_open(argv[1],O_CREAT,0644,2); /*打开一个信号灯, 初值设为2*/

    while(n++循环创建5个子进程,使它们同步运行*/
    {
    if(fork()==0)
    {
           sem_wait(sem); /*申请信号灯*/
           print(getpid()); /*调用共享代码段*/
           sleep(1);
           sem_post(sem); /*释放信号灯*/
           printf(“I’m finished,my pid is %d\n”,getpid());
           return 0;
    }
    }
    wait(); /*等待所有子进程结束*/
    sem_close(sem);
    sem_unlink(argv[1]);
    exit(0);
    }

    void print(pid_t pid)
    {
    printf(“I get it,my pid is %d\n”,pid);
    sem_getvalue(sem,&val);
    printf(“Now the value have %d\n”,val);
    }

    程序编译后运行会得到如下结果:
    #./8_2 8_2.c
    I get it,my tid is 1082330304
    Now the value have 1
    I get it,my tid is 1090718784
    Now the value have 0
    I finished,my pid is 1082330304
    I finished,my pid is 1090718784
    I get it,my tid is 1099107264
    Now the value have 1
    I get it,my tid is 1116841120
    Now the value have 0
    I finished,my pid is 1099107264
    I finished,my pid is 1116841120
    I get it,my tid is 1125329600
    Now the value have 1
    I finished,my pid is 1125329600

     

    三、基于内存的信号灯

       前面已经介绍了Posix有名信号灯。这些信号灯由一个name参数标识,它通常指代文件系统中的某个文件。然而Posix也提供基于内存的信号灯,它们由应用程序分配信号灯的内存空间,然后由系统初始化它们的值。

    7.
    名称::
    sem_init/sem_destroy
    功能:
    初始化/关闭信号等
    头文件:
    #include
    函数原形:
    int sem_init(sem_t *sem,int shared,unsigned int value);
    int sem_getvalue(sem_t *sem);
    参数:
    sem 指向信号灯的指针
    shared   作用范围
    value 信号灯初始值
    返回值:
    若成功则返回0,否则返回-1。

    基于内存的信号灯是由sem_init初始化的。sem参数指向必须由应用程序分配的sem_t变量。如果shared为0,那么待初始化的信号灯是在同一进程的各个线程共享的,否则该信号灯是在进程间共享的。当shared为零时,该信号灯必须存放在即将使用它的所有进程都能访问的某种类型的共享内存中。跟sem_open一样,value参数是该信号灯的初始值。
       使用完一个基于内存的信号灯后,我们调用sem_destroy关闭它。
    除了sem_open和sem_close外,其它的poisx有名信号灯函数都可以用于基于内存的信号灯。

    注意:posix基于内存的信号灯和posix有名信号灯有一些区别,我们必须注意到这些。
    1.sem_open不需要类型与shared的参数,有名信号灯总是可以在不同进程间共享的。
    2.sem_init不使用任何类似于O_CREAT标志的东西,也就是说,sem_init总是初始化信号灯的值。因此,对于一个给定的信号灯,我们必须小心保证只调用一次sem_init。
    3.sem_open返回一个指向某个sem_t变量的指针,该变量由函数本身分配并初始化。但sem_init的第一个参数是一个指向某个sem_t变量的指针,该变量由调用者分配,然后由sem_init函数初始化。
    4.posix有名信号灯是通过内核持续的,一个进程创建一个信号灯,另外的进程可以通过该信号灯的外部名(创建信号灯使用的文件名)来访问它。 posix基于内存的信号灯的持续性却是不定的,如果基于内存的信号灯是由单个进程内的各个线程共享的,那么该信号灯就是随进程持续的,当该进程终止时它也会消失。如果某个基于内存的信号灯是在不同进程间同步的,该信号灯必须存放在共享内存区中,这要只要该共享内存区存在,该信号灯就存在。
    5.基于内存的信号灯应用于线程很麻烦(待会你会知道为什么),而有名信号灯却很方便,基于内存的信号灯比较适合应用于一个进程的多个线程。

    下面是posix基于内存的信号灯实现一个进程的各个线程间的互次。
    #include
    #include
    #include
    #include
    #include
    #incude

    void *thread_function(void *arg); /*线程入口函数*/
    void print(void); /*共享资源函数*/

    sem_t bin_sem; /*定义信号灯*/
    int value; /*定义信号量的灯*/

    int main()
    {
    int n=0;
    pthread_t a_thread;

    if((sem_init(&bin_sem,0,2))!=0) /*初始化信号灯,初始值为2*/
    {
    perror(“sem_init”);
    exit(1);
    }
    while(n++循环创建5个线程*/
    {
    if((pthread_create(&a_thread,NULL,thread_function,NULL))!=0)
    {
    perror(“Thread creation failed”);
    exit(1);
    }
    }
    pthread_join(a_thread,NULL);/*等待子线程返回*/
    }

    void *thread_function(void *arg)
    {
    sem_wait(&bin_sem); /*等待信号灯*/
    print();
    sleep(1);
    sem_post(&bin_sem); /*挂出信号灯*/
    printf(“I finished,my pid is %d\n”,pthread_self());
    pthread_exit(arg);
    }

    void print()
    {
    printf(“I get it,my tid is %d\n”,pthread_self());
    sem_getvalue(&bin_sem,&value); /*获取信号灯的值*/
    printf(“Now the value have %d\n”,value);
    }

       posix基于内存的信号灯和有名信号灯基本是一样的,上面的几点区别就可以了。
    下面是运行结果:
    #gcc –lpthread –o seminitthread seminitthread.c
    #./seminitthread
    I get it,my tid is 1082330304
    Now the value have 1
    I get it,my tid is 1090718784
    Now the value have 0
    I finished,my pid is 1082330304
    I finished,my pid is 1090718784
    I get it,my tid is 1099107264
    Now the value have 1
    I get it,my tid is 1116841120
    Now the value have 0
    I finished,my pid is 1099107264
    I finished,my pid is 1116841120
    I get it,my tid is 1125329600
    Now the value have 1
    I finished,my pid is 1125329600

    下面的程序并不能得到我们想要的结果。
    #include
    #include
    #include
    #include

    void print(pid_t);
    sem_t *sem; /*定义Posix有名信号灯*/
    int val; /*定义信号灯当前值*/

    int main(int argc,char *argv[])
    {
    int n=0;

    sem=sem_open(argv[1],O_CREAT,0644,3); /*打开一个信号灯*/
    sem_getvalue(sem,&val); /*查看信号灯的值*/
    printf(“The value have %d\n”,val);

    while(n++循环创建5个子进程,使它们同步运行*/
    {
    if(fork()==0)
    {
           sem_wait(sem); /*申请信号灯*/
           print(getpid()); /*调用共享代码段*/
           sleep(1);
           sem_post(sem); /*释放信号灯*/
           printf(“I’m finished,my pid is %d\n”,getpid());
           return 0;
    }
    wait(); /*等待所有子进程结束*/
    return 0;
    }

    void print(pid_t pid)
    {
    printf(“I get it,my pid is %d\n”,pid);
    sem_getvalue(sem,&val);
    printf(“Now the value have %d\n”,val);
    }

    下面是运行结果:
    #cc –lpthread –o sem sem.c
    #./sem
    The value have 3
    I get it,my pid is 2236
    Now the value have 2
    I get it,my pid is 2237
    Now the value have 2
    I get it,my pid is 2238
    Now the value have 2
    I get it,my pid is 2239
    Now the value have 2
    Iget it,my pid is 2240
    Now the value have 2
    I’m finished,my pid is 2236
    I’m finished,my pid is 2237
    I’m finished,my pid is 2238
    I’m finished,my pid is 2239
    I’m finished,my pid is 2240

    问题在于sem信号灯不在共享内存区中。fork出来的子进程通常不共享父进程的内存空间。子进程是在父进程内存空间的拷贝上启动的,它跟共享内存不是一回事。

    if ($ != jQuery) { $ = jQuery.noConflict(); } var isLogined = false; var cb_blogId = 73254; var cb_entryId = 1938552; var cb_blogApp = "BloodAndBone"; var cb_blogUserGuid = "b0469f6d-1f92-df11-ba8f-001cf0cd104b"; var cb_entryCreatedDate = '2011/1/18 18:05:00';


    展开全文
  • C#的线程同步与资源共享

    千次阅读 2010-06-03 02:06:00
    随着对多线程学习的深入,你可能觉得需要了解一些有关线程共享资源的问题. .NET framework提供了很多的类和数据类型来控制对共享资源的访问。 <br />考虑一种我们经常遇到的情况:有一些全局变量和共享的...
  • linux系列之常用运维命令整理笔录

    万次阅读 多人点赞 2019-10-28 17:33:21
    单位为KB 3、top命令使用 top命令可以实时动态地查看系统的整体运行情况,是一个综合了多方信息监测系统性能和运行信息的实用工具,TOP命令是Linux下常用的性能分析工具,能够实时显示系统中各个进程的资源占用状况...
  • 查看Linux下系统资源占用常用命令

    万次阅读 2016-06-19 10:56:46
    块设备每秒接收的块数量,这里的块设备是系统上所有的磁盘和其他块设备,默认块大小是1024byte,我本机上没什么IO操作,所以一直是0,但是我曾在处理拷贝大量数据(2-3T)的机器上看过可以达到140000/s,磁盘写入...
  • AJAX跨域请求和CORS跨域资源共享

    千次阅读 2016-07-19 12:44:38
    整理了同源策略限制,出现了跨域请求的需求,原生传统的跨域请求方式,原生js和jQuery中对跨域的处理(JSONP),以及HTML5 中的CORS跨域资源共享。系统条理得梳理好传说中的跨域了!o(^▽^)o
  • C++面试常用知识点总结——基础篇

    万次阅读 多人点赞 2019-07-15 18:13:04
    温备份:的是当数据库进行备份时, 数据库的读操作可以执行, 但是不能执行写操作 冷备份:的是当数据库进行备份时, 数据库不能进行读写操作, 即数据库要下线 2.2.3、Mysql如何备份 直接拷贝数据库 ...
  • Kubernetes常用命令与常见问题

    万次阅读 2016-09-05 14:59:36
    常用命令重启Master节点for SERVICE in etcd kube-apiserver kube-controller-manager kube-scheduler; do sudo systemctl restart $SERVICE sudo systemctl enable $SERVICE sudo systemctl status $SERVICE d
  • Linux学习之路 1.Linux安装篇 1.1 Linux安装教程 1.2 vm的安装步骤 1.3 .CentOS安装步骤 ...2.3 使用 vmtools 来设置 windows 和 linux 的共享文件夹 2.4 Linux目录结构 2.5 目录结构的具体介绍 3.Linux实操篇 3....
  • Java Web前端到后台常用框架介绍

    万次阅读 多人点赞 2016-03-18 11:44:16
    //blog.csdn.net/evankaka/article/details/45501811Spring Web MVC是一种基于Java的实现了Web MVC设计模式的请求驱动类型的轻量级Web框架,即使用了MVC架构模式的思想,将web层进行职责解耦,基于请求驱动的就是...
  • 死锁的处理基本策略和常用方法

    千次阅读 2018-08-02 14:59:02
    死锁:多个进程因竞争共享资源而造成的一种僵局,若无外力作用,这些进程都将永远不能再 向前推进。 安全状态与不安全状态:安全状态系统能按某种进程顺序来为每个进程分配其所需资源,直 至最大需求,使每个...
  • 网络常用语整理之1

    千次阅读 2008-04-18 15:07:00
    1、BBS:①Bulletin Board System的缩写,电子公告板系统,国内统称论坛。②波霸,Big-Breasted Sister的缩写。 2、版主 or 斑竹:版主,也可写作板猪、斑竹。由于拼音输入造成的美妙谐音。副版主叫“板斧”。 3、...
  • SecureCRT萌新常用命令及Linux命令大全整理

    万次阅读 多人点赞 2016-12-30 13:37:49
    SecureCRT常用命令,Linux常用命令
  • linux有诸多优秀的工具帮助我们分析服务器各项性能指标和协助开发调试工作。下面只列举比较基础的命令,且一般是... A、CPU进程相关 常用工具列举下:uptime、ps、top、mpstat、pidstat等  uptime: 查看系统运行时
  • 1 引言  并发、并行、串行、同步、异步、阻塞、非阻塞、进程、线程、协程是并发编程中的常见... 并发是在同一个处理器上通过时间片轮转的方式在多个线程之间频繁切换,由于切换速度极快,所以看似多个线程似...
  • win10 常用快捷键

    千次阅读 2017-12-19 15:32:59
    常用快捷键 快速打开我的电脑 win+ E 快速打开控制面板 win+I 打开运行  win + R 打开搜索 win+Q/S 最小化  win +D 常用管理 win + X 屏幕分割 win +上/下/左/右 通知面板 win + A 笔记本用户 ...
  • Windows.h 常用API函数【转】

    万次阅读 多人点赞 2018-08-12 01:32:13
    //根据curpos所的坐标点获取窗口句柄 SendMessage(hWnd,WM_CHAR,WPARAM( 'g' ), 0 ); //发送一个字符(按键)消息g给当前鼠标所指向的窗口句柄 Sleep( 300 ); //睡眠三百毫秒,相当于等待三分之一秒 } } 这个...
  • 计算机常用重点

    千次阅读 多人点赞 2020-06-22 11:01:26
    是计算机系统的一个系统软件,能有效地组织和管理计算机系统中的硬件和软件资源,合理(公平对待不同用户程序,不发生“死锁”和“饥饿”)组织计算机工作流程,控制程序的执行,并向用户提供各种服务功能,使用户能...
  • 18 打开资源管理器---工具---文件夹选项---查看---使用简单文件共享(推荐)把前面的勾勾去掉,或者打开组策略编辑器---计算机配置----windows设置---本地策略---安全选项---网络访问:本地帐户的共享安全模式,把该...
  • 共享软件加密的一些误区

    千次阅读 2006-06-07 18:53:00
    本文发表于《电脑软件编程与维护》 2005年12期 作者:星轨(oRbIt) E_Mail :inte2000@163.com 输入您的搜索字词 提交搜索表单 共享软件通常是那种
  • 常用的HTTP请求头与响应头

    万次阅读 多人点赞 2018-02-07 17:04:47
    HTTP消息头是,在超文本传输协议( Hypertext Transfer Protocol ,HTTP)的请求和响应消息中,协议头部分的那些组件。HTTP消息头用来准确描述正在获取的资源、服务器或者客户端的行为,定义了HTTP事务中的具体...
  • Offer——知识点储备-Java基础

    万次阅读 多人点赞 2016-10-21 23:54:52
    Offer——知识点储备-Java基础网址来源: http://www.nowcoder.com/discuss/5949?type=0&order=0&pos=4&page=2 参考资料:(java方面的一些面试答案) ...
  • 他实现了用户间数据的共享,可存放全局变量。它开始于服务器的启动,直到服务器的关闭,在此期间,此对象将一直存在;这样在用户的前后连接或不同用户之间的连接中,可以对此对象的同一属性进行操作;在任何地方对此...
  • Offer——知识点储备-J2EE基础

    万次阅读 多人点赞 2016-10-27 09:00:36
    Offer——知识点储备-J2EE基础9.2 jdk 1.8的新特性(核心是Lambda 表达式)参考链接:http://www.bubuko.com/infodetail-690646.html (1)接口的默认方法 (给接口添加一个非抽象的方法实现,只需default关键字...
  • 常用设计模式

    万次阅读 多人点赞 2018-08-25 23:56:19
    保证一个类仅有一个实例,并提供一个访问它的全局访问点,避免一个全局使用的类频繁的创建和销毁,节省系统资源,提高程序效率。怎么创建唯一的实例?Java是这么创建实例的 Person p = new Person();但是这么创建会...
  • port常用和不常用端口一览表

    万次阅读 2016-07-22 13:52:46
    39 rlp resource protocol 资源定位协议 41 graphics graphics 图形 42 nameserver wins host name server wins 主机名服务 43 nicname who is "绰号" who is服务 44 mpm-flags mpm flags ...
  • Java中常用的设计模式

    万次阅读 多人点赞 2019-03-15 17:25:14
    优点:只有一个实例,节约了内存资源,提高了系统性能 缺点:  没有抽象层,不能扩展  职责过重,违背了单一性原则 首先我们写一个简单的单例类: public class Singleton { /* 持有私有静态实例...
  • 常用的RPC框架

    万次阅读 多人点赞 2018-02-05 15:32:11
    RPC(remote procedure call)是远程过程调用,比如两台服务器A和B,A服务器上部署一个应用,B服务器上部署一个应用,A服务器上的应用想调用B服务器上的应用提供的接口,由于不在一个内存空间,不能直接调用,所以...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 143,547
精华内容 57,418
关键字:

常用的共享资源指哪些