В этой теме собранны несколько небольших отличий при программировании под Windows, Linux и FreeBSD.
Если вы внимательно читаете все темы форумов на
www.bugtrack.ru, то можете дальше не читать. Все это было в других темах, здесь лишь собранно в одном месте.
Так же не читайте, если Вы считаете себя крутым программером и, и так все знаете — только потеряете время.
Если же вам лениво читать форумы от корки до корки, и вы первый раз взялись писать кросс-платформенное приложение, тогда, надеюсь, этот пост Вам поможет.
1. Select()
В ф-ии select есть сразу две проблемы.
Первая — это параметр nfds (первый параметр ф-ии select). Для Windows этот параметр не учитывается, для Unix это важный параметр который должен быть равен: максимальный дескриптор (из трех возможных fdset) плюс 1.
Вторая — второй, третий и четвертый параметры — fd_set.
Что представляет собой fd_set в Windows:
typedef struct fd_set {
u_int fd_count; // how many are SET?
SOCKET fd_array[FD_SETSIZE]; // an array of SOCKETs
} fd_set;
Это просто массив и счетчик элементов в нем. В этот массив, как и в любой другой можно положить все что угодно. Даже то, что сокетом не является. В этом случае select просто вернет ошибку.
Что собой представляет fd_set в Linux и FreeBSD:
typedef struct fd_set {
fd_mask fds_bits[howmany(FD_SETSIZE, NFDBITS)];
} fd_set;
Т.е. это всего лишь массив. Заполняется он так:
#define FD_SET(n, p) ((p)->fds_bits[(n)/NFDBITS] |= _fdset_mask(n))
Чем грозит такое отличие ? Например, под Windows можно написать следующий код:
SOCKET myset[1024];
for( int i = 0; i < 1024; i++ )
myset[i] = INVALID_SOCKET; // А чем еще инициализировать неинициализированные переменные ?
Дальше можно делать так:
if( (ret = select( 0, &fdread, 0, 0, &tv )) > 0 )
{
for( int i = 0; i < 1024; i++ )
if( FD_ISSET( myset[i], &fdread ) )
// do something
}
И это будет работать.
В Linux и FreeBSD такой код сразу даст segmentation fault.
#define FD_ISSET(n, p) ((p)->fds_bits[(n)/NFDBITS] & _fdset_mask(n))
INVALID_SOCKET определен как -1, т.е. 0xFFFFFFFF
Например в FreeBSD NFDBITS отпределен как 32 ( 4*8 ). И мы пытаемся прочесть позицию 0x07FFFFFF в fds_bits[] из 32'ух возможных ((1024 + 31)/32).
Поэтому, тот же код с минимальными исправлениями для Linux и FreeBSD должен выглядеть так:
if( (ret = select( 0, &fdread, 0, 0, &tv )) > 0 )
{
for( int i = 0; i < 1024; i++ )
if( myset[i] != INVALID_SOCKET )
if( FD_ISSET( myset[i], &fdread ) )
// do something
}
Еще в Windows FD_ISSET определен через ф-ию:
#define FD_ISSET(fd, set) __WSAFDIsSet((SOCKET)(fd), (fd_set FAR *)(set))
И что-то подсказывает, что эта ф-ия защищена от сбоев по памяти и прочих неприятных вещей.
Кстати, INVALID_SOCKET для Linux и FreeBSD надо определять самому.
2. send()
Первая проблема — это SIGPIPE. Сигнал, который посылается Unix системой приложению, если то пытается послать данные в сокет, соединение которого уже разорвано. В Windows в таком случае будет возвращена одна из ошибок.
Есть два метода борьбы с этим сигналом.
Первый — пригодный для Linux — установка флага (четвертый параметр ф-ии send() ) в MSG_NOSIGNAL.
Второй, пригодный для Linux и FreeBSD — установка обработчика сигнала, для SIGPIPE. Сам обработчик ничего не делает, просто при выходе из него программа продолжается дальше, а по ошибке, возвращаемой send, можно судить о разрыве соединения.
Вторая проблема — невозможность узнать, были ли реально отправлены данные. Теоретически это проблема не кросс-платформенного кода. В MSDN сказано: The successful completion of a send does not indicate that the data was successfully delivered.
Но практически, я ни разу не сталкивался с таким под Windows, и сразу столкнулся делая порт под FreeBSD.
Поэтому опишу проблему и решение здесь.
Разрыв соединения о котором не знают обе(или одна) стороны. Например, ваша программа под FreeBSD послала данные, ей вернулась ошибка 13 icmp — сокет будет принимать следующие данные для отправки, еще в течении некоторого времени. Они будут буфферизированы в исходящей очереди, и в конце концов утеряны, при закрытии соединения системой.
Один из способов борьбы — это поставить сокет в select(), во второй параметр (readfds). При возврате положительного числа попробовать прочесть 1 байт. Если recv вернет 0 — значит соединение было разорвано. (если вы не хотите читать из сокета, вызовите recv с флагом MSG_PEEK).
Может возникнуть ситуация когда данный метод не сработает. Например, FIN не доставлен, select с readfd вернет 0, а отправить нам пока нечего. О том что соединение разорвано мы так и не узнаем, пока не сделаем send (а он может, в структуре приложения, не сделан никогда). Параметр SO_KEEPALIVE мало пригоден. Для Linux, по умолчанию, детектирование соединения происходит раз в час, таймаут ожидания ответа — 15 минут. Т.е. 1 час 15 минут система будет считать что соединение есть.
Единственный способ борьбы — это реализация своего протокола подтверждения доставки пакета.
3. shutdown(), close() и closesocket()
В Windows принято, что после вызова closesocket() соединение закрывается. Так же, соединения закрываются при закрытии программы.
Система знает, какому процессу соответствуют сокеты, и закрывает их при смерти приложения. В Linix и FreeBSD это не так.
Необходимо явно сказать shutdown (посылка FIN) и close (разъединение дескриптора и сокета). Если этого не сделать, то после закрытия приложения сокеты еще будут висеть в системе некоторое время, в течение которого bind на "занятые" порты будет возвращать ошибку.
Такая же ситуация возникает если вы сделаете на серверной стороне приложения shutdown и close, закроете приложение, а на клиентской стороне эти ф-ии вызваны не будут. После этого Вы не сможете запустить серверное приложение в течении некоторого времени.
Методы борьбы — не известны. Только ждать.
4. Потоки.
В Windows есть только один механизм потоков, и он реализван в ядре. В Unix реализации потоков могут быть различны. Некоторые реализованны в пространстве ядра (posix thread для Linux и FreeBSD), другие могут быть в пространстве пользователя (у Рихтера они названы fibers).
Необходимо четко представлять чем Вы пользуетесь.
Потоки, реализованные в пространстве пользователя, имеют одну неприятную особенность — система о них не знает. Значит планировщик системы никогда не передаст управление другому потоку. Только процессу. Если же такой поток в процессе завис, то зависло и все приложение.
Простейший тест для определения в каком пространстве реализованы потоки (использован pthread, используйте другие ф-ии из выбранной вами библиотеки, аналогичные приведенным):
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
pthread_t th1;
pthread_t th2;
void* TH1(void* )
{
printf( "TH1 %X\n", pthread_self() );
while( true );
}
void* TH2(void* )
{
int i;
printf( "TH2 %X\n", pthread_self() );
for( i = 0; i < 10; i++ )
printf( "TH2 %X\n", pthread_self() );
sleep(5);
for( i = 0; i < 10; i++ )
printf( "TH2 %X\n", pthread_self() );
}
int main( int argc, char** argv )
{
printf( "Main thread: %X\n", pthread_self() );
pthread_create( &th1, 0, TH1, 0 );
pthread_create( &th2, 0, TH2, 0 );
sleep( 600 );
}
Если Вы видите надпись TH2... менее чем 20 раз — значит потоки реализованы в пространстве пользователя.
Методы борьбы: переходить на pthread или не допускать зависания потоков.
5. Mutex
В Windows, mutex созданый по умолчанию ведет себя рекурсивно. Т.е. сколько раз один и тот же поток вызвал для него lock, столько же раз должен быть вызван unlock.
pthread_mitex, по умолчанию, ведет себя иначе. Для того что бы его поведение соответствовало Windows mutex (по умолчанию) необходимо создать его следующим образом:
pthread_mutexattr_t attrs;
pthread_mutexattr_init(&attrs);
pthread_mutexattr_settype(&attrs, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init( &m_Mutex, &attrs );
6. Исключения C++
В Windows системные исключения и исключения C++ смешаны (это точно, если вы пользуетесь MS VC). Например, сбой по памяти, попытка деления на 0, и пр. ловятся с помощью catch(...). В Unix это не так (точно, если использовать gcc). В таких случаях приложению посылаются сигналы.
В большинстве случаев, после такого сигнала приложение можно только закрыть.
Windows код:
try
{
char buffer[1024];
strcpy( buffer, str );
}
catch(...)
{
// Внутреняя ошибка. Ну и черт с ней. Работаем дальше.
}
Unix код:
try
{
char buffer[1024];
strcpy( buffer, str );
}
catch(...)
{
// Здесь мы никогда не будем. И скорей всего, приложение уже закрыто. Премии мне не видать.
}
Методы борьбы: писать приложение так, что бы не допустить подобных ситуаций.
P.S. Все что касается сокетов — относится к TCP соединениям. Причем считается, что все используется по умолчанию. Т.е. без SO_LINGER и т.д.
P.P.S. Тема остается открытой для тех, кому есть что добавить.
оригинал —
http://bugtraq.ru/cgi-bin/forum.mcgi?type=sb&b=2&m=80610&id=2329&cp=5BQFsfAohTWhw