выделение памяти (язык C)

Вопросы написания собственного программного кода (на любых языках)

Модератор: Olej

Аватара пользователя
Olej
Писатель
Сообщения: 21338
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

выделение памяти (язык C)

Непрочитанное сообщение Olej » 10 дек 2013, 17:30

Olej писал(а): - вы же не чудовищно огромные структуры туда запихивать собираетесь? ... это в точности то же, что и просто локальные переменные объявлены
- размер стека процесса (потока, точнее говоря) устанавливается при сборке по умолчанию ... но, в принципе, его можно и изменить? ;-)
По поводу размера стека...
Он для программ пользовательского пространства:
а). выделяется достаточно большим, в отличие, скажем, от ядра Linux, где на стеке сильно экономят
б). выделяется размером в целое число страниц RAM, 4096 для 32 бит архитектуры.

1. Размер выделяемого размера (в kB) можем смотреть, это весьма много (Linux):

Код: Выделить всё

olej@notebook:~$ ulimit -s -S
8192
olej@notebook:~$ ulimit -s -H
unlimited
В других системах, в embedded Linux - это может быть существенно меньше.

Но так (ulimit) можно не только проверить, но и изменить размер стека позже запускаемых прграмм.

2. Программно (API) посмотреть и даже переустановить размер стека можно вызовами getrlimit() / setrlimit().

Код: Выделить всё

#include <stdio.h>
#include <sys/resource.h>

int main( void ) {
   struct rlimit lim;
   if( getrlimit( RLIMIT_STACK, &lim ) != 0 ) {
      printf( "ошибка getrlimit: %m" );
      return 1;
   };
   if( lim.rlim_cur != RLIM_INFINITY )
      printf( "%d : ", lim.rlim_cur );
   else
      printf( "INFINITY : " );
   if( lim.rlim_max != RLIM_INFINITY )
      printf( "%d\n", lim.rlim_max );
   else
      printf( "INFINITY\n" );
   return 0;
}
В 32-бит Linux это значение по умолчанию 8Mb (что показывает и команда ulimit):

Код: Выделить всё

olej@notebook:~/2013_WORK/examples.others.DRAFT/stack_size$ ./getss 
8388608 : INFINITY
3. Как-то управлять размером стека можно ещё и так (но деталей так сразу и не вспомню, и к какой ОС это относится):
- статически, при сборке GCC (?);
- как-то опциями команды запуска процесса;
- значением переменных окружения (каких?);

4. Как я понимаю (IMHO), в системах с MMU (а это подавляющее большинство), установка размера для стека в 8Mb вовсе не означает, что 8Mb RAM будет действительно зарезервировано и выделено под стек: выделяется какое-то ограниченное количество (1?) страниц (4Kb), а как только стек достигает границы реально выделенного вниз, новая страница физической RAM отображается в эту следующую страницу... Отсюда следуют далеко идущие последствия, то, что при выделении больших объёмов в стеке, затрачиваемое на это время может быть соизмеримо с malloc(), т.к. для распределения страниц RAM на стек потребуется тоже работа с таблицами MMU.
Но с этим нужно разбираться ... и это может радикально сильно зависеть от архитектуры.

Аватара пользователя
Olej
Писатель
Сообщения: 21338
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Re: выделение памяти (язык C)

Непрочитанное сообщение Olej » 10 дек 2013, 18:37

Виктория писал(а): Память для массивов VLA может выделяется и с использованием стека (с пересчетом границ стека на этапе выполнения)? Мое занудство объясняется особенностями моих приложений (обработка сигналов в реальном времени на микроконтроллере). Со стеком интуитивно меньше проблем, чем использование malloc().
Ответ будет зависеть от архитектуры, от наличия MMU и отображения физических страниц RAM в логические (виртуальные) адреса.
И от наличие механизма COW (copy on write).
Т.е. всё может очень сильно различаться для микроконтроллеров и x86, например.

О чём речь?
Например:

Код: Выделить всё

char BigArray[ 1<<20 ];
...
int main() {
...
   fork();
...
}
После вот этого fork() в системе с COW (Linux x86) для 1Mb массива не инициализированных данных BigArray реальная RAM выделяться не будет (да и для инициализированных данных при fork() тоже). И поэтому сама операция fork() выполнится очень быстро. Но позже, при записи (не при чтении!) в дочернем процессе BigArray - массив будет отображён с помощью MMU на выделенный (и скопированный!) для него новый участок памяти. И вот здесь произойдёт задержка.

В архитектуре без MMU () систем (некоторые микроконтроллеры) или ОС без COW (ОС QNX) - вся полная копия процесса, с полным объёмом принадлежащей ему памяти, должна быть создана в момент fork(). Это достаточно долго.
Зато все последующие обращения к BigArray будут мгновенно быстрыми.

С областью стека, как я полагаю, та же история ... в системах с MMU и COW:
- реальный стек выделяется гораздо меньше предписанного объёма...
- стек внизу завершается охранной областью (guard)...
- при достижении по записи охранной области - возникает SIGSEGV, и его обработчик запрашивает MMU на выделение ещё одной страницы RAM + отображение её в адреса впритык ниже вершины стека.

Аватара пользователя
Olej
Писатель
Сообщения: 21338
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Re: выделение памяти (язык C)

Непрочитанное сообщение Olej » 10 дек 2013, 18:54

Olej писал(а): 3. Как-то управлять размером стека можно ещё и так (но деталей так сразу и не вспомню, и к какой ОС это относится):
- статически, при сборке GCC (?);
Вот здесь подсказывают (хотя это и источник, который требует проверки ;-) ):
Действительно, в ELF размер стека НЕ прописывается. Любая
программа должна сама себе выставлять кастомный размер стека, если
нужно.
Это похоже на правду... установка размера стека линкером - это, похоже, мне из MS-DOS (MS Windows?) припомнилось. :oops:

Аватара пользователя
Olej
Писатель
Сообщения: 21338
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Re: выделение памяти (язык C)

Непрочитанное сообщение Olej » 10 дек 2013, 19:48

Olej писал(а): С областью стека, как я полагаю, та же история ... в системах с MMU и COW:
- реальный стек выделяется гораздо меньше предписанного объёма...
- стек внизу завершается охранной областью (guard)...
- при достижении по записи охранной области - возникает SIGSEGV, и его обработчик запрашивает MMU на выделение ещё одной страницы RAM + отображение её в адреса впритык ниже вершины стека.
Вот как вся эта красота выглядит в Wndows:
Стек располагается в виртуальном адресном пространстве процесса, соответственно он разбит на страницы и ему присущи все свойства «обыкновенной» виртуальной памяти, с которой мы работаем функциями VirtualAlloc, VirtualFree и т.д. Однако, специально для стека, имеется один флаг защиты памяти: PAGE_GUARD. Страница с таким атрибутом называется сторожевой. При обращении к ней генерируется исключение EXCEPTION_GUARD_PAGE. Как оно обрабатывается, мы также рассмотрим попозже. Изначально система не передает (commit) весь стек потоку, так как весь он может и не понадобится; передаются только первые две его страницы [1]. Количество передаваемых потоку страниц можно изменить с помощью все той же опции линкера «/STACK»
(понятно и из самого подтекста: это всё имеет силу исключительно для x86 архитектуры, и флаг PAGE_GUARD - это бит где-то в таблицах страниц MMU)
Для последней из передаваемых изначально станиц устанавливается флаг PAGE_GUARD. По мере разрастания дерева вызовов система передает все больше страниц стека физической памяти. Последняя страница «обычного» стека никогда не передается и всегда остается зарезервированной.
Последняя переданная страница стека всегда имеет установленный флаг PAGE_GUARD.

Аватара пользователя
Виктория
Писатель
Сообщения: 113
Зарегистрирован: 28 дек 2012, 14:05
Откуда: Самара
Контактная информация:

Re: выделение памяти (язык C)

Непрочитанное сообщение Виктория » 10 дек 2013, 21:03

Olej писал(а):
Olej писал(а): 3. Как-то управлять размером стека можно ещё и так (но деталей так сразу и не вспомню, и к какой ОС это относится):
- статически, при сборке GCC (?);
Вот здесь подсказывают (хотя это и источник, который требует проверки ;-) ):
Действительно, в ELF размер стека НЕ прописывается. Любая
программа должна сама себе выставлять кастомный размер стека, если
нужно.
Это похоже на правду... установка размера стека линкером - это, похоже, мне из MS-DOS (MS Windows?) припомнилось. :oops:
В программах для микроконтроллеров эта опция необходима. Не так то уж много памяти в запасе.
Хотя, если посмотреть в документацию по опциям компилятора gcc (для определенности - архитектуру ARM) нет такой опции, в которой размер стека задается числом. Похоже, что задание стека и распределение памяти для разных "target ARM processor" можно найти в стартовом файле. Ещё для различных toolschain могут быть специальные конфигурационные файлы.

Аватара пользователя
Olej
Писатель
Сообщения: 21338
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Re: выделение памяти (язык C)

Непрочитанное сообщение Olej » 10 дек 2013, 21:27

Виктория писал(а): В программах для микроконтроллеров эта опция необходима. Не так то уж много памяти в запасе.
Безусловно!
Но вы можете, как минимум, воспользоваться возможностями :

1. Изменить дефаултный размер стека запускаемых программ командой ulimit (в kB).

Код: Выделить всё

$ ulimit -s 2048 -S
$ ulimit -s 2048 -H
2. Из кода самой программы изменить размер её стека:

Код: Выделить всё

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/resource.h>
#include <signal.h>

static int numb = 0;
#define MB ( 1<<20 )

static void handler( int signo ) {
   printf( "Перехваченная ошибка сегментирования: %u\n", numb );
   exit( 0 );
};

void foo( void ) {
   char s[ MB ];
   printf( "%d ", numb + 1 );
   fflush( stdout );
   memset( s, 0, sizeof( s ) );
   numb++;
   foo();
}

int main( int argc, char *argv[] ) {
   struct rlimit limit;
   unsigned long size = 0, psize = sysconf( _SC_PAGESIZE );
   if( argc > 1 && atol( argv[ 1 ] ) > 0 ) {
      size = atol( argv[ 1 ] );
      printf( "запрошенный размер стека %lu\n", size );
      limit.rlim_cur = limit.rlim_max = size;
      if( setrlimit( RLIMIT_STACK, &limit ) != 0 ) {
         if( errno == EPERM )
            printf( "требуются права root: %m\n" );
         else
            printf( "ошибка setrlimit: %m\n" );
         return 1;
      }
   }
   if( getrlimit( RLIMIT_STACK, &limit ) != 0 ) {
      printf( "ошибка getrlimit: %m\n" );
      return 1;
   };
   printf( "установлен размер стека: %lu байт (%.2f страниц RAM)\n",
           limit.rlim_cur, ( (double)limit.rlim_cur / (double)psize ) );
   signal( SIGSEGV, handler );
   foo();
   return 0;
}
Вот как выделяются в изменённом стеке блоки по 1Mb:

Код: Выделить всё

olej@notebook:~/2013_WORK/examples.others.DRAFT/stack_size$ ./reset 3000000
запрошенный размер стека 3000000
установлен размер стека: 3000000 байт (732.42 страниц RAM)
1 2 Ошибка сегментирования

olej@notebook:~/2013_WORK/examples.others.DRAFT/stack_size$ ./reset 10000000
запрошенный размер стека 10000000
установлен размер стека: 10000000 байт (2441.41 страниц RAM)
1 2 3 4 5 6 7 8 9 Ошибка сегментирования

olej@notebook:~/2013_WORK/examples.others.DRAFT/stack_size$ ./reset 20000000
запрошенный размер стека 20000000
установлен размер стека: 20000000 байт (4882.81 страниц RAM)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Ошибка сегментирования
P.S. Мне удивительно, что при этом сигнал SIGSEGV не перехватывается, как, например, при разименовании NULL. И даже если учесть, что в примечаниях к setrlimit() пишут, что нужно установить альтернативный стек для обработки сигналов с помощью sigaltstack(), то и из этого ничего толком не получается.

Аватара пользователя
Olej
Писатель
Сообщения: 21338
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Re: выделение памяти (язык C)

Непрочитанное сообщение Olej » 24 дек 2013, 02:58

Виктория писал(а): Со стеком интуитивно меньше проблем, чем использование malloc().
Виктория, специально для вас и заинтересовавшись вашей фразой ... сделал специально тест-приложение, но об этом позже...

В программировании есть (так сложилось) несколько красивых народных легенд, которые, вообще то говоря, не подкрепляются никакими фактами.
Причин для возникновение таких легенд много... :
- кто-то написал в книге 70-го года что "...", а потом это и пересказывается, передаваясь от поколения преподавателей к поколению студентов и далее циклически ;-)
- потому что такие утверждения были вполне аргументированы, но в процессорных архитектурах 60-х годов, времён закладывания основ и работ Дейкстры, Хоара, Вирта ... когда в этих архитектурах память RAM была линейной, отсутствовало MMU устройства и отображения виртуальных адресов в физические, виртуализация страниц на внешние устройства хранения и т.д.

Одна показательная из таких красивых легенд состоит в том, например, что:
- при организации параллельных ветвей пользуясь инструментом процессов (fork(), exec(), ... и т.д.) время переключения контекста гораздо больше, чем если такую же параллельность реализовать пользуясь инструментом потоков (pthread_*() ... и в Win32 там свой API);
- потоки потому и называют ещё "легковесные процессы" ... пошла эта терминология, если не ошибаюсь, с документации Sun Solaris;
- а в итоге приложение, построенное на потоках будет автоматически, по умолчанию быстрее, производительнее, чем его эквивалент, построенный на параллельных процессах.
Но это неправда! И никакие эксперименты и никакие проверки не показывают этого.
Потоки действительно могут быть легковеснее, производительнее, но вовсе не потому, а из-за медлительности межпроцессного взаимодействия (IPC) процессов в изолированных адресных пространствах, но только в тех случаях и тех моментах, когда такое взаимодействие (IPC) необходимо.

Вот у относительно утверждения, часто называемого, что динамические malloc(), calloc(), new, delete - намного более затратные по времени, чем выделение памяти под локальные переменные, alloca(), или динамические массивы GCC ... - это у меня вызывает сильные подозрения. В архитектурах с MMU это тоже очень сильно похоже на те же красивые народные легенды, оставшиеся со времён малых архитектур с линейной физической памятью (LSI-11 с огромными 4 или 8 Kb RAM :-( ).

Аватара пользователя
Olej
Писатель
Сообщения: 21338
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Re: выделение памяти (язык C)

Непрочитанное сообщение Olej » 24 дек 2013, 03:21

Olej писал(а): Вот у относительно утверждения, часто называемого, что динамические malloc(), calloc(), new, delete - намного более затратные по времени, чем выделение памяти под локальные переменные, alloca(), или динамические массивы GCC ... - это у меня вызывает сильные подозрения. В архитектурах с MMU это тоже очень сильно похоже на те же красивые народные легенды, оставшиеся со времён малых архитектур с линейной физической памятью (LSI-11 с огромными 4 или 8 Kb RAM :-( ).
В Linux, для конкретности, используются несколько механизмов управления памятью, самый частый и известный из которых это сляб-алокатор, использующийся ядром по умолчанию (когда выделяется по отдельному пулу для каждого возможного размера блоков, запрашиваемых из этого пула, сляба). Но это с точки зрения ядра. А для поцесса ядро выделяет и стек и хип (для динамического выделения):
- из пула с размером блока кратным нескольким страницам RAM (4Kb для 32-бит ... или 64Kb при PAE);
- по небольшому числу страниц (2-4 страницы мелькает в описаниях)...
- как только эти страницы вычерпаны, с помощью MMU к ним отображаются ещё сколько-то там страниц реальной RAM, в непрерывную область адресов...
- только в случае со стеком это происходит вниз, а с хипом - вверх.
И в том и другом случае картина одинаковая, только симметричная.

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

Код: Выделить всё

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

#define single single1
//#define single single2

int main( int argc, char **argv ) {
   int single1[] = { 2, 5, 7, 11, 13 }, 
       single2[] = { 997, 101, 991, 109, 983, 139, 977 },
       ns = sizeof( single ) / sizeof( single[ 0 ] ),
       n = 10, i, j;
   while( 1 ) {
      void** arr = NULL; 
      if( arr ) {                  // освобождение предыдущего размещения
         for( i = 0; i < n; i++ )
            for( j = 0; j < ns; j++ )
               free( arr[ i * ns + j ] );
         free( arr );
         arr = NULL;
      }
      printf( "число размещений: " );
      fflush( stdout );
      i = scanf( "%u", &n );
      if( i <= 0 || n < 0 ) {
         printf( "\n" );
         break;
      }
      arr = calloc( ns * n, sizeof( void* ) );
      for( i = 0; i < n; i++ )     // размщение областей
         for( j = 0; j < ns; j++ )
            arr[ i * ns + j ] = malloc( single[ j ] );
      for( i = 0; i < n; i++ ) {
         for( j = 0; j < ns; j++ )
            printf( "%14p ", arr[ i * ns + j ] );
         printf( "\n" );
         for( j = 0; j < ns; j++ ) 
            if( i * ns + j + 1 < n * ns )
               printf( "%6u[%03d+%02d] ", 
                       arr[ i * ns + j + 1 ] - arr[ i * ns + j ],
                       single[ j ],
                       arr[ i * ns + j + 1 ] - arr[ i * ns + j ] - single[ j ] );
         printf( "\n" );
      }
      free( arr );
   }
   return 0;
}
С этим приложением очень любопытно поэкспериментировать!

Код: Выделить всё

olej@notebook:~/2013_WORK/examples.others.DRAFT/alloc$ ./alloc
число размещений: 3
     0x9400048      0x9400058      0x9400068      0x9400078      0x9400088
    16[002+14]     16[005+11]     16[007+09]     16[011+05]     24[013+11]
     0x94000a0      0x94000b0      0x94000c0      0x94000d0      0x94000e0
    16[002+14]     16[005+11]     16[007+09]     16[011+05]     24[013+11]
     0x94000f8      0x9400108      0x9400118      0x9400128      0x9400138
    16[002+14]     16[005+11]     16[007+09]     16[011+05]
- видно, что блоки разных некратных размеров выделяются (адреса!) из одной области памяти, непрерывно друг за другом...
- оставляя 8 байт перед блоком (идентификация размера?) и выравниваясь на 8 ... но это детали реализации, и не так важно
- но такое "непрерывное" размещение некратных блоков может быть только а). в заранее распределённую область, б). когда уже нет нужды в сляб-механизме, в). и которое произойдёт без MMU и даже без вмешательства системного вызова (до тех пор, пока не исчерпаются страницы и не нужно подгрузить)...
- а происходит такое управление простыми перемещениями указателя, и не является никак затратным!
- интересно, что при освобождении и последующем новом размещении, блоки не размещаются на те же места, а сдвигаются дальше по адресам...
- но если вы поэкспериментируете, то увидите, как через какое-то время они опять вернутся к началу хипа.

Вот ещё показательный результат на других размерах блоков (single2):

Код: Выделить всё

olej@notebook:~/2013_WORK/examples.others.DRAFT/alloc$ ./alloc
число размещений: 3
     0x8e16060      0x8e16450      0x8e164c0      0x8e168a8      0x8e16920      0x8e16d00      0x8e16d90
  1008[997+11]    112[101+11]   1000[991+09]    120[109+11]    992[983+09]    144[139+05]    984[977+07]
     0x8e17168      0x8e17558      0x8e175c8      0x8e179b0      0x8e17a28      0x8e17e08      0x8e17e98
  1008[997+11]    112[101+11]   1000[991+09]    120[109+11]    992[983+09]    144[139+05]    984[977+07]
     0x8e18270      0x8e18660      0x8e186d0      0x8e18ab8      0x8e18b30      0x8e18f10      0x8e18fa0
  1008[997+11]    112[101+11]   1000[991+09]    120[109+11]    992[983+09]    144[139+05]
Вложения
alloc.c
(1.5 КБ) 308 скачиваний

Аватара пользователя
Виктория
Писатель
Сообщения: 113
Зарегистрирован: 28 дек 2012, 14:05
Откуда: Самара
Контактная информация:

Re: выделение памяти (язык C)

Непрочитанное сообщение Виктория » 25 дек 2013, 06:55

Olej писал(а):
Виктория писал(а): Со стеком интуитивно меньше проблем, чем использование malloc().
Вот у относительно утверждения, часто называемого, что динамические malloc(), calloc(), new, delete - намного более затратные по времени, чем выделение памяти под локальные переменные, alloca(), или динамические массивы GCC ... - это у меня вызывает сильные подозрения. В архитектурах с MMU это тоже очень сильно похоже на те же красивые народные легенды, оставшиеся со времён малых архитектур с линейной физической памятью (LSI-11 с огромными 4 или 8 Kb RAM :-( ).
Меня то ещё интересуют и конфигурации при отсутствии операционной системы. Хотя во всех этих сборках bare metal архитектура процессора должна обязательно учитываться

Аватара пользователя
Olej
Писатель
Сообщения: 21338
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Re: выделение памяти (язык C)

Непрочитанное сообщение Olej » 17 янв 2014, 09:18

Возвращаемся к распределению памяти в стеке...
Olej писал(а): Массивы VLA в стандарте C99 можно посмотреть здесь: Расширение массивов.
Там их пример-объяснение:

Код: Выделить всё

void f(int diml, int dim2)
{
  int matrix[diml][dim2]; /* двумерный массив переменной длины */
  /* ... */
}
Olej писал(а): 2. кроме malloc() у нас есть ещё alloca(), который выделяет по запросу память в стеке.
Вот, собственно, из man alloca :
Because the space allocated by alloca() is allocated within the stack frame, that space is automatically freed if the function return is jumped over by a call to longjmp(3) or sig‐longjmp(3).

Do not attempt to free(3) space allocated by alloca()!
Хотя там же:
The alloca() function is machine- and compiler-dependent.
Я так понимаю (предполагаю), что динамические массивы VLA стандарт C99 вводит вместо плохо стандартизованного (между компиляторами, платформами) вызова alloca(). Что есть почти одно и то же (или совершенно одно и то же?). Только, естественно, нельзя одновременно использовать и один и другой механизм, потому что вместе, не зная друг о друге, они просто разрушат стек.

Ответить

Вернуться в «Программирование»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 10 гостей