操作系统内核 实验五

实验目的

  1. 了解进程如何让在 Linux 中彼此同步;
  2. 理解 C 语言代码所阐述的文件共享、内存共享、管道通信、信号量互斥、队列通信等 IPC 机制;
  3. 运行实验代码理解代码执行过程;
  4. 实现内核和用户程序之间的文件通信;

实验基本概念

  1. 进程是运行着的程序,每个进程都有着它自己的地址空间,这些空间由进程被允许访问的内存地址组成。进程有一个或多个执行线程,而线程是一系列执行指令的集合:单线程进程就只有一个线程,而多线程的进程则有多个线程。一个进程中的线程共享各种资源,特别是地址空间。另外,一个进程中的线程可以直接通过共享内存来进行通信。
  2. 有多种方法启动之后要进行通信的进程,但主要有两种方式:一种是一个终端被用来启动一个进程,另一个不同的终端被用来启动另一个;另一种是在一个进程(父进程)中调用系统函数 fork,以此启动另一个进程(子进程)。

理解实验原理和代码

文件共享 ./producer ./consumer

  1. 文件共享是最基础的进程间通信IPC机制。实验所给的代码中考虑了一个相对简单的例子,其中一个进程(生产者producer)创建和写入一个文件,然后另一个进程(消费者consumer)从这个相同的文件中进行读取。在使用这个IPC机制时,生产者和消费者可能恰好在同一时间访问该文
    件,从而使得输出结果不确定。为了避免竞争条件的发生,该文件在处于读或写状态时必须以某种方式处于被锁状态,从而阻止在写操作执行时和其他操作的冲突。

  2. 解决办法:生产者在写入文件时获得一个文件的排斥锁。一个排斥锁最多被一个进程所拥有。这样就可以排除掉竞争条件的发生,因为在锁被释放之前没有其他的进程可以访问这个文件; 消费者在从文件中读取内容时得到至少一个共享锁。多个读取者可以同时保有一个共享锁,但是没有写入者可以获取到文件内容,甚至在当只有一个读取者保有一个共享锁时。标准的 I/O 库中包含了一个名为 fcntl 的实用函数,它可以被用来检查或者操作一个文件上的排斥锁和共享锁。

  3. producer.c程序分析:首先声明了一个类型为struct flock的变量,它代表一个锁,对 l_type的初始化,使得这个锁是排斥锁而不是一个共享锁,假如生产者获得该锁,则其他进程不能对文件进行读写操作,直到生产者释放该锁或者显式地调用fcntl,又或者隐式地关闭这个文件。当进程终止时,所有被它打开的文件都会被自动关闭,从而释放了锁。接着初始化其他的域。主要的效果是整个文件都将被锁上。然后调用fcntl尝试排斥性地将文件锁住,并检查调用是否成功。如果生产者获得了锁,则程序向文件中写入DataString,然后改变锁的结构为F_UNLCK,调用fcntl执行解锁操作,最后关闭文件并退出。

    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 <stdlib.h>
    #include <fcntl.h>
    #include <unistd.h>
    #include <string.h>

    #define FileName "data.dat"

    void report_and_exit(const char* msg) {
    perror(msg);
    exit(-1); /* EXIT_FAILURE */
    }

    int main() {
    struct flock lock;
    lock.l_type = F_WRLCK; //读/写(独占对共享)锁
    lock.l_whence = SEEK_SET;//寻求偏移基址
    lock.l_start = 0; //文件中的第1字节
    lock.l_len = 0; //0意味着'until EOF'
    lock.l_pid = getpid(); //进程id

    int fd; //文件描述符,用于标识进程中的文件
    if ((fd = open(FileName, O_RDWR | O_CREAT, 0666)) < 0) //-1标志着错误
    report_and_exit("open failed...");

    if (fcntl(fd, F_SETLK, &lock) < 0) //F_SETLK 没有阻塞, F_SETLKW 阻塞
    report_and_exit("fcntl failed to get lock...");
    else {
    write(fd, DataString, strlen(DataString)); //填充数据文件
    fprintf(stderr, "Process %d has written to data file...\n", lock.l_pid);
    }

    /* 开始释放锁 */
    lock.l_type = F_UNLCK;
    if (fcntl(fd, F_SETLK, &lock) < 0)
    report_and_exit("explicit unlocking failed...");

    close(fd); //关闭文件,如果需要将会解锁
    return 0; //终止进程也会解锁
    }
  4. consumer.c程序分析:程序首先要检查一下文件是否被排斥性锁锁住了,即是否有生产者在写文件,然后才尝试去获取一个共享锁F_RDLCK。调用一个只读锁能够阻止其他进程向文件进行写的操作,但可以允许其他进程对文件进行读取,即共享锁可以被多个进程所保有。在获取了一个共享锁后,消费者程序将立即从文件中读取字节数据,然后在标准输出中打印这些字节的内容,接着释放锁,关闭文件并终止。

    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
    #include <stdio.h>
    #include <stdlib.h>
    #include <fcntl.h>
    #include <unistd.h>

    #define FileName "data.dat"

    void report_and_exit(const char* msg) {
    perror(msg);
    exit(-1); /* EXIT_FAILURE */
    }

    int main() {
    struct flock lock;
    lock.l_type = F_WRLCK; //读/写(独占对共享)锁
    lock.l_whence = SEEK_SET;//寻求偏移基址
    lock.l_start = 0; //文件中的第1字节
    lock.l_len = 0; //0意味着'until EOF'
    lock.l_pid = getpid(); //进程id

    int fd; //文件描述符,用于标识进程中的文件
    if ((fd = open(FileName, O_RDONLY)) < 0) //-1标志着错误
    report_and_exit("open to read failed...");

    /* 如果文件被写锁定,将无法继续 */
    fcntl(fd, F_GETLK, &lock); //如果没有写锁,将lock.l_type设置为F_UNLCK
    if (lock.l_type != F_UNLCK)
    report_and_exit("file is still write locked...");

    lock.l_type = F_RDLCK; //在阅读过程中防止任何写
    if (fcntl(fd, F_SETLK, &lock) < 0)
    report_and_exit("can't get a read-only lock...");

    /* 一次读取一个字节(恰好是ASCII码) */
    int c; //读字节缓冲区
    while (read(fd, &c, 1) > 0) //0标志着EOF
    write(STDOUT_FILENO, &c, 1); //向标准输出写入一个字节

    /* 开始释放锁 */
    lock.l_type = F_UNLCK;
    if (fcntl(fd, F_SETLK, &lock) < 0)
    report_and_exit("explicit unlocking failed...");

    close(fd);
    return 0;
    }

内存共享 ./memwriter ./memreader

  1. 对于内存共享,Linux系统提供了两类不同的API:传统的System V API和较为新颖的POSIX API。在单个应用中,这些API不能混用。但是,POSIX方式的一个坏处是它的特性仍在发展中,并且依赖于安装的内核版本,这非常影响代码的可移植性。例如,默认情况下,POSIX API 用内存映射文件来实现共享内存:对于一个共享的内存段,系统为相应的内容维护一个备份文
    件。在POSIX规范下共享内存可以被配置为不需要备份文件,但这可能会影响可移植性。实验课代码中使用的是带有备份文件的POSIX API,这既结合了内存获取的速度优势,又获得了文件存储的持久性。

  2. 内存共享中的两个程序memewritememreader使用的信号量来同步对内存的访问。一般的信号量也被叫做一个计数信号量,因为带有一个可以增加的值(通常初始化为 0)。考虑一家租用自行车的商店,在它的库存中有 100 辆自行车,还有一个供职员用于租赁的程序。每当一辆自行车被租出去,信号量就增加 1;当一辆自行车被还回来,信号量就减 1。在信号量的值为 100 之前都还可以进行租赁业务,但如果等于 100 时,就必须停止业务,直到至少有一辆自行车被还回来,从而信号量减为 99。二元信号量只有两个值:0 和1,信号量的表现为互斥,即信号量为 0 的时候,只有memwriter可以获取共享内存,在进行写操作完成后,这个进程将增加信号量的值使其为 1,从而允许memreader 来读取共享内存。

  3. memwrite.c程序分析:程序调用shm_open函数来得到作为系统协调共享内存的备份文件描述符,此时还没有内存被分配。然后调用ftruncate函数分配ByteSize字节的内存。接着memwrite调用mmap函数来获取共享内存的指针。其中调用的是calloc动态分配内存时将其初始化为 0。目前,memwrite已经准备好进行写操作了,但是它要调用sem_open创建一个信号量来确保共享内存的互斥性,写操作完成后再调用 sem_post 函数将信号量的值增加到 1,释放互斥锁,使得memreader可以执行它的读操作。memwriter也将从它自己的地址空间中取消映射,使得memwriter不能进一步地访问共享内存。

    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
    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/mman.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <unistd.h>
    #include <semaphore.h>
    #include <string.h>
    #include "shmem.h"

    void report_and_exit(const char* msg) {
    perror(msg);
    exit(-1);
    }

    int main() {
    int fd = shm_open(BackingFile, /* name from smem.h */
    O_RDWR | O_CREAT, /* read/write, create if needed */
    AccessPerms); /* access permissions (0644) */
    if (fd < 0) report_and_exit("Can't open shared mem segment...");

    ftruncate(fd, ByteSize); /* get the bytes */

    caddr_t memptr = mmap(NULL, /* let system pick where to put segment */
    ByteSize, /* how many bytes */
    PROT_READ | PROT_WRITE, /* access protections */
    MAP_SHARED, /* mapping visible to other processes */
    fd, /* file descriptor */
    0); /* offset: start at 1st byte */
    if ((caddr_t) -1 == memptr) report_and_exit("Can't get segment...");

    fprintf(stderr, "shared mem address: %p [0..%d]\n", memptr, ByteSize - 1);
    fprintf(stderr, "backing file: /dev/shm%s\n", BackingFile );

    /* semahore code to lock the shared mem */
    sem_t* semptr = sem_open(SemaphoreName, /* name */
    O_CREAT, /* create the semaphore */
    AccessPerms, /* protection perms */
    0); /* initial value */
    if (semptr == (void*) -1) report_and_exit("sem_open");

    strcpy(memptr, MemContents); /* copy some ASCII bytes to the segment */

    /* increment the semaphore so that memreader can read */
    if (sem_post(semptr) < 0) report_and_exit("sem_post");

    sleep(12); /* give reader a chance */

    /* clean up */
    munmap(memptr, ByteSize); /* unmap the storage */
    close(fd);
    sem_close(semptr);
    shm_unlink(BackingFile); /* unlink from the backing file */
    return 0;
    }
  4. memreader.c程序分析:调用shm_open使用memwrite中的文件描述符从共享内存段中获取一个指针。接着是对mmap的调用,第一个参数为NULL,这意味着让系统自己决定在虚拟内存地址的哪个地方分配内存;MAP_SHARED标志着被分配的内存在进程中是共享的。另外的保护参数AccessPerms暗示着共享内存是可读可写的。然后调用sem_open函数时,通过信号量的名字来获取信号量。但memreader随后将进入等待状态,直到memwriter将初始值为 0 的信号量的值增加。一旦等待结束,memreader将从共享内存中读取ASCII数据,然后做些清理工作并终止。

    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 <stdlib.h>
    #include <sys/mman.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <unistd.h>
    #include <semaphore.h>
    #include <string.h>
    #include "shmem.h"

    void report_and_exit(const char* msg) {
    perror(msg);
    exit(-1);
    }

    int main() {
    int fd = shm_open(BackingFile, O_RDWR, AccessPerms); /*empty to begin*/
    if (fd < 0) report_and_exit("Can't get file descriptor...");

    /* get a pointer to memory */
    caddr_t memptr = mmap(NULL, /* let system pick where to put segment */
    ByteSize, /* how many bytes */
    PROT_READ | PROT_WRITE, /* access protections */
    MAP_SHARED, /* mapping visible to other processes */
    fd, /* file descriptor */
    0); /* offset: start at 1st byte */
    if ((caddr_t) -1 == memptr) report_and_exit("Can't access segment...");

    /* create a semaphore for mutual exclusion */
    sem_t* semptr = sem_open(SemaphoreName, /* name */
    O_CREAT, /* create the semaphore */
    AccessPerms, /* protection perms */
    0); /* initial value */
    if (semptr == (void*) -1) report_and_exit("sem_open");

    /* use semaphore as a mutex (lock) by waiting for writer to increment it */
    if (!sem_wait(semptr)) { /* wait until semaphore != 0 */
    int i;
    for (i = 0; i < strlen(MemContents); i++)
    write(STDOUT_FILENO, memptr + i, 1); /* one byte at a time */
    sem_post(semptr);
    }

    /* cleanup */
    munmap(memptr, ByteSize);
    close(fd);
    sem_close(semptr);
    unlink(BackingFile);
    return 0;
    }

管道通信 ./fifoWriter ./fifoReader

  1. 管道通信发送进程以字符流形式将大量数据送入管道,接收进程可从管道接收数据,二者利用管道进行通信。

  2. 无名管道pipeUN.c程序分析:首先使用forrk创建一个进程,虽然它只有单一的源文件,在它正确执行的情况下将会发生多进程的情况。如果成功地产生一个子进程,pipeFDs[2]来保存两个文件描述符,一个用来向管道中写入,另一个从管道中写入。(数组元素pipeFDs[0]是读端的文件描述符,元素pipeFDs[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
    #include <sys/wait.h> /* wait */
    #include <stdio.h>
    #include <stdlib.h> /* exit functions */
    #include <unistd.h> /* read, write, pipe, _exit */
    #include <string.h>

    #define ReadEnd 0
    #define WriteEnd 1

    void report_and_exit(const char* msg) {
    perror(msg);
    exit(-1); /** failure **/
    }

    int main() {
    int pipeFDs[2]; /* two file descriptors */
    char buf; /* 1-byte buffer */
    const char* msg = "Nature's first green is gold\n"; /* bytes to write */

    if (pipe(pipeFDs) < 0) report_and_exit("pipeFD");
    pid_t cpid = fork();/* fork a child process */
    if (cpid < 0) report_and_exit("fork"); /* check for failure */

    if (0 == cpid) { /*** child ***/ /* child process */
    close(pipeFDs[WriteEnd]);/* child reads, doesn't write */

    while (read(pipeFDs[ReadEnd], &buf, 1) > 0)/* read until end of byte stream */
    write(STDOUT_FILENO, &buf, sizeof(buf)); /* echo to the standard output */

    close(pipeFDs[ReadEnd]);/* close the ReadEnd: all done */
    _exit(0);/* exit and notify parent at once */
    }
    else { /*** parent ***/
    close(pipeFDs[ReadEnd]);/* parent writes, doesn't read */

    write(pipeFDs[WriteEnd], msg, strlen(msg));/* write the bytes to the pipe */
    close(pipeFDs[WriteEnd]);
  3. 命令管道:无名管道没有备份文件,系统将维持一个内存缓存来将字节数据从写方传给读方。一旦写方和读方终止,这个缓存将会被回收,进而无名管道消失。相反的,命名管道有备份文件和一个不同的API

  4. fifoWriter.c程序分析:首先创建一个命名管道来写入数据,然后调用open函数,返回以一个文件描述符。fifoWriter不会一次性将所有的数据都写入,而是写入一个块,然后休息随机数目的微秒时间,接着再循环往复。关闭命名管道后,fifoWriter也将使用unlink取消对该文件的连接。

    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
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <unistd.h>
    #include <time.h>
    #include <stdlib.h>
    #include <stdio.h>

    #define MaxLoops 12000 /* outer loop */
    #define ChunkSize 16 /* how many written at a time */
    #define IntsPerChunk 4 /* four 4-byte ints per chunk */
    #define MaxZs 250 /* max microseconds to sleep */

    int main() {
    const char* pipeName = "./fifoChannel";
    mkfifo(pipeName, 0666); /* read/write for user/group/others */
    int fd = open(pipeName, O_CREAT | O_WRONLY); /* open as write-only */
    if (fd < 0) return -1; /** error **/

    int i;
    for (i = 0; i < MaxLoops; i++) { /* write MaxWrites times */
    int j;
    for (j = 0; j < ChunkSize; j++) { /*each time, write ChunkSize bytes*/
    int k;
    int chunk[IntsPerChunk];
    for (k = 0; k < IntsPerChunk; k++)
    chunk[k] = rand();
    write(fd, chunk, sizeof(chunk));
    }
    usleep((rand() % MaxZs) + 1); /* pause a bit for realism */
    }

    close(fd); /* close pipe: generates an end-of-file */
    unlink(pipeName); /* unlink from the implementing file */
    printf("%i ints sent to the pipe.\n", MaxLoops * ChunkSize * IntsPerChunk);

    return 0;
    }
  5. fifoReader.c程序分析:因为fifoWriter已经创建了命名管道,所以fifiReader只需要调用open来通过备份文件来获取管道中的内容。接着程序进入一个潜在的无限循环,在每次循环时,尝试读取 4 字节的块在读入 4 字节整数后,fifoReader 检查这个数是否为质数。这个操作代表了一个生产级别的读取器可能在接收到的字节数据上执行的逻辑操作。在示例运行中,在接收到的768000个整数中有 37682 个质数。read 返回 0 来暗示该流的结束。在这种情况下,fifoReader跳出循环,关闭命名管道,并在终止前unlink备份文件。

    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 <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <fcntl.h>
    #include <unistd.h>


    unsigned is_prime(unsigned n) { /* not pretty, but gets the job done efficiently */
    if (n <= 3) return n > 1;
    if (0 == (n % 2) || 0 == (n % 3)) return 0;

    unsigned i;
    for (i = 5; (i * i) <= n; i += 6)
    if (0 == (n % i) || 0 == (n % (i + 2))) return 0;

    return 1; /* found a prime! */
    }

    int main() {
    const char* file = "./fifoChannel";
    int fd = open(file, O_RDONLY);
    if (fd < 0) return -1; /* no point in continuing */
    unsigned count = 0, total = 0, primes_count = 0;

    while (1) {
    int next;
    int i;
    ssize_t count = read(fd, &next, sizeof(int));

    if (0 == count) break; /* end of stream */
    else if (count == sizeof(int)) { /* read a 4-byte int value */
    total++;c
    if (is_prime(next)) primes_count++;
    }
    }

    close(fd); /* close pipe from read end */
    unlink(file); /* unlink from the underlying file */
    printf("Received ints: %u, primes: %u\n", total, primes_count);

    return 0;
    }

信号量互斥 ./shutdown

  1. 信号会中断一个正在执行的程序,在这种意义下,就是用信号与这个程序进行通信。大多数的信号要么可以被忽略(阻塞)或者被处理(通过特别设计的代码)。SIGSTOP (暂停)和SIGKILL(立即停止)是最应该提及的两种信号。这种符号常量有整数类型的值,例如SIGKILL对应的值为 9。信号可以在与用户交互的情况下发生。例如,一个用户从命令行中敲了Ctrl+C来终止一个从命令行中启动的程序;Ctrl+C将产生一个SIGTERM信号。SIGTERM即终止,它可以被阻塞或者被处理,而不像SIGKILL信号那样。一个进程也可以通过信号和另一个进程通信,这样使得信号也可以作为一种IPC机制。

  2. shutdown.c程序分析:该程序由一个父进程和一个子进程组成。首先父进程尝试去fork一个子进程,如果fork成功,每个进程去执行自己的代码,即子进程执行child_code,父进程执行parent_code。子进程将会进入一个潜在的无线循环,在该循环中子进程将睡眠一秒,然后打印一个信息。来自父进程的一个SIGTERM信号将引起子进程去执行一个信号处理回调函数 graceful。这样这个信号就使子进程跳出循环,然后进行子进程和父进程的终止,并打印消息。

    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
    58
    59
    60
    61
    62
    63
    64
    #include <stdio.h>
    #include <signal.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/wait.h>

    void graceful(int signum) {
    printf("\tChild confirming received signal: %i\n", signum);
    puts("\tChild about to terminate gracefully...");

    sleep(1);

    puts("\tChild terminating now...");
    _exit(0); /* fast-track notification of parent */
    }

    void set_handler() {
    struct sigaction current;
    sigemptyset(&current.sa_mask); /* clear the signal set */
    current.sa_flags = 0;/* enables setting sa_handler, not sa_action */
    current.sa_handler = graceful; /* specify a handler */
    sigaction(SIGTERM, &current, NULL); /* register the handler */
    }

    void child_code() {
    set_handler();

    while (1) { /** loop until interrupted **/
    sleep(1);
    puts("\tChild just woke up, but going back to sleep.");
    }
    }

    void parent_code(pid_t cpid) {
    puts("Parent sleeping for a time...");
    sleep(5);

    /* Try to terminate child. */
    if (-1 == kill(cpid, SIGTERM)) {
    perror("kill");
    exit(-1);
    }
    wait(NULL); /** wait for child to terminate **/
    puts("My child terminated, about to exit myself...");
    }

    int main() {
    puts("main...");
    pid_t pid = fork();
    puts("main...2");
    if (pid < 0) {
    perror("fork");
    return -1; /* error */
    }
    puts("main...3");
    if (0 == pid){
    puts("child_code...");
    child_code();
    }else {
    puts("parent_code...");
    parent_code(pid);
    }
    return 0; /* normal */
    }

队列通信 ./sender ./receiver

  1. 消息队列是一系列的消息,每个消息包括两个部分:荷载,一个字节序列;类型,以一个正整数值得形式给定,类型用来分类消息,为了更灵活的回收。

  2. 下面展示的 4 个消息中,标记为 1 的是开头,即最接近接收端,接着连续标记为 2 的消息,最后接着一个标记为 3 的消息。假如按照严格的FIFO行为执行,消息将会以1-2-2-3这样的次序被接收。但是消息队列允许其他收取次序。例如,消息可以被接收方以3-2-1-2的次序接收。说明消息队列足够的灵活。

  3. sender.c:通过megsnd函数程序发送sender程序将发送出 6 个消息,每两个为一个类型:前两个是类型 1,接着的连个是类型 2,最后的两个为类型 3。且被配置为非阻塞的IPC_NOWAIT标志。

    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
    #include <stdio.h> 
    #include <sys/ipc.h>
    #include <sys/msg.h>
    #include <stdlib.h>
    #include <string.h>
    #include "queue.h"

    void report_and_exit(const char* msg) {
    perror(msg);
    exit(-1); /* EXIT_FAILURE */
    }

    int main() {
    key_t key = ftok(PathName, ProjectId);
    if (key < 0) report_and_exit("couldn't get key...");

    int qid = msgget(key, 0666 | IPC_CREAT);
    if (qid < 0) report_and_exit("couldn't get queue id...");

    char* payloads[] = {"msg1", "msg2", "msg3", "msg4", "msg5", "msg6"};
    int types[] = {1, 1, 2, 2, 3, 3}; /* each must be > 0 */
    int i;
    for (i = 0; i < MsgCount; i++) {
    /* build the message */
    queuedMessage msg;
    msg.type = types[i];
    strcpy(msg.payload, payloads[i]);

    /* send the message */
    msgsnd(qid, &msg, sizeof(msg), IPC_NOWAIT); /* don't block */
    printf("%s sent as type %i\n", msg.payload, (int) msg.type);
    }
    return 0;
    }
  4. receiver.c程序分析:对msgget的调用可能因为带有IPC_CREAT标志而具有误导性,但是这个标志的真实意义是如果需要就创建,否则直接获取。sender程序调用msgsnd来发送消息,而receiver调用msgrcv来接收它们。

    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
    #include <stdio.h> 
    #include <sys/ipc.h>
    #include <sys/msg.h>
    #include <stdlib.h>
    #include "queue.h"

    void report_and_exit(const char* msg) {
    perror(msg);
    exit(-1); /* EXIT_FAILURE */
    }

    int main() {
    key_t key= ftok(PathName, ProjectId); /* key to identify the queue */
    if (key < 0) report_and_exit("key not gotten...");

    int qid = msgget(key, 0666 | IPC_CREAT); /* access if created already */
    if (qid < 0) report_and_exit("no access to queue...");

    int types[] = {3, 1, 2, 1, 3, 2}; /* different than in sender */
    int i;
    for (i = 0; i < MsgCount; i++) {
    queuedMessage msg; /* defined in queue.h */
    if (msgrcv(qid, &msg, sizeof(msg), types[i], MSG_NOERROR | IPC_NOWAIT) < 0)
    puts("msgrcv trouble...");
    printf("%s received as type %i\n", msg.payload, (int) msg.type);
    }

    /** remove the queue **/
    if (msgctl(qid, IPC_RMID, NULL) < 0) /* NULL = 'no flags' */
    report_and_exit("trouble removing queue...");

    return 0;
    }

实验数据与结果

文件共享

  1. 使用make命令编译程序;

  2. 执行./producer,程序向data.dat写入数据;


  3. 在终端输入./consumer,显示data.dat中的内容

    image-20191125115447185

内存共享

  1. 向内存中写入信息

  2. 从内存中读取信息

管道通信

  1. 开启两个终端(两个终端工作目录相同)在其中一个终端输入以下命令:即先创建一个备份文件,名为2017213618,然后将管道的内容输出到stdout中。最开始并没有什么会出现在终端中,因为还没有向管道中写入任何数据。

  2. 在第二个终端输入以下命令无论在这个终端中输入什么,它都会在第一个终端中显示出来;

  3. 一旦键入Ctrl+C,就会回到正常的命令行提示符,因为管道已经被关闭了。最后通过移除实现命名管道的文件来进行清理。

    image-20191125121214465

  4. 不同的终端中启动fifoWriterfifoReader,但二者处于相同的工作目录。

信号量互斥

  1. 在终端输入./shutdown,得到如下输出,成功的模拟了信号的这种轻量的IPC方法。

队列通信

  1. 输出显示senderreceiver可以在同一个终端中启动。输出也显示消息队列是持久的,在这个例子中,sender1-1-2-2-3-3的次序发送消息,但receiver接收它们的次序为3-1-2-1-3-2,表明消息队列没有被严格的FIFO行为所拘束。