2

malloc/calloc apparently use swap space to satisfy a request that exceeds available free memory. And that pretty much hangs the system as the disk-use light remains constantly on. After it happened to me, and I wasn't immediately sure why, I wrote the following 5-line test program to check that this is indeed why the system was hanging,

/* --- test how many bytes can be malloc'ed successfully --- */
#include <stdio.h>
#include <stdlib.h>
int main ( int argc, char *argv[] ) {
  unsigned int nmalloc = (argc>1? atoi(argv[1]) : 10000000 ),
               size    = (argc>2? atoi(argv[2]) : (0) );
  unsigned char *pmalloc = (size>0? calloc(nmalloc,size):malloc(nmalloc));
  fprintf( stdout," %s malloc'ed %d elements of %d bytes each.\n",
    (pmalloc==NULL? "UNsuccessfully" : "Successfully"),
    nmalloc, (size>0?size:1) );
  if ( pmalloc != NULL ) free(pmalloc);
  } /* --- end-of-function main() --- */

And that indeed hangs the system if the product of your two command-line args exceeds physical memory. Easiest solution is some way whereby malloc/calloc automatically just fail. Harder and non-portable was to write a little wrapper that popen()'s a free command, parses the output, and only calls malloc/calloc if the request can be satisfied by the available "free" memory, maybe with a little safety factor built in.

Is there any easier and more portable way to accomplish that? (Apparently similar to this question can calloc or malloc be used to allocate ONLY physical memory in OSX?, but I'm hoping for some kind of "yes" answer.)

    E d i t
--------------

Decided to follow Tom's /proc/meminfo suggestion. That is, rather than popen()'ing "free", just directly parse the existing and easily-parsible /proc/meminfo file. And then, a one-line macro of the form

#define noswapmalloc(n) ( (n) < 1000l*memfree(NULL)/2? malloc(n) : NULL )

finishes the job. memfree(), shown below, isn't as portable as I'd like, but can easily and transparently be replaced by a better solution if/when the need arises, which isn't now.

#include <stdio.h>
#include <stdlib.h>
#define _GNU_SOURCE                     /* for strcasestr() in string.h */
#include <string.h>
char    *strcasestr();                  /* non-standard extension */

/* ==========================================================================
 * Function:    memfree ( memtype )
 * Purpose:     return number of Kbytes of available memory
 *              (as reported in /proc/meminfo)
 * --------------------------------------------------------------------------
 * Arguments:   memtype (I)     (char *) to null-terminated, case-insensitive
 *                              (sub)string matching first field in
 *                              /proc/meminfo (NULL uses MemFree)
 * --------------------------------------------------------------------------
 * Returns:     ( int )         #Kbytes of memory, or -1 for any error
 * --------------------------------------------------------------------------
 * Notes:       o
 * ======================================================================= */
/* --- entry point --- */
int     memfree ( char *memtype ) {
  /* ---
   * allocations and declarations
   * ------------------------------- */
  static char memfile[99] = "/proc/meminfo"; /* linux standard */
  static char deftype[99] = "MemFree";  /* default if caller passes null */
  FILE  *fp = fopen(memfile,"r");       /* open memfile for read */
  char  memline[999];                   /* read memfile line-by-line */
  int   nkbytes = (-1);                 /* #Kbytes, init for error */
  /* ---
   * read memfile until line with desired memtype found
   * ----------------------------------------------------- */
  if ( memtype == NULL ) memtype = deftype; /* caller wants default */
  if ( fp == NULL ) goto end_of_job;    /* but we can't get it */
  while ( fgets(memline,512,fp)         /* read next line */
  !=      NULL ) {                      /* quit at eof (or error) */
    if ( strcasestr(memline,memtype)    /* look for memtype in line */
    !=   NULL ) {                       /* found line with memtype */
      char *delim = strchr(memline,':'); /* colon following MemType */
      if ( delim != NULL )              /* NULL if file format error? */
        nkbytes = atoi(delim+1);        /* num after colon is #Kbytes */
      break; }                          /* no need to read further */
    } /* --- end-of-while(fgets()!=NULL) --- */
  end_of_job:                           /* back to caller with nkbytes */
    if ( fp != NULL ) fclose(fp);       /* close /proc/meminfo file */
    return ( nkbytes );                 /* and return nkbytes to caller */
  } /* --- end-of-function memfree() --- */

#if defined(MEMFREETEST)
int     main ( int argc, char *argv[] ) {
  char  *memtype = ( argc>1? argv[1] : NULL );
  int   memfree();
  printf ( " memfree(\"%s\") = %d Kbytes\n Have a nice day.\n",
        (memtype==NULL?" ":memtype), memfree(memtype) );
  } /* --- end-of-function main() --- */
#endif
John Forkosh
  • 502
  • 4
  • 14
  • 1
    You could just set a process limit on the amount of memory your process is allowed to use. – Tom Karzes Apr 11 '19 at 05:48
  • @TomKarzes Thanks, Tom. You mean like ulimit -v? Then the program still needs to know how much memory's available (which is the real problem the wrapper is solving by popen'ing a free command). Also, the actual program may be running as a server-side cgi, and I don't know offhand whether or not bash shell stuff would be available. – John Forkosh Apr 11 '19 at 05:57
  • 1
    `ulimit` is available from the shell, but it just calls `getrlimit` and `setrlimit`, which you can call from your C program. `getrlimit` should let you see what limits are in effect. You can also look at `/proc/meminfo` for info on your processor's physical memory. – Tom Karzes Apr 11 '19 at 06:04
  • @TomKarzes Thanks again. I wasn't aware of that. And I think it'll work!!! Or at least work robustly enough for my present purposes. – John Forkosh Apr 11 '19 at 06:08
  • 1
    Take a look at the `swapoff` command. – rici Apr 11 '19 at 06:16
  • 1
    @rici Thanks, rici. I wasn't aware of that either. And it does work! -- I just tried it, and the test program immediately fails, after swapoff -a, if the request exceeds available physical memory. (get/setrlimit seem the most portable, and can be transparently implemented in the program, without affecting the rest of the system.) – John Forkosh Apr 11 '19 at 06:25
  • 1
    Related: https://stackoverflow.com/questions/7947849/can-i-rely-on-malloc-returning-null), – Antti Haapala -- Слава Україні Apr 11 '19 at 06:53
  • @AnttiHaapala Thanks, Antti. I haven't personally experienced that problem/bug. And since that post's eight years old (okay, 7.5), I'm guessing it's been fixed in current gcc/library implementations. (And it's not really entirely on-topic with respect to the problem, which Tom and ricci addressed more directly.) – John Forkosh Apr 11 '19 at 07:10
  • @JohnForkosh what do you mean by that, it is 100 % about the same problem, but the question is different, i.e. how to turn the behaviour off. – Antti Haapala -- Слава Україні Apr 11 '19 at 07:49
  • @AnttiHaapala I meant that the referenced post says, _"return a non-NULL pointer even if the memory is not actually available, and trying to use the memory later on will trigger an error"_ And subsequent answers referred to that as a bug (not a feature:). In my case, you can use the memory **without** "triggering an error". There's no bug/problem using the returned non-NULL pointer. It's just that some of that pointed-to "memory" has to be swapped in/out when it's actually addressed by the program, causing enormous disk i/o overhead in my situation. So I'm asking for a NULL if swap's needed. – John Forkosh Apr 11 '19 at 08:31

2 Answers2

5

malloc/calloc apparently use swap space to satisfy a request that exceeds available free memory.

Well, no.

Malloc/calloc use virtual memory. The "virtual" means that it's not real - it's an artificially constructed illusion made out of fakery and lies. Your entire process is built on these artificially constructed illusions - a thread is a virtual CPU, a socket is a virtual network connection, the C language is really a specification for a "C abstract machine", a process is a virtual computer (that implements the languages' abstract machine).

You're not supposed to look behind the magic curtain. You're not supposed to know that physical memory exists. The system doesn't hang - the illusion is just slower, but that's fine because the C abstract machine says nothing about how long anything is supposed to take and does not provide any performance guarantees.

More importantly; because of the illusion, software works. It doesn't crash because there's not enough physical memory. Failure means that it takes an infinite amount of time for software to complete successfully, and "an infinite amount of time" is many orders of magnitude worse than "slower because of swap space".

How to get malloc/calloc to fail if request exceeds free physical memory (i.e., don't use swap)

If you are going to look behind the magic curtain, you need to define your goals carefully.

For one example, imagine if your process has 123 MiB of code and there's currently 1000 MiB of free physical RAM; but (because the code is in virtual memory) only a tiny piece of the code is using real RAM (and the rest of the code is on disk because the OS/executable loader used memory mapped files to avoid wasting real RAM until it's actually necessary). You decide to allocate 1000 MiB of memory (and because the OS creating the illusion isn't very good, unfortunately this causes 1000 MiB of real RAM to be allocated). Next, you execute some more code, but the code you execute isn't in real memory yet, so the OS has to fetch the code from the file on the disk into physical RAM, but you consumed all of the physical RAM so the OS has to send some of the data to swap space.

For another example, imagine if your process has 1 MiB of code and 1234 MiB of data that was carefully allocated to make sure that everything fits in physical memory. Then a completely different process is started and it allocates 6789 MiB of memory for its code and data; so the OS sends all of your process' data to swap space to satisfy the other process that you have no control over.

EDIT

The problem here is that the OS providing the illusion is not very good. When you allocate a large amount of virtual memory with malloc() or calloc(); the OS should be able to use a tiny piece of real memory to lie to you and avoid consuming a large amount of real memory. Specifically (for most modern operating systems running on normal hardware); the OS should be able to fill a huge area of virtual memory with a single page full of zeros that is mapped many times (at many virtual addresses) as "read only", so that allocating a huge amount of virtual memory costs almost no physical RAM at all (until you write to the virtual memory, causing the OS to allocate the least physical memory needed to satisfy the modifications). Of course if you eventually do write to all of the allocated virtual memory, then you'll end up exhausting physical memory and using some swap space; but this will probably happen gradually and not all at once - many tiny delays scattered over a large period of time are far less likely to be noticed than a single huge delay.

With this in mind; I'd be tempted to try using mmap(..., MAP_ANONYMOUS, ...) instead of the (poorly implemented) malloc() or calloc(). This might mean that you have to deal with the possibility that the allocated virtual memory isn't guaranteed to be initialized to zeros, but (depending on what you're using the memory for) that's likely to be easy to work around.

Brendan
  • 35,656
  • 2
  • 39
  • 66
  • Thanks, Brendan, but you're clarifying terminology without providing an actual solution like both Tom and rici did in preceding comments. And I knew that "hang" was maybe an exaggeration, although that term is often used to just mean very slow. "Crash" (which you also used) is the unambiguous term for failure. In my test cases, I had to spend ~five minutes before the machine responded to any keystrokes. (And, by the way, "swap" is also unambiguous if the request exceeds physical memory. Virtual can be either for smaller requests, but swap space must be used for larger requests.) – John Forkosh Apr 11 '19 at 07:02
  • 1
    @JohnForkosh: You're probably right - I added a potential solution. :-) – Brendan Apr 11 '19 at 07:19
  • Thanks, Brendan - I added a potential "check" :) – John Forkosh Apr 11 '19 at 08:32
  • @rici: You're saying that (the "hosted" part of) the C library isn't part of the OS; and then saying "the Linux malloc implementation" in the very next sentence, as if the malloc implementation is part of Linux (and not just part of a distro)? You can see how this is a "theoretically correct" distinction that often has no practical difference (e.g. "GNU's libC provided as part of GNU/Linux", or "Gentoo's LibC package provided with Gentoo", or...). – Brendan Apr 12 '19 at 04:42
  • @rici: Are you going to try to convince me that an OS consists of a kernel and nothing else (no boot loader, no GUI or shell, no libraries, no tools to create/manage storage devices, ...)? – Brendan Apr 12 '19 at 05:24
  • And that "artificially constructed illusion made out of fakery and lies" is made worse when the OS actively lies to you about memory being available because of memory overcommit - if you dare to try and use the memory the OS said you could, it turns around and kills your process. This does seem a bit off, though: *try using `mmap(..., MAP_ANONYMOUS, ...)` instead of the (poorly implemented) `malloc()` or `calloc()`* Huh? `mmap()` returns virtual memory that's no different from the memory returned by `malloc()`/`calloc()`. GLibC even uses `mmap()` to implement `malloc()`/`calloc()`. – Andrew Henle Apr 12 '19 at 12:35
1

Expanding on a comment I made to the original question:

If you want to disable swapping, use the swapoff command (sudo swapoff -a). I usually run my machine that way, to avoid it freezing when firefox does something it shouldn't. You can use setrlimit() (or the ulimit command) to set a maximum VM size, but that won't properly compensate for some other process suddenly deciding to be a memory hog (see above).

Even if you choose one of the above options, you should read the rest of this answer to see how to avoid unnecessary initialisation on the first call to calloc().


As for your precise test harness, it turns out that you are triggering an unfortunate exception to GNU calloc()'s optimisation.

Here's a comment (now deleted) I made to another answer, which turns out to not be strictly speaking accurate:

I checked the glibc source for the default gnu/linux malloc library, and verified that calloc() does not normally manually clear memory which has just been mmap'd. And malloc() doesn't touch the memory at all.

It turns out that I missed one exception to the calloc optimisation. Because of the way the GNU malloc implementation initialises the malloc system, the first call to calloc always uses memset() to set the newly-allocated storage to 0. Every other call to calloc() passes through the entire calloc logic, which avoids calling memset() on storage which has been freshly mmap'd.

So the following modification to the test program shows radically different behaviour:

#include <stdio.h>
#include <stdlib.h>
int main ( int argc, char *argv[] ) {
  /* These three lines were added */
  void* tmp = calloc(1000, 1); /* force initialization */
  printf("Allocated 1000 bytes at %p\n", tmp);
  free(tmp);
  /* The rest is unchanged */
  unsigned int nmalloc = (argc>1? atoi(argv[1]) : 10000000 ),
               size    = (argc>2? atoi(argv[2]) : (0) );
  unsigned char *pmalloc = (size>0? calloc(nmalloc,size):malloc(nmalloc));
  fprintf( stdout," %s malloc'ed %d elements of %d bytes each.\n",
    (pmalloc==NULL? "UNsuccessfully" : "Successfully"),
    nmalloc, (size>0?size:1) );
  if ( pmalloc != NULL ) free(pmalloc);
}

Note that if you set MALLOC_PERTURB_ to a non-zero value, then it is used to initialise malloc()'d blocks, and forces calloc()'d blocks to be initialised to 0. That's used in the test below.

In the following, I used /usr/bin/time to show the number of page faults during execution. Pay attention to the number of minor faults, which are the result of the operating system zero-initialising a previously unreferenced page in an anonymous mmap'd region (and some other occurrences, like mapping a page already present in Linux's page cache). Also look at the resident set size and, of course, the execution time.

$ gcc -Og -ggdb -Wall -o mall mall.c

$ # A simple malloc completes instantly without page faults
$ /usr/bin/time ./mall 4000000000
Allocated 1000 bytes at 0x55b94ff56260
 Successfully malloc'ed -294967296 elements of 1 bytes each.
0.00user 0.00system 0:00.00elapsed 100%CPU (0avgtext+0avgdata 1600maxresident)k
0inputs+0outputs (0major+61minor)pagefaults 0swaps

$ # Unless we tell malloc to initialise memory
$ MALLOC_PERTURB_=35 /usr/bin/time ./mall 4000000000
Allocated 1000 bytes at 0x5648c2436260
 Successfully malloc'ed -294967296 elements of 1 bytes each.
0.19user 1.23system 0:01.43elapsed 99%CPU (0avgtext+0avgdata 3907584maxresident)k
0inputs+0outputs (0major+976623minor)pagefaults 0swaps

# Same, with calloc. No page faults, instant completion.
$ /usr/bin/time ./mall 1000000000 4
Allocated 1000 bytes at 0x55e8257bb260
 Successfully malloc'ed 1000000000 elements of 4 bytes each.
0.00user 0.00system 0:00.00elapsed 100%CPU (0avgtext+0avgdata 1656maxresident)k
0inputs+0outputs (0major+62minor)pagefaults 0swaps

$ # Again, setting the magic malloc config variable changes everything
$ MALLOC_PERMUTE_=35 /usr/bin/time ./mall 1000000000 4
Allocated 1000 bytes at 0x5646f391e260
 Successfully malloc'ed 1000000000 elements of 4 bytes each.
0.00user 0.00system 0:00.00elapsed 100%CPU (0avgtext+0avgdata 1656maxresident)k
0inputs+0outputs (0major+62minor)pagefaults 0swaps
rici
  • 234,347
  • 28
  • 237
  • 341
  • Thanks, ricci. I temporarily (maybe permanently) decided to use Tom's /proc/meminfo suggestion, as elaborated in the Edit I made at the bottom of the original question above. The memfree()/2 safety factor should avoid any system resource problem that dynamically arises while the program's executing. – John Forkosh Apr 12 '19 at 09:45