Memory allocation error in memalloc

Динамическое выделение памяти malloc В предыдущей главе уже обсуждалось, что локальные переменные кладутся на стек и существую до тех пор, пока мы не вышли из функции. С одной стороны, это позволяет автоматически очищать память, с другой стороны, существует необходимость в переменных, время жизни которых мы можем контролировать самостоятельно. Кроме того, нам необходимо динамическое выделение […]

Содержание

  1. Динамическое выделение памяти
  2. malloc
  3. Освобождение памяти с помощью free
  4. Работа с двумерными и многомерными массивами
  5. calloc
  6. realloc
  7. Ошибки при выделении памяти
  8. Различные аргументы realloc и malloc.
  9. Примеры
  10. 3.5 Diagnose Leaks in Native Code
  11. 3.5.1 Track All Memory Allocation and Free Calls
  12. 3.5.2 Track All Memory Allocations in JNI Library
  13. 3.5.3 Track Memory Allocation with Operating System Support
  14. 3.5.4 Find Leaks with dbx Debugger
  15. 3.5.5 Find Leaks with libumem Tool

Динамическое выделение памяти

malloc

В предыдущей главе уже обсуждалось, что локальные переменные кладутся на стек и существую до тех пор, пока мы не вышли из функции. С одной стороны, это позволяет автоматически очищать память, с другой стороны, существует необходимость в переменных, время жизни которых мы можем контролировать самостоятельно. Кроме того, нам необходимо динамическое выделение памяти, когда размер используемого пространства заранее не известен. Для этого используется выделение памяти на куче. Недостатков у такого подхода два: во-первых, память необходимо вручную очищать, во-вторых, выдеение памяти – достаточно дорогостоящая операция.

Для выделения памяти на куче в си используется функция malloc (memory allocation) из библиотеки stdlib.h

Функция выделяет size байтов памяти и возвращает указатель на неё. Если память выделить не удалось, то функция возвращает NULL. Так как malloc возвращает указатель типа void, то его необходимо явно приводить к нужному нам типу. Например, создадим указатель, после этого выделим память размером в 100 байт.

После того, как мы поработали с памятью, необходимо освободить память функцией free.
Используя указатель, можно работать с выделенной памятью как с массивом. Пример: пользователь вводит число – размер массива, создаём массив этого размера и заполняем его квадратами чисел по порядку. После этого выводим и удаляем массив.

Здесь (int *) – приведение типов. Пишем такой же тип, как и у указателя.
size * sizeof(int) – сколько байт выделить. sizeof(int) – размер одного элемента массива.
После этого работаем с указателем точно также, как и с массивом. В конце не забываем удалять выделенную память.

Теперь представим на рисунке, что у нас происходило. Пусть мы ввели число 5.

Функция malloc выделила память на куче по определённому адресу, после чего вернула его. Теперь указатель p хранит этот адрес и может им пользоваться для работы. В принципе, он может пользоваться и любым другим адресом.
Когда функция malloc «выделяет память», то она резервирует место на куче и возвращает адрес этого участка. У нас будет гарантия, что компьютер не отдаст нашу память кому-то ещё. Когда мы вызываем функцию free, то мы освобождаем память, то есть говорим компьютеру, что эта память может быть использована кем-то другим. Он может использовать нашу память, а может и нет, но теперь у нас уже нет гарантии, что эта память наша. При этом сама переменная не зануляется, она продолжает хранить адрес, которым ранее пользовалась.

Это очень похоже на съём номера в отеле. Мы получаем дубликат ключа от номера, живём в нём, а потом сдаём комнату обратно. Но дубликат ключа у нас остаётся. Всегда можно зайти в этот номер, но в нём уже кто-то может жить. Так что наша обязанность – удалить дубликат.

Иногда думают, что происходит «создание» или «удаление» памяти. На самом деле происходит только перераспределение ресурсов.

Освобождение памяти с помощью free

Т еперь рассмотри, как происходит освобождение памяти. Переменная указатель хранит адрес области памяти, начиная с которого она может им пользоваться. Однако, она не хранит размера этой области. Откуда тогда функция free знает, сколько памяти необходимо освободить?

Очевидно, что информация о размере выделенного участка должна где-то храниться. Есть несколько решения этой проблемы.

  • 1. Можно создать карту, в которой будет храниться размер выделенного участка. Каждый раз при освобождении памяти компьютер будет обращаться к этим данным и получать нужную информацию.
  • 2. Второе решение более распространено. Информация о размере хранится на куче до самих данных. Таким образом, при выделении памяти резервируется места больше и туда записывается информация о выделенном участке. При освобождении памяти функция free «подсматривает», сколько памяти необходимо удалить.

Функция free освобождает память, но при этом не изменяет значение указателя, о чём нужно помнить.

Работа с двумерными и многомерными массивами

Д ля динамического создания двумерного массива сначала необходимо создать массив указателей, после чего каждому из элементов этого массива присвоить адрес нового массива.
Для удаления массива необходимо повторить операцию в обратном порядке — удалить сначала подмассивы, а потом и сам массив указателей.

Сначала мы создаём массив указателей, а после этого каждому элементу этого массива присваиваем адрес вновь созданного массива. Это значит, что можно

  • 1. Создавать массивы «неправильной формы», то есть массив строк, каждая из которых имеет свой размер.
  • 2. Работать по отдельности с каждой строкой массива: освобождать память или изменять размер строки.

Создадим «треугольный» массив и заполним его значениями

Чтобы создать трёхмерный массив, по аналогии, необходимо сначала определить указатель на указатель на указатель, после чего выделить память под массив указателей на указатель, после чего проинициализировать каждый из массивов и т.д.

calloc

Ф ункция calloc выделяет n объектов размером m и заполняет их нулями. Обычно она используется для выделения памяти под массивы. Синтаксис

realloc

Е щё одна важная функция – realloc (re-allocation). Она позволяет изменить размер ранее выделенной памяти и получает в качестве аргументов старый указатель и новый размер памяти в байтах:

Функция realloc может как использовать ранее выделенный участок памяти, так и новый. При этом не важно, меньше или больше новый размер – менеджер памяти сам решает, где выделять память.
Пример – пользователь вводит слова. Для начала выделяем под слова массив размером 10. Если пользователь ввёл больше слов, то изменяем его размер, чтобы хватило места. Когда пользователь вводит слово end, прекращаем ввод и выводим на печать все слова.

Хочу обратить внимание, что мы при выделении памяти пишем sizeof(char*), потому что размер указателя на char не равен одному байту, как размер переменной типа char.

Ошибки при выделении памяти

1. Бывает ситуация, при которой память не может быть выделена. В этом случае функция malloc (и calloc) возвращает NULL. Поэтому, перед выделением памяти необходимо обнулить указатель, а после выделения проверить, не равен ли он NULL. Так же ведёт себя и realloc. Когда мы используем функцию free проверять на NULL нет необходимости, так как согласно документации free(NULL) не производит никаких действий. Применительно к последнему примеру:

Хотелось бы добавить, что ошибки выделения памяти могут случиться, и просто выходить из приложения и выкидывать ошибку плохо. Решение зависит от ситуации. Например, если не хватает памяти, то можно подождать некоторое время и после этого опять попытаться выделить память, или использовать для временного хранения файл и переместить туда часть объектов. Или выполнить очистку, сократив используемую память и удалив ненужные объекты.

2. Изменение указателя, который хранит адрес выделенной области памяти. Как уже упоминалось выше, в выделенной области хранятся данные об объекте — его размер. При удалении free получает эту информацию. Однако, если мы изменили указатель, то удаление приведёт к ошибке, например

Таким образом, если указатель хранит адрес, то его не нужно изменять. Для работы лучше создать дополнительную переменную указатель, с которой работать дальше.

3. Использование освобождённой области. Почему это работает в си, описано выше. Эта ошибка выливается в другую – так называемые висячие указатели (dangling pointers или wild pointers). Вы удаляете объект, но при этом забываете изменить значение указателя на NULL. В итоге, он хранит адрес области памяти, которой уже нельзя воспользоваться, при этом проверить, валидная эта область или нет, у нас нет возможности.

Эта программа отработает и выведет мусор, или не мусор, или не выведет. Поведение не определено.

Если же мы напишем

то программа выкинет исключение. Это определённо лучше, чем неопределённое поведение. Если вы освобождаете память и используете указатель в дальнейшем, то обязательно обнулите его.

4. Освобождение освобождённой памяти. Пример

Здесь дважды вызывается free для переменной a. При этом, переменная a продолжает хранить адрес, который может далее быть передан кому-нибудь для использования. Решение здесь такое же как и раньше — обнулить указатель явно после удаления:

5. Одновременная работа с двумя указателями на одну область памяти. Пусть, например, у нас два указателя p1 и p2. Если под первый указатель была выделена память, то второй указатель может запросто скомпрометировать эту область:

Рассмотрим код ещё раз.

Теперь оба указателя хранят один адрес.

А вот здесь происходит непредвиденное. Мы решили выделить под p2 новый участок памяти. realloc гарантирует сохранение контента, но вот сам указатель p1 может перестать быть валидным. Есть разные ситуации. Во-первых, вызов malloc мог выделить много памяти, часть которой не используется. После вызова ничего не поменяется и p1 продолжит оставаться валидным. Если же потребовалось перемещение объекта, то p1 может указывать на невалидный адрес (именно это с большой вероятностью и произойдёт в нашем случае). Тогда p1 выведет мусор (или же произойдёт ошибка, если p1 полезет в недоступную память), в то время как p2 выведет старое содержимое p1. В этом случае поведение не определено.

Два указателя на одну область памяти это вообще-то не ошибка. Бывают ситуации, когда без них не обойтись. Но это очередное минное поле для программиста.

Различные аргументы realloc и malloc.

При вызове функции malloc, realloc и calloc с нулевым размером поведение не определено. Это значит, что может быть возвращён как NULL, так и реальный адрес. Им можно пользоваться, но к нему нельзя применять операцию разадресации.
Вызов realloc(NULL, size_t) эквиваленте вызову malloc(size_t).
Однако, вызов realloc(NULL, 0) не эквивалентен вызову malloc(0) 🙂 Понимайте это, как хотите.

Примеры

1. Простое скользящее среднее равно среднему арифметическому функции за период n. Пусть у нас имеется ряд измерений значения функции. Часто эти измерения из-за погрешности «плавают» или на них присутствуют высокочастотные колебания. Мы хотим сгладить ряд, для того, чтобы избавиться от этих помех, или для того, чтобы выявить общий тренд. Самый простой способ: взять n элементов ряда и получить их среднее арифметическое. n в данном случае — это период простого скользящего среднего. Так как мы берём n элементов для нахождения среднего, то в результирующем массиве будет на n чисел меньше.

Пусть есть ряд
1, 4, 4, 6, 7, 8, 9, 11, 12, 11, 15
Тогда если период среднего будет 3, то мы получим ряд
(1+4+4)/3, (4+4+6)/3, (4+6+7)/3, (6+7+8)/3, (7+8+9)/3, (8+9+11)/3, (9+11+12)/3, (11+12+11)/3, (12+11+15)/3
Видно, что сумма находится в «окне», которое скользит по ряду. Вместо того, чтобы каждый раз в цикле находить сумму, можно найти её для первого периода, а затем вычитать из суммы крайнее левое значение предыдущего периода и прибавлять крайнее правое значение следующего.
Будем запрашивать у пользователя числа и период, а затем создадим новый массив и заполним его средними значениями.

Это простой пример. Большая его часть связана со считыванием данных, вычисление среднего всего в девяти строчках.

2. Сортировка двумерного массива. Самый простой способ сортировки — перевести двумерный массив MxN в одномерный размером M*N, после чего отсортировать одномерный массив, а затем заполнить двумерный массив отсортированными данными. Чтобы не тратить место под новый массив, мы поступим по-другому: если проходить по всем элементам массива k от 0 до M*N, то индексы текущего элемента можно найти следующим образом:
j = k / N;
i = k — j*M;
Заполним массив случайными числами и отсортируем

3. Бином Ньютона. Создадим треугольную матрицу и заполним биномиальными коэффициентами

Если Вы желаете изучать этот материал с преподавателем, советую обратиться к репетитору по информатике

Источник

3.5 Diagnose Leaks in Native Code

Several techniques can be used to find and isolate native code memory leaks. In general there is no single ideal solution for all platforms. The following are some techniques to diagnose leaks in native code.

3.5.1 Track All Memory Allocation and Free Calls

A very common practice is to track all allocation and free calls of the native allocations. This can be a fairly simple process or a very sophisticated one. Many products over the years have been built up around the tracking of native heap allocations and the use of that memory.

Tools like IBM Rational Purify and the runtime checking functionality of Sun Studio dbx debugger can be used to find these leaks in normal native code situations and also find any access to native heap memory that represents assignments to uninitialized memory or accesses to freed memory. See Find Leaks with dbx Debugger.

Not all these types of tools will work with Java applications that use native code, and usually these tools are platform-specific. Because the virtual machine dynamically creates code at runtime, these tools can wrongly interpret the code and fail to run at all, or give false information. Check with your tool vendor to ensure that the version of the tool works with the version of the virtual machine you are using.

See sourceforge for many simple and portable native memory leak detecting examples. Most of these libraries and tools assume that you can recompile or edit the source of the application and place wrapper functions over the allocation functions. The more powerful of these tools allow you to run your application unchanged by interposing over these allocation functions dynamically. This is the case with the library libumem.so first introduced in Oracle Solaris 9 operating system update 3; see Find Leaks with libumem Tool.

3.5.2 Track All Memory Allocations in JNI Library

If you write a JNI library, then consider creating a localized way to ensure that your library does not leak memory, by using a simple wrapper approach.

The procedure in Example 3-7 is an easy localized allocation tracking approach for a JNI library. First, define the following lines in all source files.

Example 3-7 Define this Procedure in Source Files

Then you can use the functions in Example 3-8 to watch for leaks.

Example 3-8 Function to Watch Leaks

The JNI library would then need to periodically (or at shutdown) check the value of the total_allocated variable to verify that it made sense. The preceding code could also be expanded to save in a linked list the allocations that remained and report where the leaked memory was allocated. This is a localized and portable way to track memory allocations in a single set of sources. You would need to ensure that debug_free() was called only with the pointer that came from debug_malloc() , and you would also need to create similar functions for realloc() , calloc() , strdup() , and so forth, if they were used.

A more global way to look for native heap memory leaks would involve interposition of the library calls for the entire process.

3.5.3 Track Memory Allocation with Operating System Support

Most operating systems include some form of global allocation tracking support.

On Windows, search the MSDN library for debug support. The Microsoft C++ compiler has the /Md and /Mdd compiler options that will automatically include extra support for tracking memory allocation.

Linux systems have tools such as mtrace and libnjamd to help in dealing with allocation tracking.

Oracle Solaris operating system provides the watchmalloc tool. Oracle Solaris 9 operating system update 3 also introduced the libumem tool, see Find Leaks with libumem Tool.

3.5.4 Find Leaks with dbx Debugger

The dbx debugger includes the Runtime Checking (RTC) functionality, which can find leaks. The dbx debugger is part of Oracle Solaris Studio and also available for Linux.

Example 3-9 shows a sample dbx session.

Example 3-9 A Sample dbx Session

The output shows that the dbx debugger reports memory leaks if memory is not freed at the time the process is about to exit. However, memory that is allocated at initialization time and needed for the life of the process is often never freed in native code. Therefore, in such cases the dbx debugger can report memory leaks that are not leaks in reality.

Note: Example 3-9 used two suppress commands to suppress the leaks reported in the virtual machine, libjvm.so and the Java support library, libjava.so .

First introduced in Oracle Solaris 9 operating system update 3, the libumem.so library and the modular debugger mdb can be used to debug memory leaks. Before using libumem , you must preload the libumem library and set an environment variable as shown in Example 3-10.

Example 3-10 Set an Environment Variable for libumem

Now, run the Java application, but stop it before it exits. Example 3-11 uses truss to stop the process when it calls the _exit system call.

Example 3-11 Stop the Process

At this point you can attach the mdb debugger, as shown in Example 3-12.

Example 3-12 Attach mdb Debugger to the Process

The ::findleaks command is the mdb command to find memory leaks. If a leak is found, this command prints the address of the allocation call, buffer address, and nearest symbol.

It is also possible to get the stack trace for the allocation that resulted in the memory leak by dumping the bufctl structure. The address of this structure can be obtained from the output of the ::findleaks command.

See analyzing memory leaks using libumem for troubleshooting the cause for a memory leak.

Источник

Теги: Си память, malloc, calloc, realloc, free, Ошибки выделения памяти, Висячие указатели, Динамические массивы, Многомерные динамические массивы.

malloc

В предыдущей главе уже обсуждалось, что локальные переменные кладутся на стек и существую до тех пор, пока мы не вышли из функции.
С одной стороны, это позволяет автоматически очищать память, с другой стороны, существует необходимость в переменных, время жизни которых мы можем контролировать
самостоятельно. Кроме того, нам необходимо динамическое выделение памяти, когда размер используемого пространства заранее не известен. Для этого используется
выделение памяти на куче. Недостатков у такого подхода два: во-первых, память необходимо вручную очищать, во-вторых, выдеение памяти – достаточно дорогостоящая операция.

Для выделения памяти на куче в си используется функция malloc (memory allocation) из библиотеки stdlib.h

void * malloc(size_t size);

Функция выделяет size байтов памяти и возвращает указатель на неё. Если память выделить не удалось, то функция возвращает NULL.
Так как malloc возвращает указатель типа void, то его необходимо явно приводить к нужному нам типу. Например, создадим указатель, после этого выделим память размером в 100 байт.

#include <conio.h>
#include <stdio.h>
#include <stdlib.h>

void main() {
	int *p = NULL;
	p = (int*) malloc(100);

	free(p);
}

После того, как мы поработали с памятью, необходимо освободить память функцией free.
Используя указатель, можно работать с выделенной памятью как с массивом. Пример: пользователь вводит число – размер массива, создаём массив этого размера и заполняем
его квадратами чисел по порядку. После этого выводим и удаляем массив.

#include <conio.h>
#include <stdio.h>
#include <stdlib.h>

void main() {
	const int maxNumber = 100;
	int *p = NULL;
	unsigned i, size;

	do {
		printf("Enter number from 0 to %d: ", maxNumber);
		scanf("%d", &size);
		if (size < maxNumber) {
			break;
		}
	} while (1);

	p = (int*) malloc(size * sizeof(int));

	for (i = 0; i < size; i++) {
		p[i] = i*i;
	}

	for (i = 0; i < size; i++) {
		printf("%d ", p[i]);
	}

	_getch();
	free(p);
}

Разбираем код

p = (int*) malloc(size * sizeof(int));

Здесь (int *) – приведение типов. Пишем такой же тип, как и у указателя.
size * sizeof(int) – сколько байт выделить. sizeof(int) – размер одного элемента массива.
После этого работаем с указателем точно также, как и с массивом. В конце не забываем удалять выделенную память.

Теперь представим на рисунке, что у нас происходило. Пусть мы ввели число 5.

Выделение памяти.

Выделение памяти.

Функция malloc выделила память на куче по определённому адресу, после чего вернула его. Теперь указатель p хранит этот адрес и может
им пользоваться для работы. В принципе, он может пользоваться и любым другим адресом.
Когда функция malloc «выделяет память», то она резервирует место на куче и возвращает адрес этого участка. У нас будет гарантия, что компьютер не отдаст нашу память кому-то ещё.
Когда мы вызываем функцию free, то мы освобождаем память, то есть говорим компьютеру, что эта память может быть использована кем-то другим.
Он может использовать нашу память, а может и нет, но теперь у нас уже нет гарантии, что эта память наша. При этом сама переменная не зануляется, она
продолжает хранить адрес, которым ранее пользовалась.

Это очень похоже на съём номера в отеле. Мы получаем дубликат ключа от номера, живём в нём, а потом сдаём комнату обратно. Но дубликат ключа у нас остаётся.
Всегда можно зайти в этот номер, но в нём уже кто-то может жить. Так что наша обязанность – удалить дубликат.

Иногда думают, что происходит «создание» или «удаление» памяти. На самом деле происходит только перераспределение ресурсов.

Освобождение памяти с помощью free

Теперь рассмотри, как происходит освобождение памяти. Переменная указатель хранит адрес области памяти,
начиная с которого она может им пользоваться.
Однако, она не хранит размера этой области. Откуда тогда функция free знает, сколько памяти необходимо освободить?

Очевидно, что информация о размере выделенного участка должна где-то храниться. Есть несколько решения этой проблемы.

  • 1. Можно создать карту, в которой будет храниться размер выделенного участка. Каждый раз при освобождении памяти компьютер будет обращаться к этим данным и получать нужную информацию.
  • 2. Второе решение более распространено. Информация о размере хранится на куче до самих данных. Таким образом, при выделении памяти резервируется места больше и туда записывается информация о выделенном участке. При освобождении памяти функция free «подсматривает», сколько памяти необходимо удалить.

Функция free освобождает память, но при этом не изменяет значение указателя, о чём нужно помнить.

Работа с двумерными и многомерными массивами

Для динамического создания двумерного массива сначала необходимо создать массив указателей, после чего каждому из элементов этого массива
присвоить адрес нового массива.

Для удаления массива необходимо повторить операцию в обратном порядке — удалить сначала подмассивы, а потом и сам массив указателей.

#include <conio.h>
#include <stdio.h>
#include <stdlib.h>

#define COL_NUM 10
#define ROW_NUM 10

void main() {
	float **p = NULL;
	unsigned i;

	p = (float**) malloc(ROW_NUM * sizeof(float*));
	for (i = 0; i < ROW_NUM; i++) {
		p[i] = (float*) malloc(COL_NUM * sizeof(float));
	}

	//Здесь какой-то важный код
	for (i = 0; i < ROW_NUM; i++) {
		free(p[i]);
	}
	free(p);
}

Сначала мы создаём массив указателей, а после этого каждому элементу этого массива присваиваем адрес вновь созданного массива. Это значит, что можно

  • 1. Создавать массивы «неправильной формы», то есть массив строк, каждая из которых имеет свой размер.
  • 2. Работать по отдельности с каждой строкой массива: освобождать память или изменять размер строки.

Создадим «треугольный» массив и заполним его значениями

#include <conio.h>
#include <stdio.h>
#include <stdlib.h>

#define SIZE 10

void main() {
	int **A;
	int i, j;
	A = (int**) malloc(SIZE * sizeof(int*));

	for (i = 0; i < SIZE; i++) {
		A[i] = (int*) malloc((i + 1) * sizeof(int));
	}

	for (i = 0; i < SIZE; i++) {
		for (j = i; j > 0; j--) {
			A[i][j] = i * j;
		}
	}

	for (i = 0; i < SIZE; i++) {
		for (j = i; j > 0; j--) {
			printf("%d ", A[i][j]);
		}
		printf("n");
	}

	for (i = SIZE-1; i > 0; i--) {
		free(A[i]);
	}
	free(A);
	
	_getch();
}

Чтобы создать трёхмерный массив, по аналогии, необходимо сначала определить указатель на указатель на указатель, после чего выделить память под массив указателей на указатель,
после чего проинициализировать каждый из массивов и т.д.

calloc

Функция calloc выделяет n объектов размером m и заполняет их нулями. Обычно она используется для выделения памяти под массивы. Синтаксис

void* calloc(size_t num, size_t size);

realloc

Ещё одна важная функция – realloc (re-allocation). Она позволяет изменить размер ранее выделенной памяти и получает в качестве аргументов старый
указатель и новый размер памяти в байтах:

void* realloc(void* ptr, size_t size)

Функция realloc может как использовать ранее выделенный участок памяти, так и новый. При этом не важно, меньше или больше новый размер – менеджер памяти сам решает,
где выделять память.
Пример – пользователь вводит слова. Для начала выделяем под слова массив размером 10. Если пользователь ввёл больше слов, то изменяем его размер, чтобы хватило места.
Когда пользователь вводит слово end, прекращаем ввод и выводим на печать все слова.

#include <conio.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define TERM_WORD "end"
#define SIZE_INCREMENT 10

void main() {
	//Массив указателей на слова
	char **words;
	//Строка, которая используется для считывания введённого пользователем слова
	char buffer[128];
	//Счётчик слов
	unsigned wordCounter = 0;
	//Длина введённого слова
	unsigned length;
	//Размер массива слов. Для уменьшения издержек на выделение памяти 
	//каждый раз будем увеличивать массив слов не на одно значение, а на
	//SIZE_INCREMENT слов
	unsigned size = SIZE_INCREMENT;
	int i;

	//Выделяем память под массив из size указателей
	words = (char**) malloc(size*sizeof(char*));
	do {
		printf("%d: ", wordCounter);
		scanf("%127s", buffer);
		//Функция strcmp возвращает 0, если две строки равны
		if (strcmp(TERM_WORD, buffer) == 0) {
			break;
		}
		//Определяем длину слова
		length = strlen(buffer);
		//В том случае, если введено слов больше, чем длина массива, то
		//увеличиваем массив слов
		if (wordCounter >= size) {
			size += SIZE_INCREMENT;
			words = (char**) realloc(words, size*sizeof(char*));
		}
		//Выделяем память непосредственно под слово
		//на 1 байт больше, так как необходимо хранить терминальный символ
		words[wordCounter] = (char*) malloc(length + 1);
		//Копируем слово из буффера по адресу, который 
		//хранится в массиве указателей на слова
		strcpy(words[wordCounter], buffer);
		wordCounter++;
	} while(1);

	for (i = 0; i < wordCounter; i++) {
		printf("%sn", words[i]);
	}
	_getch();

	for (i = 0; i < wordCounter; i++) {
		free(words[i]);
	}
	free(words);
}

Хочу обратить внимание, что мы при выделении памяти пишем sizeof(char*), потому что размер указателя на char не равен одному байту, как размер переменной типа char.

Ошибки при выделении памяти

1. Бывает ситуация, при которой память не может быть выделена. В этом случае функция malloc (и calloc) возвращает NULL. Поэтому, перед выделением памяти необходимо обнулить
указатель, а после выделения проверить, не равен ли он NULL. Так же ведёт себя и realloc.
Когда мы используем функцию free проверять на NULL нет необходимости, так как согласно документации free(NULL) не производит никаких действий. Применительно к последнему примеру:

#include <conio.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define TERM_WORD "end"
#define SIZE_INCREMENT 10

void main() {
	char **words;
	char buffer[128];
	unsigned wordCounter = 0;
	unsigned length;
	unsigned size = SIZE_INCREMENT;
	int i;

	if (!(words = (char**) malloc(size*sizeof(char*)))) {
		printf("Error: can't allocate memory");
		_getch();
		exit(1);
	}

	do {
		printf("%d: ", wordCounter);
		scanf("%127s", buffer);
		
		if (strcmp(TERM_WORD, buffer) == 0) {
			break;
		}

		length = strlen(buffer);

		if (wordCounter >= size) {
			size += SIZE_INCREMENT;
			if (!(words = (char**) realloc(words, size*sizeof(char*)))) {
				printf("Error: can't reallocate memory");
				_getch();
				exit(2);
			}
		}

		if (!(words[wordCounter] = (char*)malloc(length + 1))) {
			printf("Error: can't allocate memory");
			_getch();
			exit(3);
		}

		strcpy(words[wordCounter], buffer);
		wordCounter++;
	} while(1);

	for (i = 0; i < wordCounter; i++) {
		printf("%sn", words[i]);
	}
	_getch();

	for (i = 0; i < wordCounter; i++) {
		free(words[i]);
	}
	free(words);
}

Хотелось бы добавить, что ошибки выделения памяти могут случиться, и просто выходить из приложения и выкидывать ошибку плохо. Решение
зависит от ситуации. Например, если не хватает памяти, то можно подождать некоторое время и после этого опять попытаться выделить память, или использовать
для временного хранения файл и переместить туда часть объектов. Или выполнить очистку, сократив используемую память и удалив ненужные объекты.

2. Изменение указателя, который хранит адрес выделенной области памяти. Как уже упоминалось выше, в выделенной области хранятся данные об объекте — его размер. При
удалении free получает эту информацию. Однако, если мы изменили указатель, то удаление приведёт к ошибке, например

#include <conio.h>
#include <stdio.h>
#include <stdlib.h>

void main() {
	int *p = NULL;
	if (!(p = (int*) malloc(100 * sizeof(int)))) {
		printf("Error");
		exit(1);
	}
	//Изменили указатель
	p++;
	//Теперь free не может найти метаданные об объекте
	free(p);
	//На некоторых компиляторах ошибки не будет
	_getch();
}

Таким образом, если указатель хранит адрес, то его не нужно изменять. Для работы лучше создать дополнительную переменную указатель,
с которой работать дальше.

3. Использование освобождённой области. Почему это работает в си, описано выше. Эта ошибка выливается в другую – так
называемые висячие указатели (dangling pointers или wild pointers). Вы удаляете объект, но при этом забываете изменить значение указателя на NULL. В итоге, он хранит адрес
области памяти, которой уже нельзя воспользоваться, при этом проверить, валидная эта область или нет, у нас нет возможности.

#include <conio.h>
#include <stdio.h>
#include <stdlib.h>

#define SIZE 10

void main() {
	int *p = NULL;
	int i;

	p = (int*) malloc(SIZE * sizeof(int));
	for (i = 0; i < SIZE; i++) {
		p[i] = i;
	}
	free(p);
	for (i = 0; i < SIZE; i++) {
		printf("%i ", p[i]);
	}
	_getch();
}

Эта программа отработает и выведет мусор, или не мусор, или не выведет. Поведение не определено.

Если же мы напишем

free(p);
p = NULL;

то программа выкинет исключение. Это определённо лучше, чем неопределённое поведение.
Если вы освобождаете память и используете указатель в дальнейшем, то обязательно обнулите его.

4. Освобождение освобождённой памяти. Пример

#include <conio.h>
#include <stdio.h>

void main() {
	int *a, *b;
	a = (int*) malloc(sizeof(int));
	free(a);
	b = (int*) malloc(sizeof(int));
	free(a);
	free(b);
	_getch();
}

Здесь дважды вызывается free для переменной a. При этом, переменная a продолжает хранить адрес, который может далее быть передан
кому-нибудь для использования. Решение здесь такое же как и раньше — обнулить указатель явно после удаления:

#include <conio.h>
#include <stdio.h>

void main() {
	int *a, *b;
	a = (int*) malloc(sizeof(int));
	free(a);
	a = NULL;
	b = (int*) malloc(sizeof(int));
	free(a);//вызов free(NULL) ничего не делает
	free(b);
	b = NULL;
	_getch();
}

5. Одновременная работа с двумя указателями на одну область памяти. Пусть, например, у нас два указателя
p1 и p2. Если под первый указатель была выделена память, то второй указатель может запросто скомпрометировать эту область:

#include <conio.h>
#include <stdio.h>
#include <stdlib.h>

#define SIZE 10

void main() {
	int *p1 = NULL;
	int *p2 = NULL;
	size_t i;

	p1 = malloc(sizeof(int) * SIZE);
	p2 = p1;

	for (i = 0; i < SIZE; i++) {
		p1[i] = i;
	}
	p2 = realloc(p1, SIZE * 5000 * sizeof(int));
	for (i = 0; i < SIZE; i++) {
		printf("%d ", p1[i]);
	}
	printf("n");
	for (i = 0; i < SIZE; i++) {
		printf("%d ", p2[i]);
	}
	_getch();
}

Рассмотрим код ещё раз.

	int *p1 = NULL;
	int *p2 = NULL;
	size_t i;

	p1 = malloc(sizeof(int) * SIZE);
	p2 = p1;

Теперь оба указателя хранят один адрес.

	p2 = realloc(p1, SIZE * 5000 * sizeof(int));

А вот здесь происходит непредвиденное. Мы решили выделить под p2 новый участок памяти. realloc гарантирует сохранение контента,
но вот сам указатель p1 может перестать быть валидным. Есть разные ситуации. Во-первых, вызов malloc мог выделить
много памяти, часть которой не используется. После вызова ничего не поменяется и p1 продолжит оставаться валидным. Если же
потребовалось перемещение объекта, то p1 может указывать на невалидный адрес (именно это с большой вероятностью и произойдёт в нашем случае).
Тогда p1 выведет мусор (или же произойдёт ошибка, если p1 полезет в недоступную память), в то время как p2 выведет старое содержимое
p1. В этом случае поведение не определено.

Два указателя на одну область памяти это вообще-то не ошибка. Бывают ситуации, когда без них не обойтись. Но это
очередное минное поле для программиста.

Различные аргументы realloc и malloc.

При вызове функции malloc, realloc и calloc с нулевым размером поведение не определено. Это значит, что может быть возвращён как NULL, так и реальный адрес. Им можно пользоваться,
но к нему нельзя применять операцию разадресации.
Вызов realloc(NULL, size_t) эквиваленте вызову malloc(size_t).
Однако, вызов realloc(NULL, 0) не эквивалентен вызову malloc(0) :) Понимайте это, как хотите.

Примеры

1. Простое скользящее среднее равно среднему арифметическому функции за период n. Пусть у нас имеется ряд измерений значения функции. Часто
эти измерения из-за погрешности «плавают» или на них присутствуют высокочастотные колебания. Мы хотим сгладить ряд, для того, чтобы избавиться
от этих помех, или для того, чтобы выявить общий тренд. Самый простой способ: взять n элементов ряда и получить их среднее арифметическое. n в данном
случае — это период простого скользящего среднего. Так как мы берём n элементов для нахождения среднего, то в результирующем массиве будет на n
чисел меньше.

Скользящее среднее.

Скользящее среднее.

Пусть есть ряд
1, 4, 4, 6, 7, 8, 9, 11, 12, 11, 15
Тогда если период среднего будет 3, то мы получим ряд
(1+4+4)/3, (4+4+6)/3, (4+6+7)/3, (6+7+8)/3, (7+8+9)/3, (8+9+11)/3, (9+11+12)/3, (11+12+11)/3, (12+11+15)/3
Видно, что сумма находится в «окне», которое скользит по ряду. Вместо того, чтобы каждый раз в цикле находить сумму, можно найти её для
первого периода, а затем вычитать из суммы крайнее левое значение предыдущего периода и прибавлять крайнее правое значение следующего.
Будем запрашивать у пользователя числа и период, а затем создадим новый массив и заполним его средними значениями.

#include <conio.h>
#include <stdio.h>
#include <stdlib.h>

#define MAX_INCREMENT 20

void main() {
	//Считанные числа
	float *numbers = NULL;
	//Найденные значения
	float *mean = NULL;
	float readNext;
	//Максимальный размер массива чисел
	unsigned maxSize = MAX_INCREMENT;
	//Количество введённых чисел
	unsigned curSize = 0;
	//Строка для считывания действия
	char next[2];
	//Шаг
	unsigned delta;
	//float переменная для хранения шага
	float realDelta;
	unsigned i, j;
	//Сумма чисел
	float sum;

	numbers = (float*) malloc(maxSize * sizeof(float));
	do {
		//Пока пользователь вводит строку, которая начинается с y или Y,
		//то продолжаем считывать числа
		printf("next? [y/n]: ");
		scanf("%1s", next);
		if (next[0] == 'y' || next[0] == 'Y') {
			printf("%d. ", curSize);
			scanf("%f", &readNext);
			if (curSize >= maxSize) {
				maxSize += MAX_INCREMENT;
				numbers = (float*) realloc(numbers, maxSize * sizeof(float));
			}
			numbers[curSize] = readNext;
			curSize++;
		} else {
			break;
		}
	} while(1);

	//Считываем период, он должен быть меньше, чем
	//количество элементов в массиве. Если оно равно,
	//то результатом станет среднее арифметическое всех введённых чисел
	do {
		printf("enter delta (>=%d): ", curSize);
		scanf("%d", &delta);
		if (delta <= curSize) {
			break;
		}
	} while(1);
	realDelta = (float) delta;

	//Находим среднее для первого периода
	mean = (float*) malloc(curSize * sizeof(float));
	sum = 0;
	for (i = 0; i < delta; i++) {
		sum += numbers[i];
	}

	//Среднее для всех остальных
	mean[0] = sum / delta;
	for (i = delta, j = 1; i < curSize; i++, j++) {
		sum = sum - numbers[j-1] + numbers[i];		
		mean[j] = sum / realDelta;
	}

	//Выводим. Чисел в массиве mean меньше на delta
	curSize = curSize - delta + 1;
	for (i = 0; i < curSize; i++) {
		printf("%.3f ", mean[i]);
	}

	free(numbers);
	free(mean);
	_getch();
}

Это простой пример. Большая его часть связана со считыванием данных, вычисление среднего всего в девяти строчках.

2. Сортировка двумерного массива. Самый простой способ сортировки — перевести двумерный массив MxN в одномерный размером M*N,
после чего отсортировать одномерный массив, а затем заполнить двумерный массив отсортированными данными. Чтобы не тратить место
под новый массив, мы поступим по-другому: если проходить по всем элементам массива k от 0 до M*N, то индексы текущего элемента
можно найти следующим образом:

j = k / N;
i = k - j*M;

Заполним массив случайными числами и отсортируем

#include <conio.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define MAX_SIZE_X 20
#define MAX_SIZE_Y 20

void main() {
	int **mrx = NULL;
	int tmp;
	unsigned i, j, ip, jp, k, sizeX, sizeY, flag;

	printf("cols: ");
	scanf("%d", &sizeY);
	printf("rows: ");
	scanf("%d", &sizeX);

	//Если введённый размер больше MAX_SIZE_?, то присваиваем
	//значение MAX_SIZE_?
	sizeX = sizeX <= MAX_SIZE_X? sizeX: MAX_SIZE_X;
	sizeY = sizeY <= MAX_SIZE_Y? sizeY: MAX_SIZE_Y;

	//Задаём начальное значение для генератора псевдослучайных чисел
	srand(time(NULL));
	//Выделяем память под массив указателей
	mrx = (int**) malloc(sizeX * sizeof(int*));
	for (i = 0; i < sizeX; i++) {
		//Выделяем память под строку и сразу же заполняем элементы
		//случайными значениями
		mrx[i] = (int*) malloc(sizeY * sizeof(int));
		for (j = 0; j < sizeY; j++) {
			mrx[i][j] = rand();
		}
	}

	//Выводим массив
	for (i = 0; i < sizeX; i++) {
		for (j = 0; j < sizeY; j++) {
			printf("%6d ", mrx[i][j]);
		}
		printf("n");
	}

	//Сортируем пузырьком, обходя все sizeX*sizeY элементы
	do {
		flag = 0;
		for (k = 1; k < sizeX * sizeY; k++) {
			//Вычисляем индексы текущего элемента
			j = k / sizeX;
			i = k - j*sizeX;
			//Вычисляем индексы предыдущего элемента
			jp = (k-1) / sizeX;
			ip = (k-1) - jp*sizeX;
			if (mrx[i][j] > mrx[ip][jp]) {
                tmp = mrx[i][j];
                mrx[i][j] = mrx[ip][jp];
                mrx[ip][jp] = tmp;
				flag = 1;
            }
		}
	} while(flag);

	printf("-----------------------n");
	for (i = 0; i < sizeX; i++) {
		for (j = 0; j < sizeY; j++) {
			printf("%6d ", mrx[i][j]);
		}
		free(mrx[i]);
		printf("n");
	}
	free(mrx);

	_getch();
}

3. Бином Ньютона. Создадим треугольную матрицу и заполним биномиальными коэффициентами

#include <conio.h>
#include <stdio.h>
#include <stdlib.h>

#define MAX_BINOM_HEIGHT 20

void main() {
	int** binom = NULL;
	unsigned height;
	unsigned i, j;

	printf("Enter height: ");
	scanf("%d", &height);
	height = height <= MAX_BINOM_HEIGHT? height: MAX_BINOM_HEIGHT;

	binom = (int**) malloc(height * sizeof(int*));
	for (i = 0; i < height; i++) {
		binom[i] = (int*) malloc((i + 1) * sizeof(int));
	}

	binom[0][0] = 1;
	for (i = 1; i < height; i++) {
		binom[i][0] = binom[i][i] = 1;
		for (j = i - 1; j > 0; j--) {
			binom[i][j] = binom[i-1][j-1] + binom[i-1][j];
		}
	}

	for (i = 0; i < height; i++) {
		for (j = 0; j <= i; j++) {
			printf("%4d ", binom[i][j]);
		}
		free(binom[i]);
		printf("n");
	}
	free(binom);

	_getch();
}

Если Вы желаете изучать этот материал с преподавателем, советую обратиться к
репетитору по информатике

Q&A

Всё ещё не понятно? – пиши вопросы на ящик email

Структура программы на си

malloc

Предлагаем вашему вниманию цикл статей, посвященных рекомендациям по написанию качественного кода на примере ошибок, найденных в проекте Chromium. Это шестая часть, которая будет посвящена функции malloc. Вернее, тому, почему следует обязательно проверять указатель, возвращаемый этой функцией. Скорее всего, вы не догадываетесь, какой подвох связан с malloc, потому рекомендуем познакомиться с этой статьей.

Примечание. В статье под функцией malloc часто будет подразумеваться, что речь идёт не только именно об этой функции, но и о calloc, realloc, _aligned_malloc, _recalloc, strdup и так далее. Не хочется загромождать текст статьи, постоянно повторяя названия всех этих функций. Общее у них то, что они могут вернуть нулевой указатель.

malloc

Если функция malloc не смогла выделить буфер памяти, то она возвращает NULL. Любая нормальная программа должна проверять указатели, которые возвращает функция malloc, и соответствующим образом обрабатывать ситуацию, когда память выделить не получилось.

К сожалению, многие программисты небрежно относятся к проверке указателей, а иногда сознательно не проверяют, удалось ли выделить память или нет. Их логика следующая:

Если функция malloc не смогла выделить память, то вряд ли моя программа продолжит функционировать должным образом. Скорее всего, памяти будет не хватать и для других операций, поэтому можно вообще не заморачиваться об ошибках выделения памяти. Первое же обращение к памяти по нулевому указателю приведёт к генерации Structured Exception в Windows, или процесс получит сигнал SIGSEGV, если речь идёт о Unix-подобных системах. В результате программа упадёт, что меня устраивает. Раз нет памяти, то и нечего мучаться. Как вариант, можно перехватить структурное исключение/сигнал и обрабатывать разыменовывания нулевого указателя более централизовано. Это удобнее, чем писать тысячи проверок.

Я не придумываю, я не раз общался с людьми, которые считают такой подход уместным и сознательно никогда не проверяющих результат, который возвращает функция malloc.

Кстати, существует ещё одно оправдание разработчиков, почему они не проверяют, что вернула функция malloc. Функция malloc только резервирует память, но вовсе нет гарантии, что хватит физической памяти, когда мы начнём использовать выделенный буфер памяти. Поэтому, раз всё равно гарантии нет, то и проверять не надо. Например, именно так Carsten Haitzler, являющийся одним из разработчиков библиотеки EFL Core, объяснял, почему я насчитал более 500 мест в коде библиотеки, где отсутствуют проверки. Вот его комментарий к статье:

OK so this is a general acceptance that at least on Linux which was always our primary focus and for a long time was our only target, returns from malloc/calloc/realloc can’t be trusted especially for small amounts. Linux overcommits memory by default. That means you get new memory but the kernel has not actually assigned real physical memory pages to it yet. Only virtual space. Not until you touch it. If the kernel cannot service this request your program crashes anyway trying to access memory in what looks like a valid pointer. So all in all the value of checking returns of allocs that are small at least on Linux is low. Sometimes we do it… sometimes not. But the returns cannot be trusted in general UNLESS its for very large amounts of memory and your alloc is never going to be serviced — e.g. your alloc cannot fit in virtual address space at all (happens sometimes on 32bit). Yes overcommit can be tuned but it comes at a cost that most people never want to pay or no one even knows they can tune. Secondly, fi an alloc fails for a small chunk of memory — e.g. a linked list node… realistically if NULL is returned… crashing is about as good as anything you can do. Your memory is so low that you can crash, call abort() like glib does with g_malloc because if you can’t allocate 20-40 bytes… your system is going to fall over anyway as you have no working memory left anyway. I’m not talking about tiny embedded systems here, but large machines with virtual memory and a few megabytes of memory etc. which has been our target. I can see why PVS-Studio doesn’t like this. Strictly it is actually correct, but in reality code spent on handling this stuff is kind of a waste of code given the reality of the situation. I’ll get more into that later.

Приведённые рассуждения программистов являются неправильными, и ниже я подробно объясню почему. Но прежде надо ответить на вопрос: «а причём здесь Chromium?».

Chromium

Chromium здесь при том, что в используемых в нём библиотеках имеется не менее 70 ошибок, связанных с отсутствием проверки после вызова таких функций, как malloc, calloc, realloc. Да, в самом Chromium эти функции почти нигде не используются. В Chromium применяются только контейнеры или operator new. Однако, раз ошибки есть в используемых библиотеках, то значит, можно сказать, что они есть и в Chromium. Конечно, какие-то части библиотек могут не использоваться при работе Chromium, но определять это сложно и ненужно. Всё равно надо править все ошибки.

Я не буду приводить в статье множество фрагментов кода с ошибками, так как они однотипны. Приведу для примера только одну ошибку, обнаруженную в библиотеке Yasm:

static SubStr *
SubStr_new_u(unsigned char *s, unsigned int l)
{
    SubStr *r = malloc(sizeof(SubStr));
    r->str = (char*)s;
    r->len = l;
    return r;
}

Предупреждение PVS-Studio: V522 CWE-690 There might be dereferencing of a potential null pointer ‘r’. Check lines: 52, 51. substr.h 52

В коде нет никакой защиты от нулевого указателя. Другие подобные ошибки из Chromium и используемых библиотек я собрал вместе в файл и выложил их здесь: chromium_malloc.txt. В файле упоминаются 72 ошибки, но на самом деле их может быть больше. Как я писал в вводной статье, я просматривал отчёт только поверхностно.

Согласно Common Weakness Enumeration обнаруженные ошибки PVS-Studio классифицирует как:

  1. CWE-690: Unchecked Return Value to NULL Pointer Dereference.
  2. CWE-628: Function Call with Incorrectly Specified Arguments.
  3. CWE-119: Improper Restriction of Operations within the Bounds of a Memory Buffer

Как видите, даже в таком высококачественном проекте как Chromium, можно заметить массу дефектов, связанных с отсутствием проверок. Теперь я перехожу к самому интересному и расскажу, почему проверки обязательно нужны.

Почему обязательно нужна проверка

Есть сразу 4 причины, каждой из которых достаточно, чтобы обязательно делать проверки после вызова функции malloc. Если кто-то в команде не пишет проверки, то обязательно заставьте его прочитать эту статью.

Прежде чем начать, небольшая теоретическая справка, почему возникают структурные исключения или сигналы, если происходит разыменовывание нулевого указателя. Это будет важно для дальнейшего повествования.

В начале адресного пространства одна или несколько страниц памяти защищены операционной системой от записи. Это позволяет выявить ошибки обращения к памяти по нулевому указателю, или указателю, значение которого близко к 0.

В разных операционных системах для этих целей резервируется разное количество памяти. При этом в некоторых ОС это значение можно настраивать. Поэтому нет смысла называть какое-то конкретное число зарезервированных байт памяти. Но чтобы как-то сориентировать читателя, скажу, что в Linux системах типовым значением является 64 Кбайт.

Важно, что, прибавив к нулевому указателю какое-то достаточно большое число, можно «промазать» мимо контрольных страниц памяти и случайно попасть в какие-то незащищенные от записи страницы. Таким образом можно испортить где-то какие-то данные, но операционная система этого не заметит и никакого сигнала / исключения она не сгенерирует.

Заваривайте кофе, мы начинаем!

Разыменовывание нулевого указателя — это неопределённое поведение

С точки зрения языка C и C++ разыменовывание нулевого указателя приводит к неопределенному поведению. Неопределённое поведение — это что угодно. Не думайте, что вы знаете, как будет вести себя программа, если произойдёт разыменовывание nullptr. Современные компиляторы занимаются серьезными оптимизациями, в результате чего бывает невозможно предсказать, как проявит себя та или иная ошибка в коде.

Неопределённое поведение программы — это очень плохо. Вы не должны допускать его в своём коде.

Не думайте, что сможете совладать с разыменовыванием нулевого указателя, используя обработчики структурных исключений (SEH в Windows) или сигналы (в UNIX-like системах). Раз разыменовывание нулевого указателя было, то работа программы уже нарушена, и может произойти что угодно. Давайте рассмотрим абстрактный пример, почему нельзя полагаться на SEH-обработчики и т.п.

size_t *ptr = (size_t *)malloc(sizeof(size_t) * N * 2);
for (size_t i = 0; i != N; ++i)
{
  ptr[i] = i;
  ptr[N * 2 - i - 1] = i;
}

Этот код заполняет массив от краёв к центру. К центру значения элементов увеличиваются. Это придуманный за 1 минуту пример, поэтому не гадайте, зачем такой массив кому-то нужен. Я и сам не знаю. Мне было важно, чтобы в соседних строках программы происходила запись в начало массива и куда-то в его конец. Такое иногда бывает нужно и в практических задачах, и мы рассмотрим реальный код, когда доберёмся до 4-ой причины.

Ещё раз внимательно посмотрим на эти две строки:

ptr[i] = i;
ptr[N * 2 - i - 1] = i;

С точки зрения программиста, в начале цикла произойдёт запись в элемент ptr[0], и возникнет структурное исключение/сигнал. Оно будет обработано, и всё будет хорошо.

Однако компилятор в каких-то целях оптимизации может переставить присваивания местами. Он имеет на это полное право. С точки зрения компилятора, если указатель разыменовывается, то он не может быть равен nullptr. Если указатель нулевой, то это неопределённое поведение, и компилятор не обязан думать о последствиях оптимизации.

Так вот, компилятор может решить, что в целях оптимизации выгоднее выполнить присваивания так:

ptr[N * 2 - i - 1] = i;
ptr[i] = i;

В результате в начале произойдет запись по адресу ((size_t *)nullptr)[N * 2 — 0 — 1]. Если значение N достаточно велико, то страница защиты в начале памяти будет «перепрыгнута» и значение переменной i может быть записано в какую-то ячейку, доступную для записи. В общем, произойдёт порча каких-то данных.

И только после этого будет выполнено присваивание по адресу ((size_t *)nullptr)[0]. Операционная система заметит попытку записи в контролируемую ею область и сгенерирует сигнал/исключение.

Программа может обработать это структурное исключение/сигнал. Но уже поздно. Где-то в памяти есть испорченные данные. Причем непонятно, какие данные испорчены и к каким последствиям это может привести!

Виноват ли компилятор, что поменял операции присваивания местами? Нет. Программист допустил разыменовывание нулевого указателя и тем самым ввёл программу в состояние неопределённого поведения. В данном конкретном случае неопределённое поведение программы будет заключаться в том, что где-то в памяти испорчены данные.

Вывод

Исходите из аксиомы: любое разыменовывание нулевого указателя — это неопределённое поведение программы. Не бывает «безобидного» неопределённого поведения. Любое неопределённое поведение недопустимо.

Не допускайте разыменовывания указателей, которые вернула функция malloc и её аналоги, без их предварительной проверки. Не полагайтесь на какие-то другие способы перехвата разыменовывания нулевого указателя. Следует использовать только старый добрый оператор if.

Разыменовывание нулевого указателя — это уязвимость

То, что некоторые разработчики вообще не считают за ошибку, другие воспринимают как уязвимость. Именно так обстоит дело с разыменовыванием нулевого указателя.

Одним нормально, если программа из-за разыменовывания нулевого указателя упадёт или если ошибка будет обработана каким-то общим способом с помощью перехвата сигнала/структурного исключения.

Другие считают, что разыменовывание нулевого указателя приводят к ошибке «отказ в обслуживании» и является уязвимостью. Вместо того, чтобы штатно обработать нехватку памяти, программа, или одна из нитей программы, завершает свою работу. Это может приводить к потере данных, нарушению целостности данных и так далее. Другими словами, CAD система тупо закроется, если не сможет выделить память для какой-то сложной операции, не предложив пользователю даже сохранить результат своей работы.

Не буду голословным. Есть, такая программа как Ytnef, предназначенная для декодирования TNEF потоков, например, созданных в Outlook. Так вот, разработчики приложения считают отсутствие проверки после вызова calloc уязвимостью CVE-2017-6298.

Все исправленные места, в которых могло произойти разыменовывание нулевого указателя, имели приблизительно один и тот же вид:

vl->data = calloc(vl->size, sizeof(WORD));
temp_word = SwapWord((BYTE*)d, sizeof(WORD));
memcpy(vl->data, &temp_word, vl->size);

Выводы

Если вы разрабатываете безответственное приложение, для которого упасть в процессе работы не является бедой, то да, писать проверки необязательно.

Однако, если вы разрабатываете какую-то библиотеку, то отсутствие проверок недопустимо! Вашей библиотекой могут пользоваться не только ленивые программисты, создающие безответственные приложения, типа игры Тетрис. Надо заботиться и о нормальных программистах, и нормальных программах.

Поэтому я идеологически не согласен, например, с Carsten Haitzler, что в библиотеке EFL Core нет проверок (подробности в статье). Это не позволяет построить на основе таких библиотек надёжные приложения.

В общем, если вы создаёте библиотеку, то помните, что в некоторых приложениях разыменовывание нулевого указателя — это уязвимость. Необходимо обрабатывать ошибки выделения памяти и штатно возвращать информацию об неудаче.

Где гарантии, что будет разыменовывание именно нулевого указателя?

Те, кто ленится писать проверки, почему-то думают, что разыменование затрагивает именно нулевые указатели. Да, часто именно так и бывает. Но может ли поручиться программист за код всего приложения? Уверен, что нет.

Сейчас я на практических примерах покажу, что я имею в виду. Возьмём, например, код из библиотеки LLVM-subzero, которая используется в Chromium. Если честно, я теряюсь в догадках, какая связь между проектом Chromium и LLVM, но она есть.

void StringMapImpl::init(unsigned InitSize) {
  assert((InitSize & (InitSize-1)) == 0 &&
         "Init Size must be a power of 2 or zero!");
  NumBuckets = InitSize ? InitSize : 16;
  NumItems = 0;
  NumTombstones = 0;
  
  TheTable = (StringMapEntryBase **)
             calloc(NumBuckets+1,
                    sizeof(StringMapEntryBase **) + 
                    sizeof(unsigned));

  // Allocate one extra bucket, set it to look filled
  // so the iterators stop at end.
  TheTable[NumBuckets] = (StringMapEntryBase*)2;
}

Предупреждение PVS-Studio: V522 CWE-690 There might be dereferencing of a potential null pointer ‘TheTable’. Check lines: 65, 59. stringmap.cpp 65

Сразу после выделения буфера памяти происходит запись в ячейку TheTable[NumBuckets]. Если значение переменной NumBuckets достаточно большое, то мы испортим какие-то данные с непредсказуемыми последствиями. После такой порчи вообще нет смысла рассуждать, как будет работать программа. Могут последовать самые неожиданнейшие последствия.

Аналогичные опасные присваивания я вижу ещё в двух местах этого проекта:

  • V522 CWE-690 There might be dereferencing of a potential null pointer ‘Buckets’. Check lines: 219, 217. foldingset.cpp 219
  • V769 CWE-119 The ‘NewTableArray’ pointer in the ‘NewTableArray + NewSize’ expression could be nullptr. In such case, resulting value will be senseless and it should not be used. Check lines: 218, 216. stringmap.cpp 218

Так что это не уникальный случай, а вполне типовая ситуация, когда данные записываются не точно по нулевому указателю, а по какому-то произвольному смещению.

Продолжу заочную дискуссию с Carsten Haitzler. Он утверждает, что они понимают, что делают, когда не проверяют результат вызова функции malloc. Нет, не понимают. Давайте взглянем, например, на вот такой фрагмент кода из библиотеки EFL:

static void
st_collections_group_parts_part_description_filter_data(void)
{
  ....
   filter->data_count++;
   array = realloc(filter->data,
     sizeof(Edje_Part_Description_Spec_Filter_Data) *
     filter->data_count);
   array[filter->data_count - 1].name = name;
   array[filter->data_count - 1].value = value;
   filter->data = array;
}

Предупреждение PVS-Studio: V522 There might be dereferencing of a potential null pointer ‘array’. edje_cc_handlers.c 14249

Примечание. Я использую старые исходники EFL Core Libraries, которые остались у меня со времён написания статьи про эту библиотеку. Поэтому код или номера строк могут уже не соответствовать тому, что есть сейчас. Однако это не важно для повествования.

Перед нами типовая ситуация: в буфере не хватает свободного места для хранения данных, и его следует увеличить. Для увеличения размера буфера используется функция realloc, которая может вернуть NULL.

Если это произойдёт, то вовсе не обязательно возникнет структурное исключение/сигнал из-за разыменовывания нулевого указателя. Взглянем вот на эти строчки:

array[filter->data_count - 1].name = name;
array[filter->data_count - 1].value = value;

Если значение переменной filter->data_count достаточно большое, то значения будут записаны по какому-то непонятному адресу.

В памяти будут испорчены какие-то данные, а программа продолжит своё выполнение. Последствия вновь непредсказуемые, но ничего хорошего точно не получится.

Я внимательно не стал изучать старый отчёт, касающийся EFL Core Libraries, но это точно не единственная подобная ошибка. Я заметил как минимум ещё два похожих места, где после realloc данные дописываются по какому-то индексу.

Вывод

Я вновь задаю вопрос: «где гарантии, что будет разыменовывание именно нулевого указателя?». Нет такой гарантий. Невозможно, разрабатывая или модифицируя код, помнить про только что рассмотренный нюанс. Запросто можно что-то испортить в памяти, при этом программа продолжит выполняться как ни в чём не бывало.

Единственный способ написать надёжный и правильный код — это всегда проверять результат, который вернула функция malloc. Проверь и живи спокойно.

Где гарантии, что memset заполняет память в прямом порядке?

Найдётся кто-то, кто скажет что-то подобное:

Я отлично понимаю про realloc и всё остальное, что написано в статье. Но я профессионал и, выделяя память, сразу заполняю её нулями с помощью memset. Там, где действительно необходимо, я использую проверки. Но лишние проверки после каждого malloc я писать не буду.

Вообще заполнять память сразу после выделения буфера достаточно странная идея. Странная потому, что есть функция calloc. Тем не менее, так поступают очень часто. Далеко за примером ходить не надо, вот код из библиотеки WebRTC, используемой в Chromium:

int Resampler::Reset(int inFreq, int outFreq, size_t num_channels) {
  ....
  state1_ = malloc(8 * sizeof(int32_t));
  memset(state1_, 0, 8 * sizeof(int32_t));
  ....
}

Выделяется память, затем буфер заполняется нулями. Очень частая практика, хотя, на самом деле, две строчки можно сократить до одной, используя calloc. Но всё это не важно.

Главное, что даже подобный код не безопасен! Функция memset не обязана начать заполнять память с начала и тем самым вызывать разыменовывание нулевого указателя.

Функция memset имеет право начать заполнять буфер с конца. И, если выделялся большой буфер, то могут быть затёрты какие-то полезные данные. Да, заполняя память, функция memset рано или поздно достигнет страницы, защищённой от записи, и операционная система сгенерирует структурное исключение/сигнал. Однако обрабатывать их уже не имеет смысла. К этому моменту будет испорчен большой фрагмент памяти, и дальнейшая работа программы будет непредсказуема.

Читатель может возразить, что всё это носит исключительно теоретический характер. Да, функция memset теоретически может заполнять буфер начиная с конца буфера, но на практике никто не будет так реализовывать эту функцию.

Соглашусь, что подобная реализация memset действительно экзотика, и я даже задавал вопрос на StackOverflow на эту тему. В ответе говорится:

The Linux kernel’s memset for the SuperH architecture has this property: link.

К сожалению, это код на незнакомой мне разновидности ассемблера, поэтому я не берусь рассуждать о нём. Зато ещё есть вот такая интересная реализация на языке Си. Приведу начало это функции:

void *memset(void *dest, int c, size_t n)
{
  unsigned char *s = dest;
  size_t k;
  if (!n) return dest;
  s[0] = c;
  s[n-1] = c;
  ....
}

Обратите внимание на:

s[0] = c;
s[n-1] = c;

Здесь мы возвращаемся к причине N1 «Разыменовывание нулевого указателя — это неопределённое поведение». Нет гарантии, что компилятор в целях оптимизации не поменяет присваивания местами. Если компилятор это сделает, и аргумент n будет иметь большое значение, то вначале будет испорчен какой-то байт памяти. И только потом произойдёт разыменовывание нулевого указателя.

Опять не убедительно? Хорошо, а как вам вот такая реализация:

void *memset(void *dest, int c, size_t n)
{
  size_t k;
  if (!n) return dest;
  s[0] = s[n-1] = c;
  if (n <= 2) return dest;
  ....
}

Вывод

Нельзя доверять даже функции memset. Да, это во многом искусственная и надуманная проблема. Я просто хотел показать, как много существует нюансов, если не проверять значение указателя. Просто невозможно всё это учесть. Поэтому не надо выпендриваться, а следует аккуратно проверять каждый указатель, который вернула функция malloc и аналогичные ей. И вот именно тогда вы станете профессионалом.

Заключение

Всегда сразу проверяйте указатель, который вернула функция malloc или аналогичная ей.

Как видите, анализатор PVS-Studio совсем не зря предупреждает о том, что нет проверки указателя после вызова malloc. Невозможно написать надёжный код, не делая проверки. Особенно это важно и актуально для разработчиков библиотек.

Надеюсь, теперь вы по-новому взглянете на функцию malloc, проверки указателей в коде и предупреждения анализатора PVS-Studio. Не забудьте показать эту статью своим коллегам и начать использовать PVS-Studio. Желаю всем поменьше багов.

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. Why it is important to check what the malloc function returned.

Please consider subscribing to LWN

Subscriptions are the lifeblood of LWN.net. If you appreciate this
content and would like to see more of it, your subscription will
help to ensure that LWN continues to thrive. Please visit
this page to join up and keep LWN on
the net.

There is a saying that you need to spend money to make money, though
this apparent paradox is easily resolved with a start-up loan and the
discipline of balancing expenses against income. A similar logic
applies to the management of memory in an operating system kernel such
as Linux: sometimes you need to allocate memory to free memory. Here, too,
discipline is needed, though the typical consequences of not
being sufficiently careful is not bankruptcy but rather a deadlock.

The history of how the Linux kernel developed its
balance between saving and spending is interesting as a
microcosm of how Linux development proceeds, and useful in
understanding how to handle future deadlocks when they occur. A good
place to start this history is in early 1998 with the introduction of
__GFP_IO in Linux 2.1.80pre3.

__GFP_IO in 2.1.80pre3

Any memory allocation request in Linux includes a gfp_t argument,
which is a set of flags to guide how the get_free_page() function
can go about locating a free page. 2.1.80pre3
marks a change in
this argument’s type; it went from being a simple enumerated type to being
a bitmask.
The concepts embodied in each flag were present previously, but this
is the first time that they could be explicitly identified.

__GFP_IO was one of the new flags. If it was set, then
get_free_pages() was allowed to call shm_swap() to write some pages
out to swap. If shm_swap() needed to allocate any
buffer_head structures to
complete the writeout, it would be careful not to set __GFP_IO.
Without this protection, an infinite recursion could easily happen,
which would quickly exhaust the stack and cause the kernel to crash.

We have __GFP_IO in the kernel today, but, despite having the same
name, it is a different flag. Having been introduced for 2.1.80, the
original __GFP_IO was removed in 2.1.116, to be replaced with…

PF_MEMALLOC in 2.1.116

In the distant
past (August 1998), we did not have change logs of
nearly the quality that we have today, so an operating-system archaeologist
is left to
guess at the reasons for changes. All we can be really sure of is that
the (per-request) __GFP_IO flag to get_free_page()
disappeared, and a new
per-process flag called PF_MEMALLOC appeared to take over the task
of avoiding recursion.

One clear benefit of this change is that it is more focused in
addressing one particular issue: recursion is clearly a per-process
issue and so a per-process flag is fitting. Previously, many memory
allocation sites would avoid __GFP_IO when they didn’t really need
to, just in case. Now each call site doesn’t need to worry about the
problem of recursion; that concern is addressed precisely where it is needed.

The code comments here highlight an important aspect of memory
allocation:

	 * The "PF_MEMALLOC" flag protects us against recursion:
	 * if we need more memory as part of a swap-out effort we
	 * will just silently return "success" to tell the page
	 * allocator to accept the allocation.

When possible, get_free_page() will just pluck a page
off the free list and return it as quickly as it can. When that is not
possible, it does not satisfy itself with freeing just one page, but
will try to free quite a few, to save work next time. Thus, it
is re-stocking that startup loan.

A particular consequence of PF_MEMALLOC is that the memory allocator
won’t try too hard to gets lots of pages; it will make do with what it
has.

This means that processes with the PF_MEMALLOC flag set will have
access to
the last
dregs of free memory, while other processes will need to go out and free up
lots of memory first before they can use any. This property of
PF_MEMALLOC is still present and somewhat more formal in the current
kernel. The memory allocator has a concept of «watermarks» such that,
if the amount of free memory is below the chosen watermark, the allocator will
try to free more memory rather than return what it has. Different
__GFP flags can select different watermark levels (min, low, high).
PF_MEMALLOC causes all watermarks to be ignored; if any memory is
available at all, it will be returned.

PF_MEMALLOC effectively says «It is time to stop saving and start
spending, or we’ll have no product to sell».

In consequence of this, PF_MEMALLOC is now used more broadly than
just for avoiding recursion (though it still has that role). Several
kernel threads, such as those for nbd, the network block device,
iscsi_tcp, and the MMC card controller, all set PF_MEMALLOC,
presumably so they can be sure to get memory whenever they are called
upon to write out a page of memory (so it can be freed).

In contrast, the MTD driver (which manages NAND flash and has a
similar role to the MMC card driver) stopped
using the PF_MEMALLOC
flag in 2.6.33 with a comment suggesting it was an inappropriate
usage. Whether the other uses in the kernel are still justified is a question
too deep for our present discussion.

__GFP_IO in 2.2.0pre6

When __GFP_IO reappears
it has a similar purpose as the
original, but for an importantly different reason.
To understand that reason, it suffices to look at a comment in the code:

	/*
	 * Don't go down into the swap-out stuff if
	 * we cannot do I/O! Avoid recursing on FS
	 * locks etc.
	 */

The concern here still involves recursion, but it also involves locks,
such as the per-inode mutex, the page lock, or various others.
Calling into a filesystem to write out a page may require taking a
lock. If any such lock is held when allocating memory then it is
important to avoid calling into any filesystem code that might try to
acquire the same lock. In those cases, the
code must be careful not to pass __GFP_IO; in other cases, it is
perfectly safe to include that flag.

So while PF_MEMALLOC avoids the specific recursion of
get_free_page() calling into get_free_page(), __GFP_IO is more
general and prevents any function holding a lock from calling, through
get_free_page(), into any other function which might want that lock.
The risk here isn’t exhausting the stack as with PF_MEMALLOC; the
risk is a deadlock.

One might wonder why a GFP flag was used for this rather than a
process flag, which would effectively say «I am holding a filesystem
lock», given that the previous experience with __GFP_IO wasn’t a
success. Like many software designs, it probably just «seemed like a
good idea at the time».

__GFP_FS in 2.4.5.8

This flag started life named __GFP_BUFFER in 2.4.5.1, but didn’t
really work properly until 2.4.5.8 when it was renamed to __GFP_FS.
Apparently there was a thinko
in the original design, which required
not only a range of code changes, but also a new name.

__GFP_FS effectively split some functionality away from __GFP_IO
so that where there was one flag, now there were two. Only three
combinations of the two were expected: neither, both, or the new
possibility of just the __GFP_IO flag being set. This would allow
buffers that were already prepared to be written out, but would
prohibit any calls into filesystems to prepare those buffers. I/O
activity would be permitted, but filesystem activity would not.

Presumably, the fact that __GFP_IO previously had such a broad effect was
harming performance, in that it had to be excluded in places where some
I/O was still possible. Refining the rules by adding a new flag led to
more flexibility, and so fewer impediments to performance.

PF_FSTRANS in 2.5.36

This new process flag appeared when XFS
support was merged into Linux
in late 2002. Its purpose was to indicate that a filesystem
transaction (hence the name) was being prepared, meaning that any
write to the filesystem would likely block until the transaction
processing was complete.

The effect of this flag was to exclude __GFP_FS from any memory
allocation request which happened while PF_FSTRANS was set, or at
least any request from within the XFS code. Other requests would not
be affected, but then other code that allocated memory would be
unlikely to be called while the flag was set.

Another way to see this flag is that, in the same way that the
original __GFP_IO was converted to PF_MEMALLOC, now __GFP_FS is
being converted to a process flag, too. In this case, the conversion is
not complete, though.

Back in the halcyon days of 2.1.116, removing a flag like __GFP_IO
was quite straightforward — there were few users and the implications of
the change
could be easily understood. In the more complex times of 2.5.36,
such a step would be far from easy. Carefully adding new
functionality is one thing, removing something that is entrenched is
quite another, as we have seen with the Big
Kernel Lock and the
sleep_on()
interface. Allowing either the new flag or the absence of
the old to have the same effect is not a big cost and it was best to
leave things that were working alone.

Skipping ahead of ourselves a little to 3.6-rc1,
the PF_FSTRANS
flag also gets used by NFS. Rather than setting it during a
transaction, NFS sets it while constructing and transmitting an RPC
request onto the network, so the name is now slightly less appropriate.

Also the effect of the flag on NFS is not exactly to clear __GFP_FS,
but simply to avoid a call to transmit a COMMIT request inside
nfs_release_page(), which is also avoided if __GFP_FS is missing.

This is a superficially different usage than the usage by XFS, but it has
a generally similar effect for a generally similar reason. Modifying
the flag to have a more global effect of clearing GFP_FS and maybe
renaming it to PF_MEMALLOC_NOFS might not be a bad idea.

set_gfp_allowed_mask() in 2.6.34

This function actually appeared
in 2.6.31,
but becomes more interesting in 2.6.34.

gfp_allowed_mask is a global variable holding a set of GFP flags
which are allowed to be honored — all others are ignored. In
particular, __GFP_FS, __GFP_IO, and __GFP_WAIT (which generally
allows get_free_page() to wait for memory to be freed by other
processes) are sometimes disabled via this mechanism. Thus it is a bit like
PF_FSTRANS, except that it affects more processes and disables more
flags.

gfp_allowed_mask came about while providing support for
kmalloc()
earlier in the boot process. During early boot, interrupts are still
disabled and any attempt to allocate memory with __GFP_WAIT or
certain other flags can trigger a warning from the lockdep
checker. It would be
surprising if memory were so tight during boot that the allocator
actually needed to wait, but getting rid of warnings is generally a
good thing, so gfp_allowed_mask was initialized to exclude the three
flags mentioned, and these were added back in once the boot process
was complete.

One thing we have learned over the years is that boot isn’t as special
as we sometimes think: whether it is suspend and resume, or hotplug
hardware which teaches us this, it seems to be a lesson we keep
finding new perspectives on.

In that light, it is perhaps unsurprising that, in 2.6.34,
the use of this mask was extended to
cover suspend and resume (though an early version of the original
patch did mention the importance of suspend).

In the case of memory allocation deadlocks, the suspend case is more
significant than the boot case. During boot there is usually lots of
free memory — not so during suspend, when we may well be
short of memory. It wasn’t warnings that prompted this change, but
genuine deadlocks.

Suspend and resume are largely orderly processes, with devices
being put to sleep in sequence, and then woken again in the reverse
sequence. So it would not be enough just for block devices to avoid
using __GFP_IO (which they already do). Rather, every driver must
avoid the __GFP_IO flag, and others, as the target block device of
some write request, might be sequenced with this driver so that it is
already asleep, and will not awake before this one is completely awake.

Having a system-wide setting to disable these flags may be a bit
excessive — just the process which is sequencing suspend might be
sufficient — but it is certainly an easy fix and, as it cannot affect
normal running of the system, it is thus a safe fix.

PF_MEMALLOC_NOIO in 3.9-rc1

Just as suspend/resume has taught us that boot-time is not that much
of a special case, so too runtime power management has taught us that
suspend isn’t all that much of a special case either.

If a block device is runtime-suspended to save power, then obviously it
cannot handle requests to write out a dirty page of memory until it
has woken up, and until any devices it depends on (a USB controller, a PCI
bus) are awake too. So none of these devices can safely perform
memory allocation using __GFP_IO.

In order to ensure this, we could use set_gfp_allowed_mask() while a
device was suspending or resuming, but if multiple such devices were
suspending or resuming we could easily lose track of when to restore the
right mask.

So this
change introduces a process flag much like PF_FSTRANS,
only to disable __GFP_IO rather than __GFP_FS. It also takes care
to record the old value whenever the flag is set, and restore that old
value when done. To know when to set this flag, a memalloc_noio flag
is introduced for each device; it is then propagated into the
parents in the device tree. PF_MEMALLOC_NOIO is set whenever
calling into the power management code for any device with
memalloc_noio set.

As both the early boot processing and the suspend/resume processing
are largely single-threaded (or have dedicated threads), it is quite
possible that setting PF_MEMALLOC_NOIO and PF_FSTRANS on those
threads would be a sufficient alternative to using
set_gfp_allowed_mask(). However, as there is no clear benefit from
such a change, and no clear certainty that it would work, it is safer,
once again, to leave that which works alone.

Patterns that emerge

Amid all these details there are a couple of patterns which stand
out.

The first is repeated refinement of the «avoid recursion» concept. At
first it was implicit in an enumerated value passed to
get_free_page(), then it was made explicit in the first __GFP_IO,
and then the PF_MEMALLOC flag. Next it was extended to cover more subtle forms of
recursion with the second version of __GFP_IO and, finally, that was
split into
two separate flags to express an even wider range of recursion
scenarios that can be separately avoided.

It is conceivable that there is room for further refinement. We could
have separate flags for different sorts of locks — one for
page locks and one for inode locks, for example. There is no evidence
that this would presently be useful, but Linux isn’t really finished
yet, so we just don’t know.

The second pattern is the repeated discovery that just having a GFP flag
often isn’t enough — three times a new process flag was added because
sometimes it isn’t just a single allocation that needs to be
controlled, but all allocations made by a given process. Is it only a
matter of time before we get either a process flag which disables
__GFP_WAIT or a per-process gfp_allowed_mask?

As a footnote to this pattern, it is amusing that in 3.6-rc1,
as part
of adding support for swap-over-NFS, a new flag, __GFP_MEMALLOC, was
added which has much the same effect as PF_MEMALLOC in ignoring the
normal low-watermarks and providing access to the last reserves of
memory.

This, together with the per-socket sk_allocation mask, allows
certain TCP sockets (those which NFS is performing swap over) to access
those last reserves to make sure that swap-out always succeeds.

Clearly there is need for both GFP flags and process flags, as well as
some per-device and per-socket flags.

We’ve not seen the last of this

While studying history can be generally enlightening, it can also be
specifically useful as it is in this case. Next week, we will use this
understanding of memory allocation deadlocks to explore some
deadlocks which have long been possible in a certain configuration,
but which now need to be removed.

Index entries for this article
Kernel gfp_t
Kernel Memory management
GuestArticles Brown, Neil


(Log in to post comments)

Понравилась статья? Поделить с друзьями:
  • Memory allocation error cannot load command system halted
  • Memory address error at xxxx
  • Memory access violation как исправить venus hostage
  • Mediaerror 3 fired by video element ошибка wink
  • Mediaelement error format error как исправить ошибку