如何在C语言中使用Linux共享内存

165

我的项目有些问题。

我一直在尝试找到一个关于如何使用 fork() 的共享内存的示例文档,但是一直没有成功。

基本上,场景是这样的:当用户启动程序时,我需要将两个值存储在共享内存中: current_path 是一个char*类型的变量,而 file_name 也是char*类型的变量。

根据命令参数,使用 fork() 启动一个新进程,并且该进程需要读取和修改存储在共享内存中的 current_path 变量,而 file_name 变量是只读的。

你能否指导我找到一个好的共享内存教程(如果可能附带示例代码)?


6
你可以考虑使用线程而不是进程,这样整个内存就可以共享而无需进行其他技巧。 - elomage
1
以下答案讨论了System V IPC机制,shmget()等,以及使用MAP_ANON(又名MAP_ANONYMOUS)的纯mmap()方法——尽管MAP_ANON未被POSIX定义。此外,还有POSIX shm_open()shm_close()用于管理共享内存对象。_ […继续…]_ - Jonathan Leffler
2
这些具有与System V IPC共享内存相同的优点 - 共享内存对象可以在创建它的进程的生命周期之外持久存在(直到某个进程执行shm_unlink()为止),而使用mmap()的机制需要一个文件和MAP_SHARED来持久化数据(而MAP_ANON排除了持久性)。在shm_open()规范的Rationale部分中有一个完整的示例。 - Jonathan Leffler
6个回答

240

有两种方法:shmgetmmap。我会谈论mmap,因为它更现代化、灵活,但如果您更喜欢使用旧的工具,请参考man shmget或此教程)。

mmap()函数可用于分配内存缓冲区,并具有高度可定制的参数来控制访问和权限,并在必要时将它们与文件系统存储关联。

以下函数创建一个可以与其子进程共享的内存缓冲区:

#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>

void* create_shared_memory(size_t size) {
  // Our memory buffer will be readable and writable:
  int protection = PROT_READ | PROT_WRITE;

  // The buffer will be shared (meaning other processes can access it), but
  // anonymous (meaning third-party processes cannot obtain an address for it),
  // so only this process and its children will be able to use it:
  int visibility = MAP_SHARED | MAP_ANONYMOUS;

  // The remaining parameters to `mmap()` are not important for this use case,
  // but the manpage for `mmap` explains their purpose.
  return mmap(NULL, size, protection, visibility, -1, 0);
}
下面是一个使用上述定义的函数来分配缓冲区的示例程序。父进程将写入一条消息,然后fork,并等待其子进程修改缓冲区。两个进程都可以读写共享内存。
#include <string.h>
#include <unistd.h>

int main() {
  char parent_message[] = "hello";  // parent process will write this message
  char child_message[] = "goodbye"; // child process will then write this one

  void* shmem = create_shared_memory(128);

  memcpy(shmem, parent_message, sizeof(parent_message));

  int pid = fork();

  if (pid == 0) {
    printf("Child read: %s\n", shmem);
    memcpy(shmem, child_message, sizeof(child_message));
    printf("Child wrote: %s\n", shmem);

  } else {
    printf("Parent read: %s\n", shmem);
    sleep(1);
    printf("After 1s, parent read: %s\n", shmem);
  }
}

77
这就是为什么Linux对于经验不足的开发人员来说很令人沮丧。手册并不解释如何实际使用它,也没有示例代码。:( - bleepzter
63
哈哈,我知道你的意思,但实际上这是因为我们不习惯阅读 man 手册。当我学会阅读并熟悉它们后,它们比那些糟糕的带有特定演示的教程更加有用。我记得在操作系统课程考试期间,我只使用 man 手册作为参考材料,最终得到了满分(10/10)。 - salezica
21
shmget 是一种非常老式的、有些人认为已经被废弃的共享内存方法... 更好的选择是使用 mmapshm_open、普通文件,或者直接使用 MAP_ANONYMOUS - R.. GitHub STOP HELPING ICE
5
你们说得对,我会在以后的答案中指出这一点供参考。 - salezica
5
这个答案因某些原因变得受欢迎,所以我决定让它更值得一读。仅用了四年时间。 - salezica
显示剩余10条评论

34

这里是一个共享内存的示例:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define SHM_SIZE 1024  /* make it a 1K shared memory segment */

int main(int argc, char *argv[])
{
    key_t key;
    int shmid;
    char *data;
    int mode;

    if (argc > 2) {
        fprintf(stderr, "usage: shmdemo [data_to_write]\n");
        exit(1);
    }

    /* make the key: */
    if ((key = ftok("hello.txt", 'R')) == -1) /*Here the file must exist */ 
{
        perror("ftok");
        exit(1);
    }

    /*  create the segment: */
    if ((shmid = shmget(key, SHM_SIZE, 0644 | IPC_CREAT)) == -1) {
        perror("shmget");
        exit(1);
    }

    /* attach to the segment to get a pointer to it: */
    if ((data = shmat(shmid, NULL, 0)) == (void *)-1) {
        perror("shmat");
        exit(1);
    }

    /* read or modify the segment, based on the command line: */
    if (argc == 2) {
        printf("writing to segment: \"%s\"\n", argv[1]);
        strncpy(data, argv[1], SHM_SIZE);
    } else
        printf("segment contains: \"%s\"\n", data);

    /* detach from the segment: */
    if (shmdt(data) == -1) {
        perror("shmdt");
        exit(1);
    }

    return 0;
}

步骤:

  1. 使用ftok将路径名和项目标识符转换为System V IPC键

  2. 使用shmget分配一个共享内存段

  3. 使用shmat将由shmid标识的共享内存段附加到调用进程的地址空间中

  4. 在内存区域上执行操作

  5. 使用shmdt进行分离


9
你为什么将0转换为void*而不使用NULL? - Clément Péau
1
然而,这段代码无法处理共享内存的删除。程序退出后,必须通过ipcrm -m 0手动删除它。 - bumfo

14

这些是用于使用共享内存的包含文件

#include<sys/ipc.h>
#include<sys/shm.h>

int shmid;
int shmkey = 12222;//u can choose it as your choice

int main()
{
  //now your main starting
  shmid = shmget(shmkey,1024,IPC_CREAT);
  // 1024 = your preferred size for share memory
  // IPC_CREAT  its a flag to create shared memory

  //now attach a memory to this share memory
  char *shmpointer = shmat(shmid,NULL);

  //do your work with the shared memory 
  //read -write will be done with the *shmppointer
  //after your work is done deattach the pointer

  shmdt(&shmpointer, NULL);

8

尝试这段代码示例,我已经测试过了,来源:http://www.makelinux.net/alp/035

#include <stdio.h> 
#include <sys/shm.h> 
#include <sys/stat.h> 

int main () 
{
  int segment_id; 
  char* shared_memory; 
  struct shmid_ds shmbuffer; 
  int segment_size; 
  const int shared_segment_size = 0x6400; 

  /* Allocate a shared memory segment.  */ 
  segment_id = shmget (IPC_PRIVATE, shared_segment_size, 
                 IPC_CREAT | IPC_EXCL | S_IRUSR | S_IWUSR); 
  /* Attach the shared memory segment.  */ 
  shared_memory = (char*) shmat (segment_id, 0, 0); 
  printf ("shared memory attached at address %p\n", shared_memory); 
  /* Determine the segment's size. */ 
  shmctl (segment_id, IPC_STAT, &shmbuffer); 
  segment_size  =               shmbuffer.shm_segsz; 
  printf ("segment size: %d\n", segment_size); 
  /* Write a string to the shared memory segment.  */ 
  sprintf (shared_memory, "Hello, world."); 
  /* Detach the shared memory segment.  */ 
  shmdt (shared_memory); 

  /* Reattach the shared memory segment, at a different address.  */ 
  shared_memory = (char*) shmat (segment_id, (void*) 0x5000000, 0); 
  printf ("shared memory reattached at address %p\n", shared_memory); 
  /* Print out the string from shared memory.  */ 
  printf ("%s\n", shared_memory); 
  /* Detach the shared memory segment.  */ 
  shmdt (shared_memory); 

  /* Deallocate the shared memory segment.  */ 
  shmctl (segment_id, IPC_RMID, 0); 

  return 0; 
} 

3
这段代码很好,但我认为它没有展示客户端如何通过使用shmgetshmat从不同的进程访问共享内存段,而这正是共享内存的重点... =(翻译:这是一份不错的代码,但我认为它没有展示如何在客户端中通过使用shmgetshmat来访问共享内存段,而这实际上是共享内存的核心所在... =( - étale-cohomology

8
这里是一个mmap的例子:
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

/*
 * pvtmMmapAlloc - creates a memory mapped file area.  
 * The return value is a page-aligned memory value, or NULL if there is a failure.
 * Here's the list of arguments:
 * @mmapFileName - the name of the memory mapped file
 * @size - the size of the memory mapped file (should be a multiple of the system page for best performance)
 * @create - determines whether or not the area should be created.
 */
void* pvtmMmapAlloc (char * mmapFileName, size_t size, char create)  
{      
  void * retv = NULL;                                                                                              
  if (create)                                                                                         
  {                                                                                                   
    mode_t origMask = umask(0);                                                                       
    int mmapFd = open(mmapFileName, O_CREAT|O_RDWR, 00666);                                           
    umask(origMask);                                                                                  
    if (mmapFd < 0)                                                                                   
    {                                                                                                 
      perror("open mmapFd failed");                                                                   
      return NULL;                                                                                    
    }                                                                                                 
    if ((ftruncate(mmapFd, size) == 0))               
    {                                                                                                 
      int result = lseek(mmapFd, size - 1, SEEK_SET);               
      if (result == -1)                                                                               
      {                                                                                               
        perror("lseek mmapFd failed");                                                                
        close(mmapFd);                                                                                
        return NULL;                                                                                  
      }                                                                                               

      /* Something needs to be written at the end of the file to                                      
       * have the file actually have the new size.                                                    
       * Just writing an empty string at the current file position will do.                           
       * Note:                                                                                        
       *  - The current position in the file is at the end of the stretched                           
       *    file due to the call to lseek().  
              *  - The current position in the file is at the end of the stretched                    
       *    file due to the call to lseek().                                                          
       *  - An empty string is actually a single '\0' character, so a zero-byte                       
       *    will be written at the last byte of the file.                                             
       */                                                                                             
      result = write(mmapFd, "", 1);                                                                  
      if (result != 1)                                                                                
      {                                                                                               
        perror("write mmapFd failed");                                                                
        close(mmapFd);                                                                                
        return NULL;                                                                                  
      }                                                                                               
      retv  =  mmap(NULL, size,   
                  PROT_READ | PROT_WRITE, MAP_SHARED, mmapFd, 0);                                     

      if (retv == MAP_FAILED || retv == NULL)                                                         
      {                                                                                               
        perror("mmap");                                                                               
        close(mmapFd);                                                                                
        return NULL;                                                                                  
      }                                                                                               
    }                                                                                                 
  }                                                                                                   
  else                                                                                                
  {                                                                                                   
    int mmapFd = open(mmapFileName, O_RDWR, 00666);                                                   
    if (mmapFd < 0)                                                                                   
    {                                                                                                 
      return NULL;                                                                                    
    }                                                                                                 
    int result = lseek(mmapFd, 0, SEEK_END);                                                          
    if (result == -1)                                                                                 
    {                                                                                                 
      perror("lseek mmapFd failed");                  
      close(mmapFd);                                                                                  
      return NULL;                                                                                    
    }                                                                                                 
    if (result == 0)                                                                                  
    {                                                                                                 
      perror("The file has 0 bytes");                           
      close(mmapFd);                                                                                  
      return NULL;                                                                                    
    }                                                                                              
    retv  =  mmap(NULL, size,     
                PROT_READ | PROT_WRITE, MAP_SHARED, mmapFd, 0);                                       

    if (retv == MAP_FAILED || retv == NULL)                                                           
    {                                                                                                 
      perror("mmap");                                                                                 
      close(mmapFd);                                                                                  
      return NULL;                                                                                    
    }                                                                                                 

    close(mmapFd);                                                                                    

  }                                                                                                   
  return retv;                                                                                        
}                                                                                                     

open 添加了文件 I/O 开销。请使用 shm_open 替代。 - osvein
1
@Spookbuster,在某些实现的shm_open中,会在底层调用open(),因此我不同意您的评估;这里有一个例子:https://code.woboq.org/userspace/glibc/sysdeps/posix/shm_open.c.html - Leo
1
虽然一些shm_open()实现在底层使用open(),但POSIX对shm_open()产生的文件描述符的要求较低。例如,实现不需要支持像read()和write()这样的I/O函数来处理shm_open()文件描述符,这使得某些实现可以针对shm_open()进行优化,而这种优化无法用于open()。如果您只是要使用mmap(),那么应该使用shm_open()。 - osvein
1
大多数Linux-glibc设置通过使用tmpfs来支持shm_open()进行这样的优化。虽然通常可以通过open()访问相同的tmpfs,但没有可移植的方法来知道它的路径。shm_open()允许您以可移植的方式使用该优化。 POSIX使shm_open()具有比open()更好的性能潜力。并非所有实现都会利用该潜力,但它不会比open()表现得更差。但我同意我的说法open()总是增加开销太过笼统。 - osvein

0
我简化了 @salezica 的答案,使其能够在父子进程之间读写共享的 int
同时将包装函数重命名为 shared_malloc
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>

#include <string.h>
#include <unistd.h>

void* shared_malloc(size_t size) {
  int prot = PROT_READ | PROT_WRITE;
  int flags = MAP_SHARED | MAP_ANONYMOUS;
  return mmap(NULL, size, prot, flags, -1, 0);
}

int main() {
  int* shared_x = (int *)shared_malloc(sizeof(int));
  * shared_x = 3;
  int pid = fork();
  if (pid == 0) {
    printf("Child read: %d\n", *shared_x);
    * shared_x = 4;
    printf("Child wrote: %d\n", *shared_x);
  } else {
    printf("Parent read: %d\n", *shared_x);
    while(* shared_x == 3);
    printf("After sync, parent read: %d\n", *shared_x);
  }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接