操作系统上机随笔《实验一》

OK,今天来写一下这个实验一

1,实验目的

深刻理解线程和进程的概念,掌握线程与进程在组成成分上的差别,以及与其相适应的通讯方式和应用目标。

2,实验内容

  • 以Linux系统进程和线程机制为背景,掌握fork()和clone()系统调用的形式和功能,以及与其相适应的高级通讯方式。由fork派生的子进程之间通过pipe通讯,由clone创建的线程之间通过共享内存通讯,对于后者需要考虑互斥问题。
  • 以生产者/消费者问题为例,通过实验理解fork()和clone()两个系统调用的区别。程序要求能够创建4个进程或线程,其中包括两个生产者和两个消费者,生产者和消费者之间能够传递数据。

3,实验准备

  • fork系统调用

所需头文件

#include <sys/types.h> // 提供类型 pid_t 的定义

#include <unistd.h>//是定义fork()的地方,所以必须包含

函数说明建立一个新的进程
函数原型pid_t fork(void)
函数返回值0:返回给子进程
子进程的ID(大于0的整数):返回给父进程
-1:出错,返回给父进程,错误原因存于errno中
错误代码EAGAIN:内存不足
ENOMEM:内存不足,无法配置核心所需的数据结构空间

fork系统调用用于从已存在进程中创建一个新进程,新进程称为子进程,而原进程称为父进程。fork调用一次,返回两次,这两个返回分别带回它们各自的返回值,其中在父进程中的返回值是子进程的进程号,而子进程中的返回值则返回 0。因此,可以通过返回值来判定该进程是父进程还是子进程。

使用fork函数得到的子进程是父进程的一个复制品,它从父进程处继承了整个进程的地址空间,包括进程上下文、进程堆栈、内存信息、打开的文件描述符、信号控制设定、进程优先级、进程组号、当前工作目录、根目录、资源限制、控制终端等,而子进程所独有的只有它的进程号、计时器等。因此可以看出,使用fork系统调用的代价是很大的,它复制了父进程中的数据段和堆栈段里的绝大部分内容,使得fork系统调用的执行速度并不很快。

fork的返回值这样设计是有原因的,fork在子进程中返回0,子进程仍可以调用getpid函数得到自己的进程ID,也可以调用getppid函数得到父进程的进程ID。在父进程中使用getpid函数可以得到自己的进程ID,然而要想得到子进程的进程ID,只有将fork的返回值记录下来,别无它法。

fork的另一个特性是所有由父进程打开的文件描述符都被复制到子进程中。父、子进程中相同编号的文件描述符在内核中指向同一个file结构体,也就是说,file结构体的引用计数要增加。

由于代码段(加载到内存的执行码)在内存中是只读的,所以父子进程可共用代码段,而数据段和堆栈段子进程则完全从父进程复制拷贝了一份。

在Linux中,对fork进行了优化,调用时采用写时复制 (COW,copy on write)的方式,在系统调用fork生成子进程的时候,不马上为子进程复制父进程的资源,而是在遇到“写入”(对资源进行修改)操作时才复制资源。它使得一个普通的fork调用非常类似于vfork,但又避免了vfork的缺点,使得vfork变得没有必要。

fork()调用实例:

  1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <sys/types.h>
  4 
  5 int main(){
  6     pid_t pid = fork();
  7     int count = 0;
  8     if(pid < 0){    //返回值为负数,调用fork失败
  9         printf("fork failed");
 10     }else if(pid > 0){  //返回值大于0,该值包含新创建子进程的进程ID
 11         printf("我是父进程,进程号为:%d\n", getpid());
 12         count++;
 13     }else{  //返回值==0,返回到新创建的子进程
 14         printf("我是子进程,进程号为:%d\n", getpid());
 15         count++;
 16     }
 17     printf("输出count的值:%d\n", count);
 18     return 0;
 19 }

输出效果

  • clone系统调用

函数原型如下:

#define _GNU_SOURCE
#include <sched.h>
int clone(int (*func)(void*),void *child_stack,int flags,void *func_arg,....
           /*pid_t *ptid,struct user_desc *tls,pid_t *ctid*/);
                  
                                     Return process ID of child on success,or -1 on error

Linux将创建进程和执行所创建的进程分为2个阶段。第一个阶段是创建。父进程首先复制子进程,所复制出来的子进程拥有自己的任务结构体和系统堆栈,除此之外所有资源都与父进程共享。Linux提供两种方式复制子进程:一个是fork(),另外一个是clone()。fork()函数复制时将父进程的所以资源都通过复制数据结构进行了复制,然后传递给子进程,所以fork()函数不带参数;clone()函数则是将部分父进程的资源的数据结构进行复制,复制哪些资源是可选择的,这个可以通过参数设定,所以clone()函数带参数,没有复制的资源可以通过指针共享给子进程。Clone()函数的声明如下:

int clone(int (*func)(void*),void *child_stack,int flags,void *func_arg,....
           /*pid_t *ptid,struct user_desc *tls,pid_t *ctid*/);

如同fork(),由clone()创建的新进程几近于父进程的翻版。但是与fork()不同的是,克隆生成的子进程继续运行时不以调用处为起点,转而去调用以参数func所指定的函数,func又称为子函数。调用子函数时的参数由func_arg指定。经过转换,子函数可对改参数的含义自由解读,例如可以作为整型值(int),也可以视为指向结构的指针。

当函数func返回或者是调用exit()(或者_exit())之后,克隆产生的子进程就会终止。照例,父进程可以通过wait()一类函数来等待克隆子进程。

因为克隆产生的子进程可能共享父进程内存,所以它不能使用父进程的栈。调用者必须分配一块大小适中的内存空间供子进程的栈使用,同时将这块内存的指针置于参数child_stack中。

参数flags服务于双重目的。首先,其低字节中存放着子进程的终止信号,子进程退出时其父进程将收到这一信号。(如果克隆产生的子进程因信号而终止,父进程依然会收到SIGCHLD信号)该字节也可能为0,这时将不会产生任何信号。

clone()函数中的flags参数是各位掩码的组合:

CLONE_PARENT   创建的子进程的父进程是调用者的父进程,新进程与创建它的进程成了“兄弟”而不是“父子”

 CLONE_FS           子进程与父进程共享相同的文件系统,包括root、当前目录、umask

  CLONE_FILES      子进程与父进程共享相同的文件描述符(file descriptor)表

  CLONE_NEWNS   在新的namespace启动子进程,namespace描述了进程的文件hierarchy

  CLONE_SIGHAND   子进程与父进程共享相同的信号处理(signal handler)表

  CLONE_PTRACE   若父进程被trace,子进程也被trace

  CLONE_VFORK     父进程被挂起,直至子进程释放虚拟内存资源

 CLONE_VM           子进程与父进程运行于相同的内存空间

 CLONE_PID          子进程在创建时PID与父进程一致

  CLONE_THREAD    Linux 2.4中增加以支持POSIX线程标准,子进程与父进程共享相同的线程群

  • pipe系统调用

管道(pipe)是Linux系统中重要的进程间通信(IPC)机制,又分为匿名管道(anonymous pipe)和命名管道(named pipe/FIFO)两种。

管道是半双工工作的,也就是可以A进程读B进程写,也可以B进程读A进程写,但是A、B两个进程不能同时读写。

匿名管道在父进程中通过系统调用int pipe(int fd[2])创建。fd[]为两个文件描述符的数组,其中fd[0]固定为管道的读端,fd[1]固定为管道的写端,不能弄反。在父进程fork出子进程之后,使用管道的两方分别关闭fd[0]和fd[1],就可以操作管道了。

管道的读写用最基本的read()/write()系统调用来实现。注意当管道读取端没有关闭且管道已满时,write()会被阻塞;而当管道写入端没有关闭且管道为空时,read()会被阻塞。当然,如果管道的读写两端都被关闭,管道就会消失。

  • wait()

函数原型:

#include <sys/types.h> /* 提供类型pid_t的定义 */
#include <sys/wait.h>
pid_t wait(int *status)

进程一旦调用了wait,就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。

参数status用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。但如果我们对这个子进程是如何死掉的毫不在意,只想把这个僵尸进程消灭掉,(事实上绝大多数情况下,我们都会这样想),我们就可以设定这个参数为NULL,就象下面这样:
pid = wait(NULL);
如果成功,wait会返回被收集的子进程的进程ID,如果调用进程没有子进程,调用就会失败,此时wait返回-1,同时errno被置为ECHILD。

参数status:

如果参数status的值不是NULL,wait就会把子进程退出时的状态取出并存入其中,这是一个整数值(int),指出了子进程是正常退出还是被非正常结束的(一个进程也可以被其他进程用信号结束,我们将在以后的文章中介绍),以及正常结束时的返回值,或被哪一个信号结束的等信息。由于这些信息被存放在一个整数的不同二进制位中,所以用常规的方法读取会非常麻烦,人们就设计了一套专门的宏(macro)来完成这项工作,下面我们来学习一下其中最常用的两个:
1,WIFEXITED(status) 这个宏用来指出子进程是否为正常退出的,如果是,它会返回一个非零值。
(请注意,虽然名字一样,这里的参数status并不同于wait唯一的参数--指向整数的指针status,而是那个指针所指向的整数,切记不要搞混了。)
2,WEXITSTATUS(status) 当WIFEXITED返回非零值时,我们可以用这个宏来提取子进程的返回值,如果子进程调用exit(5)退出,WEXITSTATUS(status)就会返回5;如果子进程调用exit(7),WEXITSTATUS(status)就会返回7。请注意,如果进程不是正常退出的,也就是说,WIFEXITED返回0,这个值就毫无意义。

进程的一生
下面就让我用一些形象的比喻,来对进程短暂的一生作一个小小的总结:
随着一句fork,一个新进程呱呱落地,但它这时只是老进程的一个克隆。
然后随着exec,新进程脱胎换骨,离家独立,开始了为人民服务的职业生涯。
人有生老病死,进程也一样,它可以是自然死亡,即运行到main函数的最后一个"}",从容地离我们而去;也可以是自杀,自杀有2种方式,一种是调用exit函数,一种是在main函数内使用return,无论哪一种方式,它都可以留下遗书,放在返回值里保留下来;它还甚至能可被谋杀,被其它进程通过另外一些方式结束他的生命。
进程死掉以后,会留下一具僵尸,wait和waitpid充当了殓尸工,把僵尸推去火化,使其最终归于无形。
这就是进程完整的一生。

  • sem_wait(&s)和sem_post(&s)

信号量: 
信号量是IPC结构中的一种,是进程间通信的一种方法,也可以解决同一进程不同线程之间的通信问题。它是用来保证两个或多个关键代码段不被并发调用,防止多个进程同时对共享资源进行操作。

原理: 
在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。

形象理解: 

以一个停车场的运作为例。假设停车场只有三个车位,一开始三个车位都是空的。这时如果同时来了五辆车,看门人允许其中三辆直接进入,然后放下车拦,剩下的车则必须在入口等待,此后来的车也都不得不在入口处等待。这时,有一辆车离开停车场,看门人得知后,打开车拦,放入外面的一辆进去,如果又离开两辆,则又可以放入两辆,如此往复。 
在这个停车场系统中,车位是公共资源,每辆车好比一个线程,看门人起的就是信号量的作用。

两种操作: 
抽象的来讲,信号量的特性如下:信号量是一个非负整数(车位数),所有通过它的线程/进程(车辆)都会将该整数减一(通过它当然是为了使用资源),当该整数值为零时,所有试图通过它的线程都将处于等待状态。 
1. Wait(等待) 
当一个线程调用Wait操作时,它要么得到资源然后将信号量减一,要么一直等下去(指放入阻塞队列),直到信号量大于等于一时。 
2. Release(释放) 
实际上是在信号量上执行加一操作,对应于车辆离开停车场,该操作之所以叫做“释放”是因为释放了由信号量守护的资源。

两个函数: 
sem_post函数(函数原型 int sem_post(sem_t *sem);) 
作用是给信号量的值加上一个“1”。 当有线程阻塞在这个信号量上时,调用这个函数会使其中一个线程不在阻塞,选择机制是有线程的调度策略决定的。 
sem_wait函数(函数原型 int sem_wait(sem_t * sem);) 
它的作用是从信号量的值减去一个“1”,但它永远会先等待该信号量为一个非零值才开始做减法。

一种使用方法:
可以使用信号量完成类似于传递signal的功能:
某一个线程要在一定条件下完成特定功能,由其他多个线程提供条件。此时,其他线程调用sem_post()使信号量加一,本线程调用sem_wait()函数阻塞等待,信号量来了方可退出阻塞。
此种情况本线程只调用sem_wait(),之后不调用sem_post()。

  • pthread_mutex_lock(&mutex)和pthread_mutex_unlock(&mutex)

互斥锁的概念

mutex 是一种简单的加锁的方法来控制对共享资源的访问, mutex 只有两种状态,即上锁(lock)和解锁(unlock)

在访问该资源前,首先应申请 mutex,如果 mutex 处于 unlock 状态,则会申请到 mutex 并立即 lock

如果 mutex处于 lock 状态, 则默认阻塞申请者

初始化互斥锁

mutex 用 pthread_mutex_t 数据类型表示,在使用互斥锁前,必须先对它进行初始化

静态分配的互斥锁:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

动态分配互斥锁:

pthread_mutex_t mutex;
 
//在所有使用过此互斥锁的线程都不再需要使用时候, 应调用销毁互斥锁
pthread_mutex_init(&mutex, NULL);
 
//销毁互斥锁
pthread_mutex_destroy
#include <pthread.h>
 
/*
 *功能:
 *  初始化一个互斥锁
 *参数:
 *  mutex:互斥锁地址
 *  attr:互斥锁的属性,NULL 为默认的属性
 *return:
 *  成功:0
 *  失败:非0
 */
int pthread_mutex_init(pthread_mutex_t *mutex, 
                       const pthread_mutexattr_t *attr);

互斥锁上锁

#include <pthread.h>
 
/*
 *功能:
 *  对互斥锁上锁, 若已经上锁, 则调用者一直阻塞到互斥锁解锁
 *参数:
 *  mutex:互斥锁地址
 *return:
 *  成功:0
 *  失败:非0
 */
int pthread_mutex_lock(pthread_mutex_t *mutex);
#include <pthread.h>
 
/*
 *功能:
 *  对互斥锁上锁,若已经上锁,则上锁失败,函数立即返回
 *参数:
 *  mutex:互斥锁地址
 *return:
 *  成功:0
 *  失败:非0
 */
int pthread_mutex_trylock(pthread_mutex_t *mutex);

互斥锁解锁

#include <pthread.h>
 
/*
 *功能:
 *  对指定的互斥锁解锁
 *参数:
 *  mutex:互斥锁地址
 *return:
 *  成功:0
 *  失败:非0
 */
int pthread_mutex_unlock(pthread_mutex_t *mutex);

销毁互斥锁

#include <pthread.h>
 
/*
 *功能:
 *  销毁指定的一个互斥锁
 *参数:
 *  mutex:互斥锁地址
 *return:
 *  成功:0
 *  失败:非0
 */
int pthread_mutex_destroy(pthread_mutex_t *mutex);

4,实验设计

  • 用pipe()创建一个管道文件,然后用fork()创建两个生产进程和两个消费进程,它们之间通过pipe()传递信息。
  • 用clone()创建四个轻进程(线程),用参数指明共享内存等资源,通过共享内存模拟生产消费问题,利用pthread_mutex_lock(), pthread_mutex_unlock()等函数实现对共享存储区访问的互斥。

fork系统调用实验代码

#include "sys/types.h"
#include "sys/file.h"
#include "unistd.h"
char r_buf[4];  //读缓冲
char w_buf[4];  //写缓冲
int pipe_fd[2];
pid_t pid1, pid2, pid3, pid4;
int producer(int id);
int consumer(int id);
int main(int argc,char **argv){  
    if(pipe(pipe_fd)<0){//传输出错
        printf("pipe create error \n");
        exit(-1);
    }
    else{
        printf("pipe is created successfully!\n");
        //谁占用管道谁进行数据传输
        if((pid1=fork())==0)
           producer(1);
        if((pid2=fork())==0)
           producer(2);
        if((pid3=fork())==0)
           consumer(1);
        if((pid4=fork())==0)
           consumer(2);
    }
    close(pipe_fd[0]);  //需要加上这两句
    close(pipe_fd[1]);  //否者会有读者或者写者永远等待
    int i,pid,status;
    for(i=0;i<4;i++)
       pid=wait(&status);  //返回子进程状态
   exit(0);
}
//生产者
int producer(int id){
    printf("producer %d is running!\n",id);
    close(pipe_fd[0]);
    int i=0;
    for(i=1;i<10;i++){
        sleep(3);
        if(id==1) //生产者1
             strcpy(w_buf,"aaa\0");
        else  //生产者2
             strcpy(w_buf,"bbb\0");
        if(write(pipe_fd[1],w_buf,4)==-1)
            printf("write to pipe error\n");    
    }
    close(pipe_fd[1]);
    printf("producer %d is over!\n",id);
    exit(id);
}
//消费者
int consumer(int id){
    close(pipe_fd[1]); 
    printf("      producer %d is running!\n",id);
    if (id==1)  //消费者1
        strcpy(w_buf,"ccc\0");
    else  //消费者2
        strcpy(w_buf,"ddd\0");
    while(1){
         sleep(1);
         strcpy(r_buf,"eee\0");
         if(read(pipe_fd[0],r_buf,4)==0)
             break;     
         printf("consumer %d get %s, while the w_buf is %s\n",id,r_buf,w_buf);
    }
    close(pipe_fd[0]);
    printf("consumer %d is over!\n", id);
    exit(id);
}

输出结果:

说明:

我在自己的虚拟机上的/experiment01里面的fork_ex.c里面写了该部分c代码,然后使用命令“gcc fork_ex.c -o fork_ex”生成可执行文件fork_ex,然后使用命令“./fork_ex”执行,然后上图就是运行结果。可以看到producer1首先运行,然后是producer2,然后是consumer1,最后是consumer2,然后四个子进程运行过程中由于生产者的顺序是aaa,bbb,aaa,bbb...所以consumer打印的顺序也是如此,由于生产者是sleep(3),消费者是sleep(1),所以当生产者生产了第一个aaa的时候,是consumer2接收的,然后顺序依次进行。

分析fork()实验结果:

函数功能:

main函数:

执行过程中首先开启管道“pipe(pipe_fd)”,然后无错误的话主进程依次创建四个子进程执行producer(1),producer(2),consumer(1),consumer(2),然后调用四次wait等待子进程全部结束,然后主进程也结束。

producer函数:

close(pipe_fd[0]),把读关闭,开始写,在每次循环中先sleep,3秒,然后把对应aaa/bbb写到w_buf里,然后调用write输入管道。While结束时打印标识,然后exit结束进程。

consumer函数:

close(pipe_fd[1]),把写关闭,开始读,首先把自己对应的ccc/ddd写到自己的w_buf,后面打印出来,这可以表现出主进程和子进程有独立的堆栈空间。然后在while循环中,虽然使用了strcpy(r_buf,"eee\0");但是我们又使用了read(pipe_fd[0],r_buf,4),把r_buf内容更新成管道里面的,所以永远也不会打印出eee,最后while结束打印标识,然后exit结束进程。

函数之间调用关系:

main函数在四个if条件后依次生成四个子进程,每一个子进程调用对应的producer或者consumer进程,在producer进程或者consumer进程中,四个子进程是同步进行的,通过自己对应的sleep来控制对应打印的顺序。

关键语句功能说明:

if((pid1=fork())==0)

           producer(1);

        if((pid2=fork())==0)

           producer(2);

        if((pid3=fork())==0)

           consumer(1);

        if((pid4=fork())==0)

           consumer(2);

这一段,首先主进程执行到pid1=fork()时,由于fork(),所以创建了一个子进程1,然后主进程的pid1>0,所以不执行if函数体,继续到下一个if,而子进程1由于pid1==0,所以会执行producer(1),在producer(1)中,子进程1执行exit(id)后子进程1就结束了。(exit()函数在调用exit系统调用之前要检查文件的打开情况,把文件缓冲区中的内容写回文件,就是”清理I/O缓冲”)然后主进程以同样的方法创建了子进程2执行producer(2),子进程3执行consumer(1),子进程4执行consumer(2)。

for(i=0;i<4;i++)

           pid=wait(&status);  //返回子进程状态

        exit(0);

这一段主要是主进程调用四次wait(),确保其产生的四个子进程都结束了,如果有未结束的子进程,则主进程在此阻塞直到有子进程结束。然后等四个子进程都结束了,主进程也调用exit(0),进程结束。

if(write(pipe_fd[1],w_buf,4)==-1)

            printf("write to pipe error\n");

这一段在执行write(pipe_fd[i],w_buf,4)时,就把w_buf的内容写到管道里面了,如果发生错误,就打印error。

        if(read(pipe_fd[0],r_buf,4)==0)

             break;   

printf("consumer %d get %s, while the w_buf is %s\n",id,r_buf,w_buf);

这一段在执行read(pipe_fd[0],r_buf,4)时,就把管道里的内容读到r_buf里面了,如果没读到什么东西,那就break这个while循环体,在下次重新执行read,所以在这个printf语句中永远也不会打印出eee。

clone系统调用实验代码:

#define _GNU_SOURCE   
#include "sched.h"
#include<sys/types.h>
#include<sys/syscall.h>
#include<unistd.h>
#include <pthread.h>
#include "stdio.h"
#include "stdlib.h"
#include "semaphore.h"
#include "sys/wait.h"
#include "string.h"


int producer(void * args);
int consumer(void * args);
pthread_mutex_t mutex;
sem_t product;
sem_t warehouse;

char buffer[8][4];
int bp=0;

int main(int argc,char** argv){
    
    pthread_mutex_init(&mutex,NULL);//初始化
    sem_init(&product,0,0);
    sem_init(&warehouse,0,8);
    int clone_flag,arg,retval;
    char *stack;
    //clone_flag=CLONE_SIGHAND|CLONE_VFORK
//clone_flag=CLONE_VM|CLONE_FILES|CLONE_FS|CLONE_SIGHAND;
 clone_flag=CLONE_VM|CLONE_SIGHAND|CLONE_FS|    CLONE_FILES;
    //printf("clone_flag=%d\n",clone_flag);
    int i;
    for(i=0;i<2;i++){  //创建四个线程
        arg = i;
        //printf("arg=%d\n",*(arg));
        stack =(char*)malloc(4096); 
        retval=clone(producer,&(stack[4095]),clone_flag,(void*)&arg);
        //printf("retval=%d\n",retval);
        stack=(char*)malloc(4096); 
        retval=clone(consumer,&(stack[4095]),clone_flag,(void*)&arg);
       //printf("retval=%d\n\n",retval);
usleep(1);
    }
   
    exit(1);
}

int producer(void *args){
    int id = *((int*)args);
    int i;
    for(i=0;i<10;i++){
        sleep(i+1);  //表现线程速度差别
        sem_wait(&warehouse);
        pthread_mutex_lock(&mutex);
        if(id==0)
            strcpy(buffer[bp],"aaa/0");
        else
            strcpy(buffer[bp],"bbb/0");
        bp++;
        printf("producer %d produce %s in %d\n",id,buffer[bp-1],bp-1);
        pthread_mutex_unlock(&mutex);
        sem_post(&product);
    }
    printf("producer %d is over!\n",id);
    exit(id);
}

int consumer(void *args){
    int id = *((int*)args);
    int i;
    for(i=0;i<10;i++)
    {
        sleep(10-i);  //表现线程速度差别
        sem_wait(&product);
        pthread_mutex_lock(&mutex);
        bp--;
        printf("consumer %d get %s in %d\n",id,buffer[bp],bp+1);
        strcpy(buffer[bp],"zzz\0");
        pthread_mutex_unlock(&mutex);
        sem_post(&warehouse);
    }
    printf("consumer %d is over!\n",id);
    
    exit(id);
}

修改:

1,我在每个producer和consumer最后加上了一个exit(id),这样的话子进程运行结束后就是自动结束。

2,另外在程序的最开始需要加上一个#define _GNU_SOURCE。

3,另外我把printf("consumer %d get %s in %d\n",id,buffer[bp],bp+1);改成了printf("consumer %d get %s in %d\n",id,buffer[bp],bp);这样的话我认为输出在逻辑上更加合理。

输出结果:

 

说明:

我在自己的虚拟机上的/experiment01里面的clone.c里面写了该部分c代码,然后使用命令“gcc -pthread clone.c -o clone”生成可执行文件clone,然后使用命令“./clone”执行,然后上图就是运行结果。可以看到producer0首先运行,然后是producer1,由于最开始consumer休眠时间太长了,所以又是producer不断运行,运行一段时间后就出现了producer和consumer交替的情况。

分析clone()实验结果:

函数功能:

main函数:

创建信号量,使用clone创建对应四个子进程,执行响应函数

producer函数:

将aaa/bbb放在buffer数组里面,同时打印出标识

consumer函数:

将buffer里的东西取出来并打印出来。

PS:由此我们可以看出我们通过参数设定clone使子进程和父进程共享了存储空间,同时为了由于多个进程共享内存,所以我们又引入了互锁机制。

函数之间调用关系:

在main函数里我们依次利用clone创建了四个子进程,并且依次调用producer(0),consumer(0),producer(1),consumer(1)。在子进程过程中,通过合理的sleep,使buffer里的内容不断写入读出。

关键语句功能说明:

sem_init(&product,0,0);

sem_init(&warehouse,0,8);

这一段是使用sem_init创建信号量,由该函数的第二个参数为0我们可以知道该信号量用于多线程间的同步。

clone_flag=CLONE_VM|CLONE_SIGHAND|CLONE_FS|CLONE_FILES;

CLONE_VM表示子进程与父进程运行于相同的内存空间

CLONE_SIGHAND表示子进程与父进程共享相同的信号处理(signal handler)表

CLONE_FS表示子进程与父进程共享相同的文件系统,包括root、当前目录、umask

CLONE_FILES表示子进程与父进程共享相同的文件描述符(file descriptor)表

        retval=clone(producer,&(stack[4095]),clone_flag,(void*)&arg);

retval=clone(consumer,&(stack[4095]),clone_flag,(void*)&arg);

这一段是使用clone函数进行创建新的线程,clone()函数是将部分父进程的资源的数据结构进行复制,复制哪些资源是可选择的,这个可以通过参数设定,所以clone()函数带参数

        sem_wait(&warehouse);

        pthread_mutex_lock(&mutex);

        if(id==0)

            strcpy(buffer[bp],"aaa/0");

        else

            strcpy(buffer[bp],"bbb/0");

        bp++;

        printf("producer %d produce %s in %d\n",id,buffer[bp-1],bp-1);

        pthread_mutex_unlock(&mutex);

        sem_post(&product);

这一段是producer内部我们要先使信号量warehouse减一个,然后我们把aaa/bbb写到对应的位置,然后打印出标识。同时我们注意到我们使用了互锁量使只能同时有一个进程执行上述代码段.

5,思考题

  • 在Linux环境中,除pipe和clone公共内存通讯外,还可以采用shm和msg通讯方式,试查阅相关资料了解这些通讯方式,并使用上述任一种方式模拟实现“生产者/消费者”问题。
  • 比较Linux系统中pipe、clone、shm和msg四种高级通讯方法的优缺点以及各自适应的环境。

一:

#include <stdio.h>

#include <unistd.h>

#include <string.h>

#include <sys/ipc.h>

#include <sys/shm.h>

#include <error.h>



#define SIZE 1024



int main()

{

        int shmid;

        char *shmaddr;

        struct shmid_ds buf;

        int flag = 0;

        int pid;

        //创建共享内存

        shmid = shmget(IPC_PRIVATE, SIZE, IPC_CREAT|0600);

        if ( shmid < 0 )

        {

                perror("get shm ipc_id error");

                return -1;

        }

        //创建子进程 调用fork会返回两次 父进程返回创建的进程,子进程返回0

        pid = fork();

        if( pid == 0 )

        {//子进程

                //将共享内存映射到本地进程

                shmaddr = (char *)shmat(shmid, NULL, 0);

                if( (int)shmaddr == -1 )

                {

                        perror("shmat addr error");

                        return -1;

                }



                strcpy( shmaddr, "Hi,I am child process!\n");

                //断开映射

                shmdt( shmaddr);

                return 0;

        }

        else if( pid > 0 )

        {//父进程

                //可能会出现父进程比子进程提前结束执行,所以,延迟等待子进程先执行完

                sleep(3);

                //得到共享内存的状态,把共享内存的shmid_ds结构复制到buf中

                flag = shmctl ( shmid, IPC_STAT, &buf);

                if( flag == -1 )

                {

                        perror("shmctl shm error");

                        return -1;

                }



                printf("shm_segsz=%d bytes\n", buf.shm_segsz);

                printf("parent pid=%d,shm_cpid=%d\n",getpid(),buf.shm_cpid);

                printf("chlid pid=%d,shm_lpid=%d\n",pid,buf.shm_lpid);

                //把共享内存映射到本地

                shmaddr = (char *)shmat(shmid, NULL, 0);

                if( (int)shmaddr == -1 )

                {

                        perror("shmat addr error");

                        return -1;

                }

                printf("%s",shmaddr);

                //断开共享内存连接

                shmdt(shmaddr);

                //删除共享内存

                shmctl(shmid,IPC_RMID,NULL);

        }

        else

        {

                perror("fork error");

                shmctl(shmid,IPC_RMID,NULL);

        }

        return 0;

}

二:

管道通信(PIPE)
      无名管道简单方便.但局限于单向通信的工作方式.并且只能在创建它的进程及其子孙进程之间实现管道的共享:有名管道虽然可以提供给任意关系的进程使用.但是由于其长期存在于系统之中,使用不当容易出错。

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

共享内存通信(SHARED MEMORY)
      共享内存针对消息缓冲的缺点改而利用内存缓冲区直接交换信息,无须复制,快捷、信息量大是其优点。但是共享内存的通信方式是通过将共享的内存缓冲区直接附加到进程的虚拟地址空间中来实现的.因此,这些进程之间的读写操作的同步问题操作系统无法实现。必须由各进程利用其他同步工具解决。另外,由于内存实体存在于计算机系统中.所以只能由处于同一个计算机系统中的诸进程共享。不方便网络通信。

参考

  • 5
    点赞
  • 63
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

HereIs_linwuwu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值