Linux下进程间通信方式与线程间通讯

本文深入解析了进程间通信的各种机制,包括管道、消息队列、共享内存、信号量和套接字,阐述了它们的工作原理、优缺点及应用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

线程和进程的基本问题

https://www.cnblogs.com/wuyepeng/p/9859341.html

线程和进程的比较

1.进程是资源分配的基本单位。

2.线程是独立调度的基本单位。

3.在同一个进程中,线程的切换不会引起进程的切换。在不同的进程中进行线程切换,如从一个进程中的线程切换到另一个进程中的线程会引起进程的切换。

4.一个进程至少包含一个线程,线程共享整个进程的资源

5.进程结束后它所拥有的所有线程都将被销毁,但是线程结束并不响应其他线程

6.线程运行时一般都需要同步和互斥,因为他们共享进程的所有资源

7.线程有自己的私有TCB,线程id,进程也有自己的PCB,进程id

8.在开销方面:每个进程都有独立的数据空间,进程之间的切换会有较大的开销,线程是共享数据空间的,线程之间的切换开销会小很多。创建一个进程需要给他申请内存空间,创建线程则不需要,相比之下创建进程比创建线程的开销大很多。

线程可分为用户级线程和内核级线程

 

线程和进程的比较

https://www.cnblogs.com/wuyepeng/p/9729789.html

一个程序就是一个进程,而一个程序中的多个任务则被称为线程。进程是表示资源分配的基本单位,又是调度运行的基本单位。线程是进程中执行运算的最小单位,亦即执行处理机调度的基本单位。

进程和线程的关系

(1)一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。线程是操作系统可识别的最小执行和调度单位。
(2)资源分配给进程,同一进程的所有线程共享该进程的所有资源。同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量。
(3)处理机分给线程,即真正在处理机上运行的是线程。
(4)线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。
如果把上课的过程比作进程,那么每个学生就是一个线程,他们共享教室,即线程共享进程的内存空间。每一个时刻,只能一个学生问老师问题,老师回答完毕,轮到下一个。即线程在一个时间片内占有cpu

一个关于进程和线程的例子:

  试想当两个程序员在两个独立的办公室一起做一个软件项目,他们可以安静地工作、不互相干扰,并且他们人手一套参考手册。但是,他们沟通起来就有些困难,比起可以直接互相交谈,他们必须使用电话、电子邮件或到对方的办公室进行直接交流。并且,管理两个办公室需要有一定的经费支出,还需要购买多份参考手册。

  假设,让开发人员同在一间办公室办公,他们可以自由的对某个应用程序设计进行讨论,也可以在纸或白板上轻易的绘制图表,对设计观点进行辅助性阐释。现在,你只需要管理一个办公室,只要有一套参考资料就够了。遗憾的是,开发人员可能难以集中注意力,并且还可能存在资源共享的问题(比如,“参考手册哪去了?”)

  以上两种方法,描绘了并发的两种基本途径。每个开发人员代表一个线程,每个办公室代表一个处理器。第一种途径是有多个单线程的进程,这就类似让每个开发人员拥有自己的办公室,而第二种途径是在单一进程里有多个线程,如同一个办公室里有两个开发人员。让我们在一个应用程序中简单的分析一下这两种途径。

 

  线程很像轻量级的进程:每个线程互相独立运行,且线程可以在不同的指令序列中运行。但是,进程中的所有线程都共享地址空间(堆上的空间共享,栈上的空间独立),并且所有线程访问到大部分数据——全局变量仍然是全局的,指针、对象的引用或数据可以在线程之间传递。虽然,进程之间通常共享内存,但是这种共享通常也是很难建立,且难以管理。因为,同一数据的内存地址在不同的进程中是不相同。

 

  地址空间共享,以及缺少线程间数据的保护,使得操作系统的记录工作量减少,所以使用多线程相关的开销远小于使用多个进程,不过,共享内存的灵活性是有代价的:如果数据要被多个线程访问,那么程序员必须确保每个线程所访问到的数据是一致的。

 

进程通信和线程通信

https://www.cnblogs.com/wuyepeng/p/9746405.html

每个进程有自己的地址空间。两个进程中的地址即使值相同,实际指向的位置也不同。进程间通信一般通过操作系统的公共区进行。同一进程中的线程因属同一地址空间,可直接通信。进程不仅是系统内部独立运行的实体,而且是独立竞争资源的实体。
线程也被称为轻权进程,同一进程的线程共享全局变量和内存,使得线程之间共享数据很容易也很方便,但会带来某些共享数据的互斥问题。许多程序为了提高效率也都是用了线程来编写。父子进程的派生是非常昂贵的,而且父子进程的通讯需要ipc或者其他方法来实现,比较麻烦。而线程的创建就花费少得多,并且同一进程内的线程共享全局存储区,所以通讯方便。线程的缺点也是由它的优点造成的,主要是同步,异步和互斥的问题,值得在使用的时候小心设计。


只有进程间需要通信,同一进程的线程share地址空间,没有通信的必要,但要做好同步/互斥mutex,保护共享的全局变量。线程拥有自己的独立的栈。同步/互斥是原语primitives.
进程间通信无论是信号,管道pipe还是共享内存都是由操作系统保证的,是系统调用.


线程间通信:由于多线程共享地址空间和数据空间,所以多个线程间的通信是一个线程的数据可以直接提供给其他线程使用,而不必通过操作系统(也就是内核的调度)。

进程间的通信则不同,它的数据空间的独立性决定了它的通信相对比较复杂,需要通过操作系统。以前进程间的通信只能是单机版的,现在操作系统都继承了基于套接字(socket)的进程间的通信机制。这样进程间的通信就不局限于单台计算机了,实现了网络通信。

一、进程间的通信方式
# 匿名管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
# 有名管道 (namedpipe) : 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
# 信号量(semophore ) : 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
# 消息队列( messagequeue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
# 信号 (sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
# 共享内存(shared memory ) :共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
# 套接字(socket ) : 套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。


二、线程间的通信方式

# 锁机制:包括互斥锁、条件变量、读写锁
*互斥锁提供了以排他方式防止数据结构被并发修改的方法。
*读写锁允许多个线程同时读共享数据,而对写操作是互斥的。
*条件变量可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。
# 信号量机制(Semaphore):包括无名线程信号量和命名线程信号量
# 信号机制(Signal):类似进程间的信号处理
线程间的通信目的主要是用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制。

 

Linux下进程间通信方式——共享内存

https://www.cnblogs.com/wuyepeng/p/9748889.html

1.什么是共享内存?

共享内存就是允许两个或多个进程共享一定的存储区就如同 malloc() 函数向不同进程返回了指向同一个物理内存区域的指针。当一个进程改变了这块地址中的内容的时候,其它进程都会察觉到这个更改。因为数据不需要在客户进程和服务进程之间复制,数据直接写到内存,不用若干次数据拷贝,所以这是最快的一种IPC

注:共享内存没有任何的同步与互斥机制,所以要使用信号量来实现对共享内存的存取的同步。

共享内存特点和优势

 

当中共享内存的大致原理相信我们可以看明白了,就是让两个进程地址通过页表映射到同一片物理地址以便于通信,你可以给一个区域里面写入数据,理所当然你就可以从中拿取数据,这也就构成了进程间的双向通信,而且共享内存是IPC通信当中传输速度最快的通信方式没有之一,理由很简单,客户进程和服务进程传递的数据直接从内存里存取、放入,数据不需要在两进程间复制,没有什么操作比这简单了。再者用共享内存进行数据通信,它对数据也没啥限制。

最后就是共享内存的生命周期随内核。即所有访问共享内存区域对象的进程都已经正常结束,共享内存区域对象仍然在内核中存在(除非显式删除共享内存区域对象),在内核重新引导之前,对该共享内存区域对象的任何改写操作都将一直保留;简单地说,共享内存区域对象的生命周期跟系统内核的生命周期是一致的,而且共享内存区域对象的作用域范围就是在整个系统内核的生命周期之内。

 

缺陷

但是,共享内存也并不完美,共享内存并未提供同步机制,也就是说,在一个服务进程结束对共享内存的写操作之前,并没有自动机制可以阻止另一个进程(客户进程)开始对它进行读取。这明显还达不到我们想要的,我们不单是在两进程间交互数据,还想实现多个进程对共享内存的同步访问,这也正是使用共享内存的窍门所在。基于此,我们通常会用平时常谈到和用到 信号量来实现对共享内存同步访问控制。

 

与共享内存有关的函数

所有的函数共用头文件

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

3.1 创建共享内存——>shmget() 函数(share memory get,类比CreateFileMapping or OpenFileMapping)

int shmget(key_t key, size_t size, int shmflg);
                //成功返回共享内存的ID,出错返回-1  

(1)第一个参数key是长整型(唯一非零),系统建立IPC通讯 ( 消息队列、 信号量和 共享内存) 时必须指定一个ID值。通常情况下,该id值通过ftok函数得到,由内核变成标识符,要想让两个进程看到同一个信号集,只需设置key值不变就可以。

 (2)第二个参数size指定共享内存的大小,它的值一般为一页大小的整数倍(未到一页,操作系统向上对齐到一页,但是用户实际能使用只有自己所申请的大小)。

 (3)第三个参数shmflg是一组标志,创建一个新的共享内存,将shmflg 设置了IPC_CREAT标志后,共享内存存在就打开。而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的共享内存,如果共享内存已存在,返回一个错误。一般我们会还或上一个文件权限

 

3.2操作共享内存———>shmctl()函数(share memory control)

int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
                //成功返回0,出错返回-1

(1)第一个参数,shm_id是shmget函数返回的共享内存标识符。

(2)第二个参数,cmd是要采取的操作,它可以取下面的三个值 :    

IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值。    

IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值    

IPC_RMID:删除共享内存段

(3)第三个参数,buf是一个结构指针,它指向共享内存模式和访问权限的结构。 shmid_ds结构至少包括以下成员 

struct shmid_ds 
{ 
    uid_t shm_perm.uid; 
    uid_t shm_perm.gid; 
    mode_t shm_perm.mode; 
};

3.3挂接操作———>shmat()函数(share memory attach,类比MapViewOfFile)

创建共享存储段之后,将进程连接到它的地址空间

void *shmat(int shm_id, const void *shm_addr, int shmflg);
                    //成功返回指向共享存储段的指针,出错返回-1 

(1)第一个参数,shm_id是由shmget函数返回的共享内存标识。

(2)第二个参数,shm_addr指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址。

(3)第三个参数,shm_flg是一组标志位,通常为0

3.4分离操作———>shmdt()函数(share memory dettach,类比UnmapViewOfFile)

该操作不从系统中删除标识符和其数据结构,要显示调用shmctl(带命令IPC_RMID)才能删除它

int shmdt(const void *shmaddr);
            //成功返回0,出错返回-1

(1)addr参数是以前调用shmat时的返回值

 

4.模拟实现进程间的通信方式———>共享内存

三、使用共享内存进行进程间通信

说了这么多,又到了实战的时候了。下面就以两个不相关的进程来说明进程间如何通过共享内存来进行通信。其中一个文件shmread.c创建共享内存,并读取其中的信息,另一个文件shmwrite.c向共享内存中写入数据。为了方便操作和数据结构的统一,为这两个文件定义了相同的数据结构,定义在文件shmdata.c中。结构shared_use_st中的written作为一个可读或可写的标志,非0:表示可读,0表示可写,text则是内存中的文件。

shmdata.h的源代码如下:

#ifndef _SHMDATA_H_HEADER
#define _SHMDATA_H_HEADER
#define TEXT_SZ 2048
struct shared_use_st
{  
    int written;//作为一个标志,非0:表示可读,0表示可写 
    char text[TEXT_SZ];//记录写入和读取的文本
};
#endif

  源文件shmread.c的源代码如下:

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/shm.h>
#include "shmdata.h"
int main()
{  
    int running = 1;//程序是否继续运行的标志  
    void *shm = NULL;//分配的共享内存的原始首地址   
    struct shared_use_st *shared;//指向shm   
    int shmid;//共享内存标识符 
    
    //创建共享内存   
    shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666|IPC_CREAT);
    if(shmid == -1)
    {      
        fprintf(stderr, "shmget failed\n");
        exit(EXIT_FAILURE);
    }   
    
    //将共享内存连接到当前进程的地址空间
    shm = shmat(shmid, 0, 0);
    if(shm == (void*)-1)   
    {  
        fprintf(stderr, "shmat failed\n"); 
        exit(EXIT_FAILURE);
    }  
    printf("\nMemory attached at %X\n", (int)shm);  

    //设置共享内存   
    shared = (struct shared_use_st*)shm;   
    shared->written = 0;
    while(running)//读取共享内存中的数据 
    {       
        //有数据可读取       
        if(shared->written != 0)
        {      
            printf("You wrote: %s", shared->text);      
            sleep(rand() % 3);          //读取完数据,设置written使共享内存可写
            shared->written = 0;         //输入了end,退出循环(程序)  
            if(strncmp(shared->text, "end", 3) == 0)    
                running = 0;       
        }      
        else
        {
            //没有数据可读
            sleep(1);  
        }
    }   
    
    //把共享内存从当前进程中分离
    if(shmdt(shm) == -1)   
    {      
        fprintf(stderr, "shmdt failed\n");     
        exit(EXIT_FAILURE);
    }   

    //删除共享内存   
    if(shmctl(shmid, IPC_RMID, 0) == -1)   
    {  
        fprintf(stderr, "shmctl(IPC_RMID) failed\n");  
        exit(EXIT_FAILURE);
    }  
    exit(EXIT_SUCCESS);
}

  源文件shmwrite.c的源代码如下:

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/shm.h>
#include "shmdata.h"
int main()
{  
    int running = 1;   
    void *shm = NULL;  
    struct shared_use_st *shared = NULL;
    char buffer[BUFSIZ + 1];//用于保存输入的文本
    int shmid;  

    //创建共享内存
    shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666|IPC_CREAT);
    if(shmid == -1)
    {  
        fprintf(stderr, "shmget failed\n");
        exit(EXIT_FAILURE);
    }   

    //将共享内存连接到当前进程的地址空间
    shm = shmat(shmid, (void*)0, 0);   
    if(shm == (void*)-1)
    {  
        fprintf(stderr, "shmat failed\n");     
        exit(EXIT_FAILURE);
    }  
    printf("Memory attached at %X\n", (int)shm);    

    //设置共享内存   
    shared = (struct shared_use_st*)shm;   
    while(running)//向共享内存中写数据  
    {       
        //数据还没有被读取,则等待数据被读取,不能向共享内存中写入文本       
        while(shared->written == 1)     
        {          
            sleep(1);      
            printf("Waiting...\n");
        }       

        //向共享内存中写入数据       
        printf("Enter some text: ");       
        fgets(buffer, BUFSIZ, stdin);      
        strncpy(shared->text, buffer, TEXT_SZ);      
        
        //写完数据,设置written使共享内存段可读       
        shared->written = 1;     

        //输入了end,退出循环(程序)  
        if(strncmp(buffer, "end", 3) == 0)         
            running = 0;   
    }   

    //把共享内存从当前进程中分离
    if(shmdt(shm) == -1)   
    {      
        fprintf(stderr, "shmdt failed\n");     
        exit(EXIT_FAILURE);
    }  
    sleep(2);  
    exit(EXIT_SUCCESS);
}

  结果截图如下:

 

分析:

1、程序shmread创建共享内存,然后将它连接到自己的地址空间。在共享内存的开始处使用了一个结构struct_use_st。该结构中有个标志written,共享内存中的written被设置为0,程序等待其他进程写数据。当它不为0时,表示有数据可读,程序就从共享内存中读取数据并输出,然后重置设置共享内存中的written为0,即让其可被shmwrite进程写入数据。

2、程序shmwrite取得共享内存并连接到自己的地址空间中。检查共享内存中的written,是否为0,若不是,表示共享内存中的数据还没有被读,则等待其他进程读取完成,并提示用户等待。若共享内存的written为0,表示其他进程对共享内存进行读取完毕,需要写数据,则提示用户输入文本,写完并再次设置共享内存中的written为1,表示写完成,其他进程可对共享内存进行可以进行读操作。

 

四、关于前面的例子的安全性讨论

这个程序是不安全的,当有多个程序同时向共享内存中读写数据时,问题就会出现。可能你会认为,可以改变一下written的使用方式,例如,只有当written为0时进程才可以向共享内存写入数据,而当一个进程只有在written不为0时才能对其进行读取,同时把written进行加1操作,读取完后进行减1操作。这就有点像文件锁中的读写锁的功能。咋看之下,它似乎能行得通。但是这都不是原子操作,所以这种做法是行不能的。试想当written为0时,如果有两个进程同时访问共享内存,它们就会发现written为0,于是两个进程都对其进行写操作,显然不行。当written为1时,有两个进程同时对共享内存进行读操作时也是如些,当这两个进程都读取完是,written就变成了-1.

要想让程序安全地执行,就要有一种进程同步的进制,保证在进入临界区的操作是原子操作。例如,可以使用前面所讲的信号量来进行进程的同步。因为信号量的操作都是原子性的。

 

五、使用共享内存的优缺点

1、优点:我们可以看到使用共享内存进行进程间的通信真的是非常方便,而且函数的接口也简单,数据的共享还使进程间的数据不用传送,而是直接访问内存,也加快了程序的效率。同时,它也不像匿名管道那样要求通信的进程有一定的父子关系。

 

2、缺点:共享内存没有提供同步的机制,这使得我们在使用共享内存进行进程间通信时,往往要借助其他的手段来进行进程间的同步工作。

 

Linux下进程间通信方式——信号量(Semaphore)

https://www.cnblogs.com/wuyepeng/p/9748552.html

1.信号量

信号量本质上是一个计数器(不设置全局变量是因为进程间是相互独立的,而这不一定能看到,看到也不能保证++引用计数为原子操作),用于多进程对共享数据对象的读取,它和管道有所不同,它不以传送数据为主要目的,它主要是用来保护共享资源(信号量也属于临界资源),使得资源在一个时刻只有一个进程独享。

2.信号量的工作原理

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

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

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

在信号量进行PV操作时都为原子操作(因为它需要保护临界资源)

注:原子操作:单指令的操作称为原子的,单条指令的执行是不会被打断的

3.二元信号量

二元信号量(Binary Semaphore)是最简单的一种锁(互斥锁),它只用两种状态:占用与非占用。所以它的引用计数为1。

4.进程如何获得共享资源

(1)测试控制该资源的信号量

(2)信号量的值为正,进程获得该资源的使用权,进程将信号量减1,表示它使用了一个资源单位

(3)若此时信号量的值为0,则进程进入挂起状态(进程状态改变),直到信号量的值大于0,若进程被唤醒则返回至第一步。

注:信号量通过同步与互斥保证访问资源的一致性。

5.与信号量相关的函数

所有函数共用头文件

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h> 

5.1创建信号量

  

int semget(key_t key,int nsems,int flags)//返回:成功返回信号集ID,出错返回-1

(1)第一个参数key是长整型(唯一非零),系统建立IPC通讯 ( 消息队列、 信号量和 共享内存) 时必须指定一个ID值。通常情况下,该id值通过ftok函数得到,由内核变成标识符,要想让两个进程看到同一个信号集,只需设置key值不变就可以。

(2)第二个参数nsem指定信号量集中需要的信号量数目,它的值几乎总是1。

(3)第三个参数flag是一组标志,当想要当信号量不存在时创建一个新的信号量,可以将flag设置为IPC_CREAT与文件权限做按位或操作。 
设置了IPC_CREAT标志后,即使给出的key是一个已有信号量的key,也不会产生错误。而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误。一般我们会还或上一个文件权限

5.2删除和初始化信号量

int semctl(int semid, int semnum, int cmd, ...);

如有需要第四个参数一般设置为union semnu arg;定义如下

union semun
{ 
    int val;  //使用的值
    struct semid_ds *buf;  //IPC_STAT、IPC_SET 使用的缓存区
    unsigned short *arry;  //GETALL,、SETALL 使用的数组
    struct seminfo *__buf; // IPC_INFO(Linux特有) 使用的缓存区
};

(1)sem_id是由semget返回的信号量标识符

(2)semnum当前信号量集的哪一个信号量

(3)cmd通常是下面两个值中的其中一个 
SETVAL:用来把信号量初始化为一个已知的值。p 这个值通过union semun中的val成员设置,其作用是在信号量第一次使用前对它进行设置。 
IPC_RMID:用于删除一个已经无需继续使用的信号量标识符,删除的话就不需要缺省参数,只需要三个参数即可。

5.3改变信号量的值

int semop(int semid, struct sembuf *sops, size_t nops);    //semaphore operate 

(1)nsops:进行操作信号量的个数,即sops结构变量的个数,需大于或等于1。最常见设置此值等于1,只完成对一个信号量的操作

(2)sembuf的定义如下:  

struct sembuf{ 
    short sem_num;   //除非使用一组信号量,否则它为0 
    short sem_op;   //信号量在一次操作中需要改变的数据,通常是两个数,                                        
                    //一个是-1,即P(等待)操作, 
                    //一个是+1,即V(发送信号)操作。 
    short sem_flg; //通常为SEM_UNDO,使操作系统跟踪信号量, 
                  //并在进程没有释放该信号量而终止时,操作系统释放信号量 
}; 

5.4sembuf中sem_flg的设置问题

通常设置为SEM_UNDO,使操作系统跟踪信号量, 并在进程没有释放该信号量而终止时,操作系统释放信号量 ,例如在二元信号量中,你不释放该信号量 而异常退出,就会导致别的进程一直申请不到信号量,而一直处于挂起状态。

是否设置sem_flg为SEM_UNDO的区别

6.模拟实现信号量实现进程间通信

用一个上课的时候讲过的简单的例子来说明一下:

#include <stdio.h>
#include <stdlib.h>
#include <semaphore.h>
#include <errno.h>
#define total 20
sem_t remain, apple, pear, mutex;
static unsigned int vremain = 20, vapple = 0, vpear = 0;
void *father(void *);
void *mather(void *);
void *son(void *);
void *daughter(void *);
void print_sem();
int main()
{  
    pthread_t fa, ma, so, da;  
    sem_init(&remain, 0, total);//总数初始化为20 
    sem_init(&apple, 0, 0);//盆子中苹果数, 开始为0  
    sem_init(&pear, 0, 0);//盆子中梨子数, 开始为0   
    sem_init(&mutex, 0, 1);//互斥锁, 初始为1 
    pthread_create(&fa, NULL, &father, NULL);  
    pthread_create(&ma, NULL, &mather, NULL);  
    pthread_create(&so, NULL, &son, NULL); 
    pthread_create(&da, NULL, &daughter, NULL);    
    for(;;);
}
void *father(void *arg)
{  
    while(1)
    {      
        sem_wait(&remain);     
        sem_wait(&mutex);      
        printf("父亲: 放苹果之前, 剩余空间=%u, 苹果数=%u\n", vremain--, vapple++);
        printf("父亲: 放苹果之后, 剩余空间=%u, 苹果数=%u\n", vremain, vapple);
        sem_post(&mutex);      
        sem_post(&apple);  
        sleep(1);  
    }
}
void *mather(void *arg)
{  
    while(1)
    {      
        sem_wait(&remain);     
        sem_wait(&mutex);      
        printf("母亲: 放梨子之前, 剩余空间=%u, 梨子数=%u\n", vremain--, vpear++);
        printf("母亲: 放梨子之后, 剩余空间=%u, 梨子数=%u\n", vremain, vpear);
        sem_post(&mutex);  
        sem_post(&pear);   
        sleep(2);  
    }
}
void *son(void *arg)
{  
    while(1)
    {      
        sem_wait(&pear);   
        sem_wait(&mutex);   
        printf("儿子: 吃梨子之前, 剩余空间=%u, 梨子数=%u\n", vremain++, vpear--);
        printf("儿子: 吃梨子之后, 剩余空间=%u, 梨子数=%u\n", vremain, vpear);
        sem_post(&mutex);  
        sem_post(&remain);     
        sleep(3);
    }
}
void *daughter(void *arg)
{  
    while(1)
    {  
        sem_wait(&apple);  
        sem_wait(&mutex);
        printf("女儿: 吃苹果之前, 剩余空间=%u, 苹果数=%u\n", vremain++, vapple--);
        printf("女儿: 吃苹果之前, 剩余空间=%u, 苹果数=%u\n", vremain, vapple);   
        sem_post(&mutex);  
        sem_post(&remain); 
        sleep(3);  
    }
}
void print_sem()
{  
    int val1, val2, val3;
    sem_getvalue(&remain, &val1);  
    sem_getvalue(&apple, &val2);   
    sem_getvalue(&pear, &val3);
    printf("Semaphore: remain:%d, apple:%d, pear:%d\n", val1, val2, val3);
}

  编译时记得要加上  -lpthread  否则sem_ 都是未定义的。

因为在主函数中做了一个死循环所以这个程序会一直跑下去

 

 

Linux下进程间通信方式——使用消息队列

https://www.cnblogs.com/wuyepeng/p/9748728.html

一、什么是消息队列

消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。  每个数据块都被认为含有一个类型,接收进程可以独立地接收含有不同类型的数据结构。我们可以通过发送消息来避免命名管道的同步和阻塞问题。但是消息队列与命名管道一样,每个数据块都有一个最大长度的限制。

 

Linux用宏MSGMAX和MSGMNB来限制一条消息的最大长度和一个队列的最大长度。

 

二、在Linux中使用消息队列

Linux提供了一系列消息队列的函数接口来让我们方便地使用它来实现进程间的通信。它的用法与其他两个System V PIC机制,即信号量和共享内存相似。

 

1、msgget函数

该函数用来创建和访问一个消息队列。它的原型为:

1

int msgctl(int msgid, int command, struct msgid_ds *buf);

  

command是将要采取的动作,它可以取3个值,

    IPC_STAT:把msgid_ds结构中的数据设置为消息队列的当前关联值,即用消息队列的当前关联值覆盖msgid_ds的值。

    IPC_SET:如果进程有足够的权限,就把消息列队的当前关联值设置为msgid_ds结构中给出的值

    IPC_RMID:删除消息队列

 

buf是指向msgid_ds结构的指针,它指向消息队列模式和访问权限的结构。msgid_ds结构至少包括以下成员:

1

2

3

4

5

6

struct msgid_ds

{

    uid_t shm_perm.uid;

    uid_t shm_perm.gid;

    mode_t shm_perm.mode;

};

  

成功时返回0,失败时返回-1.

 

三、使用消息队列进行进程间通信

马不停蹄,介绍完消息队列的定义和可使用的接口之后,我们来看看它是怎么让进程进行通信的。由于可以让不相关的进程进行行通信,所以我们在这里将会编写两个程序,msgreceive和msgsned来表示接收和发送信息。根据正常的情况,我们允许两个程序都可以创建消息,但只有接收者在接收完最后一个消息之后,它才把它删除。

 

接收信息的程序源文件为msgreceive.c的源代码为:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

/*使用消息队列通信*/

#include <unistd.h>

#include <stdlib.h>

#include <stdio.h>

#include <string.h>

#include <errno.h>

#include <sys/msg.h>

struct msg_st

{  

    long int msg_type; 

    char text[BUFSIZ];

};

int main()

{  

    int running = 1;   

    int msgid = -1;

    struct msg_st data;

    long int msgtype = 0; //注意1     //建立消息队列   

    msgid = msgget((key_t)1234, 0666 | IPC_CREAT); 

    if(msgid == -1)

    {      

        fprintf(stderr, "msgget failed with error: %d\n"errno);  

        exit(EXIT_FAILURE);

    }   //从队列中获取消息,直到遇到end消息为止 

    while(running) 

    {  

        if(msgrcv(msgid, (void*)&data, BUFSIZ, msgtype, 0) == -1)  

        {  

            fprintf(stderr, "msgrcv failed with errno: %d\n"errno);  

            exit(EXIT_FAILURE);    

        }  

        printf("You wrote: %s\n",data.text);        //遇到end结束  

        if(strncmp(data.text, "end", 3) == 0)      

            running = 0;   

    }   //删除消息队列   

    if(msgctl(msgid, IPC_RMID, 0) == -1)   

    {      

        fprintf(stderr, "msgctl(IPC_RMID) failed\n");  

        exit(EXIT_FAILURE);

    }  

    exit(EXIT_SUCCESS);

}

发送信息的程序的源文件msgsend.c的源代码为:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

#include <unistd.h>

#include <stdlib.h>

#include <stdio.h>

#include <string.h>

#include <sys/msg.h>

#include <errno.h>

#define MAX_TEXT 512

struct msg_st

{  

    long int msg_type; 

    char text[MAX_TEXT];

};

int main()

{  

    int running = 1;   

    struct msg_st data;

    char buffer[BUFSIZ];

    int msgid = -1;     //建立消息队列

    msgid = msgget((key_t)1234, 0666 | IPC_CREAT);

    if(msgid == -1)

    {  

        fprintf(stderr, "msgget failed with error: %d\n"errno);  

        exit(EXIT_FAILURE);

    }   //向消息队列中写消息,直到写入end

    while(running) 

    {       //输入数据 

        printf("Enter some text: ");   

        fgets(buffer, BUFSIZ, stdin);  

        data.msg_type = 1;    //注意2    

        strcpy(data.text, buffer);      //向队列发送数据  

        if(msgsnd(msgid, (void*)&data, MAX_TEXT, 0) == -1) 

        {      

            fprintf(stderr, "msgsnd failed\n");    

            exit(EXIT_FAILURE);    

        }       //输入end结束输入    

        if(strncmp(buffer, "end", 3) == 0)     

            running = 0;   

        sleep(1);

    }  

    exit(EXIT_SUCCESS);

}

  运行结果如下:

 

四、例子分析——消息类型

 

这里主要说明一下消息类型是怎么一回事,注意msgreceive.c文件main函数中定义的变量msgtype(注释为注意1),它作为msgrcv函数的接收信息类型参数的值,其值为0,表示获取队列中第一个可用的消息。再来看看msgsend.c文件中while循环中的语句data.msg_type = 1(注释为注意2),它用来设置发送的信息的信息类型,即其发送的信息的类型为1。所以程序msgreceive能够接收到程序msgsend发送的信息。

 

如果把注意1,即msgreceive.c文件main函数中的语句由long int msgtype = 0;改变为long int msgtype = 2;会发生什么情况,msgreceive将不能接收到程序msgsend发送的信息。因为在调用msgrcv函数时,如果msgtype(第四个参数)大于零,则将只获取具有相同消息类型的第一个消息,修改后获取的消息类型为2,而msgsend发送的消息类型为1,所以不能被msgreceive程序接收。重新编译msgreceive.c文件并再次执行,其结果如下:

我们可以看到,msgreceive并没有接收到信息和输出,而且当msgsend输入end结束后,msgreceive也没有结束,通过jobs命令我们可以看到它还在后台运行着。

 

五、消息队列与命名管道的比较

 

消息队列跟命名管道有不少的相同之处,通过与命名管道一样,消息队列进行通信的进程可以是不相关的进程,同时它们都是通过发送和接收的方式来传递数据的。在命名管道中,发送数据用write,接收数据用read,则在消息队列中,发送数据用msgsnd,接收数据用msgrcv。而且它们对每个数据都有一个最大长度的限制。

 

与命名管道相比,消息队列的优势在于,1、消息队列也可以独立于发送和接收进程而存在,从而消除了在同步命名管道的打开和关闭时可能产生的困难。2、同时通过发送消息还可以避免命名管道的同步和阻塞问题,不需要由进程自己来提供同步方法。3、接收程序可以通过消息类型有选择地接收数据,而不是像命名管道中那样,只能默认地接收。

 

Linux下进程间通信方式——pipe(管道)

 https://www.cnblogs.com/wuyepeng/p/9747557.html

   每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程A把数据从用户空间拷到内核缓冲区,进程B再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信。

不同进程间的通信本质:进程之间可以看到一份公共资源;而提供这份资源的形式或者提供者不同,造成了通信方式不同,而 pipe就是提供这份公共资源的形式的一种。

2.匿名管道

2.1管道的创建

管道是由调用pipe函数来创建

1

2

3

#include <unistd.h>

int pipe (int fd[2]);

                         //返回:成功返回0,出错返回-1 

  fd参数返回两个文件描述符,fd[0]指向管道的读端,fd[1]指向管道的写端。fd[1]的输出是fd[0]的输入。

2.2管道如何实现进程间的通信

(1)父进程创建管道,得到两个件描述符指向管道的两端

(2)父进程fork出子进程,子进程也有两个文件描述符指向同管道。

(3)父进程关闭fd[0],子进程关闭fd[1],即子进程关闭管道读端,父进程关闭管道写端(因为管道只支持单向通信)。子进程可以往管道中写,父进程可以从管道中读,管道是由环形队列实现的,数据从写端流入从读端流出,这样就实现了进程间通信。

2.3如和用代码实现管道通信

2.3如和用代码实现管道通信

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

#include<stdio.h>

#include<unistd.h>

#include<string.h>

#include<errno.h>

int main()

{

    int fd[2];

    int ret=pipe(fd);

    if(ret==-1)

    {

        perror("pipe error\n");

        return -1;

    }

    pid_t id=fork();

    if(id==0)

    {

        int i=0;

        close(fd[0]);

        char* child="I am child!";

        while(i<5)

        {

            write(fd[1],child,strlen(child)+1);

            sleep(2);

            i++;

        }

    }

    else if(id>0)

    {

        close(fd[1]);

        char msg[100];

        int j=0;

        while(j<5)

        {

            memset(msg,'\0',sizeof(msg));

            ssize_t s=read(fd[0],msg,sizeof(msg));

            if(s>0)

            {

                msg[s-1]='\0';

            }

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

            j++;

        }

    }

    else

    {

        perror("fork error\n");

        return -1;

    }

    return 0;

}

  

运行结果:每隔2秒打印一次I am child! 并且打印了五次。

 

2.4管道读取数据的四种的情况

(1)读端不读(fd[0]未关闭),写端一直写 

 

 

(2)写端不写(fd[1]未关闭),但是读端一直读 

(3)读端一直读,且fd[0]保持打开,而写端写了一部分数据不写了,并且关闭fd[1]。 

如果一个管道读端一直在读数据,而管道写端的引⽤计数⼤于0决定管道是否会堵塞,引用计数大于0,只读不写会导致管道堵塞。

(4)读端读了一部分数据,不读了且关闭fd[0],写端一直在写且f[1]还保持打开状态。

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

#include<stdio.h>

#include<unistd.h>

#include<string.h>

#include<errno.h>

int main()

{

    int fd[2];

    int ret=pipe(fd);

    if(ret==-1)

    {

        perror("pipe error\n");

        return -1;

    }

    pid_t id=fork();

    if(id==0)

    {

        int i=0;

        close(fd[0]);

        char *child="I am child!";

        while(i<10)

        {

            write(fd[1],child,strlen(child)+1);

            sleep(2);

            i++;

        }

    }

    else if(id>0)

    {

        close(fd[1]);

        char msg[100];

        int status=0;

        int j=0;

        while(j<5)

        {

            memset(msg,'\0',sizeof(msg));

            ssize_t s=read(fd[0],msg,sizeof(msg));

            if(s>0)

            {

                msg[s-1]='\0';

            }

            printf("%s %d\n",msg,j);

            j++;

        }

        //写方还在继续,而读方已经关闭它的读端 

        close(fd[0]);

        pid_t ret=waitpid(id,&status,0);

        printf("exitsingle(%d),exit(%d)\n",status&0xff,(status>>8)&0xff);

        //低八位存放该子进程退出时是否收到信号 

        //此低八位子进程正常退出时,退出码是多少

    }

    else

    {

        perror("fork error\n");

        return -1;

    }

    return 0;

}

  运行结果:

 使用kill -l 查看13号信号,可以知道13号信号代表SIGPIPE。

 

总结: 
如果一个管道的写端一直在写,而读端的引⽤计数是否⼤于0决定管道是否会堵塞,引用计数大于0,只写不读再次调用write会导致管道堵塞; 
如果一个管道的读端一直在读,而写端的引⽤计数是否⼤于0决定管道是否会堵塞,引用计数大于0,只读不写再次调用read会导致管道堵塞; 
而当他们的引用计数等于0时,只写不读会导致写端的进程收到一个SIGPIPE信号,导致进程终止,只写不读会导致read返回0,就像读到件末尾样。

 

2.5管道特点

1.管道只允许具有血缘关系的进程间通信,如父子进程间的通信。

2.管道只允许单向通信。

3.管道内部保证同步机制,从而保证访问数据的一致性。

4.面向字节流

5.管道随进程,进程在管道在,进程消失管道对应的端口也关闭,两个进程都消失管道也消失。

2.6管道容量大小

测试管道容量大小只需要将写端一直写,读端不读且不关闭fd[0],即可。 
测试代码:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

#include <stdio.h>

#include <unistd.h>

#include <string.h>

#include <errno.h>

int main()

{

    int fd[2];

    int ret = pipe(fd);

    if (ret == -1)

    {

        perror("pipe error\n");

        return -1;

    }

    pid_t id = fork();

    if (id == 0)

    {//child

        int i = 0;

        close(fd[0]);

        char *child = "I am  child!";

        while (++i)

        {

            printf("pipe capacity: %d\n", i*(strlen(child) + 1));

          //printf要写在write前面否则会因为write写满了而阻塞就不会进行下面的代码了,会使得输出计算少一次

            write(fd[1], child, strlen(child) + 1);

            

        }

        close(fd[1]);

    }

    else if (id>0)

    {//father

        close(fd[1]);//父进程的读端不能关闭,如果关闭了子进程写端会因为异常而退出

        waitpid(id, NULL, 0);

    }

    else

    {//error

        perror("fork error\n");

        return -1;

    }

    return  0;

}   

  

  可以看到写到65520之后管道堵塞了,而65536即为64K大小即为管道的容量

原理是:我们写端每次写入的数据大小是13,统计我们可以进行多少次写入,写入次数*13就是管道容量,因为65533+13=65546>65536所以就不能继续输入了,有因为内存对齐问题,所以我们可以知道容量一定是64k

 

 Linux进程通信的几种方式总结

https://www.cnblogs.com/wuyepeng/p/9747515.html

进程通信的目的


  • 数据传输 
    一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几M字节之间

  • 共享数据 
    多个进程想要操作共享数据,一个进程对共享数据

  • 通知事 
    一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。

  • 资源共享 
    多个进程之间共享同样的资源。为了作到这一点,需要内核提供锁和同步机制。

  • 进程控制 
    有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

linux使用的进程间通信方式


 管道(pipe),流管道(s_pipe)和有名管道(FIFO)

信号(signal)

消息队列

共享内存

信号量

套接字(socket)

管道( pipe )


管道这种通讯方式有两种限制,一是半双工的通信,数据只能单向流动,二是只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。

流管道s_pipe: 去除了第一种限制,可以双向传输.

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

信号量( semophore )


信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。

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

消息队列( message queue )


消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。

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

信号 ( singal )


信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

主要作为进程间以及同一进程不同线程之间的同步手段。

共享内存( shared memory )


共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。

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

套接字( socket )


套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信

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

 

各种通信方式的比较和优缺点:

  1. 管道:速度慢,容量有限,只有父子进程能通讯

  2. FIFO:任何进程间都能通讯,但速度慢

  3. 消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题

  4. 信号量:不能传递复杂消息,只能用来同步

  5. 共享内存区:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存

如果用户传递的信息较少或是需要通过信号来触发某些行为.前文提到的软中断信号机制不失为一种简捷有效的进程间通信方式.

但若是进程间要求传递的信息量比较大或者进程间存在交换数据的要求,那就需要考虑别的通信方式了。

无名管道简单方便.但局限于单向通信的工作方式.并且只能在创建它的进程及其子孙进程之间实现管道的共享:

有名管道虽然可以提供给任意关系的进程使用.但是由于其长期存在于系统之中,使用不当容易出错.所以普通用户一般不建议使用。

消息缓冲可以不再局限于父子进程,而允许任意进程通过共享消息队列来实现进程间通信,并由系统调用函数来实现消息发送和接收之间的同步,从而使得用户在使用消息缓冲进行通信时不再需要考虑同步问题,使用方便,但是信息的复制需要额外消耗CPU的时间,不适宜于信息量大或操作频繁的场合。

共享内存针对消息缓冲的缺点改而利用内存缓冲区直接交换信息,无须复制,快捷、信息量大是其优点。

但是共享内存的通信方式是通过将共享的内存缓冲区直接附加到进程的虚拟地址空间中来实现的,因此,这些进程之间的读写操作的同步问题操作系统无法实现。必须由各进程利用其他同步工具解决。另外,由于内存实体存在于计算机系统中,所以只能由处于同一个计算机系统中的诸进程共享。不方便网络通信。

共享内存块提供了在任意数量的进程之间进行高效双向通信的机制。每个使用者都可以读取写入数据,但是所有程序之间必须达成并遵守一定的协议,以防止诸如在读取信息之前覆写内存空间等竞争状态的出现。

不幸的是,Linux无法严格保证提供对共享内存块的独占访问,甚至是在您通过使用IPC_PRIVATE创建新的共享内存块的时候也不能保证访问的独占性。 同时,多个使用共享内存块的进程之间必须协调使用同一个键值。

 

多线程有什么用?

https://www.cnblogs.com/wuyepeng/p/9724771.html

作者:pansz
链接:https://www.zhihu.com/question/19901763/answer/13299543
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
 

这么解释问题吧:

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

多线程的问题是多个人同时吃一道菜的时候容易发生争抢,例如两个人同时夹一个菜,一个人刚伸出筷子,结果伸到的时候已经被夹走菜了。。。此时就必须等一个人夹一口之后,在还给另外一个人夹菜,也就是说资源共享就会发生冲突争抢。

 

1。对于 Windows 系统来说,【开桌子】的开销很大,因此 Windows 鼓励大家在一个桌子上吃菜。因此 Windows 多线程学习重点是要大量面对资源争抢与同步方面的问题。

 

2。对于 Linux 系统来说,【开桌子】的开销很小,因此 Linux 鼓励大家尽量每个人都开自己的桌子吃菜。这带来新的问题是:坐在两张不同的桌子上,说话不方便。因此,Linux 下的学习重点大家要学习进程间通讯的方法。

--
补充:有人对这个开桌子的开销很有兴趣。我把这个问题推广说开一下。

开桌子的意思是指创建进程。开销这里主要指的是时间开销。
可以做个实验:创建一个进程,在进程中往内存写若干数据,然后读出该数据,然后退出。此过程重复 1000 次,相当于创建/销毁进程 1000 次。在我机器上的测试结果是: 
UbuntuLinux:耗时 0.8 秒 
Windows7:耗时 79.8 秒 
两者开销大约相差一百倍。

这意味着,在 Windows 中,进程创建的开销不容忽视。换句话说就是,Windows 编程中不建议你创建进程,如果你的程序架构需要大量创建进程,那么最好是切换到 Linux 系统。

大量创建进程的典型例子有两个,一个是 gnu autotools 工具链,用于编译很多开源代码的,他们在 Windows 下编译速度会很慢,因此软件开发人员最好是避免使用 Windows。另一个是服务器,某些服务器框架依靠大量创建进程来干活,甚至是对每个用户请求就创建一个进程,这些服务器在 Windows 下运行的效率就会很差。这"可能"也是放眼全世界范围,Linux 服务器远远多于 Windows 服务器的原因。

--
再次补充:如果你是写服务器端应用的,其实在现在的网络服务模型下,开桌子的开销是可以忽略不计的,因为现在一般流行的是按照 CPU 核心数量开进程或者线程,开完之后在数量上一直保持,进程与线程内部使用协程或者异步通信来处理多个并发连接,因而开进程与开线程的开销可以忽略了。

另外一种新的开销被提上日程:核心切换开销。

现代的体系,一般 CPU 会有多个核心,而多个核心可以同时运行多个不同的线程或者进程。

当每个 CPU 核心运行一个进程的时候,由于每个进程的资源都独立,所以 CPU 核心之间切换的时候无需考虑上下文。

当每个 CPU 核心运行一个线程的时候,由于每个线程需要共享资源,所以这些资源必须从 CPU 的一个核心被复制到另外一个核心,才能继续运算,这占用了额外的开销。换句话说,在 CPU 为多核的情况下,多线程在性能上不如多进程。

因而,当前面向多核的服务器端编程中,需要习惯多进程而非多线程。

 

Linux/Unix 多线程通信

https://www.cnblogs.com/wuyepeng/p/9749956.html

线程间无需特别的手段进行通信,因为线程间可以共享数据结构,也就是一个全局变量可以被两个线程同时使用。 不过要注意的是线程间需要做好同步,一般用 mutex。 可以参考一些比较新的 UNIX/Linux 编程的书,都会提到 Posix 线程编程,比如《UNIX环境高级编程(第二版)》、《UNIX系统编程》等等。 Linux 的消息属于 IPC,也就是进程间通信,线程用不上。

  • 使用多线程的理由之一是和进程相比,它是一种非常”节俭”的多任务操作方式。

    我们知道,在 Linux 系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种”昂贵”的多任务工作方式。 而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。

  • 使用多线程的理由之二是线程间方便的通信机制。

    对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。 线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。 当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为 static 的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。

Table of Contents

  • Table of Contents
  • 通信方式
    • 信号
    • 互斥锁
    • 条件变量
    • 信号量
    • 信号量与线程锁、条件变量
  • 简单的多线程程序
    • 修改线程的属性
    • 线程的数据处理
    • 互斥锁
    • 信号量

通信方式

信号

Linux 用 pthread_kill 对线程发信号。

Windows 用 PostThreadMessage 进行线程间通信,但实际上极少用这种方法。还是利用同步多一些 LINUX 下的同步和 Windows 原理都是一样的。不过 Linux 下的 singal 中断也很好用。

用好信号量,共享资源就可以了。

互斥锁

互斥锁,是一种信号量,常用来防止两个进程或线程在同一时刻访问相同的共享资源。

需要的头文件:pthread.h

互斥锁标识符:pthread_mutex_t

  1. 互斥锁初始化:

    函数原型: int pthread_mutex_init (pthread_mutex_t* mutex,const pthread_mutexattr_t* mutexattr);

    函数传入值: mutex:互斥锁。

    mutexattr:

    • PTHREAD_MUTEX_INITIALIZER 创建快速互斥锁。
    • PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP 创建递归互斥锁。
    • PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP 创建检错互斥锁。

    函数返回值:成功:0;出错:-1

  2. 互斥操作函数

    • int pthread_mutex_lock(pthread_mutex_t* mutex); //上锁
    • int pthread_mutex_trylock (pthread_mutex_t* mutex); //只有在互斥被锁住的情况下才阻塞
    • int pthread_mutex_unlock (pthread_mutex_t* mutex); //解锁
    • int pthread_mutex_destroy (pthread_mutex_t* mutex); //清除互斥锁

    函数传入值:mutex:互斥锁。

    函数返回值:成功:0;出错:-1

    使用形式:

    1

    2

    3

    4

    5

    6

    pthread_mutex_t mutex;

    pthread_mutex_init (&mutex, NULL); /*定义*/

    ...

    pthread_mutex_lock(&mutex); /*获取互斥锁*/

    ... /*临界资源*/

    pthread_mutex_unlock(&mutex); /*释放互斥锁*/

      

    如果一个线程已经给一个互斥量上锁了,后来在操作的过程中又再次调用了该上锁的操作,那么该线程将会无限阻塞在这个地方,从而导致死锁。这就需要互斥量的属性。

    互斥量分为下面三种:

    1. 快速型。这种类型也是默认的类型。该线程的行为正如上面所说的。
    2. 递归型。如果遇到我们上面所提到的死锁情况,同一线程循环给互斥量上锁,那么系统将会知道该上锁行为来自同一线程,那么就会同意线程给该互斥量上锁。
    3. 错误检测型。如果该互斥量已经被上锁,那么后续的上锁将会失败而不会阻塞,pthread_mutex_lock()操作将会返回EDEADLK。

    互斥量的属性类型为pthread_mutexattr_t。 声明后调用pthread_mutexattr_init()来创建该互斥量。然后调用 pthread_mutexattr_settype来设置属性。 格式如下:int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int kind);

    第一个参数attr,就是前面声明的属性变量;第二个参数kind,就是我们要设置的属性类型。他有下面几个选项:

    • PTHREAD_MUTEX_FAST_NP
    • PTHREAD_MUTEX_RECURSIVE_NP
    • PTHREAD_MUTEX_ERRORCHECK_NP

    下面给出一个使用属性的简单过程:

    1

    2

    3

    4

    5

    6

    pthread_mutex_t mutex;

    pthread_mutexattr_t attr;

    pthread_mutexattr_init(&attr);

    pthread_mutexattr_settype(&attr,PTHREAD_MUTEX_RECURSIVE_NP);

    pthread_mutex_init(&mutex,&attr);

    pthread_mutex_destroy(&attr);

      

    前面我们提到在调用pthread_mutex_lock()的时候,如果此时mutex已经被其他线程上锁,那么该操作将会一直阻塞在这个地方。如果我们此时不想一直阻塞在这个地方,那么可以调用下面函数:pthread_mutex_trylock。

    如果此时互斥量没有被上锁,那么pthread_mutex_trylock将会返回0,并会对该互斥量上锁。如果互斥量已经被上锁,那么会立刻返回EBUSY。

条件变量

需要的头文件:pthread.h

条件变量标识符:pthread_cond_t

  1. 互斥锁的存在问题:

    互斥锁一个明显的缺点是它只有两种状态:锁定和非锁定。设想一种简单情景:多个线程访问同一个共享资源时,并不知道何时应该使用共享资源,如果在临界区里 加入判断语句,或者可以有效,但一来效率不高,二来复杂环境下就难以编写了,这是我们需要一个结构,能在条件成立时触发相应线程,进行变量修改和访问。

  2. 条件变量:

    条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,它常和互斥锁一起使用。使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化。一旦其它的某个线程改变了条件变量,它将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。 这些线程将重新锁定互斥锁并重新测试条件是否满足。

  3. 条件变量的相关函数

    • pthread_cond_t cond = PTHREAD_COND_INITIALIZER; //条件变量结构
    • int pthread_cond_init(pthread_cond_t cond, pthread_condattr_tcond_attr);
    • int pthread_cond_signal(pthread_cond_t *cond);
    • int pthread_cond_broadcast(pthread_cond_t *cond);
    • int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
    • int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,
    • const struct timespec *abstime);
    • int pthread_cond_destroy(pthread_cond_t *cond);

    详细说明

  4. 创建和注销

    条件变量和互斥锁一样,都有静态动态两种创建方式

    1. 静态方式

      静态方式使用PTHREAD_COND_INITIALIZER常量,如下:

      pthread_cond_t cond=PTHREAD_COND_INITIALIZER

    2. 动态方式

      动态方式调用pthread_cond_init()函数,API定义如下:

      int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr)

      尽管POSIX标准中为条件变量定义了属性,但在LinuxThreads中没有实现,因此cond_attr值通常为NULL,且被忽略。

      注销一个条件变量需要调用pthread_cond_destroy(),只有在没有线程在该条件变量上等待的时候才能注销这个条件变量,否则返回 EBUSY。因为Linux实现的条件变量没有分配什么资源,所以注销动作只包括检查是否有等待线程。API定义如下:int pthread_cond_destroy(pthread_cond_t *cond)

  5. 等待和激发

    1. 等待

      • int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex) //等待
      • int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,
      • const struct timespec *abstime) //有时等待

      等待条件有两种方式:无条件等待pthread_cond_wait()和计时等待pthread_cond_timedwait(),其中计时等待方式 如果在给定时刻前条件没有满足,则返回ETIMEOUT,结束等待,其中abstime以与time()系统调用相同意义的绝对时间形式出现,0表示格林 尼治时间1970年1月1日0时0分0秒。

      无论哪种等待方式,都必须和一个互斥锁配合,以防止多个线程同时请求pthread_cond_wait()(或 pthread_cond_timedwait(),下同)的竞争条件(Race Condition)。mutex互斥锁必须是普通锁(PTHREAD_MUTEX_TIMED_NP)或者适应锁 (PTHREAD_MUTEX_ADAPTIVE_NP),且在调用pthread_cond_wait()前必须由本线程加锁 (pthread_mutex_lock()),而在更新条件等待队列以前,mutex保持锁定状态,并在线程挂起进入等待前解锁。在条件满足从而离开 pthread_cond_wait()之前,mutex将被重新加锁,以与进入pthread_cond_wait()前的加锁动作对应。

    2. 激发

      激发条件有两种形式,pthread_cond_signal()激活一个等待该条件的线程,存在多个等待线程时按入队顺序激活其中一个;而pthread_cond_broadcast()则激活所有等待线程。</p>

  6. 其他操作

    pthread_cond_wait ()和pthread_cond_timedwait()都被实现为取消点,因此,在该处等待的线程将立即重新运行,在重新锁定mutex后离开 pthread_cond_wait(),然后执行取消动作。也就是说如果pthread_cond_wait()被取消,mutex是保持锁定状态的, 因而需要定义退出回调函数来为其解锁。

    pthread_cond_wait实际上可以看作是以下几个动作的合体:

    解锁线程锁;

    等待条件为true;

    加锁线程锁;

    使用形式:

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    // 线程一代码

    pthread_mutex_lock(&mutex);

    if (条件满足)

    pthread_cond_signal(&cond);

    pthread_mutex_unlock(&mutex);

    <p>// 线程二代码

    pthread_mutex_lock(&mutex);

    while (条件不满足)

    pthread_cond_wait(&cond, &mutex);

    pthread_mutex_unlock(&mutex);

    /*线程二中为什么使用while呢?因为在pthread_cond_signal和pthread_cond_wait返回之间,有时间差,假设在这 个时间差内,条件改变了,显然需要重新检查条件。也就是说在pthread_cond_wait被唤醒的时候可能该条件已经不成立。*/

      

信号量

信号量其实就是一个计数器,也是一个整数。 每一次调用 wait 操作将会使 semaphore 值减一,而如果 semaphore 值已经为 0,则 wait 操作将会阻塞。 每一次调用post操作将会使semaphore值加一。

需要的头文件:semaphore.h

信号量标识符:sem_t

主要函数:

sem_init

功能: 用于创建一个信号量,并初始化信号量的值。

函数原型: int sem_init (sem_t* sem, int pshared, unsigned int value);

函数传入值: sem:信号量。

pshared:决定信号量能否在几个进程间共享。 由于目前 Linux 还没有实现进程间共享信息量,所以这个值只能取0。 value:初始计算器

函数返回值: 0:成功;-1:失败。

其他函数

1

2

3

4

5

6

7

8

9

//等待信号量

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 sem_destroy (sem_t* sem);

  

功能:sem_wait和sem_trywait相当于 P 操作,它们都能将信号量的值减一, 两者的区别在于若信号量的值小于零时,sem_wait 将会阻塞进程,而 sem_trywait 则会立即返回。

sem_post 相当于 V 操作,它将信号量的值加一,同时发出唤醒的信号给等待的进程(或线程)。

sem_getvalue 得到信号量的值。

sem_destroy 摧毁信号量。

使用形式:

1

2

3

4

5

6

sem_t sem;

sem_init(&sem, 0, 1); /*信号量初始化*/

...

sem_wait(&sem);   /*等待信号量*/

... /*临界资源*/

sem_post(&sem);   /*释放信号量*/

  

信号量与线程锁、条件变量

信号量与线程锁、条件变量相比还有以下几点不同:

  1. 锁必须是同一个线程获取以及释放,否则会死锁。而条件变量和信号量则不必。
  2. 信号的递增与减少会被系统自动记住,系统内部有一个计数器实现信号量,不必担心会丢失,而唤醒一个条件变量时,如果没有相应的线程在等待该条件变量,这次唤醒将被丢失。

简单的多线程程序

首先在主函数中,我们使用到了两个函数,pthread_create 和 pthread_join,并声明了一个 pthread_t型的变量。

pthread_t 在头文件 pthread.h 中已经声明,是线程的标示符

函数 pthread_create 用来创建一个线程,函数原型:

1

2

3

4

#include <pthread.h>

 

int pthread_create(pthread_t *threadconst pthread_attr_t *attr,

  void *(*start_routine) (void *), void *arg);

  

  • 第一个参数为指向线程标识符的指针,
  • 第二个参数用来设置线程属性,
  • 第三个参数是线程运行函数的起始地址,
  • 最后一个参数是运行函数的参数。

若我们的函数thread不需要参数,所以最后一个参数设为空指针。第二个参数我们也设为空指针,这样将生成默认属性的线程。

返回值:

  • 0: 当创建线程成功时
  • non-zero: 说明创建线程失败,常见的错误返回代码为
    • EAGAIN 表示系统限制创建新的线程,例如线程数目过多了;
    • EINVAL 表示第二个参数代表的线程属性值非法。

创建线程成功后,新创建的线程则运行参数三和参数四确定的函数, 原来的线程则继续运行下一行代码。

函数pthread_join用来等待一个线程的结束。函数原型为:

1

2

3

#include <pthread.h>

 

int pthread_join(pthread_t threadvoid **retval);

  

第一个参数为被等待的线程标识符,第二个参数为一个用户定义的指针,它可以用来存储被等待线程的返回值。这个函数是一个线程阻塞的函数,调用它的函数将一直等待到被等待的线程结束为止,当函数返回时,被等待线程的资源被收回。

一个线程的结束有两种途径,

  • 一种是象我们上面的例子一样,函数结束了,调用它的线程也就结束了;
  • 另一种方式是通过函数 pthread_exit 来实现。它的函数原型为:

1

2

3

#include <pthread.h>

 

void pthread_exit(void *retval);

  

唯一的参数是函数的返回代码,只要 pthread_join 中的第二个参数 retval 不是NULL,这个值将被传递给 retval。

最后要说明的是,一个线程不能被多个线程等待,否则第一个接收到信号的线程成功返回,其余调用 pthread_join 的线程则返回错误代码 ESRCH

修改线程的属性

设置线程绑定状态的函数为 pthread_attr_setscope,它有两个参数,第一个是指向属性结构的指针,第二个是绑定类型,它有两个取值:

  • PTHREAD_SCOPE_SYSTEM(绑定的)
  • PTHREAD_SCOPE_PROCESS(非绑定的)

下面的代码即创建了一个绑定的线程。

1

2

3

4

5

6

7

8

9

10

#include <pthread.h>

 

pthread_attr_t attr;

pthread_t tid;

 

/*初始化属性值,均设为默认值*/

pthread_attr_init(&attr);

pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);

 

pthread_create(&tid, &attr, (void *)task_func, NULL);

  

线程的数据处理

和进程相比,线程的最大优点之一是数据的共享性,各个进程共享父进程处沿袭的数据段,可以方便的获得、修改数据。

但这也给多线程编程带来了许多问题。我们必须当心有多个不同的进程访问相同的变量。 许多函数是不可重入的,即同时不能运行一个函数的多个拷贝(除非使用不同的数据段)。 在函数中声明的静态变量常常带来问题,函数的返回值也会有问题。 因为如果返回的是函数内部静态声明的空间的地址,则在一个线程调用该函数得到地址后使用该地址指向的数据时,别的线程可能调用此函数并修改了这一段数据。 在进程中共享的变量必须用关键字 volatile 来定义,这是为了防止编译器在优化时(如 gcc 中使用 -OX 参数)改变它们的使用方式。 为了保护变量,我们必须使用信号量、互斥等方法来保证我们对变量的正确使用。

互斥锁

互斥锁用来保证一段时间内只有一个线程在执行一段代码。必要性显而易见:假设各个线程向同一个文件顺序写入数据,最后得到的结果一定是灾难性的

信号量

原来总是用互斥锁(MUTEX)和环境变量(cond)去控制线程的通信,用起来挺麻烦的,用信号量(SEM)来通信控制就方便多了!

用到信号量就要包含semaphore.h头文件。
可以用sem_t类型来声明一个型号量。

#include <semaphore.h>
sem_t * sem_open(const char *name, int oflag, ...);

用 int sem_init(sem_t *sem, int pshared, unsigned int value) 函数来初始化型号量, 第一个参数就是用 sem_t 声明的信号量, 第二变量如果为 0,表示这个信号量只是当前进程中的型号量,如果不为 0,这个信号量可能可以在两个进程中共享。 第三个参数就是初始化信号量的多少值。

  • sem_wait(sem_t *sem) 函数用于接受信号,当 sem > 0 时就能接受到信号,然后将 sem–;
  • sem_post(sem_t *sem) 函数可以增加信号量。
  • sem_destroy(sem_t *sem) 函数用于解除信号量。

以下是一个用信号控制的一个简单的例子:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

#include <stdio.h>

#include <semaphore.h>

#include <pthread.h>

 

sem_t sem1, sem2;

 

void *thread1(void *arg) {

    sem_wait(&sem1);

    setbuf(stdout,NULL);//这里必须注意,由于下面输出"hello"中没有‘n’符,所以可能由于输出缓存已满,造成输不出东西来,所以用这个函数把输出缓存清空

    printf("hello ");

    sem_post(&sem2);

}

 

void *thread2(void *arg) {

    sem_wait(&sem2);

    printf("world!n");

}

 

int main() {

    pthread_t t1, t2;

 

    sem_init(&sem1,0,1);//初始化化信号量为1,所以会先打印线程1

    sem_init(&sem2,0,0);//初始化信号量为0

 

    pthread_create(&t1,NULL,thread1,NULL);

    pthread_create(&t2,NULL,thread2,NULL);

 

    pthread_join(t1,NULL);

    pthread_join(t2,NULL);

 

    sem_destroy(&sem1);

    sem_destroy(&sem2);

 

    return 0;

}

  

//程序的实现是控制先让thread1线程打印"hello "再让thread2线程打印"world!" 

mutex互斥体只用于保护临界区的代码(访问共享资源),而不用于锁之间的同步,即一个线程释放mutex锁后,马上又可能获取同一个锁,而不管其它正在等待该mutex锁的其它线程。

semaphore信号量除了起到保护临界区的作用外,还用于锁同步的功能,即一个线程释放semaphore后,会保证正在等待该semaphore的线程优先执行,而不会马上在获取同一个semaphore。

如果两个线程想通过一个锁达到输出1,2,1,2,1,2这样的序列,应使用semaphore, 而使用 mutex 的结果可能为1,1,1,1,1,2,2,2,111…..。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值