Задать вопрос

Синхронизация в Си или чем отличается вывод в файл от вывода в stdout

Привет всем!

В университете задали написать программу на Си. Суть такова:
./lastmsg [-i id] [-c|-d] ["Message"]
Если передаётся флаг -c, то создаётся новый сегмент памяти (Shared memory). Если передается «Message», то на stdout выводится то сообщение, которое было записано в shared memory, а потом оно заменятеся тем Message, который передался программе. Идея в том, что надо синхронизировать доступ в этот сегмент. Флаг -i задает id сегмента — это не особо важно в данном случае. Вроде программу написал правильно. Следующий код всегда выводит по три раза буквы a, b, c1234 один раз):
#!/bin/sh
./lastmsg -c 1234
(
for i in `seq 3`; do ./lastmsg a & ./lastmsg b & ./lastmsg c & done; wait
./lastmsg -d
)


Теперь начинается сама проблема. Когда я пытаюсь перенаправить stdout в файл, то «синхронизация» магическим образом пропадает, т.е.:
#!/bin/sh
./lastmsg -c 1234
(
for i in `seq 3`; do ./lastmsg a & ./lastmsg b & ./lastmsg c & done; wait
./lastmsg -d
) > temp
grep -c a temp
grep -c b temp
grep -c c temp
rm -f temp

Должно выводить в консоль: 3 3 3 (на новой строке каждый раз), но выводит иногда 2 3 2, иногда 2 3 3 — т.е. каждый раз разные значенея (иногда и 3 3 3 выводит). Мне кажется, что проблема в операторе ">", но на 100% быть не могу уверен. Поиск по гуглу ничего не дал.

Каковы ваши мысли на этот счет? Есть ли способ решить проблему?

P.S. для чего нужно выводить в файл, если при выводе в консоль всё работает? Если я захочу проверить синхронизацию 1000 параллельных процессов (`seq 1000`), то это единственный способ как это можно проверить (ну не считать же 3000 символов вручную?)

Заранее спасибо!

UPD:
Вот собственно сама программа:
Скрытый текст
/**
* @file lastmsg.c
* @author Ivan Pryakin
* @date 10.12.2013
*
* @brief Main file
*
* @details This program takes creates a new shared memory segement. Its key can be specified with -i option, by default it is equal
* to student id. If -c option is given, it creates a new shared memory segment and attaches structure 'myshm' to it.
* If [Message] is specified it writes this string to shared memory segment, but before outputs whatever string that was
* there before + new line character. If -d option is specified, it deattaches and removes memory segment and outputs
* whatever string that was in there before.
**/

#include <stdio.h>
#include <sys/shm.h>
#include <seqev.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <signal.h>

#define MAX_DATA (50) /**< maximum length of a message */
#define PERMISSION (0600) /**< permissions for shared memory segment */
#define DEFAULT_KEY (1028223) /**< default key for shared memory segment */

static char* program; /**< program name (used only for usage() function) */

struct myshm {
	/* state:
	*	false = OK
	*	true  = TERMINATING
	*	
	* Its purpose is to avoid 'race conditions', i.e. when two cuncurrent processes receive SIGINT/SIGQUIT signal,
	* only one of them has to free resources.
	*/
	bool state;
	char data[MAX_DATA];
};

/* holds identifiers for sharedmemory segment/event counter/sequencer */
struct shm_keys 
{
	int shmid;
	eventcounter_t *event;
	sequencer_t *sequencer;
};

/* has two structures - one that holds data and one that holds IDs */
struct t_sharedmemory
{
	struct myshm *shared;
	struct shm_keys keys;
};

struct t_sharedmemory flags;


/**
* @brief this function writes how to use this program.
* @details it is executed when an unknown option is given or no <cmd> is given. Global variable: program
*/
static void usage(void);

/**
* @brief free resources
* @details removes shared memory segment, destroys event counter/sequencer
*/
void free_resources(void);

/**
* @brief signal handling
* @details when any of the two signals arrive (SIGINT or SIGQUIT), this function will be called
* it frees resources and quits a program with EXIT_FAILURE return value
* @param signum - not used
*/
void sig_handler(int signum);


/**
* @brief create/get shared memory segment, output the string in memory segement, overwrite it with new string
* @details creates a new shared memory segement. Its key can be specified with -i option, by default it is equal
* to student id. If -c option is given, it creates a new shared memory segment and attaches structure 'myshm' to it.
* If [Message] is specified it writes this string to shared memory segment, but before outputs whatever string that was
* there before + new line character. If -d option is specified, it deattaches and removes memory segment and outputs
* whatever string that was in there before. Main function uses declared structure with name 'flags'.
*/
int main (int argc, char **argv)
{	
	/* if SIGINT or SIGQUIT signal arrives, call function 'sig_handler' */
	signal(SIGINT, sig_handler);
	signal(SIGQUIT, sig_handler);
	
	program = argv[0];
	
	/* keys for shared memory segment/event counter/sequencer */
	key_t shm_id = DEFAULT_KEY;
	key_t evn_id = DEFAULT_KEY+1;
	key_t seq_id = DEFAULT_KEY+2;
	
	/* message to be written to shared memory segment*/
	char message[MAX_DATA];
	
	/* for syncronization */
	int ticket = -1;
	
	
	/* for options/argument handling */
	char c;
	char *end;
	bool create = false;
	bool delete = false;
	
	/* options/argument handling */
	while ((c = getopt (argc, argv, "i:cd")) != -1)
	{
		switch (c)
		{
			case 'i':
				shm_id = (key_t) strtol(optarg, &end, 10);
				evn_id = (key_t) (shm_id + 1);
				seq_id = (key_t) (evn_id + 1);
				if (*end)
				{
					(void)fprintf(stderr, "%s: error converting -i argument to long integer\n", program);
					return EXIT_FAILURE;
				}
				break;
			case 'c':
				create = true;
				break;
			case 'd':
				delete = true;
				break;
			case '?':
				usage();
				return EXIT_FAILURE;
			default:
				assert(0);
		}
	}
	
	if (create && delete)
	{
		usage();
		return EXIT_FAILURE;
	}
	
	/* on delete no message should be written to shared memory */
	if (delete)
	{
		message[0] = '\0';
	}
	else if (optind != argc) /* if message was given, copy it to variable 'message'*/
	{
		strncpy(message, argv[optind], MAX_DATA-1);
		if (strlen(argv[optind]) > MAX_DATA-1)
			message[MAX_DATA] = '\0';
	}
	
	if (!(create || delete) && message[0] == '\0')
	{
		usage();
		return EXIT_FAILURE;
	}
	
	/* get a shared memory region */
	flags.keys.shmid = shmget (shm_id, sizeof(*flags.shared), IPC_CREAT | PERMISSION);
	if (flags.keys.shmid == -1)
	{
		(void)fprintf(stderr, "%s: error creating shared memory segment\n", program);
		return EXIT_FAILURE;
	}
	
	/* attach shared memory segment to the address space of the calling process */
	flags.shared = shmat(flags.keys.shmid, NULL, 0);
	if (flags.shared == (struct myshm *)-1)
	{
		(void)fprintf(stderr, "%s: error attaching memory segment\n", program);
		return EXIT_FAILURE;
	}
	
	/* create event counter */
	flags.keys.event = create_eventcounter(evn_id);
	if (flags.keys.event == NULL)
	{
		(void)fprintf(stderr, "%s: error creating eventcounter\n", program);
		return EXIT_FAILURE;
	}
	
	/* create sequencer */	
	flags.keys.sequencer = create_sequencer(seq_id);
	if (flags.keys.sequencer == NULL)
	{
		(void)fprintf(stderr,"%s: error creating sequencer\n", program);
		return EXIT_FAILURE;
	}
	
	/* get a ticket and wait if neccessary */
	ticket = sticket(flags.keys.sequencer);
	eawait(flags.keys.event, ticket);
	
	if (!create)
	{
		fflush(stdout);
		(void)fprintf(stdout, "%s\n", flags.shared->data);
	}
	
	if ((message[0] != '\0') && (!delete))
		strcpy(flags.shared->data, message);
	
	/* critical section end */
	
	/* advance eventcounter, so that other concurrent process (that is currently waiting)
	 * can start execution of critical section
	 */
	if (delete)
	{
		free_resources();
		return EXIT_SUCCESS;
	}
	//(void) fprintf(stderr,"eventcounter=%ld ticket=%d message=%s\n\n", eread(flags.keys.event), ticket, message);
	eadvance(flags.keys.event);
	
	
	
	return EXIT_SUCCESS;
}


static void usage(void)
{
	(void) fprintf(stderr, "Usage: %s [-i id] [-c|-d] [\"Message\"]\n", program);
}

void free_resources(void)
{	
	/* detach from shared memory */
	if (shmdt(flags.shared) == -1)
	{
		(void)fprintf(stderr, "%s: error detaching shared memory segment\n", program);
		exit(EXIT_FAILURE);
	}
	
	/* mark segment to be destroyed */
	if (shmctl(flags.keys.shmid, IPC_RMID, NULL) == -1)
	{
		(void)fprintf(stderr, "%s: error removing shared memory segment\n", program);
		exit(EXIT_FAILURE);
	}
	
	/* remove event counter */	
	if (rm_eventcounter(flags.keys.event) == -1)
	{
		(void)fprintf(stderr, "%s: error removing event counter\n", program);
		exit(EXIT_FAILURE);
	}
	
	/* remove sequence counter*/
	if (rm_sequencer(flags.keys.sequencer) == -1)
	{
		(void)fprintf(stderr, "%s: error removing sequencer\n", program);
		exit(EXIT_FAILURE);
	}
}

void sig_handler(int signum)
{
	/* avoid race conditions */
	if (flags.shared->state) return;
	flags.shared->state = true;
	
	/* free resources and exit with error */
	free_resources();
	(void) fprintf(stderr, "%s:signal caught...terminating\n", program);
	exit(EXIT_FAILURE);
}

Или кто препдопичатет через пастебин: pastebin.com/267DRUK0

Makefile (компилируется без предупреждений):
Скрытый текст
CC = gcc
CFLAGS = -std=c99 -Wall -g -pedantic -DENDEBUG -D_BSD_SOURCE -D_XOPEN_SOURCE=500
LFLAGS = -lseqev

all: lastmsg

lastmsg: lastmsg.c
	$(CC) $(CFLAGS) -o lastmsg lastmsg.c $(LFLAGS)
	
clean:
	rm -f lastmsg 

Пояснения к коду:
Наиболее важными являются строки 201-224 — это то место, где и происходит синхронизация процессов, вывод в консоль на 207 строке, запись в память в 211.
fflush(stdout); не помог, но вывод на stderr (вместо stdout) принес результаты — при `seq 3` синхронизируется отлично (3 3 3 каждый раз), но вот при `seq 1000` уже идет расхождение (970 980 970 примерно такие числа). Отсюда следует, что ошибка всё-таки в программе. Я правильно понял?
В программе для синхронизации используется https://github.com/osue-tuwien/SE библиотека (её и надо использовать по заданию). Её придется дополнительно установить и потом через линкинг присоеденить к программе (поэтому в Makefille -lseqev опция стоит).
  • Вопрос задан
  • 4182 просмотра
Подписаться 4 Оценить 1 комментарий
Решения вопроса 1
@lesha_penguin
1. Стандартный вывод никак не отличается от остальных файлов. Собственно, стандартным выводом может быть любой файл, терминал, пайп, сокет… и т.д. Это на уровне самой «оси».

2. Но ваша «проблема» — не на уровне оси, а на уровне абстракции FILE* из «stdio.h». Ваша проблема это «проблема» буферизации ввода/вывода. Ввод/вывод который «stdio.h» он идет не напрямую, а через буфер. Который может быть «без буферизации» / «построчная буферизация» / «полная буферизация». И это не баг а фича, буферизация способна нехило увеличить производительность ввода вывода.
Когда библиотека времени исполнения инициализирует stdin,stdout,stderr для дескрипторов 0,1,2 она смотрит на типы файлов (спрашивает у оси). И если это «терминал» то включает построчную буферизацию (т.е. системный вызов write() реально происходит когда заполняется строка до '\n'), а если «файл» то полную (т.е. пока буфер не заполнится его в файл не сбрасываем, либо по явному вызову fflush(sdtout);).

3. Собственно, вам поможет явное выполнение:
setlinebuf(stdout); // Включаем построчную буферизацию, как для терминала


Все! Вы конечно, можете вообще выключить буферизацию, но этого не стоит делать.
Ответ написан
Пригласить эксперта
Ответы на вопрос 4
deleted-mifki
@deleted-mifki
> то это единственный способ как это можно проверить (ну не считать же 3000 символов вручную?)

скопируйте из консоли, сохраните в файл, да проверяйте…
Ответ написан
CleverMouse
@CleverMouse
Попробуйте в shell-скрипте открывать файл не через ">", а через ">>", убедившись, что перед операцией файла не существует.
Возможно, различные процессы каким-то образом получают несвязанные копии дескриптора файла, в которых позиция в файле не обновляется при печати из других процессов; тогда процесс 1 может напечатать свою букву a, а потом процесс 2 печатает свою букву b на ту же позицию в файле, затирая a от первого процесса. С консолью такой проблемы нет, потому что она не seekable, в ней нет позиции как таковой. Открывание файла для дозаписи преследует ту же цель — перед каждой операцией записи позиция будет устанавливаться на конец файла.
Ответ написан
jcmvbkbc
@jcmvbkbc
"I'm here to consult you" © Dogbert
Налицо две проблемы:
— если запускать скрипт как есть общее количество строк в temp не всегда равно 3 * n + 1. Это проблема записи в один файл без O_APPEND. Решается заменой > temp на >> temp, как предложил CleverMouse
— даже когда общее количество строк правильное, количество вхождений разных символов бывает разным. Это явно проблема кода, либо lastmsg, либо libseqev.
Ответ написан
vsespb
@vsespb
может fflush(stdout)
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы