进程间通信笔记-记录上锁&&信号量&&共享内存

第九章 记录上锁

记录锁是读写锁的一种扩展类型,可用于亲缘关系或无亲缘关系的进程之间共享某个文件的读与写。被锁住的文件通过文件描述符进行访问,执行上锁的操作函数是fcntl,这种类型的锁通常在内核中维护。这些锁用于不同进程间上锁,而不是用于同一进程内的不同线程间

记录锁的功能是:一个进程正在读或修改文件的某个部分时,可以阻止其他进程修改同一文件区,即其锁定的是文件的一个区域或整个文件。记录锁有两种类型:共享读锁,独占写锁。基本规则是:多个进程在一个给定的字节上可以有一把共享的读锁,但在一个给定字节上的写锁只能有一个进程独用。即:如果在一个给定的字节上已经有一把读或多把读锁,则不能在该字节上再加写锁;如果在一个字节上已经有一把独占性的写锁,则不能再对它加任何读锁。

  采用Uinx打印假脱机处理系统举例说明记录锁的作用。打印假脱机处理系统使用技巧是给每台打印机准备一个文件,它只是一个单行执行的ASCII文本文件,其中还有待用的下一个序列号,需要给某个打印作业赋一个序列号的每个进程都得经历以下三个步骤:

(1)读序列号文件

(2)使用其中的序列号

(3)给序列号加1并写回文件中

存在问题:当某个进程在执行这个三个步骤时,另一个进程可能在执行同样的三个步骤,这将导致结果混乱。

解决办法:一个进程能够设置某个锁,以宣称没有其他进程能够访问相应的文件,直到第一个进程完成访问为止。

#include
int fcntl(int fd, int cmd, ... /* struct flock *arg */);

struct flock
{
    short l_type;   /* F_RDLCK, F_WRLCK, F_UNLCK */
    short l_whence; /* SEEK_SET, SEEK_CUR, SEEK_END */
  off_t l_start;  /* relative starting offset in bytes */
    off_t l_len;    /* #bytes; 0 means until end-of-file */
    pid_t l_pid;    /* PID return by F_GETLK */
};

cmd参数有三个值:

F_SETLK:获取或释放由arg指向的flock结构所描述的锁,若无法获取锁,则立刻返回一个EACCES或EAGAIN错误而不阻塞。
F_SETLKW :与F_SETLK不同的是,如果无法获取的锁,将阻塞直到获取锁为止,W即wait
F_GETLK:检查由arg指向的锁以确定是否有某个已存在的锁。若不存在锁在将arg指向的l_type成员设置为F_UNLCK,若存在锁,则返回arg所指向的flock结构信息。

flock结构描述锁的类型(读出锁或写入锁)以及待锁住的字节范围。锁定整个文件的两种方法:

(1)指定l_whence成员为SEEK_SET,l_start成员为0,l_len成员为0。(最常用方法)

(2)使用lseek把读写指针定位到文件,然后指定l_whence成员为SEEK_CUR、l_start成员为0,l_len成员为0。

注意:对于一个打开着某个文件的给定进程来说,当它关闭该文件的所有描述符或它本身终止时,与该文件关联的所有锁都被删除。锁不能通过fork由子进程继承。

记录上锁不应同标准io库一块使用,因为这个函数库会执行内部缓冲,为避免问题,使用write和read

劝告性上锁

  Posix记录上锁称为劝告性上锁,内核维护着已由各个进程上锁的所有文件的正确信息。针对于协作进程是足够了,但是对于非协作进程,结果是不可预料的。
强制性上锁
  有些系统提供了强制性锁(mandatory locking)。使用强制性锁后,内核将检查每个read和write请求,以验证其操作不会干扰由某个进程持有的某个锁。对于通常的阻塞式描述字,与某个强制性锁冲突的read或write将把调用进程投入睡眠,直到该锁释放为止。对于非阻塞式描述字,与某个强制性锁冲突的read或write将导致它们返回一个EAGAIN错误。对某个特定文件施行强制性锁,应满足:

组成员执行位必须关闭;
SGID位必须打开。
强制性锁不需要新的系统调用。虽然强制性上锁有一定作用,但多个进程在更新同一个文件时,仍然会导致混乱。进程之间还是需要某种上锁形式的协作。

读出者和写入者的优先级
在父进程持有读写锁进行读出时,子进程1比子进程2提前申请写入锁,但是子进程2申请读出锁立刻成功,子进程1等父进程和子进程2释放读出锁后才获取写入锁。这样一来,只要连续不断的发出读出锁请求,写入者就可能因获取不了写入锁而“挨饿”。

第二个例子:说明内核是以FIFO顺序准予上锁请求,而不管上锁请求的类型。

启动一个守护进程的唯一副本
记录上锁的一个常见用途是确保某个程序(守护程序)在任何时刻只有一个副本在运行

文件作锁用
利用O_CREAT和O_EXEL标志的open实现锁函数。如果文件已存在,则会返回一个EEXIST错误,表示已经上锁了。解锁就是删除这个文件

第十章 POSIX信号量

信号量(semaphore)是一种用于提供不同进程间或一个给定进程的不同线程间同步手段的原语。信号量的使用主要是用来保护共享资源,使得资源在一个时刻只有一个进程(线程)所拥有。信号量的值为正的时候,说明它空闲。所测试的线程可以锁定而使用它。若为0,说明它被占用,测试的线程要进入睡眠队列中,等待被唤醒。Posix信号量分为有名信号量和无名信号量(也叫基于内存的信号量)。

信号量除了可以像互斥锁那样使用外,它还具有互斥锁没有的特性。互斥锁必须由锁住它的线程解锁,信号量的挂出不必有执行过它的等待操作的同一个线程执行。

互斥锁、条件变量、信号量之间的差异:

  • 互斥锁必须由给他上锁的线程解锁,信号量的挂出不必有执行过他的等待操作的统一线程执行
  • 互斥锁要么被锁住,要么被解开(类似于二值信号量)
    有名信号量
#include
sem_t* sem_open(const char *name,int oflag /*mode_t mode,unsigned int value*/);
int sem_close(sem_t *sem);
int sem_unlink(const char *name);
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_post(sem_t *sem);
int sem_getvalue(sem_t *sem, int *valp);

当进程结束时,内核对其上任然打开的所有有名信号量自动执行信号量关闭操作。关闭信号量并没有将其从系统中删除。sem_unlink函数使信号量从系统中删除,其过程跟unlink类似

当某个线程使用完某个信号量,调用sem_post

基于内存的信号量

int sem_init(sem_t *sem, int shared, unsigned int value);
int sem_destroy(sem_t *sem);

如果shared=0,那么待初始化的信号量是在同一进程的各个线程间共享的,否则该信号量是在进程间共享的,此时该信号量必须存放在某种类型的共享内存区中,使得用它的进程能够访问该共享内存区。value是该信号量的初始值。

当信号量在当个进程内的各个线程间共享,这个信号量具有随进程的持续性。如果信号量在各个进程间共享,这个信号量就必须放在共享内存区域,因此只要共享内存区存在,这个信号量就会继续存在

多个生产者,单个消费者
多个生产者,多个消费者
多个缓冲区
把读写操作分割到两个线程中还需要线程间某种形式的通知。当缓冲区准备好写出时,读入线程必须通知写出者线程。同样,当缓冲区准备好重新读入时,写出者线程必须通知读入者线程。

双缓冲区,操作的速度不能走的快于最慢的速度

第12章 共享内存区介绍

共享内存可以说是最有用的进程间通信方式,也是最快的IPC形式。两个不同进程A、B共享内存的意思是,同一块物理内存被映射到进程A、B各自的进程地址空间。进程A可以即时看到进程B对共享内存中数据的更新,反之亦然。由于多个进程共享同一块内存区域,必然需要某种同步机制,互斥锁和信号量都可以。

采用共享内存通信的一个显而易见的好处是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝。对于像管道和消息队列等通信方式,则需要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次数据:一次从输入文件到共享内存区,另一次从共享内存区到输出文件。实际上,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时,再重新建立共享内存区域。而是保持共享区域,直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在解除映射时才写回文件的。因此,采用共享内存的通信方式效率是非常高的。

默认情况下通过fork派生的子进程并不与父进程共享内存区。

一旦内存映射了一个文件,就不在使用read write lseek来访问这个文件

#include
void* mmap ( void * addr , size_t len , int prot , int flags , int fd , off_t offset )

mamap函数把一个文件或一个Posix共享内存区对象映射到调用进程的地址空间。参数fd为即将映射到进程空间的文件描述字,一般由open()返回,同时,fd可以指定为-1,此时须指定flags参数中的MAP_ANON,表明进行的是匿名映射(不涉及具体的文件名,避免了文件的创建及打开,很显然只能用于具有亲缘关系的进程间通信)。

len是映射到调用进程地址空间的字节数,它从被映射文件开头offset个字节开始算起。prot 参数指定共享内存的访问权限。可取如下几个值的或:PROT_READ(可读) , PROT_WRITE (可写), PROT_EXEC (可执行), PROT_NONE(不可访问)。

为了从某个进程的地址空间删除一个映射关系,我们调用munmap

#include
int munmap(void *addr, size_t len);
int msync(void *addr, size_t len);

addr是调用mmap()时返回的地址,len是映射区的大小。当映射关系解除后,对原来映射地址的访问将导致段错误发生。如果被映射区使用MAP_PRIVATE标志映射的,那么调用进程对他所做的变动都会被丢弃。

msync用于同步硬盘中文件内容与内存映射区内容。
不是所有文件都能进行内存映射。

4.4BSD提供匿名内存映射,它避免了文件的创建和打开。mmap的flag参数指定为MAP_SHARED|MAP_ANON,把fd参数设置为-1.这样的内存区初始化为0.

SVR4提供/dev/zero设备文件,可以open它之后在mmap调用中使用得到的描述符。从该设备读返回的字节全为0,往这个文件写的任何字节都被丢弃。

访问内存映射的对象
内存映射到一个普通文件,内存映射区的大小通常就是这个文件的大小。文件大小与内存映射区的大小可以不同。
进程间通信笔记-记录上锁&&信号量&&共享内存_第1张图片

第十三章 posix共享内存区

Posix提供了两种在无亲缘关系进程间共享内存区的方法:
(1)内存映射文件:先有open函数打开,然后调用mmap函数把得到的描述符映射到当前进程地址空间中的一个文件(第十二章所用到的就是这种方法)。

(2)共享内存区对象:先有shm_open打开一个Posix IPC名字(也可以是文件系统中的一个路径名),然后调用mmap将返回的描述符映射到当前进程的地址空间。

两种方法多需要调用mmap,差别在于作为mmap的参数之一的描述符的获取手段。

Posix共享内存区涉及以下两个步骤要求:

(1)指定一个名字参数调用shm_open,以创建一个新的共享内存区对象或打开一个已经存在的共享内存区对象。

(2)调用mmap把这个共享内存区映射到调用进程的地址空间。

注意:mmap用于把一个内存区对象映射到调用进程地址空间的是该对象的一个已经打开描述符。

#include
int shm_open(const char *name, int oflag, mode_t mode);
int shm_unlink(const char *name);

shm_unlink函数删除一个共享内存区对象的名字。删除一个名字不会影响对于其底层之城对象的现有引用,知道对于这个对象的引用全部关闭为止,删除一个名字仅仅放置后续的open、mq_open、或者sem_open调用取得成功

#include
int ftruncate(int fd, off_t length);
#include 
#include
int fstat(int fd, struct stat *buf);

ftruncate函数可以修改普通文件和共享内存区对象的大小。对于共享内存区对象,函数把对象的大小设置为length字节
当打开一个已存在的共享内存区对象,可以调用fstat函数获取有关这个对象的信息。

同一共享内存区对象映射到不同进程的地址空间,起始地址可以不一样

你可能感兴趣的:(Linux)