LINUX.ORG.RU

Странный баг при работе с разделяемой памятью в C


0

0

Есть сервер и несколько клиентов. Сервер создает блок разделяемой памяти (примерно на 16Мб размером) и пишет туда какие то данные. Клиенты запускаются, подключают этот блок для чтения и читают их оттуда, после чего завершают работу. Клиенты, прошу заметить, на PHP, используют extension на C, который собственно и занимается подключением и чтением этого блока разделяемой памяти. Все вроде бы работает хорошо и прекрасно, однако периодически (примерно в 5-10% случаев) при исполнении клиента, происходит совершенно странная дикость (или дикая странность, как угодно). Блок памяти подключается на ура (ошибок не возвращает, адрес сегмента, отображенного в память процесса, похож на правду). Однако в нем одни нули. Т.е. никаких данных нет, одни нули. Только в конце блока есть немного мусора (начиная со смещения F0F008). Для того чтобы от сего неприятного явления избавиться, достаточно запустить клиент заново, и как правило он подключает и читает блок нормально. Я честно говоря в ступоре, никаких идей даже нет.

Если у кого нибудь есть идеи, с чем сие связано и как лечить, буду очень рад )

Может быть там действительно одни нули в тот момент, когда эта память зачитывается? Как происходит синхронизация сервера и клиентов? Нет ли здесь просто-напросто рейса?

gzh
()

Все, что угодно может быть.

Шареная память, наверное, System V IPC?

А в extension на C на блок памяти volatile имеется?

А че за процессор?

Die-Hard ★★★★★
()

Еще вопрос важный: а как синхронизация устроена?

Почему клиент уверен, что, когда он подключил блок, там уже что-то есть?

Die-Hard ★★★★★
()
Ответ на: комментарий от Die-Hard

По-другому: автор, показывай сорцы! ;)

mv ★★★★★
()

думаю, кто-то разрушает память(знаю, охрененно сложный вывод:)). А, может, баг в самом экстеншене на C, попробуй его подебажить.

true_admin ★★★★★
()

Клиента под strace'ом запусти, выхлоп нормального запуска и ненормального покажи.

mv ★★★★★
()

С самого начала сервер считывает данные с файлов, организует определенные структуры в этом блоке памяти и более их не стирает. Т.е. я абсолютно точно уверен что он их не стирает ) Никто кроме сервера доступ на запись к блоку памяти не получает. Память да System V IPC. на блок памяти volatile нету. попробую поставить. проц старенький, на домашнем серваке тестирую, Celeron Tualatin 1300.

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

Насчет strace поподробнее, я в этом деле если честно нуб (разработке для линуха).

mephisto123
() автор топика
Ответ на: комментарий от mephisto123

strace php script.php. Найти кусок с нужным mmap, строк по 20 от этого места в обе стороны выделить.

mv ★★★★★
()
Ответ на: комментарий от mv

Собственно при нормальном исполнении клиента идет gettimeofday({1217620049, 448999}, NULL) = 0 gettimeofday({1217620049, 449221}, NULL) = 0 gettimeofday({1217620049, 449481}, NULL) = 0 gettimeofday({1217620049, 449727}, NULL) = 0 gettimeofday({1217620049, 449966}, NULL) = 0 send(3, "get cfexpdbase/41/acc1\r\n", 24, 0) = 24 select(4, [3], NULL, NULL, {1, 0}) = 1 (in [3], left {1, 0}) recv(3, "VALUE cfexpdbase/41/acc1 1 423\r\n"..., 8192, 0) = 462 gettimeofday({1217620049, 451409}, NULL) = 0 gettimeofday({1217620049, 451657}, NULL) = 0 gettimeofday({1217620049, 451881}, NULL) = 0 gettimeofday({1217620049, 452161}, NULL) = 0 gettimeofday({1217620049, 452484}, NULL) = 0 shmget(0x1983267a, 16777216, 0444) = 425985 shmat(425985, 0, SHM_RDONLY) = 0xb6cb7000 time(NULL) = 1217620049 shmdt(0xb6cb7000) = 0 gettimeofday({1217620049, 453758}, NULL) = 0

на gettimeofday не пугаться, эт пхп нарно зачем то интересуется по 20 раз на дню ) send select recv - это получение закэшированной в мемкэше переменной. само подключение сегмента - shmget-shmat-shmdt.

mephisto123
() автор топика
Ответ на: комментарий от mephisto123

gettimeofday({1217620049, 448999}, NULL) = 0
gettimeofday({1217620049, 449221}, NULL) = 0
gettimeofday({1217620049, 449481}, NULL) = 0
gettimeofday({1217620049, 449727}, NULL) = 0
gettimeofday({1217620049, 449966}, NULL) = 0
send(3, "get cfexpdbase/41/acc1\r\n", 24, 0) = 24
select(4, [3], NULL, NULL, {1, 0})      = 1 (in [3], left {1, 0})
recv(3, "VALUE cfexpdbase/41/acc1 1 423\r\n"..., 8192, 0) = 462
gettimeofday({1217620049, 451409}, NULL) = 0
gettimeofday({1217620049, 451657}, NULL) = 0
gettimeofday({1217620049, 451881}, NULL) = 0
gettimeofday({1217620049, 452161}, NULL) = 0
gettimeofday({1217620049, 452484}, NULL) = 0
shmget(0x1983267a, 16777216, 0444)      = 425985
shmat(425985, 0, SHM_RDONLY)            = 0xb6cb7000
time(NULL)                              = 1217620049
shmdt(0xb6cb7000)                       = 0
gettimeofday({1217620049, 453758}, NULL) = 0

вот так лучше.

mephisto123
() автор топика
Ответ на: комментарий от mephisto123

а баг стрейсом пока не ловится. он как повезет... иногда часто а иногда долго нету.

mephisto123
() автор топика
Ответ на: комментарий от anonymous

в defs.h определены они... тупо ключ от балды выбран и скопирован в оба хэдера (и у клиента и у сервера). размер там вычисляется на основе других дефайнов дефайном же. совпадают они у клиента и сервера 100%.

mephisto123
() автор топика
Ответ на: комментарий от anonymous

shmat у меня так и вызывается. а SHM_LOCK нужен как я понял чтобы система не сбрасывала блок памяти в своп. у меня на тестовом серваке итак своп пустой, так что не думаю что в этом проблема.

mephisto123
() автор топика
Ответ на: комментарий от mephisto123

Блин сроки поджимают, сделал пока костыль, клиент пытается найти то что ищет в памяти 5 раз с промежутком в 10мс между попытками. Если ни разу не получается то выдает ошибку, если получается то все хорошо ))

Хотелось бы по человечески таки сделать, так что жду еще идей )

mephisto123
() автор топика
Ответ на: комментарий от mephisto123

на самом деле работать сие будет исключительно на моем веб-сервере, соответственно зависимость от машины тут никак не скажется. а разделяемая память - самый быстрый (с точки зрения производительности) способ реализовать то что мне нужно.

mephisto123
() автор топика
Ответ на: комментарий от mephisto123

> shmat у меня так и вызывается Гм - в логе у вас написано shmat( ..., 0, ...)

anonymous
()

Как временный костыль - добавите в самое начало данных общей памяти magic string. А в клиенте проверяйте ее на совпадение и если нет совпадения - переподключайтесь к общей памяти заново.

anonymous
()
Ответ на: комментарий от anonymous

0 это и есть NULL. в коде написано NULL. а как оно выглядит в strace - определяется тем, как он считает нужным выводить тот или иной параметр. Где то константы пишет типа SHM_RDONLY, где то hex-значения, где то десятичные значения. по поводу костыля - примерно так же и реализовал. интересно таки где проблема.

mephisto123
() автор топика
Ответ на: комментарий от mephisto123

> на блок памяти volatile нету.

Тогда сразу -- правильно работать не будет.

> ...на домашнем серваке тестирую.

То есть, на сервере только одно процессорное ядро? Значит, где-то в логике ошибка. Ищи, где-то есть грубые райсы.

И еще раз -- как тот, кто запускает клиентов, узнает, что их пора запускать?

Die-Hard ★★★★★
()

Ответ на удалённое.

Меня ничего не раздражает. Я вам просто говорю простую доступную истину — данные нельзя передавать в двоичном формате без необходимости. Ваша эта "жажда скорости" уже себя проявила в 5-10% случаев.

anonymous
()
Ответ на: комментарий от Die-Hard

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

mephisto123
() автор топика
Ответ на: комментарий от mephisto123

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

Жажда скорости не просто так, сервер рассчитан на не одну сотню одновременно сидящих в онлайне юзеров.

mephisto123
() автор топика
Ответ на: комментарий от mephisto123

> почему не передать в двоичном формате?

Действительно почему?

Почему пайпы и сокеты текстовые?

Почему нужны api?

Почему я должен рассказывать пых-пых кодерам основы программирования?

> Жажда скорости не просто так, сервер рассчитан на не одну сотню одновременно сидящих в онлайне юзеров.

Ещё раз. Если это не фотографии космоса в высоком разрешении (или нечто аналогичное), то бинарность там вообще не нужна. Если это «они», то бинарность нужна только им.

anonymous
()
Ответ на: комментарий от anonymous

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

mephisto123
() автор топика
Ответ на: комментарий от mephisto123

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

Legioner ★★★★★
()
Ответ на: комментарий от anonymous

> Почему пайпы и сокеты текстовые? Почему нужны api?

Ты плохо понимаешь, что пишешь.

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

Die-Hard ★★★★★
()
Ответ на: комментарий от mephisto123

> сделал volatile. все равно глючит

Волатильными сделал буфера и на клиенте, и на сервере?

> сервер запускается заранее. если он не запущен, обращения к клиентским скриптам просто будут завершаться ошибкой

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

Еще вопрос -- у клиетов случаются форки? Форки несколько нетривиально взаимодействуют с шареными сегментами.

Вообще, гадать, не видя кода, можно долго. Если речь идет об однопроцессорной машине, то (с точностью до бага в ядре) где-то грубая гонка.

> логика проста донельзя, проверял раз 10

Иногда и по 100 раз прповеришь, а оно совсем тривиальным оказывается...

Die-Hard ★★★★★
()
Ответ на: комментарий от Die-Hard

Обрисую еще раз. Есть сервер. Он запускается с самого начала, когда еще нет апача и клиентов. Читает что надо откуда надо и создает сей блок памяти с определенными структурами данных. После этого он может там что-то менять, однако характер изменений не такой, что "стереть кусок и перезаписать", а такой, что он например меняет значение определенного слова (2 байта имеются ввиду) в этом блоке памяти и т.п. Больше никто на запись этот блок не подключает. После этого запускается апач, когда все 100% готово. На апач стучатся клиенты, вызываются php-скрипты, которые вызывают в свою очередь функции экстеншна, а они подключают разделяемый сегмент и читают оттуда то, что сервер понаписал, получая таким образом актуальную информацию о состоянии сервера. Форков у клиентов не случается. Насчет тривиальности большинства багов согласен на 200% ))

Код покажу в след посте)

mephisto123
() автор топика
Ответ на: комментарий от mephisto123

Вот функция экстеншна. Так сказать обертка для настоящей.

PHP_FUNCTION(mapgetplayercoords)
{
   zval *(args[4]);
   unsigned long pid,i;
   unsigned short argvals[4];
   long result;
   if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC,"lzzzz",&pid,&(args[0]),&(args[1]),&(args[2]),&(args[3]))==FAILURE) {
      WRONG_PARAM_COUNT;
   }
   for (i=0;i<4;i++) if (!PZVAL_IS_REF(args[i])) {
      zend_error(E_WARNING,"Parameter wasnt passed by reference");
      RETURN_LONG(-1);
   }
   result=SM_InitIPC(0); // подключаем блок 
   if (result) RETURN_LONG(-2);
   result=MGetPlayerCoords(pid,&(argvals[0]),&(argvals[1]),&(argvals[2]),&(argvals
[3])); // здесь настоящий вызов
   MClearSMPtrs(); 
   SM_FinishIPC(0); // отключаем блок 
   if (!result) { // переносим полученные данные для возврата php-скрипту
      for (i=0;i<4;i++) ZVAL_LONG(args[i],argvals[i]);
      RETURN_LONG(0);
   }
   else { // забиваем нулями, чтобы php-скрипт получил адекватные значения
      for (i=0;i<4;i++) ZVAL_LONG(args[i],0);
      RETURN_LONG(result-2);
   }
}

Вот собственно функция которая работает. И которая глючит) есть еще несколько функций которые читают что-то из разделяемого блока, но они работают отлично. Глючит только эта функция.

int MGetPlayerCoords(unsigned long id,unsigned short *map,unsigned short *location,unsigned short *x,unsigned short *y)
{
   volatile TPlayerObject *players;
   unsigned long i;
   if (!zonebase) zonebase=(unsigned char *)SM_GetZoneBase();
   if (!zonebase) return -1;
   players=(TPlayerObject *)((unsigned char *)zonebase+DYNAMIC_LIST_OFFSET);
   for (i=0;i<DYNAMIC_LIST_ENTRIES;i++) {
      if (players[i].name[0]&&(players[i].pid==id)&&((time(NULL)-players[i].lastrefresh)
<=REFRESH_TIMEOUT)) break;
   }
   if (i==DYNAMIC_LIST_ENTRIES) return -2;
   *map=players[i].map;
   *location=players[i].location;
   *x=players[i].x;
   *y=players[i].y;
   return 0;
}

На имена переменных не пугаться, у меня иногда они глупо выглядят для посторонних )

mephisto123
() автор топика
Ответ на: комментарий от mephisto123

Еще поясню) как думаю видно, сервер суть сервер карты и местонахождения всех игроков и объектов в некоторой игре.

players[i].name[0] тут используется как флаг того, что эта структура используется (если игрок уходит в оффлайн то первый символ имени его тупо забивается нулем, и когда нужно будет свободное место для помещения нового игрока, то эта структура может быть использована).

игроки идентифицируются по их id.

mephisto123
() автор топика
Ответ на: комментарий от mephisto123

Что такое

if (!zonebase) zonebase=(unsigned char *)SM_GetZoneBase(); ?

zonebase -- глобальная переменная? А почему оно тут не может сглючить -- если zonebase!=NULL, то кто ей мешает на мусор показывать?

Как она инициализируется?

Вообще, volatile там явно не на месте.... Покажи SM_GetZoneBase() и SM_InitIPC()

Die-Hard ★★★★★
()
Ответ на: комментарий от mephisto123

Согласен с каментом выше: если тебя ссылает куда-то не туда, то видимо это SM_GetZoneBase() виновато. Оно базу берёт относительно некоей переменной?

anonymous
()
Ответ на: комментарий от mephisto123

А здесь не может быть ошибки:

"time(NULL)-players[i].lastrefresh"

Не в том смысле, что вызывать в цикле time(NULL) совсем не Ъ с учетом борьбы за производительность, а то что в этом случае возвращается -2, и условие "if (!result)" срабатывает, а argvals[] не заполнен.

mky ★★★★★
()
Ответ на: комментарий от mky

если result=-2 то условие (!result) истинным не будет о_О. !result эквивалентно result==0.

zonebase статично глобально объявлено в модуле, где есть функция MGetPlayerCoords и другие подобные.
SM_GetZoneBase возвращает всего лишь значение статичной глобальной переменной в модуле который с разделяемой памятью:

unsigned char *SM_GetZoneBase(void)
{
   return shmem;
}

обе переменных (zonebase, shmem) объявлены в своих модулях как volatile и инициализируются NULL'ом:

static volatile unsigned char *zonebase=NULL;
static volatile unsigned char *shmem=NULL;

функция MClearPtrs очищает глобальные переменные в модуле с MGetPlayerCoords, 
и вызывается каждый раз после завершения работы с разделяемой памятью, так что они всегда равны NULL если там ничего нет.
Кроме того, тот вызов MGetPlayerCoords, который глючит, вызывается первым из всех вызовов, работающих с разделяемой памятью, 
а стало быть на момент вызова переменные равны своим инициализирующим значениям (NULL).

int MClearSMPtrs(void)
{
   zonebase=NULL;
   mapptrs=NULL;
}

Функция SM_InitIPC:

int SM_InitIPC(short docreate)
{
   long result;
   result=shmget(SHM_KEY,SHM_SIZE,docreate?(0644|IPC_CREAT):0444);
   if (result==-1) return -1;
   shmid=result;
   result=(long)shmat(shmid,NULL,docreate?0:SHM_RDONLY);
   if (result==-1) return -1;
   if (result==0) return -1;
   shmem=(unsigned char *)result;
   if (docreate) {
      memset(shmem,0,SHM_SIZE);
      mblocks=NULL;
   }
   return 0;
}

собственно флаг docreate говорит о том, надо ли создавать блок (используется на сервере) или только подключить его (на клиенте) - функция одна и та же.

mephisto123
() автор топика
Ответ на: комментарий от mephisto123

насчет вызовов time() в цикле - спасибо за совет, исправлюсь :)

mephisto123
() автор топика
Ответ на: комментарий от mephisto123

Мне как то не нравится преобразование void* -> long, а потом long -> unsigned char *.

ИМХО, добавили бы переменную void * vresult, а ставнивали ее как то так: "if ( vresult == (void *) -1 )"

И еще, вы не обнуляете shmem, то есть если при первом вызове она получила не NULL значение, если при повторном вызове произвойдет ошибка в shmget или shmat, то у нее останется старое значение, которое и вернется SM_GetZoneBase.

А вобще, по хорошему, надо не просто return -1, а сообщение об ошибке с указанием errno. Может так какое ограничение IPC вылазит.

mky ★★★★★
()
Ответ на: комментарий от mephisto123

Вот еще непонятка:

Что будет, если в процессе чтения клиентом сервер поменяет данные? Я не заметил никаких блокировок. Может, все же, сервер трет?

Кстати, по поводу общих принципов: работать с шареной памятью, действительно, быстро, но вот скорости приаттачивания / детачивания сегмента никто не обещал. В твоем случае ты каждый раз присобачиваешь сегмент, тянешь оттуда _маленький_ кусочек и отцепляешь сегмент. Это, скорее всего, медленнее, чем спросить у сервера этот кусочек через пайп (сокет).

И volatile тут, кстати, и не нужен даже.

Die-Hard ★★★★★
()
Ответ на: комментарий от mky

> Мне как то не нравится преобразование void* -> long, а потом long -> unsigned char *.

У всех размер - машинное слово. Чем не нравится?

mv ★★★★★
()
Ответ на: комментарий от Die-Hard

1. Сервер ничего там не трет 100%, он просто может поменять данные немного, например поменять координаты игрока, или пометить игрока как оффлайн (первый символ имени = 0). Сама идея тут была обойтись почти без блокировок за счет интересной схемы реализации. Долго обьяснять, это в принципе к делу отношения не имеет. Могу сказать лишь, что сервер никогда ничего не забивает нулями в сегменте кроме момента запуска, он может только менять данные типа координат и флагов у игроков и объектов.
2. Уже поменял этот принцип, сам согласен что криво было. Теперь везде где нужна разделяемая память, проверяется zonebase на NULL. если NULL, оно пытается подключить сегмент. Если ошибка, то выдает ошибку (при любой ошибке скрипт пишет ошибку и останавливается, так что shmem в этом случае использован не будет). Если все нормально, то инициализирует zonebase и при последующих вызовах, требующих данных из сегмента, будет использоваться уже подключенный.
3. А чем плохо преобразование void * -> long ? указатели суть числа типа long. преобразование целого -1 в void * ничем не отличается от преобразования void * в число для сравнения.

mephisto123
() автор топика
Ответ на: комментарий от mky

> вы не обнуляете shmem, то есть если при первом вызове она получила не NULL значение, если при повторном вызове произвойдет ошибка в shmget или shmat, то у нее останется старое значение, которое и вернется SM_GetZoneBase.

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

Хотя, конечно, надо ошибку shmat повнимательнее обрабатывать. Весьма вероятно, дело именно в этом.

Die-Hard ★★★★★
()
Ответ на: комментарий от Die-Hard

int SM_FinishIPC(short destroy)
{
   int result;
   result=shmdt(shmem);
   if (result<0) return -1;
   if (destroy) {
      result=shmctl(shmid,IPC_RMID,NULL);
      if (result<0) return -1;
   }
   shmem=NULL;
   return 0;
}

дело в том, что shmem как раз таки обнуляется при отключении сегмента.

mephisto123
() автор топика
Ответ на: комментарий от Die-Hard

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

А один ли раз? Apache ничего не может с пулом процессов мутить? Тот факт, что автор с помощью strace php ... не может отловить бажный случай (10% сбоев - это дофига, должно быстро пойматься) наводит как раз на такие мысли.

mv ★★★★★
()
Ответ на: комментарий от mephisto123

Минимум в двух местах shmem не обнуляется. Ошибки ты не обрабатываешь, логи не ведёшь.

mv ★★★★★
()
Ответ на: комментарий от mephisto123

> Уже поменял этот принцип...

А глюк не пропал?

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

Все же, я не понял, как оно будет работать без блокировок. Ты просто не можешь средствами языка атомарно проверить флаг и захватить его. Если оставаться в рамках SysV, без семафоров не обойтись.

> ...или пометить игрока как оффлайн (первый символ имени = 0).

Ты проверяешь первый символ -- он не 0. Начинаешь работать -- он уже 0...

> если NULL, оно пытается подключить сегмент.

Вот тут волатильность буферов уже необходима.

> А чем плохо преобразование void * -> long ?

Да нормально все. Просто обычно в другую сторону делается, так принято.

Die-Hard ★★★★★
()
Ответ на: комментарий от Die-Hard

так, с этого места поподробнее) где shmem не обнуляется?

глюк не пропал. замечу, что при установке на live сервер у некоторых юзверей вообще периодически скрипты отваливаются с segmentation fault. как бы это выловить... пока вернул все как по старому было.

прошу помидорами не кидаться, как удобнее всего (и чтобы не тормозило) вести логи в линухе ? я прост как писал, нуб совсем.

> Ты проверяешь первый символ -- он не 0. Начинаешь работать -- он уже 0...

Ничего страшного тут не будет, просто игрок на секунду дольше будет считаться онлайн чем нужно ) никакого затирания всей памяти тут все равно не будет )

А буферы уже волатильные все. Все указатели на данные в сегменте включая zonebase и shmem.

mephisto123
() автор топика
Ответ на: комментарий от mephisto123

> глюк не пропал. замечу, что при установке на live сервер у некоторых юзверей вообще периодически скрипты отваливаются с segmentation fault. как бы это выловить... пока вернул все как по старому было.

Система тебе даёт явный намёк, что адрес примапленного сегмента у тебя неправильный. Лезешь в область, где нет ничего, получаешь segmentation fault.

mv ★★★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.