Linux中如何共享存储

发布时间:2021-10-27 11:32:13 作者:小新
来源:亿速云 阅读:134

这篇文章主要为大家展示了“Linux中如何共享存储”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下“Linux中如何共享存储”这篇文章吧。

核心概念

进程是运行着的程序,每个进程都有着它自己的地址空间,这些空间由进程被允许访问的内存地址组成。进程有一个或多个执行线程,而线程是一系列执行指令的集合:单线程进程就只有一个线程,而多线程的进程则有多个线程。一个进程中的线程共享各种资源,特别是地址空间。另外,一个进程中的线程可以直接通过共享内存来进行通信,尽管某些现代语言(例如 Go)鼓励一种更有序的方式,例如使用线程安全的通道。当然对于不同的进程,默认情况下,它们能共享内存。

有多种方法启动之后要进行通信的进程,下面所举的例子中主要使用了下面的两种方法:

***个例子采用了上面使用终端的方法。这些代码示例的 ZIP 压缩包可以从我的网站下载到。

共享文件

程序员对文件访问应该都已经很熟识了,包括许多坑(不存在的文件、文件权限损坏等等),这些问题困扰着程序对文件的使用。尽管如此,共享文件可能是最为基础的 IPC 机制了。考虑一下下面这样一个相对简单的例子,其中一个进程(生产者 producer)创建和写入一个文件,然后另一个进程(消费者 consumer)从这个相同的文件中进行读取:

          writes +-----------+ readsproducer-------->| disk file |<-------consumer                 +-----------+

在使用这个 IPC 机制时最明显的挑战是竞争条件可能会发生:生产者和消费者可能恰好在同一时间访问该文件,从而使得输出结果不确定。为了避免竞争条件的发生,该文件在处于状态时必须以某种方式处于被锁状态,从而阻止在操作执行时和其他操作的冲突。在标准系统库中与锁相关的 API 可以被总结如下:

共享锁可以提升效率。假如一个进程只是读入一个文件的内容,而不去改变它的内容,就没有什么原因阻止其他进程来做同样的事。但如果需要写入内容,则很显然需要文件有排斥锁。

标准的 I/O 库中包含一个名为 fcntl 的实用函数,它可以被用来检查或者操作一个文件上的排斥锁和共享锁。该函数通过一个文件描述符(一个在进程中的非负整数值)来标记一个文件(在不同的进程中不同的文件描述符可能标记同一个物理文件)。对于文件的锁定, Linux 提供了名为 flock 的库函数,它是 fcntl 的一个精简包装。***个例子中使用 fcntl 函数来暴露这些 API 细节。

示例 1. 生产者程序
#include <stdio.h>#include <stdlib.h>#include <fcntl.h>#include <unistd.h> #define FileName "data.dat" void report_and_exit(const char* msg) {  [perror][4](msg);  [exit][5](-1); /* EXIT_FAILURE */} int main() {  struct flock lock;  lock.l_type = F_WRLCK;    /* read/write (exclusive) lock */  lock.l_whence = SEEK_SET; /* base for seek offsets */  lock.l_start = 0;         /* 1st byte in file */  lock.l_len = 0;           /* 0 here means 'until EOF' */  lock.l_pid = getpid();    /* process id */   int fd; /* file descriptor to identify a file within a process */  if ((fd = open(FileName, O_RDONLY)) < 0)  /* -1 signals an error */    report_and_exit("open to read failed...");   /* If the file is write-locked, we can't continue. */  fcntl(fd, F_GETLK, &lock); /* sets lock.l_type to F_UNLCK if no write lock */  if (lock.l_type != F_UNLCK)    report_and_exit("file is still write locked...");   lock.l_type = F_RDLCK; /* prevents any writing during the reading */  if (fcntl(fd, F_SETLK, &lock) < 0)    report_and_exit("can't get a read-only lock...");   /* Read the bytes (they happen to be ASCII codes) one at a time. */  int c; /* buffer for read bytes */  while (read(fd, &c, 1) > 0)    /* 0 signals EOF */    write(STDOUT_FILENO, &c, 1); /* write one byte to the standard output */   /* Release the lock explicitly. */  lock.l_type = F_UNLCK;  if (fcntl(fd, F_SETLK, &lock) < 0)    report_and_exit("explicit unlocking failed...");   close(fd);  return 0;}

上面生产者程序的主要步骤可以总结如下:

示例 2. 消费者程序
#include <stdio.h>#include <stdlib.h>#include <fcntl.h>#include <unistd.h> #define FileName "data.dat" void report_and_exit(const char* msg) {  [perror][4](msg);  [exit][5](-1); /* EXIT_FAILURE */} int main() {  struct flock lock;  lock.l_type = F_WRLCK;    /* read/write (exclusive) lock */  lock.l_whence = SEEK_SET; /* base for seek offsets */  lock.l_start = 0;         /* 1st byte in file */  lock.l_len = 0;           /* 0 here means 'until EOF' */  lock.l_pid = getpid();    /* process id */   int fd; /* file descriptor to identify a file within a process */  if ((fd = open(FileName, O_RDONLY)) < 0)  /* -1 signals an error */    report_and_exit("open to read failed...");   /* If the file is write-locked, we can't continue. */  fcntl(fd, F_GETLK, &lock); /* sets lock.l_type to F_UNLCK if no write lock */  if (lock.l_type != F_UNLCK)    report_and_exit("file is still write locked...");   lock.l_type = F_RDLCK; /* prevents any writing during the reading */  if (fcntl(fd, F_SETLK, &lock) < 0)    report_and_exit("can't get a read-only lock...");   /* Read the bytes (they happen to be ASCII codes) one at a time. */  int c; /* buffer for read bytes */  while (read(fd, &c, 1) > 0)    /* 0 signals EOF */    write(STDOUT_FILENO, &c, 1); /* write one byte to the standard output */   /* Release the lock explicitly. */  lock.l_type = F_UNLCK;  if (fcntl(fd, F_SETLK, &lock) < 0)    report_and_exit("explicit unlocking failed...");   close(fd);  return 0;}

相比于锁的 API,消费者程序会相对复杂一点儿。特别的,消费者程序首先检查文件是否被排斥性的被锁,然后才尝试去获得一个共享锁。相关的代码为:

lock.l_type = F_WRLCK;...fcntl(fd, F_GETLK, &lock); /* sets lock.l_type to F_UNLCK if no write lock */if (lock.l_type != F_UNLCK)  report_and_exit("file is still write locked...");

fcntl 调用中的 F_GETLK 操作指定检查一个锁,在本例中,上面代码的声明中给了一个 F_WRLCK 的排斥锁。假如特指的锁不存在,那么 fcntl 调用将会自动地改变锁类型域为 F_UNLCK 以此来显示当前的状态。假如文件是排斥性地被锁,那么消费者将会终止。(一个更健壮的程序版本或许应该让消费者会儿,然后再尝试几次。)

假如当前文件没有被锁,那么消费者将尝试获取一个共享(read-only)锁(F_RDLCK)。为了缩短程序,fcntl 中的 F_GETLK 调用可以丢弃,因为假如其他进程已经保有一个读写锁,F_RDLCK   的调用就可能会失败。重新调用一个只读锁能够阻止其他进程向文件进行写的操作,但可以允许其他进程对文件进行读取。简而言之,共享锁可以被多个进程所保有。在获取了一个共享锁后,消费者程序将立即从文件中读取字节数据,然后在标准输出中打印这些字节的内容,接着释放锁,关闭文件并终止。

下面的 % 为命令行提示符,下面展示的是从相同终端开启这两个程序的输出:

% ./producerProcess 29255 has written to data file... % ./consumerNow is the winter of our discontentMade glorious summer by this sun of York

在本次的代码示例中,通过 IPC  传输的数据是文本:它们来自莎士比亚的戏剧《理查三世》中的两行台词。然而,共享文件的内容还可以是纷繁复杂的,任意的字节数据(例如一个电影)都可以,这使得文件共享变成了一个非常灵活的  IPC 机制。但它的缺点是文件获取速度较慢,因为文件的获取涉及到读或者写。同往常一样,编程总是伴随着折中。下面的例子将通过共享内存来做  IPC,而不是通过共享文件,在性能上相应的有极大的提升。

共享内存

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

下面的共享内存例子中包含两个程序,分别名为 memwritermemreader,并使用信号量来调整它们对共享内存的获取。在任何时候当共享内存进入一个写入者场景时,无论是多进程还是多线程,都有遇到基于内存的竞争条件的风险,所以,需要引入信号量来协调(同步)对共享内存的获取。

memwriter 程序应当在它自己所处的终端首先启动,然后 memreader 程序才可以在它自己所处的终端启动(在接着的十几秒内)。memreader 的输出如下:

This is the way the world ends...

在每个源程序的最上方注释部分都解释了在编译它们时需要添加的链接参数。

首先让我们复习一下信号量是如何作为一个同步机制工作的。一般的信号量也被叫做一个计数信号量,因为带有一个可以增加的值(通常初始化为  0)。考虑一家租用自行车的商店,在它的库存中有 100 辆自行车,还有一个供职员用于租赁的程序。每当一辆自行车被租出去,信号量就增加  1;当一辆自行车被还回来,信号量就减 1。在信号量的值为 100 之前都还可以进行租赁业务,但如果等于 100  时,就必须停止业务,直到至少有一辆自行车被还回来,从而信号量减为 99。

二元信号量是一个特例,它只有两个值:0 和 1。在这种情况下,信号量的表现为互斥量(一个互斥的构造)。下面的共享内存示例将把信号量用作互斥量。当信号量的值为 0 时,只有 memwriter 可以获取共享内存,在写操作完成后,这个进程将增加信号量的值,从而允许 memreader 来读取共享内存。

示例 3. memwriter 进程的源程序
/** Compilation: gcc -o memwriter memwriter.c -lrt -lpthread **/#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][4](msg);  [exit][5](-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][7](stderr, "shared mem address: %p [0..%d]\n", memptr, ByteSize - 1);  [fprintf][7](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][8](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;}

下面是 memwritermemreader 程序如何通过共享内存来通信的一个总结:

示例 4. memreader 进程的源代码
/** Compilation: gcc -o memreader memreader.c -lrt -lpthread **/#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][4](msg);  [exit][5](-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][6](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;}

memwritermemreader 程序中,共享内存的主要着重点都在 shm_openmmap 函数上:在成功时,***个调用返回一个备份文件的文件描述符,而第二个调用则使用这个文件描述符从共享内存段中获取一个指针。它们对 shm_open 的调用都很相似,除了 memwriter 程序创建共享内存,而 `memreader 只获取这个已经创建的内存:

int fd = shm_open(BackingFile, O_RDWR | O_CREAT, AccessPerms); /* memwriter */int fd = shm_open(BackingFile, O_RDWR, AccessPerms); /* memreader */

有了文件描述符,接着对 mmap 的调用就是类似的了:

caddr_t memptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

mmap 的***个参数为 NULL,这意味着让系统自己决定在虚拟内存地址的哪个地方分配内存,当然也可以指定一个地址(但很有技巧性)。MAP_SHARED 标志着被分配的内存在进程中是共享的,***一个参数(在这个例子中为 0 ) 意味着共享内存的偏移量应该为***个字节。size 参数特别指定了将要分配的字节数目(在这个例子中是 512);另外的保护参数(AccessPerms)暗示着共享内存是可读可写的。

memwriter 程序执行成功后,系统将创建并维护备份文件,在我的系统中,该文件为 /dev/shm/shMemEx,其中的 shMemEx 是我为共享存储命名的(在头文件 shmem.h 中给定)。在当前版本的 memwritermemreader 程序中,下面的语句

shm_unlink(BackingFile); /* removes backing file */

将会移除备份文件。假如没有 unlink 这个语句,则备份文件在程序终止后仍然持久地保存着。

memreadermemwriter 一样,在调用 sem_open 函数时,通过信号量的名字来获取信号量。但 memreader 随后将进入等待状态,直到 memwriter 将初始值为 0 的信号量的值增加。

if (!sem_wait(semptr)) { /* wait until semaphore != 0 */

一旦等待结束,memreader 将从共享内存中读取 ASCII 数据,然后做些清理工作并终止。

共享内存 API 包括显式地同步共享内存段和备份文件。在这次的示例中,这些操作都被省略了,以免文章显得杂乱,好让我们专注于内存共享和信号量的代码。

即便在信号量代码被移除的情况下,memwritermemreader 程序很大几率也能够正常执行而不会引入竞争条件:memwriter 创建了共享内存段,然后立即向它写入;memreader 不能访问共享内存,直到共享内存段被创建好。然而,当一个写操作处于混合状态时,***实践需要共享内存被同步。信号量 API 足够重要,值得在代码示例中着重强调。


以上是“Linux中如何共享存储”这篇文章的所有内容,感谢各位的阅读!相信大家都有了一定的了解,希望分享的内容对大家有所帮助,如果还想学习更多知识,欢迎关注亿速云行业资讯频道!

推荐阅读:
  1. 【我的Linux,我做主!】常用共享存储--NFS服务配置应用与实战
  2. Linux CentOS7系统NFS共享存储服务

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

linux

上一篇:如何用php实现定时自动跳转

下一篇:在java中如何编写规范的代码

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》