linux进程间同步

原文链接:http://www.jianshu.com/p/eba17ea42e78

为了能够有效的控制多个进程之间的沟通过程,OS必须提供一定的同步机制保证进程之间不会自说自话而是有效的协同工作。比如在共享内存的通信方式中,两个或者多个进程都要对共享的内存进行数据写入,那么怎么才能保证一个进程在写入的过程中不被其它的进程打断,保证数据的完整性呢?又怎么保证读取进程在读取数据的过程中数据不会变动,保证读取出的数据是完整有效的呢?常用的同步方式有:

  • 互斥锁
  • 条件变量
  • 读写锁
  • 记录锁(文件锁)
互斥锁

所谓互斥,从字面上理解就是互相排斥。因此互斥锁从字面上理解就是一个进程拥有了这个锁,它将排斥其它所有的进程访问被锁住的东西,其它的进程如果需要锁就只能等待,等待拥有锁的进程把锁打开后才能继续运行。互斥锁的主要特点是互斥锁的释放必须由上锁的进(线)程释放,如果拥有锁的进(线)程不释放,那么其它的进(线)程永远也没有机会获得所需要的互斥锁。互斥锁主要用于线程之间的同步

条件变量

上文中提到,对于互斥锁而言,如果拥有锁的进(线)程不释放锁,其它进(线)程永远没机会获得锁,也就永远没有机会继续执行后续的逻辑。在实际环境下,一个线程A需要改变一个共享变量X的值,为了保证在修改的过程中X不会被其它的线程修改,线程A必须首先获得对X的锁。现在假如A已经获得锁了,由于业务逻辑的需要,只有当X的值小于0时,线程A才能执行后续的逻辑,于是线程A必须把互斥锁释放掉,然后继续“忙等”。如下面的伪代码所示:

// get x lock
while(x <= 0){
     // unlock x ;
     // wait some time 
    // get x lock
}
// unlock x

这种方式是比较消耗系统的资源的,因为进程必须不停的主动获得锁、检查X条件、释放锁、再获得锁、再检查、再释放,一直到满足运行的条件的时候才可以。因此我们需要另外一种不同的同步方式,当线程X发现被锁定的变量不满足条件时会自动的释放锁并把自身置于等待状态,让出CPU的控制权给其它线程。其它线程此时就有机会去修改X的值,当修改完成后再通知那些由于条件不满足而陷入等待状态的线程。这是一种通知模型的同步方式,大大的节省了CPU的计算资源,减少了线程之间的竞争,而且提高了线程之间的系统工作的效率。这种同步方式就是条件变量。坦率的说,从字面意思上来将,“条件变量”这四个字是不太容易理解的。我们可以把“条件变量”看做是一个对象,一个铃铛,一个会响的铃铛。当一个线程在获得互斥锁之后,由于被锁定的变量不满足继续运行的条件时,该线程就释放互斥锁并把自己挂到这个“铃铛”上。其它的线程在修改完变量后,它就摇摇“铃铛”,告诉那些挂着的线程:“你们等待的东西已经变化了,都醒醒看看现在的它是否满足你们的要求。”于是那些挂着的线程就知道自己醒来看自己是否能继续跑下去了。

读写锁

互斥锁是排他性锁,条件变量出现后和互斥锁配合工作能够有效的节省系统资源并提高线程之间的协同工作效率。互斥锁的目的是为了独占,条件变量的目的是为了等待和通知。考虑一个文件有多个进程要读取其中的内容,但只有1个进程有写的需求。我们知道读文件的内容不会改变文件的内容,这样即使多个进程同时读相同的文件也没什么问题,大家都能和谐共存。当写进程需要写数据时,为了保证数据的一致性,所有读的进程就都不能读数据了,否则很可能出现读出去的数据一半是旧的,一半是新的状况,逻辑就乱掉了。
为了防止读数据的时候被写入新的数据,读进程必须对文件加上锁。现在假如我们有2个进程都同时读,如果我们使用上面的互斥锁和条件变量,当其中一个进程在读取数据的时候,另一个进程只能等待,因为它得不到锁。从性能上考虑,等待进程所花费的时间是完全的浪费,因为这个进程完全可以读文件内容而不会影响第一个,但是这个进程没有锁,所以它什么也做不了,只能等,等到花儿都谢了。所以呢,我们需要一种其它类型的同步方式来满足上面的需求,这就是读写锁。读写锁的出现能够有效的解决多进程并行读的问题。每一个需要读取的进程都申请读锁,这样大家互不干扰。当有进程需要写如数据时,首先申请写锁。如果在申请时发现有读(或者写)锁存在,则该写进程必须等待,一直等到所有的读(写)锁完全释放为止。读进程在读取之前首先申请读锁,如果所读数据被写锁锁定,则该读进程也必须等待读锁被释放位置。很自然的,多个读锁是可以共存的,但写锁是完全互相排斥的。
条件变量是另一种常用的变量。它也常常被保存为全局变量,并和互斥锁合作。

条件变量的另外一种解释

假设这样一个状况: 有100个工人,每人负责装修一个房间。当有10个房间装修完成的时候,老板就通知相应的十个工人一起去喝啤酒。
我们如何实现呢?老板让工人在装修好房间之后,去检查已经装修好的房间数。但多线程条件下,会有竞争条件的危险。也就是说,其他工人有可能会在该工人装修好房子和检查之间完成工作。采用下面方式解决:

/*mu:  global mutex,
   cond: global codition variable,
   num: global int
*/
mutex_lock(mu)
num = num + 1; /*worker build the room*/
if (num <= 10) { /*worker is within the first 10 to finish*/    
    cond_wait(mu, cond);     /*wait*/
    printf("drink beer");
}else if (num = 11) { 
  /*workder is the 11th to finish*/
  cond_broadcast(mu, cond);        /*inform the other 9 to wake up*/}
mutex_unlock(mu);

上面使用了条件变量。条件变量除了要和互斥锁配合之外,还需要和另一个全局变量配合(这里的num, 也就是装修好的房间数)。这个全局变量用来构成各个条件。

具体思路如下。我们让工人在装修好房间(num = num + 1)之后,去检查已经装修好的房间数( num < 10 )。由于mu被锁上,所以不会有其他工人在此期间装修房间(改变num的值)。如果该工人是前十个完成的人,那么我们就调用cond_wait()函数。cond_wait()做两件事情,一个是释放mu,从而让别的工人可以建房。另一个是等待,直到cond的通知。这样的话,符合条件的线程就开始等待。
当有通知(第十个房间已经修建好)到达的时候,condwait()会再次锁上mu。线程的恢复运行,执行下一句prinft("drink beer") (喝啤酒!)。从这里开始,直到mutex_unlock(),就构成了另一个互斥锁结构。
那么,前面十个调用cond_wait()的线程如何得到的通知呢?我们注意到elif if,即修建好第11个房间的人,负责调用cond_broadcast()。这个函数会给所有调用cond_wait()的线程放送通知,以便让那些线程恢复运行

条件变量特别适用于多个线程等待某个条件的发生。如果不使用条件变量,那么每个线程就需要不断尝试获得互斥锁并检查条件是否发生,这样大大浪费了系统的资源。

记录锁(文件锁)

为了增加并行性,我们可以在读写锁的基础上进一步细分被锁对象的粒度。比如一个文件中,读进程可能需要读取该文件的前1k个字节,写进程需要写该文件的最后1k个字节。我们可以对前1k个字节上读锁,对最后1k个自己上写锁,这样两个进程就可并发工作了。记录锁中的所谓“记录”其实是“内容”的概念。使用读写锁可以锁定一部分,而不是整个文件。文件锁可以认为是记录锁的一个特例,当使用记录锁锁定文件的所有内容时,此时的记录锁就可以称为文件锁了。

信号灯
一般意义下,信号灯是一个具有整数值的对象,它支持两种操作P()和V()。P()操作减少信号灯的值,如果新的信号灯的值小于0,则操作阻塞;V()操作增加信号灯的值,如果结果值大于或等于0,则唤醒一个等待的进程。通常用信号灯来做进程的同步和互斥。
最简单形式的信号灯就是内存中一个存储位置,它的取值可以由多个进程检验和设置。至少对于相关的进程来讲,对信号灯的检验和设置操作是不可中断的或者说是原子的:只要启动就不能终止。目前许多处理器提供检验和设置操作指令,如Intel处理器的sete等指令。检验和设置操作的结果是信号灯当前值与设置值的和,可以是正或者负。根据检验和设置操作的结果,一个进程可能必须睡眠直到信号灯的值被另一个进程改变。信号灯可以用于实现临界区(critical regions),就是重要的代码区,同一时刻只能有一个进程运行的代码区域。
比如,有许多协作的进程要从同一个数据文件中读写记录,并且希望对文件的访问必须严格地协调。那么,可以使用一个信号灯,将其初值设为1,用两个信号灯操作(P、V 操作),将进程中对文件操作的代码括起来。第一个信号灯操作检查并把信号灯的值减小,第二个操作检查并增加它。访问文件的第一个进程试图减小信号灯的值,如果它成功(事实上,它肯定成功),信号灯的取值将变为0,这个进程现在可以继续运行并使用数据文件。但是,如果此时另一个进程需要使用这个文件,它也试图减少信号灯的数值,它会失败,因为信号灯的值将要变成-1(但是,信号灯的值仍然保持为0,没有变成-1),这个进程会被挂起直到第一个进程处理完数据文件。当第一个进程处理完数据文件后,它会增加信号灯的值使其重新变为1。现在等待的进程会被唤醒,这次它减小信号灯的尝试会成功。

<think>嗯,用户让我介绍一下Linux进程间同步的方式,得详细一点。我需要先回忆一下Linux进程间同步的几种主要方法。首先,进程间同步通常用于协调多个进程对共享资源的访问,避免竞态条件和数据不一致的问题。常见的机制有哪些呢? 对了,有信号量(Semaphore)、文件锁(File Lock)、互斥锁(Mutex)、条件变量(Condition Variable)、管道(Pipe)、消息队列(Message Queue)、共享内存(Shared Memory)结合信号量,还有信号(Signal)。可能还有事件fd(eventfd)或者Futex(快速用户空间互斥锁)之类的。不过用户可能希望了解比较基础和常用的方法,所以应该先集中在这几个主要机制上。 接下来,每个机制的要点是什么?比如信号量,分为System V和POSIX两种,需要说明它们的区别和使用场景。文件锁的话,有建议性锁和强制性锁,但Linux下可能常用的是fcntl的锁。互斥锁通常在多线程中使用,但进程间的话可能需要放在共享内存中。条件变量同样,通常和互斥锁一起使用,但跨进程的话可能也需要共享内存。 管道和消息队列属于IPC机制,但也能用于同步。共享内存需要结合其他同步手段,比如信号量,来保证数据的一致性。信号的话,虽然主要用于通知,但也可以用来做简单的同步,比如SIGUSR1之类的,不过可能不够可靠,因为信号处理有异步问题。 然后,用户可能想知道每种方式的优缺点,适用场景。比如信号量适合复杂的同步控制,文件锁适合文件资源的同步,管道和消息队列适合传输数据的同时进行同步,共享内存速度最快但需要额外同步机制。 需要注意在解释每种机制时,给出具体的函数或命令,比如semget、semop、semctl属于System V信号量,而sem_init、sem_wait属于POSIX。文件锁用fcntl或者flock。互斥锁进程间使用时,需要放在共享内存,并且设置PTHREAD_PROCESS_SHARED属性。 另外,可能用户在实际应用中会遇到死锁或者竞争的问题,所以需要强调正确使用这些机制的重要性。比如信号量的PV操作要配对,文件锁的释放要及时,共享内存的同步必须谨慎处理。 有没有可能遗漏了一些机制?比如eventfd结合epoll也可以用于事件通知,或者Futex,但Futex更多是底层实现,用户可能不太直接使用。或者信号量、互斥锁的不同实现之间的区别需要说明清楚。 结构方面,应该分点介绍每个同步方式,包括原理、使用步骤、示例代码、优缺点和应用场景。这样用户能清晰理解每种方法的适用情况。比如: 1. 信号量 - System V 和 POSIX 的区别 - 示例代码片段(关键函数) - 适用场景:多进程需要控制资源访问数量 2. 文件锁 - 建议锁和强制锁 - fcntl的使用方法 - 适用场景:协调文件访问 3. 互斥锁和条件变量 - 需要共享内存和属性设置 - 示例说明 4. 管道和消息队列 - 半双工通信,阻塞特性用于同步 5. 共享内存+信号量 - 高效的数据共享,但需配合信号量 6. 信号 - 简单通知,但不可靠 最后总结对比,帮助用户根据需求选择合适的方式。需要注意术语的准确性,比如System V和POSIX的API区别,避免混淆。同时,代码示例可能用C语言,但用户可能也需要命令行工具的介绍,比如ipcs、ipcrm管理System V IPC对象。 还要检查是否有错误,比如互斥锁进程间使用的条件是否正确,是否需要PTHREAD_PROCESS_SHARED属性,共享内存的创建和附加步骤是否正确。确保每个机制的描述准确,不会误导用户。</think>### Linux 进程间同步方式详解 Linux进程间同步(IPC Synchronization)主要用于协调多个进程对共享资源的访问,避免竞态条件(Race Condition)和数据不一致。以下是常见的同步方式及其实现原理: --- #### 一、信号量(Semaphore) - **原理**:通过计数器控制资源访问权限,支持 **P操作(等待/减)** 和 **V操作(释放/加)**。 - **类型**: 1. **System V 信号量**:使用 `semget`, `semop`, `semctl` 等系统调用。 ```c int sem_id = semget(IPC_PRIVATE, 1, 0666 | IPC_CREAT); semop(sem_id, &P_operation, 1); // P操作 semop(sem_id, &V_operation, 1); // V操作 ``` 2. **POSIX 信号量**:更轻量,支持命名/未命名信号量(`sem_init`, `sem_wait`, `sem_post`)。 - **适用场景**:多进程资源计数(如控制最大并发数)。 --- #### 二、文件锁(File Lock) - **原理**:通过锁定文件区域实现互斥。 - **建议性锁**:依赖进程主动检查。 - **强制性锁**:由内核强制限制读写。 - **实现**: ```c struct flock lock; lock.l_type = F_WRLCK; // 写锁 fcntl(fd, F_SETLKW, &lock); // 阻塞加锁 fcntl(fd, F_UNLCK, &lock); // 释放锁 ``` - **适用场景**:协调多个进程对同一文件的访问。 --- #### 三、互斥锁(Mutex)与条件变量(Condition Variable) - **原理**: - **互斥锁**:确保同一时间仅一个进程访问临界区。 - **条件变量**:与互斥锁配合,实现等待/通知机制。 - **跨进程使用**: - 需将锁/条件变量放入 **共享内存**。 - 设置属性 `PTHREAD_PROCESS_SHARED`。 - **示例**: ```c pthread_mutexattr_t attr; pthread_mutexattr_init(&attr); pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED); pthread_mutex_init(mutex, &attr); ``` --- #### 四、管道(Pipe)与命名管道(FIFO) - **原理**:利用管道的 **阻塞特性** 实现同步。 - 无名管道(`pipe`):仅用于父子进程。 - 命名管道(`mkfifo`):允许任意进程通信。 - **同步逻辑**: - 读端阻塞等待数据,写端写入数据后触发读端继续执行。 --- #### 五、消息队列(Message Queue) - **原理**:通过发送/接收消息实现同步。 - **System V 消息队列**:`msgget`, `msgsnd`, `msgrcv`。 - **POSIX 消息队列**:`mq_open`, `mq_send`, `mq_receive`。 - **特点**:支持优先级,可携带结构化数据。 --- #### 六、共享内存 + 信号量 - **原理**: 1. **共享内存**(`shmget`, `mmap`):提供高效数据共享。 2. **信号量**:控制对共享内存的访问。 - **示例流程**: ```c // 1. 创建共享内存 int shm_id = shmget(IPC_PRIVATE, size, 0666); // 2. 附加到进程地址空间 char *shm_ptr = shmat(shm_id, NULL, 0); // 3. 使用信号量同步操作 sem_wait(&sem); // 进入临界区 // 读写共享内存... sem_post(&sem); // 离开临界区 ``` --- #### 七、信号(Signal) - **原理**:通过发送信号(如 `SIGUSR1`)通知其他进程。 - **局限性**:信号处理是异步的,不适合复杂同步逻辑。 - **示例**: ```c // 进程A注册信号处理函数 signal(SIGUSR1, handler); // 进程B发送信号 kill(pid, SIGUSR1); ``` --- ### 对比与选型建议 | 方式 | 性能 | 复杂度 | 适用场景 | |---------------|-------|--------|----------------------------| | 信号量 | 高 | 中 | 资源计数、复杂同步逻辑 | | 文件锁 | 中 | 低 | 文件访问互斥 | | 共享内存+信号量| 极高 | 高 | 高频数据交换(需同步) | | 管道/FIFO | 中 | 低 | 简单数据传递与同步 | | 消息队列 | 中 | 中 | 结构化数据传输 | --- ### 注意事项 1. **死锁预防**:确保锁的获取/释放成对出现。 2. **资源泄漏**:及时释放信号量、共享内存等资源。 3. **优先级反转**:高优先级进程避免长时间等待低优先级进程。 通过合理选择同步机制,可显著提升多进程程序的可靠性和性能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值