对进程的理解和POSIX 信号量的使用

  • 这两天看了一个不错的操作系统mooc,对Linux进程有了一个基本的认识,感觉一些网上的资料只是照搬概念。

1 对进程的理解

  • 为什么要有操作系统?
    操作系统本质上就是一个程序,是为了我们更方便的使用计算机硬件。
    其实我们看操作系统的功能就可以知道为什么要有操作系统了。比如操作系统的一个功能是进程管理,在裸机编程时,我们执行的程序功能一般都比较单一,或者我们也可以写很多很多的函数,放在while循环里,然后通过串口或者按键选择执行哪个功能,但想一想就非常复杂。
    同时,一个重要的问题是,我们在执行一个对硬盘,对显示屏等外设的读写任务时,速度是非常慢的,程序会等待在这个地方,CPU此时也不做什么运算,处于等待状态。
    那在裸机编程时,一个解决方法是:程序中设置DMA读取指定数量的数据,然后程序转去执行其他工作。等到DMA传输完成后触发对于中断,再对数据进行处理。首先这个过程实现起来虽然不算麻烦,但是如果有多段程序呢? 同时可能这多段程序都想要读写硬盘,感觉瞬间就混乱了。
    那有了linux之后呢? 我们直接写几个程序,编译后,让它们在后台执行就完事了。

  • 进程管理
    在多个程序中,每个程序往往并不是时刻利用CPU,所以linux内核会对这多个程序进行调度(单核就是交替执行,多核是让程序在多个处理器核心上交替执行和切换),提高CPU的利用率,尽量让这个几个程序都较快的执行完,这也就是操作系统的进程管理的通俗理解。同时对用户来说,调度完全是一个黑箱,我们不知道某一个时间点具体在执行哪一个程序。
    进程在切换时,PCB也要切换,有点麻烦,所以能否只切换程序段,而需要的资源保持不变?这就是线程,线程既保留了并发的优点,也减小了程序切换的代价。
    在这里插入图片描述

2 同步

  • 进程的同步指的是让程序合理有序的推进(向下运行),其实就是通过让某些进程等待来实现的。

  • 多个进程不合理有序推进的例子:
    (下面以多线程举例,因为多个子进程是不共享全局变量的,创建子进程时,只要对全局变量进行写,就创建一个副本,也就是多个子进程的全局变量是不共享的。但出现的问题类似。)
    比如多个线程修改一个全局变量,那么因为内核的调度,很可能造成一个线程在运算之后,还没有给全局变量赋新值时cpu就切换到了另一个线程,在新的线程中对全局变量修改后,可能切回到原来的线程,这时候这个线程再使用它计算的结果对全局变量进行修改,就导致了全局变量的值不是我们想要的那样。实际遇到的情况可能更混乱。

  • 对于这个问题,一个直观的想法就是,能不能使一个线程对全局变量修改时,cpu不能切换到其他线程?,这种方法是有的,就是原子指令(atomic operation),我们可以使用原子指令来对变量的值进行修改。但是原子指令的数量和功能是有限的,而实际上我们可能想要执行的不是简单的修改一个变量,而是一段较长的程序。 所以下面引出临界区的概念。

  • 我们把一段运行结果不想被调度影响的程序叫做临界区,程序在临界区执行时,依然会切换到其他线程(进程),但是其他线程中的临界区不能被执行,只会在临界区前等待,然后经过多次切换,重新回到原线程时,继续执行临界区的程序,所以最终的执行结果不会受到调度的影响。
    我们经常能看到互斥锁,临界区等名词,具体参考以下内容,感觉很多博客反而会造成误导,这位大佬的回答印证了我的理解。
    互斥锁,同步锁,临界区,互斥量,信号量,自旋锁之间联系是什么? - Tim Chen的回答 - 知乎

  • 上面说的过程称为临界区保护。实现临界区保护有三种方式:

    1. 一是纯软件方法,如peterson算法,面包店算法
    2. 二是关闭内核调度功能,修改变量后,再开启。但这种方式不适用于多核。
    3. 三是通过原子操作(atomic operation)进行加锁,原子操作是不需要被同步的,也就是不会被调度打断。可以通过原子操作对另一个全局变量判断是否为0,并且置1,在临界区结束后对该变量置0。这个全局变量被称为互斥锁。这也是实现临界区保护的主要方法。
  • 信号量(semophore)其实就是一个变量,可以用来进行复杂的同步,而不仅仅是互斥,互斥只是说明临界区执行时不会被打断,但不能控制更具体一些的执行流程。比如用信号量表示某种资源可用的数量,某个进程在资源为2时执行某种操作。在早期的linux版本(如0.11)中没有提供信号量的操作,需要手动实现,所以信号量的操作需要放到临界区进行保护。

  • POSIX中信号量的PV操作(加减)是原子操作,所以不需要考虑信号量修改的互斥问题,因为原子操作不会被调度打断。当信号量的值只取0,1时,就是实现了互斥锁。如果信号量不只是取0,1,就不能实现互斥锁的功能,也就不能实现临界区保护。

3 POSIX 信号量的使用

  • POSIX信号量的值大于等于0。当值大于0时,进程不会阻塞,等于0时,进程会阻塞。
  • 常用的一种方式是,用信号量表示一种资源,进程判断资源是否为0,如果为0则阻塞,不为0则对信号量减1执行某种功能,执行完之后,对信号量加1,表示将资源返回。这种情况下只需要用到一个信号量。
  • POSIX信号量操作的相关函数就不赘述了。

3.1 设想以下问题:

  • 运用多进程及进程间通信模拟实现以下过程:
    收货员不断往仓库放入两类货物,一类需要运往北京,一类需要运往上海,放置于不同区域。同时有两名送货员,一名专门负责从仓库取出运往北京的货物并运送,一名专门负责从仓库取出运往上海的货物并运送。

3.2 分析

用A表示发往北京的货物,用B表示发往上海的货物,分别使用两个信号量来代表两类货物的数量。

POSIX信号量的PV操作为原子操作(atomic operation),不会因cpu的调度而被打断。用两个进程分别模拟送货员,不断运出货物,即不断进行P操作,当货物数量为0,即信号量值为0时等待。用一个进程模拟收货员,不断放入货物,即不断对两个信号量进行P操作。

但是实际生产中库存都是有限的,因此我们设两种货物的最大库存为20,当达到最大库存时,不再进行生产。初始库存为10。

在经典的生产者-消费者问题中,我们可以看到如下伪代码:

    // 生产者
    while(1)
    {
        P(empty);
    	...
        V(full);
    }

    // 消费者
    while(1)
    {
        P(full);
        ...
        V(empty);
    }

其中full和empty都是信号量,PV操作为对信号量的值进行减加。full表示可用货物,empty表示可用的空余空间,二者之和为仓库容量。如果在本问题中,即有一个收货员放入两种货物到两个不同区域,如果还使用这种方式,就会造成当一种货物空余空间不足时,就会导致程序阻塞,即另一种货物也无法放入它的仓库。

因此,收货员使用以下形式的伪代码:

    while(1)
    {	
    	if(full1 < BUFFER_SIZE1)
        {	
        	V(full1);	
        }
        if(full2 < BUFFER_SIZE2)
        {
        	V(full2);
        }
    }

使用POSIX信号量时,P和V都是原子操作,因此P(empty)是不可被打断的,保证了empty的值时最新的。如果我们使用 if(full < BUFFER_SIZE) ,获取full值之后可能被会切换到其他进程,导致做比较时full值不是最新的,可能已经是其他进程修改过的。

但是这种情况在这个问题中没有影响,因为V(full)是原子操作,不会造成full的值混乱。虽然可能会出现以下情况:获取到的full值为20,BUFFER_SIZE为20,但是在比较时,full1的实际值已经变成了19,所以本来应该要执行V(full)的,但不会执行。这造成了逻辑上的瑕疵,但对整体运行是没有影响的,因为不会造成仓库溢出。如果想解决这里的问题,我们可以对比较语句加上临界区保护,即在比较完成前,不允许其他进程对信号量进行修改。

因为只有一个收货员,我们假设优先处理货物A,货物A和货物B每次放入仓库需要0.6s,送货员每次拿出需要0.5s。

程序写好后,实验时有多种方式,比如再写个程序,在其中通过fork启动各个编译好的程序,或者是写一个shell脚本,每个程序后台执行,将各自的输出重定向到文本文件。我实验时为了方便,选择了直接开3个shell,在其中分别启动3个程序。

首先启动收货员程序producer.out,如下图,在达到最大库存时,两种货物都停止了放入。
在这里插入图片描述

之后先启动第一个送货员程序consumer1 .out,此时producer.out程序的输出如下图,可以发现只有A在变化。

在这里插入图片描述

最后启动第二个送货员程序consumer2.out,此时货物B的数量也发生了变化。

在这里插入图片描述

等待一段时间后,可以发现A和B数量都变成了0,因为设置的收货速度大于送货速度。
在这里插入图片描述

3.3 代码

    // producer.c
    #include<stdio.h>
    #include<unistd.h>
    #include<semaphore.h>
    #include<fcntl.h>
    
    #define SEM_A "goods_a"
    #define SEM_B "goods_b"
    
    #define BUFFER_SIZE 20
    int main()
    {
    
            sem_t * pSemA = sem_open(SEM_A, O_CREAT, 0666, 10);
            sem_t * pSemB = sem_open(SEM_B, O_CREAT, 0666, 10);
            int semVal;
    
    //      for(int i=0;i<20;i++)
            while(1)
            {
                    sem_getvalue(pSemA, &semVal);
                    if(semVal < BUFFER_SIZE)
                    {
                            usleep(600000);
                        // 需要用sleep而不是delay,delay不可被打断,用sleep更合适
                            sem_post(pSemA);
                            printf("Number of Goods A: %d\n",semVal);
    
                    }
    
                    sem_getvalue(pSemB, &semVal);
                    if(semVal < BUFFER_SIZE)
                    {
                            usleep(600000);
                            sem_post(pSemB);
                            printf("Number of Goods B: %d\n",semVal);
                    }
            
            
            }
            sem_close(pSemA);
            sem_close(pSemB);
            return 0;
     
    }
    // consumer1.c
    #include<stdio.h>
    #include<unistd.h>
    #include<semaphore.h>
    #include<fcntl.h>
    
    #define SEM_A "goods_a"
    
    int main()
    {
    
            sem_t * pSemA = sem_open(SEM_A, O_CREAT, 0666, 10);
            int semVal;
    
            while(1)
            {
                    sem_wait(pSemA);
                    sem_getvalue(pSemA, &semVal);
                    printf("%d\n",semVal);
                    usleep(500000);
            }
            sem_close(pSemA);
            sem_unlink(SEM_A);
            return 0;
    }
    // consumer2.c
    #include<stdio.h>
    #include<unistd.h>
    #include<semaphore.h>
    #include<fcntl.h>
    
    #define SEM_B "goods_b"
    
    int main()
    {
    
            sem_t * pSemB = sem_open(SEM_B, O_CREAT, 0666, 10);
            int semVal;
    
            while(1)
            {
                    sem_wait(pSemB);
                    sem_getvalue(pSemB, &semVal);
                    printf("%d\n",semVal);
                    usleep(500000);
            }
            sem_close(pSemB);
            sem_unlink(SEM_B);
            return 0;
    }
    // unlink.c
    /* 
    使用ctrl-c终止进程后,信号量虽然很close,但不会unlink,
    也就信号量的值依然保存在内核中,因此写了个程序来重置信号量的值。
    */
    #include<stdio.h>
    #include<unistd.h>
    #include<semaphore.h>
    #include<fcntl.h>
    
    #define SEM_A "goods_a"
    #define SEM_B "goods_b"
    
    int main()
    {
    
            sem_t * pSemA = sem_open(SEM_A, O_CREAT , 0666, 5);
    
            sem_close(pSemA);
            sem_unlink(SEM_A);
    
            sem_t * pSemB = sem_open(SEM_B, O_CREAT , 0666, 5);
    
            sem_close(pSemB);
            sem_unlink(SEM_B);
    
    
            return 0;
    }

感慨一下,自从写好了一个Makefile之后,每次用到Makefile时在之前基础上稍微改改就行了。

# Makefile
#
# Date: October 16, 2018
# Author: Yan He


CC = gcc  # The compiler 

APP_SRC = $(wildcard *.c)                      # Get all "*.c" filename in current directory
TARGETS = $(patsubst %.c, %.out, $(APP_SRC))   # Replace the ".c" by ".out"

# INC_SRC = $(wildcard ./lib/*.c ./include/src/*.c)  # Get all "*.c"  file in lib and include
# INCLUDES = -I ./include -I .  #The path to find included file

LIBS = -L ./lib   # The path to find library file

CCFLAGS = -lpthread -g -Wall -O0   #The needed flag. If the vesion is Release, '-g' need to be removed.


all:$(TARGETS)

$(TARGETS):%.out:%.c
        @ echo CXX/LD -o $@ $< 
        @ $(CC) $^ $(CCFLAGS) -o $@

clean:
        rm -rf *.out *.o $(TARGETS)  

.PHONY:clean

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值