您好、欢迎来到现金彩票网!
当前位置:秒速牛牛开奖 > 同步通信量 >

linux内核剖析(十)进程间通信之-信号量semaphore

发布时间:2019-07-07 07:11 来源:未知 编辑:admin

  信号量的使用主要是用来保护共享资源,使得资源在一个时刻只有一个进程(线程)所拥有。

  信号量的值为正的时候,说明它空闲。所测试的线程可以锁定而使用它。若为0,说明它被占用,测试的线程要进入睡眠队列中,等待被唤醒。

  为了防止出现因多个程序同时访问一个共享资源而引发的一系列问题,我们需要一种方法,它可以通过生成并使用令牌来授权,在任一时刻只能有一个执行线程访问代码的临界区域。

  临界区域是指执行数据更新的代码需要独占式地执行。而信号量就可以提供这样的一种访问机制,让一个临界区同一时间只有一个线程在访问它,也就是说信号量是用来调协进程对共享资源的访问的。

  信号量是一个特殊的变量,程序对其访问都是原子操作,且只允许对它进行等待(即P(信号变量))和发送(即V(信号变量))信息操作。

  最简单的信号量是只能取0和1的变量,这也是信号量最常见的一种形式,叫做二进制信号量。而可以取多个正整数的信号量被称为通用信号量。这里主要讨论二进制信号量。

  由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),他们的行为是这样的:

  P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行

  V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1.

  举个例子,就是两个进程共享信号量sv,一旦其中一个进程执行了P(sv)操作,它将得到信号量,并可以进入临界区,使sv减1。而第二个进程将被阻止进入临界区,因为当它试图执行P(sv)时,sv为0,它会被挂起以等待第一个进程离开临界区域并执行V(sv)释放信号量,这时第二个进程就可以恢复执行。

  在学习信号量之前,我们必须先知道Linux提供两种信号量:

  用户态进程使用的信号量,这种信号量又分为POSIX信号量和SYSTEM V信号量。

  有名信号量,其值保存在文件中, 所以它可以用于线程也可以用于进程间的同步。无名信号量,其值保存在内存中。

  而SYSTEM V信号量则是一个或多个信号量的集合,它对应的是一个信号量结构体,这个结构体是为SYSTEM V IPC服务的,信号量只不过是它的一部分。常用于进程间同步。

  从使用的角度,System V信号量是复杂的,而Posix信号量是简单。比如,POSIX信号量的创建和初始化或PV操作就很非常方便。

  Linux内核的信号量在概念和原理上与用户态的System V的IPC机制信号量是一样的,但是它绝不可能在内核之外使用,它是一种睡眠锁。

  如果有一个任务想要获得已经被占用的信号量时,信号量会将其放入一个等待队列(它不是站在外面痴痴地等待而是将自己的名字写在任务队列中)然后让其睡眠。

  当持有信号量的进程将信号释放后,处于等待队列中的一个任务将被唤醒(因为队列中可能不止一个任务),并让其获得信号量。

  它与自旋锁的差异:由于争用信号量的进程在等待锁重新变为可用时会睡眠,所以信号量适用于锁会被长时间持有的情况;

  相反,锁被短时间持有时,使用信号量就不太适宜了,因为睡眠、维护等待队列以及唤醒所花费的开销可能比锁占用的全部时间表还要长;

  由于执行线程在锁被争用时会睡眠,所以只能在进程上下文中才能获得信号量锁,因为在中断上下文中是不能进行调试的;持有信号量的进行也可以去睡眠,当然也可以不睡眠,因为当其他进程争用信号量时不会因此而死锁;不能同时占用信号量和自旋锁,因为自旋锁不可以睡眠而信号量锁可以睡眠。相对而来说信号量比较简单,它不会禁止内核抢占,持有信号量的代码可以被抢占。

  信号量还有一个特征,就是它允许多个持有者,而自旋锁在任何时候只能允许一个持有者。

  当然我们经常遇到也是只有一个持有者,这种信号量叫二值信号量或者叫互斥信号量。允许有多个持有者的信号量叫计数信号量,在初始化时要说明最多允许有多少个持有者(Count值)

  信号量在创建时需要设置一个初始值,表示同时可以有几个任务可以访问该信号量保护的共享资源,初始值为1就变成互斥锁(Mutex),即同时只能有一个任务可以访问信号量保护的共享资源。

  当任务访问完被信号量保护的共享资源后,必须释放信号量,释放信号量通过把信号量的值加1实现,如果信号量的值为非正数,表明有任务等待当前信号量,因此它也唤醒所有等待该信号量的任务。

  内核信号量类似于自旋锁,因为当锁关闭着时,它不允许内核控制路径继续进行。然而,当内核控制路径试图获取内核信号量锁保护的忙资源时,相应的进程就被挂起。只有在资源被释放时,进程才再次变为可运行。

  只有可以睡眠的函数才能获取内核信号量;中断处理程序和可延迟函数都不能使用内核信号量。

  上面已经提到了内核信号量使用了等待队列wait_queue来实现阻塞操作。

  当某任务由于没有某种条件没有得到满足时,它就被挂到等待队列中睡眠。当条件得到满足时,该任务就被移出等待队列,此时并不意味着该任务就被马上执行,因为它又被移进工作队列中等待CPU资源,在适当的时机被调度。

  内核信号量是在内部使用等待队列的,也就是说该等待队列对用户是隐藏的,无须用户干涉。由用户真正使用的等待队列我们将在另外的篇章进行详解。

  该宏声明一个信号量name是直接将结构体中count值设置成n,此时信号量可用于实现进程间的互斥量。

  该函数也用于初始化一个互斥锁,但它把信号量sem的值设置为0,即一开始就处在已锁状态。

  注意:对于信号量的初始化函数Linux最新版本存在变化,如init_MUTEX和init_MUTEX_LOCKED等初始化函数目前新的内核中已经没有或者更换了了名字等

  因此建议以后在编程中遇到需要使用信号量的时候尽量采用sema_init(struct semaphore *sem, int val)函数,因为这个函数就目前为止从未发生变化。

  该函数用于获得信号量sem,它会导致睡眠,因此不能在中断上下文(包括IRQ上下文和softirq上下文)使用该函数。该函数将把sem的值减1,如果信号量sem的值非负,就直接返回,否则调用者将被挂起,直到别的任务释放该信号量才能继续运行。

  该函数功能与down类似,不同之处为,down不会被信号(signal)打断,但down_interruptible能被信号(比如Ctrl+C)打断,因此该函数有返回值来区分是正常返回还是被信号中断,如果返回0,表示获得信号量正常返回,如果被信号打断,返回-EINTR

  该函数试着获得信号量sem,如果能够立刻获得,它就获得该信号量并返回0,否则,表示不能获得信号量sem,返回值为非0值。因此,它不会导致调用者睡眠,可以在中断上下文使用。

  该函数释放信号量sem,即把sem的值加1,如果sem的值为非正数,表明有任务等待该信号量,因此唤醒这些等待者。

  在驱动程序中,当多个线程同时访问相同的资源时(驱动中的全局变量时一种典型的

  共享资源),可能会引发“竞态“,因此我们必须对共享资源进行并发控制。Linux内核中

  解决并发控制的最常用方法是自旋锁与信号量(绝大多数时候作为互斥锁使用)。

  如果一个读写信号量当前没有被写者拥有并且也没有写者等待读者释放信号量,那么任何读者都可以成功获得该读写信号量;

  否则,读者必须被挂起直到写者释放该信号量。如果一个读写信号量当前没有被读者或写者拥有并且也没有写者等待该信号量,那么一个写者可以成功获得该读写信号量,否则写者将被挂起,直到没有任何访问者。因此,写者是排他性的,独占性的。

  读写信号量有两种实现,一种是通用的,不依赖于硬件架构,因此,增加新的架构不需要重新实现它,但缺点是性能低,获得和释放读写信号量的开销大;另一种是架构相关的,因此性能高,获取和释放读写信号量的开销小,但增加新的架构需要重新实现。在内核配置时,可以通过选项去控制使用哪一种实现。

  读者调用该函数来得到读写信号量sem。该函数会导致调用者睡眠,因此只能在进程上下文使用。

  该函数类似于down_read,只是它不会导致调用者睡眠。它尽力得到读写信号量sem,如果能够立即得到,它就得到该读写信号量,并且返回1,否则表示不能立刻得到该信号量,返回0。因此,它也可以在中断上下文使用。

  写者使用该函数来得到读写信号量sem,它也会导致调用者睡眠,因此只能在进程上下文使用。

  该函数类似于down_write,只是它不会导致调用者睡眠。该函数尽力得到读写信号量,如果能够立刻获得,就获得该读写信号量并且返回1,否则表示无法立刻获得,返回0。它可以在中断上下文使用。

  读者使用该函数释放读写信号量sem。它与down_read或down_read_trylock配对使用。如果down_read_trylock返回0,不需要调用up_read来释放读写信号量,因为根本就没有获得信号量。

  该函数用于把写者降级为读者,这有时是必要的。因为写者是排他性的,因此在写者保持读写信号量期间,任何读者或写者都将无法访问该读写信号量保护的共享资源,对于那些当前条件下不需要写访问的写者,降级为读者将,使得等待访问的读者能够立刻访问,从而增加了并发性,提高了效率。

  读写信号量适于在读多写少的情况下使用,在linux内核中对进程的内存映像描述结构的访问就使用了读写信号量进行保护。

  当对低开销、短期、中断上下文加锁,优先考虑自旋锁;当对长期、持有锁需要休眠的任务,优先考虑信号量。

  无名信号量的创建就像声明一般的变量一样简单,例如:sem_t sem_id。然后再初始化该无名信号量,之后就可以放心使用了。

  无名信号量常用于多线程间的同步,同时也用于相关进程间的同步。也就是说,无名信号量必须是多个进程(线程)的共享变量,无名信号量要保护的变量也必须是多个进程(线程)的共享变量,这两个条件是缺一不可的。

  若pshared0 用于多个相关进程间的同步(即由fork产生的)

  若有1个或更多的线程或进程调用sem_wait阻塞在该信号量上,该函数返回两种值:

  注意:在这些函数中,只有sem_post是信号安全的函数,它是可重入函数

  无名信号量的常见用法是将要保护的变量放在sem_wait和sem_post中间所形成的

  如果想要某个特定的顺序的线个信号量来实现。例如下面的例程是线先执行完,然后线才继续执行,直至结束。

  说是相关进程,是因为本程序中共有2个进程,其中一个是另外一个的子进程(由fork产生)的。

  本来对于fork来说,子进程只继承了父进程的代码副本,mutex理应在父子进程中是相互独立的两个变量,但由于在初始化mutex的时候,由pshared = 1指定了mutex处于共享内存区域,所以此时mutex变成了父子进程共享的一个变量。此时,mutex就可以用来同步相关进程了。

  这决定了它的用途非常广:既可以用于线程,也可以用于相关进程间,甚至是不相关进程。

  由于有名信号量的值是保存在文件中的,所以对于相关进程来说,子进程是继承了父进程的文件描述符,那么子进程所继承的文件描述符所指向的文件是和父进程一样的,当然文件里面保存的有名信号量值就共享了。

  有名信号量在使用的时候,和无名信号量共享sem_wait和sem_post函数。

  区别是有名信号量使用sem_open代替sem_init,另外在结束的时候要像关闭文件一样去关闭这个有名信号量。

  当oflag = O_CREAT时,若name指定的信号量不存在时,则会创建一个,而且后面的mode和value参数必须有效。若name指定的信号量已存在,则直接打开该信号量,

  在做这个之前,要确定所有对这个有名信号量的引用都已经通过sem_close()函数关闭了,然后只需在退出或是退出处理函数中调用sem_unlink()去删除系统中的信号量,

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

  前面已经说过,有名信号量是位于共享内存区的,那么它要保护的资源也必须是位于共享内存区,只有这样才能被无相关的进程所共享。

  在下面这个例子中,服务进程和客户进程都使用shmget和shmat来获取得一块共享内存资源。然后利用有名信号量来对这块共享内存资源进行互斥保护。

  这是信号量值的集合,而不是单个信号量。相关的信号量操作函数由sys/ipc.h引用。

  ystem V 信号量在内核中维护,其中包括二值信号量 、计数信号量、计数信号量集。

  二值信号量 : 其值只有0、1 两种选择,0表示资源被锁,1表示资源可用;

  其中ipc_perm 结构是内核给每个进程间通信对象维护的一个信息结构,其成员包含所有者用户id,所有者组id、创建者及其组id,以及访问模式等;semid_ds结构体中的sem结构是内核用于维护某个给定信号量的一组值的内部结构,其结构定义:

  SYSTEM V信号量是SYSTEM V IPC(即SYSTEM V进程间通信)的组成部分,其他的有SYSTEM V消息队列,SYSTEM V共享内存。而关键字和IPC描述符无疑是它们的共同点,也使用它们,就不得不先对它们进行熟悉。这里只对SYSTEM V信号量进行讨论。

  IPC描述符相当于引用ID号,要想使用SYSTEM V信号量(或MSG、SHM),就必须用IPC描述符来调用信号量。而IPC描述符是内核动态提供的(通过semget来获取),用户无法让服务器和客户事先认可共同使用哪个描述符,所以有时候就需要到关键字KEY来定位描述符。

  某个KEY只会固定对应一个描述符(这项转换工作由内核完成),这样假如服务器和

  客户事先认可共同使用某个KEY,那么大家就都能定位到同一个描述符,也就能定位到同一个信号量,这样就达到了SYSTEM V信号量在进程间共享的目的。

  代表创建信号量的个数,如果只是访问而不创建则可以指定该参数为0,我们一旦创建了该信号量,就不能更改其信号量个数,只要你不删除该信号量,你就是重新调用该函数创建该键值的信号量,该函数只是返回以前创建的值,不会重新创建;

  指定该信号量的读写权限,当创建信号量时不许加IPC_CREAT ,若指定IPC_CREAT

  semget函数执行成功后,就产生了一个由内核维持的类型为semid_ds结构体的信号量集,返回semid就是指向该信号量集的引索。

  nsems0 : 创建一个信的信号量集,指定集合中信号量的数量,一旦创建就不能更改。

  返回的是一个称为信号量标识符的整数,semop和semctl函数将使用它。

  该集合中的每个信号量不初始化,这些结构是在semctl,用参数SET_VAL,SETALL初始化的。

  * 服务器可以指定关键字IPC_PRIVATE创建一个新IPC结构,将返回的标识符存放在某处(例如一个文件)以便客户机取用。关键字 IPC_PRIVATE保证服务器创建一个新IPC结构。这种技术的缺点是:服务器要将整型标识符写到文件中,然后客户机在此后又要读文件取得此标识符。

  IPC_PRIVATE关键字也可用于父、子关系进程。父进程指定 IPC_PRIVATE创建一个新IPC结构,所返回的标识符在fork后可由子进程使用。子进程可将此标识符作为exec函数的一个参数传给一个新程序。

  在一个公用头文件中定义一个客户机和服务器都认可的关键字。然后服务器指定此关键字创建一个新的IPC结构。这种方法的问题是该关键字可能已与一个 IPC结构相结合,在此情况下,get函数(msgget、semget或shmget)出错返回。服务器必须处理这一错误,删除已存在的IPC结构,然后试着再创建它。当然,这个关键字不能被别的程序所占用。

  客户机和服务器认同一个路径名和课题I D(课题I D是0 ~ 2 5 5之间的字符值) ,然后调用函数ftok将这两个值变换为一个关键字。这样就避免了使用一个已被占用的关键字的问题。

  使用ftok并非高枕无忧。有这样一种例外:服务器使用ftok获取得一个关键字后,该文件就被删除了,然后重建。此时客户端以此重建后的文件来ftok所获取的关键字就和服务器的关键字不一样了。所以一般商用的软件都不怎么用ftok。

  一般来说,客户机和服务器至少共享一个头文件,所以一个比较简单的方法是避免使用ftok,而只是在该头文件中存放一个大家都知道的关键字。

  第二个参数sops为一个结构体数组指针,结构体定义在sys/sem.h中,结构体如下

  当cmd ==GETALL时,就将信号量集的所有值返回到arg.array指定的数组中。

  有一个长度为N的缓冲池为生产者和消费者所共有,只要缓冲池未满,生产者便可将

  消息送入缓冲池;只要缓冲池未空,消费者便可从缓冲池中取走一个消息。生产者往缓冲池

http://4kci.com/tongbutongxinliang/400.html
锟斤拷锟斤拷锟斤拷QQ微锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷微锟斤拷
关于我们|联系我们|版权声明|网站地图|
Copyright © 2002-2019 现金彩票 版权所有