Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Newsletter Hub
Free Learning
Arrow right icon
timer SALE ENDS IN
0 Days
:
00 Hours
:
00 Minutes
:
00 Seconds
Arrow up icon
GO TO TOP
Hands-On System Programming with Linux

You're reading from   Hands-On System Programming with Linux Explore Linux system programming interfaces, theory, and practice

Arrow left icon
Product type Paperback
Published in Oct 2018
Publisher Packt
ISBN-13 9781788998475
Length 794 pages
Edition 1st Edition
Languages
Tools
Arrow right icon
Authors (2):
Arrow left icon
Kaiwan N. Billimoria Kaiwan N. Billimoria
Author Profile Icon Kaiwan N. Billimoria
Kaiwan N. Billimoria
 Aivazian Aivazian
Author Profile Icon Aivazian
Aivazian
Arrow right icon
View More author details
Toc

Table of Contents (21) Chapters Close

Preface 1. Linux System Architecture FREE CHAPTER 2. Virtual Memory 3. Resource Limits 4. Dynamic Memory Allocation 5. Linux Memory Issues 6. Debugging Tools for Memory Issues 7. Process Credentials 8. Process Capabilities 9. Process Execution 10. Process Creation 11. Signaling - Part I 12. Signaling - Part II 13. Timers 14. Multithreading with Pthreads Part I - Essentials 15. Multithreading with Pthreads Part II - Synchronization 16. Multithreading with Pthreads Part III 17. CPU Scheduling on Linux 18. Advanced File I/O 19. Troubleshooting and Best Practices 20. Other Books You May Enjoy

The glibc malloc(3) API family

In Chapter 2, Virtual Memory, we learned that there are regions or segments meant for the use of dynamic memory-allocation within the process of Virtual Address Space (VAS). The heap segment is one such dynamic region—a free gift of memory made available to the process for its runtime consumption.

How exactly does the developer exploit this gift of memory? Not just that, the developer has to be extremely careful with matching memory allocations to subsequent memory frees, otherwise the system isn't going to like it!

The GNU C library (glibc) provides a small but powerful set of APIs to enable the developer to manage dynamic memory; the details of their usage is the content of this section.

As you will come to see, the memory-management APIs are literally a handful: malloc(3), calloc, realloc, and free. Still, using them correctly remains a challenge! The subsequent sections (and chapters) will reveal why this is the case. Read on!

The malloc(3) API

Perhaps one of the most common APIs used by application developers is the renowned malloc(3).

The foo(3) syntax indicates that the foo function is in section 3 of the manual (the man pages) a library API, not a system call. We recommend you develop the habit of reading the man pages. The man pages are available online, and you can find them at https://linux.die.net/man/.

We use malloc(3) to dynamically allocate a chunk of memory at runtime. This is as opposed to static—or compile-time – memory-allocation where we make a statement, such as:

char buf[256];

In the preceding case, the memory has been statically allocated (at compile-time).

So, how exactly do you use malloc(3)? Let's check out its signature:

#include <stdlib.h>
void *malloc(size_t size);

The parameter to malloc(3) is the number of bytes to allocate. But what is the size_t data type? Obviously, it's not a C primitive data type; it's a typedef – long unsigned int on your typical 64-bit platform (the exact data type does vary with the platform; the important point is that it's always unsigned it cannot be negative. On a 32-bit Linux, it will be unsigned int). Ensuring that your code precisely matches the function signature and data types is crucial in writing robust and correct programs. While we're at it, ensure that you include the header file that the man page displays with the API signature.

To print a variable of the size_t type within a printf, use the %zu format specifier:
size_t sz = 4 * getpagesize();
[...]
printf("size = %zu bytes\n", sz);

In this book, we will not delve into the internal implementation details regarding how malloc(3) and friends actually store, allocate, and free memory (refer the Further reading section on the GitHub repository.) Suffice to say, the internal implementation strives to be as efficient as can be; using these APIs is usually considered the right way to perform memory-management.

The return value is a pointer to the zeroth byte of the newly-allocated memory region on success, and NULL on failure.

You will come across, shall we say optimists, who say things such as, "Don't bother checking malloc for failure, it never fails". Well, take that sage advice with a grain of salt. While it's true that malloc would rarely fail, the fact is (as you shall see), it could fail. Writing defensive code – code that checks for the failure case immediately – is a cornerstone of writing solid, robust programs.

So, using the API is very straightforward: as an example, allocate 256 bytes of memory dynamically, and store the pointer to that newly allocated region in the ptr variable:

void *ptr;
ptr = malloc(256);

As another typical example, the programmer needs to allocate memory for a data structure; let's call it struct sbar. You could do so like this:

    struct sbar {
int a[10], b[10];
char buf[512];
} *psbar;

psbar = malloc(sizeof(struct sbar));
// initialize and work with it
[...]
free(psbar);

Hey, astute reader! What about checking the failure case? It's a key point, so we will rewrite the preceding code like so (and of course it would be the case for the malloc(256) code snippet too):

struct [...] *psbar;
sbar = malloc(sizeof(struct sbar));
if (!sbar) {
<... handle the error ...>
}

Let's use one of the powerful tracing tools ltrace to check that this works as expected; ltrace is used to display all library APIs in the process-execution path (similarly, use strace to trace all system calls). Let's assume that we compile the preceding code and the resulting binary executable file is called tst:

$ ltrace ./tst 
malloc(592) = 0xd60260
free(0xd60260) = <void>
exit(0 <no return ...>
+++ exited (status 0) +++
$

We can clearly see malloc(3) (and the fact that the example structure we used took up 592 bytes on an x86_64), and its return value (following the = sign). The free API follows, and then it simply exits.

It's important to understand that the content of the memory chunk allocated by malloc(3) is considered to be random. Thus, it's the programmer's responsibility to initialize the memory before reading from it; if you fail to do so, it results in a bug called Uninitialized Memory Read (UMR) (more on this in the next chapter).

malloc(3) always returns a memory region that is aligned on an 8-byte boundary. Need larger alignment values? Use the posix_memalign(3) API. Deallocate its memory as usual with free(3).
Details can be found on the man page at https://linux.die.net/man/3/posix_memalign.

Examples of using the posix_memalign(3) API can be found in the Locking memory and Memory protection sections.

malloc(3) – some FAQs

The following are some FAQs that will help us to learn more about malloc(3):

  • FAQ 1 : How much memory can malloc(3) allocate with a single call?

A rather pointless question in practical terms, but one that is often asked!

The parameter to malloc(3) is an integer value of the size_t data type, so, logically, the maximum number we can pass as a parameter to malloc(3) is the maximum value a size_t can take on the platform. Practically speaking, on a 64-bit Linux, size_t will be 8 bytes, which of course, in bits is 8*8 = 64. Therefore, the maximum amount of memory that can be allocated in a single malloc(3) call is 2^64!

So, how much is it? Let's be empirical (it's important to read in Chapter 19, Troubleshooting and Best Practices, and the brief discussion there on The empirical approach).and actually try it out (note that the following code snippet has to be linked with the math library using the -lm switch):

    int szt = sizeof(size_t);
float max=0;
max = pow(2, szt*8);
printf("sizeof size_t = %u; "
"max value of the param to malloc = %.0f\n",
szt, max);

The output, on an x86_64:

sizeof size_t = 8; max param to malloc = 18446744073709551616

Aha! That's a mighty large number; more readably, it's as follows:

2^64 = 18,446,744,073,709,551,616 = 0xffffffffffffffff

That's 16 EB (exabytes, which is 16,384 PB, which is 16 million TB)!

So, on a 64-bit OS, malloc(3) can allocate a maximum of 16 EB in a single call. In theory.

As usual, there's more to it: please see FAQ 2; it will reveal that the theoretical answer to this question is 8 exabytes (8 EB).

In practice, obviously, this would be impossible because, of course, that's the entire usermode VAS of the process itself. In reality, the amount of memory that can be allocated is limited by the amount of free memory contiguously available on the heap. Actually, there's more to it. As we shall soon learn (in the How malloc(3) really behaves section), memory for malloc(3) can come from other regions of the VAS, too. Don't forget there's a resource limit on data segment size; the default is usually unlimited, which as we discussed in this chapter, really means that there's no artificial limit imposed by the OS.

So, in practice, it's best to be sensible, not assume anything, and check the return value for NULL.

As an aside, what's the maximum value a size_t can take on a 32-bit OS? Accordingly, we compile on x86_64 for 32-bit by passing the -m32 switch to the compiler:

$ gcc -m32 mallocmax.c -o mallocmax32 -Wall -lm
$ ./mallocmax32
*** max_malloc() ***
sizeof size_t = 4; max value of the param to malloc = 4294967296
[...]
$

Clearly, it's 4 GB (gigabytes) again, the entire VAS of a 32-bit process.

  • FAQ 2: What if I pass malloc(3) a negative argument?

The data type of the parameter to malloc(3), size_t, is an unsigned integer quantity it cannot be negative. But, humans are imperfect, and Integer OverFlow (IOF) bugs do exist! You can imagine a scenario where a program attempts to calculate the number of bytes to allocate, like this:

num = qa * qb;

What if num is declared as a signed integer variable and qa and qb are large enough that the result of the multiplication operation causes an overflow? The num result will then wrap around and become negative! malloc(3) should fail, of course. But hang on: if the num variable is declared as size_t (which should be the case), the negative quantity will turn into some positive quantity!

The mallocmax program has a test case for this.

Here is the output when run on an x86_64 Linux box:

*** negative_malloc() ***
size_t max = 18446744073709551616
ld_num2alloc = -288225969623711744
szt_num2alloc = 18158518104085839872
1. long int used: malloc(-288225969623711744) returns (nil)
2. size_t used: malloc(18158518104085839872) returns (nil)
3. short int used: malloc(6144) returns 0x136b670
4. short int used: malloc(-4096) returns (nil)
5. size_t used: malloc(18446744073709547520) returns (nil)

Here are the relevant variable declarations:

const size_t onePB    = 1125899907000000; /* 1 petabyte */
int qa = 28*1000000;
long int ld_num2alloc = qa * onePB;
size_t szt_num2alloc = qa * onePB;
short int sd_num2alloc;

Now, let's try it with a 32-bit version of the program.

Note that on a default-install Ubuntu Linux box, the 32-bit compile may fail (with an error such as fatal error: bits/libc-header-start.h: No such file or directory). Don't panic: this usually implies that the compiler support for building 32-bit binaries isn't present by default. To get it (as mentioned in the Hardware-Software List document), install the multilib compiler package: sudo apt-get install gcc-multilib.

Compile it for 32-bit and run it:

$ ./mallocmax32 
*** max_malloc() ***
sizeof size_t = 4; max param to malloc = 4294967296
*** negative_malloc() ***
size_t max = 4294967296
ld_num2alloc = 0
szt_num2alloc = 1106247680
1. long int used: malloc(-108445696) returns (nil)
2. size_t used: malloc(4186521600) returns (nil)
3. short int used: malloc(6144) returns 0x85d1570
4. short int used: malloc(-4096) returns (nil)
5. size_t used: malloc(4294963200) returns (nil)
$

To be fair, the compiler does warn us:

gcc -Wall   -c -o mallocmax.o mallocmax.c
mallocmax.c: In function ‘negative_malloc’:
mallocmax.c:87:6: warning: argument 1 value ‘18446744073709551615’ exceeds maximum object size 9223372036854775807 [-Walloc-size-larger-than=]
ptr = malloc(-1UL);
~~~~^~~~~~~~~~~~~~
In file included from mallocmax.c:18:0:
/usr/include/stdlib.h:424:14: note: in a call to allocation function ‘malloc’ declared here
extern void *malloc (size_t __size) __THROW __attribute_malloc__ __wur;
^~~~~~
[...]

Interesting! The compiler answers our FAQ 1 question now:

[...] warning: argument 1 value ‘18446744073709551615’ exceeds maximum object size 9223372036854775807 [-Walloc-size-larger-than=] [...]

The maximum value you can allocate as per the compiler seems to be 9223372036854775807.

Wow. A little calculator time reveals that this is 8192 PB = 8 EB! So, we must conclude that the correct answer to the previous question: How much memory can malloc allocate with a single call? Answer: 8 exabytes. Again, in theory.

  • FAQ 3: What if I use malloc(0)?

Not much; depending on the implementation, malloc(3) will return NULL, or, a non-NULL pointer that can be passed to free. Of course, even if the pointer is non-NULL, there is no memory, so don't attempt to use it.

Let's try it out:

    void *ptr;
ptr = malloc(0);
free(ptr);

We compile and then run it via ltrace:

$ ltrace ./a.out 
malloc(0) = 0xf50260
free(0xf50260) = <void>
exit(0 <no return ...>
+++ exited (status 0) +++
$

Here, malloc(0) did indeed return a non-NULL pointer.

  • FAQ 4: What if I use malloc(2048) and attempt to read/write beyond 2,048 bytes?

This is a bug of course an out-of-bounds memory-access bug, further defined as a read or write buffer overflow. Hang on please, the detailed discussion of memory bugs (and subsequently, how to find and fix them) is the subject of Chapter 5, Linux Memory Issues, and Chapter 6, Debugging Tools for Memory Issues.

malloc(3) – a quick summary

So, let's summarize the key points regarding usage of the malloc(3) API:

  • malloc(3) dynamically (at runtime) allocates memory from the process heap
    • As we shall soon learn, this is not always the case
  • The single parameter to malloc(3) is an unsigned integer value—the number of bytes to allocate
  • The return value is a pointer to the start of the newly allocated memory chunk on success, or NULL on failure:
    • You must check for the failure case; don't just assume it will succeed
    • malloc(3) always returns a memory region that is aligned on an 8-byte boundary
  • The content of the newly allocated memory region is considered to be random
    • You must initialize it before reading from any part of it
  • You must free the memory you allocate

The free API

One of the golden rules of development in this ecosystem is that programmer-allocated memory must be freed.

Failure to do so leads to a bad situation a bug, really called memory leakage; this is covered in some depth in the next chapter. Carefully matching your allocations and frees is essential.

Then again, in smaller real-world projects (utils), you do come across cases where memory is allocated exactly once; in such cases, freeing the memory is pedantic as the entire virtual address space is destroyed upon process-termination. Also, using the alloca(3) API implies that you do not need to free the memory region (seen later in, Advanced features section). Nevertheless, you are advised to err on the side of caution!

Using the free(3) API is straightforward:

void free(void *ptr);

It accepts one parameter: the pointer to the memory chunk to be freed. ptr must be a pointer returned by one of the malloc(3) family routines: malloc(3), calloc, or realloc[array].

free does not return any value; don't even attempt to check whether it worked; if you used it correctly, it worked. More on free is found in the Where does freed memory go? section. Once a memory chunk is freed, you obviously cannot attempt to use any part of that memory chunk again; doing so will result in a bug (or what's called UB undefined behavior).

A common misconception regarding free() sometimes leads to its being used in a buggy fashion; take a look at this pseudocode snippet:

void *ptr = NULL;
[...]
while(<some-condition-is-true>) {
if (!ptr)
ptr = malloc(n);

[...
<use 'ptr' here>
...]

free(ptr);
}

This program will possibly crash in the loop (within the <use 'ptr' here> code) in a few iterations. Why? Because the ptr memory pointer is freed and is attempting to be reused. But how come? Ah, look carefully: the code snippet is only going to malloc(3) the ptr pointer if it is currently NULL, that is, its programmer has assumed that once we free() memory, the pointer we just freed gets set to NULL. This is not the case!!

Be wary and be defensive in writing code. Don't assume anything; it's a rich source of bugs. Importantly, our Chapter 19, Troubleshooting and Best Practices, covers such points)

free – a quick summary

So, let's summarize the key points regarding the usage of the free API:

  • The parameter passed to free(3) must be a value returned by one of the malloc(3) family APIs (malloc(3), calloc, or realloc[array]).
  • free has no return value.
  • Calling free(ptr) does not set ptr to NULL (that would be nice, though).
  • Once freed, do not attempt to use the freed memory.
  • Do not attempt to free the same memory chunk more than once (it's a bug UB).
  • For now, we will assume that freed memory goes back to the system.
  • For Heaven's sake, do not forget to free memory that was dynamically allocated earlier. The forgotten memory is said to have leaked out and that's a really hard bug to catch! Luckily, there are tools that help us catch these bugs. More in Chapter 5, Linux Memory Issues, and Chapter 6, Debugging Tools for Memory Issues.

The calloc API

The calloc(3) API is almost identical to malloc(3), differing in two main respects:

  • It initializes the memory chunk it allocates to the zero value (that is, ASCII 0 or NULL, not the number 0)
  • It accepts two parameters, not one

The calloc(3) function signature is as follows:

void *calloc(size_t nmemb, size_t size);

The first parameter, nmemb, is n members; the second parameter, size, is the size of each member. In effect, calloc(3) allocates a memory chunk of (nmemb*size) bytes. So, if you want to allocate memory for an array of, say, 1,000 integers, you can do so like this:

    int *ptr;
ptr = calloc(1000, sizeof(int));

Assuming the size of an integer is 4 bytes, we would have allocated a total of (1000*4) = 4000 bytes.

Whenever one requires memory for an array of items (a frequent use case in applications is an array of structures), calloc is a convenient way to both allocate and simultaneously initialize the memory.

Demand paging (covered later in this chapter), is another reason programmers use calloc rather than malloc(3) (in practice, this is mostly useful for realtime applications). Read up on this in the up coming section.

The realloc API

The realloc API is used to resize an existing memory chunk—to grow or shrink it. This resizing can only be performed on a piece of memory previously allocated with one of the malloc(3) family of APIs (the usual suspects: malloc(3), calloc, or realloc[array]). Here is its signature:

void *realloc(void *ptr, size_t size);

The first parameter, ptr, is a pointer to a chunk of memory previously allocated with one of the malloc(3) family of APIs; the second parameter, size, is the new size of the memory chunk—it can be larger or smaller than the original, thus growing or shrinking the memory chunk.

A quick example code snippet will help us understand realloc:

void *ptr, *newptr;
ptr = calloc(100, sizeof(char)); // error checking code not shown here
newptr = realloc(ptr, 150);
if (!newptr) {
fprintf(stderr, "realloc failed!");
free(ptr);
exit(EXIT_FAILURE);
}
< do your stuff >
free(newptr);

The pointer returned by realloc is the pointer to the newly resized chunk of memory; it may or may not be the same address as the original ptr. In effect, you should now completely disregard the original pointer ptr and regard the realloc-returned newptr pointer as the one to work with. If it fails, the return value is NULL (check it!) and the original memory chunk is left untouched.

A key point: the pointer returned by realloc(3), newptr, is the one that must be subsequently freed, not the original pointer (ptr) to the (now resized) memory chunk. Of course, do not attempt to free both pointers, as that to is a bug.

What about the contents of the memory chunk that just got resized? They remain unchanged up to MIN(original_size, new_size). Thus, in the preceding example, MIN(100, 150) = 100, the contents of memory up to 100 bytes will be unchanged. What about the remainder (50 bytes)? It's considered to be random content (just like malloc(3)).

The realloc(3) – corner cases

Consider the following code snippet:

void *ptr, *newptr;
ptr = calloc(100, sizeof(char)); // error checking code not shown here
newptr = realloc(NULL, 150);

The pointer passed to realloc is NULL? The library treats this as equivalent to a new allocation – malloc(150); and all the implications of the malloc(3) That's it.

Now, consider the following code snippet:

void *ptr, *newptr;
ptr = calloc(100, sizeof(char)); // error checking code not shown here
newptr = realloc(ptr, 0);

The size parameter passed to realloc is 0? The library treats this as equivalent to free(ptr). That's it.

The reallocarray API

A scenario: you allocate memory for an array using calloc(3); later, you want to resize it to be, say, a lot larger. We can do so with realloc(3); for example:

struct sbar *ptr, *newptr;
ptr = calloc(1000, sizeof(struct sbar)); // array of 1000 struct sbar's
[...]
// now we want 500 more!
newptr = realloc(ptr, 500*sizeof(struct sbar));

Fine. There's an easier way, though—using the reallocarray(3) API. Its signature is as follows:

void *reallocarray(void *ptr, size_t nmemb, size_t size);

With it, the code becomes simpler:

[...]
// now we want 500 more!
newptr = reallocarray(ptr, 500, sizeof(struct sbar));

The return value of reallocarray is pretty identical to that of the realloc API: the new pointer to the resized memory chunk on success (it may differ from the original), NULL on failure. If it fails, the original memory chunk is left untouched.

reallocarray has one real advantage over realloc safety. From the man page on realloc(3), see this snippet:

... However, unlike that realloc() call, reallocarray() fails safely in the case where the  multiplication  would  overflow.   If  such  an  overflow occurs, reallocarray() returns NULL, sets errno to ENOMEM, and leaves the original block of memory unchanged.

Also realize that the reallocarray API is a GNU extension; it will work on modern Linux but should not be considered portable to other OSes.

Finally, consider this: some projects have strict alignment requirements for their data objects; using calloc (or even allocating said objects via malloc(3)) can result in subtle bugs! Later in this chapter, we'll use the posix_memalign(3) API—it guarantees allocating memory to a given byte alignment (you specify the number of bytes)! For example, requiring a memory-allocation to be aligned to a page boundary is a fairly common occurrence (Recall, malloc always returns a memory region that is aligned on an 8-byte boundary).

The bottom line: be careful. Read the documentation, think, and decide which API would be appropriate given the circumstances. More on this in the Further reading section on the GitHub repository.

lock icon The rest of the chapter is locked
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime