Lucky辉-面经准备(操作系统)

进程和线程

概念

进程:是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竞争计算机系统资源的基本单位。

线程:是进程的一个执行单元,是进程内可调度实体。比进程更小的独立运行的基本单位。线程也被称为轻量级进程。

区别

  1. 进程是资源分配的最小单位。 线程是CPU调度的基本单位。
  2. 进程拥有独立的内存单元。同一进程下的多个线程共享内存。线程只独享指令流执行的必要资源,如寄存器和栈。
  3. 线程共享本进程的资源如内存、I/O、cpu等,不利于资源的管理和保护,而进程之间的资源是独立的,能很好的进行资源管理和保护。
  4. 进程间不会相互影响,多考虑通信。线程间会相互影响,多考虑同步,通信可不通过内核直接通信。
  5. 进程编程调试简单可靠性高,但是创建销毁切换开销大。线程正相反,开销小,切换速度快,但是编程调试相对复杂。
  6. 多进程要比多线程健壮,一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。
  7. 每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口,执行开销大。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,执行开销小。

 线程独占和共享的资源

共享资源

  1. 进程申请的堆内存
  2. 进程打开的文件描述符
  3. 进程的全局数据(可用于线程之间通信)
  4. 进程ID、进程组ID
  5. 进程目录
  6. 信号处理器

独占资源

  1. 线程ID,同一进程中每个线程拥有唯一的线程ID。
  2. 寄存器组的值,由于线程间是并发运行的,每个线程有自己不同的运行线索,当从一个线 程切换到另一个线程上 时,必须将原有的线程的寄存器集合的状态保存,以便将来该线程在被重新切换到时能得以恢复。
  3. 线程堆栈,线程可以进行函数调用,必然会使用大函数堆栈。
  4. 错误返回码,线程执行出错时,必须明确是哪个线程出现何种错误,因此不同的线程应该拥有自己的错误返回码变量。
  5. 信号屏蔽码,由于每个线程所感兴趣的信号不同,所以线程的信号屏蔽码应该由线程自己管理。但所有的线程都共享同样的信号处理器。
  6. 线程的优先级,由于线程需要像进程那样能够被调度,那么就必须要有可供调度使用的参数,这个参数就是线程的优先级。

进程间通信

由于进程间相互独立不共享内存空间和资源,不能直接使用全局变量直接通信,但是进程是共享内核空间的,可通过内核空间进行进程间通信,如下图:

进程间通信的方法有:

管道

先看实现:

$ mkfifo mypipe //创建一个管道,命名为mypipe
$ echo "hello world!" > mypipe //发送信息到管道内
/* 接下来键入的命令都没有响应了,我认为是发送信息到管道但是没有取用,这个进程就阻塞在这里了 */

$ cat < mypipe //从先前创建的管道中取信息
hello world! //打印先前输入的信息
$ ls -l mypipe //查看创建的管道信息,实质上管道是个文件
/* 以上部分需要重开一个进程进行,执行cat操作后原先阻塞的进程也成功运行了,可以看图 */

实际上管道操作就很简单,创建管道然后输入内容就好 ,但是使用起来是比较麻烦的

同时我使用的是命名管道,可以形成一个管道文件,因此可以在任意两个进程之间进行通信连接,但匿名管道是没有指定的文件的,网上查阅了解到匿名管道只能在父与子进程或者同一个父进程的子进程之间通信(这一点比较好理解,毕竟匿名管道没有文件实体,那么管道的文件内容只能靠fork的操作传递给子进程).

命名管道应该是双向交替通信的,即双方都能发送消息接收消息,只是不能同时接发消息,并且管道内的信息在未完全取出时就是单向的,这个可以尝试,当两个进程同时像一个管道发送消息时,这个管道就卡死了.

至于匿名管道可以参考这篇文章进程通信详解

这里我觉得不会常用就不加赘述了,这位大佬描述得很详细了.

消息队列

消息队列就是管道的改善版,前面提到管道在实际通信时是单向的,仅允许一方发送一方接受,但是消息队列是允许双方发送和接受的——就像有一个独立的队列存在,任意进程都可以向其发送消息,接收方可以指定队列的序号,接收信息的大小等来进行自由地接收消息;我认为最重要的是他不会像命名管道那样极容易发生阻塞,同步问题

我使用过RabbitMQ,使用起来是很简单的,只需要连接上消息队列就好

当然消息队列也有一定的缺陷,比如发送信息的大小限制,因为内核中的消息队列大小其实是确定分配好的,在创建消息队列时便需要指定.

目前其实是云计算方面用消息队列比较多,将消息队列部署在云端服务器上可以方便多个计算机进行实时的通信

关于RabbitMQ的详细使用方法见这篇文章RabbitMQ详解

或者直接上官方文档看,比较容易上手.

共享内存

和消息队列有点相似的是共享内存也是独立于进程的,其实也就是独立的一块内存空间供各个进程取用,发送消息,获取变量等等——其背后的原理是现代计算机的进程都采用虚拟内存的技术,通过映射到不同物理内存实现进程的相互独立,但是共享内存这一段虚拟内存是公用的,映射到同一块物理内存.

相较于消息队列,共享内存的通信使用更为方便,信息取用更自由

原理图如下:

还有一个重要的点在于共享内存用的就是实际的物理地址,直接取用即可,无需像消息队列那样将变量信息从内核态拷贝到用户态或者反过来,避免了消息的拷贝开销(因为用的是虚拟内存映射)

信号量

共享内存存在的一个问题是这块内存各个进程都能使用,假设多个进程对同一块内存同时进行读写,就会发生冲突(毕竟共享内存的机制下,公用的那块内存对每个进程来说都相当于是自己的一块内存)

由于以上问题的存在就有了信号量这个机制,作为<<操作系统>>的重要内容,信号量要解决的其实不是进程的通信问题,而是进程间的互斥同步问题

信号量表示资源的数量,控制信号量的方式有两种原子操作:

  • 一个是 P 操作,这个操作会把信号量减去 -1,相减后如果信号量 < 0,则表明资源已被占用,进程需阻塞等待;相减后如果信号量 >= 0,则表明还有资源可使用,进程可正常继续执行。
  • 另一个是 V 操作,这个操作会把信号量加上 1,相加后如果信号量 <= 0,则表明当前有阻塞中的进程,于是会将该进程唤醒运行;相加后如果信号量 > 0,则表明当前没有阻塞中的进程;

P 操作是用在进入共享资源之前,V 操作是用在离开共享资源之后,这两个操作是必须成对出现的。

原理如下图:

具体的过程如下:

  • 进程 A 在访问共享内存前,先执行了 P 操作,由于信号量的初始值为 1,故在进程 A 执行 P 操作后信号量变为 0,表示共享资源可用,于是进程 A 就可以访问共享内存。
  • 若此时,进程 B 也想访问共享内存,执行了 P 操作,结果信号量变为了 -1,这就意味着临界资源已被占用,因此进程 B 被阻塞。
  • 直到进程 A 访问完共享内存,才会执行 V 操作,使得信号量恢复为 0,接着就会唤醒阻塞中的线程 B,使得进程 B 可以访问共享内存,最后完成共享内存的访问后,执行 V 操作,使信号量恢复到初始值 1。

 这是互斥信号量,是为了阻止多个进程在同一资源上(共享内存)的冲突

也有多进程需要协同操作共同进行的情况,这时候就需要同步信号量

例如,进程 A 是负责生产数据,而进程 B 是负责读取数据,这两个进程是相互合作、相互依赖的,进程 A 必须先生产了数据,进程 B 才能读取到数据,所以执行是有前后顺序的。

那么这时候,就可以用信号量来实现多进程同步的方式,我们可以初始化信号量为 0

具体过程:

  • 如果进程 B 比进程 A 先执行了,那么执行到 P 操作时,由于信号量初始值为 0,故信号量会变为 -1,表示进程 A 还没生产数据,于是进程 B 就阻塞等待;
  • 接着,当进程 A 生产完数据后,执行了 V 操作,就会使得信号量变为 0,于是就会唤醒阻塞在 P 操作的进程 B;
  • 最后,进程 B 被唤醒后,意味着进程 A 已经生产了数据,于是进程 B 就可以正常读取数据了。

 可以发现,信号初始化为 0,就代表着是同步信号量,它可以保证进程 A 应在进程 B 之前执行。

信号

处理异常进程时的工具,实质上时shell相关的操作,包括kill和键入Ctrl+c,用户进程接收到相关的信号命令后会有以下不同的反应:

1.执行默认操作。Linux 对每种信号都规定了默认操作,例如,上面列表中的 SIGTERM 信号,就是终止进程的意思。Core 的意思是 Core Dump,也即终止进程后,通过 Core Dump 将当前进程的运行状态保存在文件里面,方便程序员事后进行分析问题在哪里。

2.捕捉信号。我们可以为信号定义一个信号处理函数。当信号发生时,我们就执行相应的信号处理函数。

3.忽略信号。当我们不希望处理某些信号的时候,就可以忽略该信号,不做任何处理。有两个信号是应用进程无法捕捉和忽略的,即 SIGKILL 和 SEGSTOP,它们用于在任何时候中断或结束某一进程。

可以在shell内通过如下操作查看各种信号

$ kill -l
 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
 6) SIGABRT	 7) SIGBUS	 8) SIGFPE	 9) SIGKILL	10) SIGUSR1
11) SIGSEGV	12) SIGUSR2	13) SIGPIPE	14) SIGALRM	15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD	18) SIGCONT	19) SIGSTOP	20) SIGTSTP
21) SIGTTIN	22) SIGTTOU	23) SIGURG	24) SIGXCPU	25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF	28) SIGWINCH	29) SIGIO	30) SIGPWR
31) SIGSYS	34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX

 Socket

通过套接字实现不同主机上的进程通信,分为TCP和UDP两种,和计网内容重合,之后细说

TCP流程 

  • 服务端和客户端初始化 socket,得到文件描述符;
  • 服务端调用 bind,将绑定在 IP 地址和端口;
  • 服务端调用 listen,进行监听;
  • 服务端调用 accept,等待客户端连接;
  • 客户端调用 connect,向服务器端的地址和端口发起连接请求;
  • 服务端 accept 返回用于传输的 socket 的文件描述符;
  • 客户端调用 write 写入数据;服务端调用 read 读取数据;
  • 客户端断开连接时,会调用 close,那么服务端 read 读取数据的时候,就会读取到了 EOF,待处理完数据后,服务端调用 close,表示连接关闭。

这里需要注意的是,服务端调用 accept 时,连接成功了会返回一个已完成连接的 socket,后续用来传输数据。

所以,监听的 socket 和真正用来传送数据的 socket,是「两个」 socket,一个叫作监听 socket,一个叫作已完成连接 socket

成功连接建立之后,双方开始通过 read 和 write 函数来读写数据,就像往一个文件流里面写东西一样。

UDP流程

UDP 是没有连接的,所以不需要三次握手,也就不需要像 TCP 调用 listen 和 connect,但是 UDP 的交互仍然需要 IP 地址和端口号,因此也需要 bind。

对于 UDP 来说,不需要要维护连接,那么也就没有所谓的发送方和接收方,甚至都不存在客户端和服务端的概念,只要有一个 socket 多台机器就可以任意通信,因此每一个 UDP 的 socket 都需要 bind。

另外,每次通信时,调用 sendto 和 recvfrom,都要传入目标主机的 IP 地址和端口。

Unix Domain Socket

Unix域套接字只能用于在同一个计算机的进程间进行通信。虽然网络套接字也可以用于单机进程间的通信,但是使用Unix域套接字效率会更高,因为Unix域套接字仅仅进行数据复制,不会执行在网络协议栈中需要处理的添加、删除报文头、计算校验和、计算报文顺序等复杂操作,因而在单机的进程间通信中,更加推荐使用Unix域套接字

线程间通信

同个进程下的线程之间都是共享进程的资源,只要是共享变量都可以做到线程间通信,比如全局变量,所以对于线程间关注的不是通信方式,而是关注多线程竞争共享资源的问题,信号量也同样可以在线程间实现互斥与同步:

  • 互斥的方式,可保证任意时刻只有一个线程访问共享资源;
  • 同步的方式,可保证线程 A 应在线程 B 之前执行;

 线程同步

操作系统范围内的同步指的是指定线程或进程的运行次序,"同"更偏向于协同吧(我理解的)

先看一段代码:

#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

#define NLOOP 5000 //循环次数

int count = 0;//全局资源

void* func(void* p)
{
	int i,val;
	for (i = 0; i < NLOOP; i++)
	{
		val = count;
		printf("count = %d for %d\n",val+1,GetCurrentThreadId());//打印count以及当前的线程id(仅限于windows) 
		count = val + 1;
		
		usleep(100);//减缓线程执行速度,增加资源冲突概率
    }
	return NULL;
}

int main()
{
	pthread_t tidA, tidB;
	
	pthread_create(&tidA, NULL, &func, NULL);//线程A 对count++
	pthread_create(&tidB, NULL, &func, NULL);//线程B 对count++
	sleep(1);
	pthread_join(tidA, NULL);
	pthread_join(tidB, NULL);
	
	return 0;
}

直接运行三次看截图:

  •  第一次运行,结束时count  = 5000
  •  第二次运行,结束时count = 5154
  •  第三次运行,结束时count = 5014

 

显然三次运行的结果是不一样的,何以至此呢?

  • 对于全局变量count,两个线程是共用的,当进行一次对count取值然后打印的操作时,实际上是从内存里取值拷贝到寄存器,打印再加一之后才返回内存保存对count的修改的
  • 当两个线程并行运行时,可能对count的取值时机恰好相遇,此时修改count的值前后的顺序就看cpu在两个线程之间的分配结果了
  • 举个例子,当count=100时,A线程对count取值+1,B也对count取值+1,理论上来说count=102,但是倘若A线程返回count值之前B已经对count进行了取值,那么count=101
  • 用时序图的方式表示这种冲突(矛盾):在这里插入图片描述

         接下来通过互斥量来解决这个问题

互斥量

  • 互斥量是pthread_mutex_t类型的变量。
  • 互斥量有两种状态:lock(上锁)、unlock(解锁)
  • 当对一个互斥量加锁后,其他任何试图访问互斥量的线程都会被堵塞,直到当前线程释放互斥锁上的锁。如果释放互斥量上的锁后,有多个堵塞线程,这些线程只能按一定的顺序得到互斥量的访问权限,完成对共享资源的访问后,要对互斥量进行解锁,否则其他线程将一直处于阻塞状态。

        有了互斥量之后就可以避免多个线程同时对同一个资源进行访问的问题了,对之前的例子加上代码:

#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

#define NLOOP 5000 //循环次数

int count = 0;//全局资源
pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;//定义锁

void* func(void* p)
{
	int i,val;
	for (i = 0; i < NLOOP; i++)
	{
		pthread_mutex_lock(&counter_mutex);//上锁
		val = count;
		printf("count = %d for %d\n",val+1,GetCurrentThreadId());//打印count以及当前的线程id(仅限于windows) 
		count = val + 1;
		pthread_mutex_unlock(&counter_mutex);//解锁
		
		usleep(100);//减缓线程执行速度,增加资源冲突概率
    }
	return NULL;
}

int main()
{
	pthread_t tidA, tidB;
	
	pthread_create(&tidA, NULL, &func, NULL);//线程A 对count++
	pthread_create(&tidB, NULL, &func, NULL);//线程B 对count++
	sleep(1);
	pthread_join(tidA, NULL);
	pthread_join(tidB, NULL);
	
	return 0;
}

        避免多次运行麻烦,这里只运行两次:

  • 第一次,count=10000
  • 第二次,count=10000 

         结果表明两次运行的结果和预期的结果是相符的        

        这里再附上<pthread.h>提供的一些关于互斥量的函数(api)

#include <pthread.h>

//pthread_mutex_t是锁类型,用来定义互斥锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

//互斥锁的初始化
//restrict,C语言中的一种类型限定符,用于告诉编译器,对象已经被指针所引用,不能通过除该指针外所有其他直接或间接的方式修改该对象的内容。 第二个参数一般为NULL
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);

//上锁
int pthread_mutex_lock(pthread_mutex_t *mutex);

//判断是否上锁
//返回值:0表示已上锁,非0表示未上锁。
int pthread_mutex_trylock(pthread_mutex_t *mutex);

//解锁 
int pthread_mutex_unlock(pthread_mutex_t *mutex);

//销毁互斥锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);

        有锁就会有死锁,死锁发生的一些情况:

  • 同一个线程已拥有A锁的情况下,再次请求获取A锁,导致线程阻塞
    解决方法:使用完资源后立刻解锁
  • 线程一拥有A锁,再次请求获取B锁,同时线程二拥有B锁,请求获取A锁,导致线程阻塞
    解决方法:当拥有锁的情况下,请求获取另外一把锁失败时,释放已拥有的锁

条件变量

        互斥量主要解决的是互斥问题,对于没有关系的两个线程互斥量是足够的,但当两个线程之间有一定的前后关系时,互斥量是解决不了问题的(毕竟谁都能拿锁,仅仅取决于cpu的分配).

        条件变量是线程的另外一种同步机制,这些同步对象为线程提供了会合的场所,理解起来就是两个(或者多个)线程需要碰头(或者说进行交互-一个线程给另外的一个或者多个线程发送消息),我们指定在条件变量这个地方发生,一个线程用于修改这个变量使其满足其它线程继续往下执行的条件,其它线程则接收条件已经发生改变的信号。

        条件变量同锁一起使用使得线程可以以一种无竞争的方式等待任意条件的发生。所谓无竞争就是,条件改变这个信号会发送到所有等待这个信号的线程。而不是说一个线程接受到这个消息而其它线程就接收不到了。

        经典问题,生产者-消费者问题:生产者生产了资源消费者才可以使用,这是有严格的前后次序的

        生产者-消费者模型:

  • 首先消费者需要访问共享资源首先要去拿到锁访问条件变量,条件变量说现在还没有资源,所以消费者释放锁,阻塞等待,直到生产者生产出资源后,将资源先放到公共区后,再告诉条件变量,现在有资源了,然后条件变量再去唤醒阻塞线程,这些阻塞的线程被唤醒后需要去争抢锁,先拿到锁的线程优先访问共享资源在这里插入图片描述

        上代码:

#include <stdlib.h>
#include <pthread.h>
#include <stdio.h>

typedef struct msg {
	struct msg *next;
	int num;
}MSG_T;

MSG_T *head;//消息头结点

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;//互斥锁
//pthread_cond_t has_product = PTHREAD_COND_INITIALIZER; //条件变量 本文通过init定义条件变量
pthread_cond_t mycond;


//生产者
void *producer(void *p)
{
	int times = 20;
	MSG_T* mp;
	while (1) {
		mp = malloc( sizeof(MSG_T) );

		mp->num = rand() % 1000 + 1;//生成随机数1到1000
		printf("Produce %d, the %dth times\n", mp->num, 21-times);

		//将资源放入公共区
		pthread_mutex_lock(&lock);
		mp->next = head;
		head = mp;
		pthread_mutex_unlock(&lock);

		//通知条件变量唤醒线程
		pthread_cond_signal(&mycond);
		times--;
		if(times==0)	break;
		//sleep(rand() % 5);
	}
}

//消费者
void *consumer(void *p)
{
	int times = 20;
	MSG_T* mp;
	while (1) {
		pthread_mutex_lock(&lock);//上锁
		printf("Start consume:\n");
		while (head == NULL)//当没有数据时,wait阻塞等待
			pthread_cond_wait(&mycond, &lock);//当条件变量不符合的时候 释放锁并等待

		mp = head;
		head = mp->next;
		pthread_mutex_unlock(&lock);//解锁
		printf("Consume %d, the %dth times\n", mp->num, 21-times);
		free(mp);
		times--;
		if(times==0)	break;
		//sleep(rand() % 5);
	}
}


int main(int argc, char *argv[])
{
	pthread_t pid, cid;

	if (pthread_cond_init(&mycond, NULL) != 0)
	{
		printf("cond error'\n");
		exit(1);
	}

	srand(time(NULL));//加入随机因子

	pthread_create(&pid, NULL, producer, NULL);
	pthread_create(&cid, NULL, consumer, NULL);

	pthread_join(pid, NULL);
	pthread_join(cid, NULL);

	return 0;
}

        这里用链表来表示公共资源,当链表为空时消费者线程需要进行等待 

         运行结果:

        可以看到并不是开始消费了就进行消费操作,很多时候是开始消费之后由于没有资源而阻塞,等待生产者生产完成并发送信号才发送.

        同样值得注意的是代码中生产者和消费者上锁的时机是不同的,生产者是生产完成将资源放入公共区时才上锁,但消费者是一开始进行消费就上锁了(当然当没有资源陷入阻塞时会释放锁)——这里我想强调的是"对谁上锁":是对公共资源上锁,当访问公共资源时,不管是增加资源还是减少资源,都应该上锁,就像原子性的PV操作,都是针对公共资源的(包括对信号量的访问);原因是对资源操作不是直接操作,在计算机底层都需要经过寄存器的过程,这个过程会造成时序上的差别而导致结果不如理论预期

        附上C中条件变量的函数原语(api):

#include <pthread.h>

//全局定义条件变量
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;

//初始化条件变量
//cond参数为条件变量指针,通过该函数实现条件变量赋初值;cond_attr参数通常为NULL
int pthread_cond_init(pthread_cond_t *cond,pthread_condattr_t *cond_attr); 

//销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond); 

//自动释放mutex锁,等待条件满足
//这个函数的过程我们必须了解,首先对互斥锁进行解锁;然后自身堵塞等待;当等待条件达成,注意这时候函数并未返回,而是重新获得锁并返回。
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);

//自动释放mutex锁,等待条件满足,如果在abstime时间内还没有满足,则返回错误
int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,const timespec *abstime);
 
//让等待条件满足的线程中某一个被唤醒
int pthread_cond_signal(pthread_cond_t *cond);

//让等待条件满足的线程中全部被唤醒 (广播)
int pthread_cond_broadcast(pthread_cond_t *cond);

POSIX信号量

        信号量实际上就是互斥锁+计数器,公共区的资源有个明确的限定的时候就可以使用信号量,我自己体会的是信号量比条件变量更多变,条件变量在解决生产者-消费者模型时只需要满足有资源就可以执行消费操作;但信号量可以解决临界区大小的问题(也就是生产者生产行为也会受限).

        设想新的生产者-消费者问题模型:生产者只有五个空位够用,当生产了五个资源后必须等到消费者消费了才能再进行消费,这里用条件变量就比较不适合,但是用信号量的话只用指定两个信号量就可以了:即空位数和当前资源数.

        上代码:

#include <stdlib.h>
#include <pthread.h>
#include <stdio.h>
#include <semaphore.h>

#define NUM 5

int queue[NUM];//定义一个环形队列
sem_t blank_number, product_number;//定义两个信号量

//生产者
void *producer(void *arg)
{
	int p = 0;
	int times = 20;
	int blankNum = 0;
	while (1) {
		sem_getvalue(&blank_number,&blankNum);
		printf("Start produce: the blanks leave now is %d\n", blankNum);//表明开始生产,并打印当前剩余的空位数 
		sem_wait(&blank_number);//信号量blank_number--,可以比喻成盘子(最多5个盘子)盘子数量-1,

		queue[p] = rand() % 1000 + 1;//产生随机数
		printf("Produce %d, the %dth times\n", queue[p], 21-times);

		sem_post(&product_number);//信号量product_number++ ,比喻成菜,现在菜的数量+1 消费者可以使用

		p = (p + 1) % NUM;//队列下标偏移
		times--;
		if(times==0)	break;
		//sleep(rand() % 5);
	}
}

//消费者
void *consumer(void *arg)
{
	int c = 0;
	int times = 20;
	int productNum = 0;
	while (1) {
		sem_getvalue(&product_number,&productNum);
		printf("Start consume: the products leave now is %d\n", productNum);//表明开始消费,并打印当前剩余的资源数
		sem_wait(&product_number);//信号量product_number--,现在菜的数量-1

		printf("Consume %d, the %dth times\n", queue[c], 21-times);
		queue[c] = 0;//清空当前下标队列

		sem_post(&blank_number);//信号量 blank_number++,资源使用完了 盘子数量+1

		c = (c + 1) % NUM;//队列下标偏移
		times--;
		if(times==0)	break;
		//sleep(rand() % 5);
	}
}


int main(int argc, char *argv[])
{
	pthread_t pid, cid;

	sem_init(&blank_number, 0, NUM);//值为5
	sem_init(&product_number, 0, 0);//初始值为0

	pthread_create(&pid, NULL, producer, NULL);
	pthread_create(&cid, NULL, consumer, NULL);

	pthread_join(pid, NULL);
	pthread_join(cid, NULL);

	sem_destroy(&blank_number);
	sem_destroy(&product_number);
	return 0;
}

        看运行结果:         注意到某一次consume开始后由于没有资源而发生阻塞,直到有了资源且cpu的时间片分到消费者的线程后才进行消费

         确实就是互斥锁+计数器

        附上信号量的一些函数原语(api):

#include <semaphore.h>

//初始化
//sem: 要进行初始化的信号量对象
//pshared:控制着信号量的类型,如果值为0,表示它是当前进程的局部信号量;否则,其他进程就能够共享这个信号量
//value:赋给信号量对象的一个整数类型的初始值调用成功时 返回 0;
int sem_init(sem_t *sem,int pshared,unsigned value);

//p操作 -1
int sem_wait(sem_t *sem);

//v操作 +1
int  sem_post(sem_t *sem);

//销毁信号量
int sem_destory(sem_t *sem);

一些总结:

        线程同步其实只需要互斥锁就能全部完成,原理上只需要不停轮询就好,但是轮询难以把握轮询的时间间隔,同时也会造成计算资源的浪费,所以有了信号量和条件变量的存在,就是通过主动阻塞主动唤醒的机制来动态地控制线程的同步.

        互斥锁,信号量,饥饿读写锁,写优先读写锁,这4个可以互相替代。只要系统自带任意一个,另外3个都可以自己实现。

  • 一个互斥锁+一个以互斥锁为元素的集合+一个计数器,可以实现一个信号量。
  • 信号量设为1,就是一个互斥锁。
  • 两个互斥锁,可以实现一个饥饿读写锁。
  • 两个饥饿读写锁,可以实现一个写优先读写锁。
  • 读写锁只上写锁,就是互斥锁。

        这里没提到条件变量,条件变量比较特殊,使用场景很多,解决生产者-消费者模型似乎业界公认最好的方法是条件变量,我理解的条件变量和信号量的差别:

  • 信号量内部使用了value,而条件变量更像是外部使用了value.两者的value语义也不同.
  • 信号量自身wait和signal的原子操作保证了value的同步,条件变量只能多添加一个互斥量来实现value的同步.
  • 信号量内部定义value限制了其局限性,即只能对int类型资源的变化进行同步;条件变量实际上并不一定使用int类型value:while(P!=5),while(P!=NULL),while(a==b)等都可以作为一种条件.
  • 信号量只能一次唤醒一个特定的进程,条件变量可以广播(网上查的)

        通过一个变量来控制线程的run/stop是件很cool的事情,避免了线程主动去访问疯狂地轮询某个条件来自己控制自己.使用的话,C范围内的使用都可以直接看<semaphore.h>或<pthread.h>,掌握使用方法能调用api就好

又是一些总结

        进程是一个main(),线程是一个具体的函数执行过程(不包括main())

  • 进程在考虑通信,通信要解决的是如何从用户态到内核态(进程只会共用一个内核,其他完全不共用),这个和操作系统联系更深
  • 线程在考虑同步,同步要解决的是如何有序如期地访问公共资源(公用的一个进程内的资源或变量),这个似乎完全可以在用户态完成,和操作系统联系多的也就是线程切换时关于cpu的计算分配了

        写文章时有借鉴到这篇线程同步和这篇进程通信

        目前就总结这么多了,东拼西凑的,如果有幸让您看到这篇文章并且有所收获,我荣幸之至;由于都是看书和上网查的一些内容,我不能保证完全正确,有问题的话欢迎交流

本人微信: KingOfShit_Rick

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值