简体   繁体   中英

C Linux program using named pipes works as expected when one process at a time writes into the FIFO, but breaks when more instances do it

My program has a consumer and multiple producers. The producers each read a different file and write their content into a FIFO in N-sized chunks, with a leading parameter for the consumer to interpret.

The consumer is supposed to take these chunks and compose an output file where each line corresponds to one producer. The leading parameter from the chunk is used to determine the owner of the chunk and where to write it (it's a line number number in the output file).

My problem is, even though it works mostly fine when there's one producer, any more make the resulting file a mess. Also there are some unexpected excessive \n but they aren't critical.

This is my expected output:

aaaaa1a aaaaaaa2a aaa3a aaaaaaaaaaa4a
bbbbbbbbbbb1b bbbbbbb2b bbbbbbbbbbbbbb3b bbbbbbb4b bbbbbbbbbb5b bb6b
cccccccccc1c cccc2c cccccccc3c ccccc4c ccccccccc5c ccccccccccccc6c

but that's what I get:

aaaaa1a aaaaaaa2a aaa3a aaaaaaaaaaa4a2  bbbbbbb43 cccccccc53  cccccccc2  bbbbbbbb2 b5b bb6b3 cccc6c2
bbbbbbbbbbb1b bbbbbbb2b bbbbbbbbbbbbbb3b
cccccccccc1c cccc2c cccccccc3c ccccc4c c

There's an unexpected cutoff in the later lines and the chunks become mixed up.

I think it's a problem with how I handle the named pipes, because I'm printing the "raw input" before further processing and I can see that I'm reading invalid data from the pipe. But AFAIK Linux has atomic writes for small chunks of data for FIFO. Maybe the reads aren't caring about the writes and that's where lies the problem?

Consumer code:

#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

char *filename;

size_t getFileSize(FILE *fp) {
    fseek(fp, 0, SEEK_END);
    size_t len = ftell(fp);
    rewind(fp);
    printf("length %ld \n", len);
    return len;
}
int nthFunctionCall = 1;

void printFile(FILE *file) {
    char *fileContent = NULL;
    if(file != NULL) {
        size_t size = getFileSize(file);
        fileContent = malloc((size /* + 1*/) * sizeof(char));
        fread(fileContent, 1, size, file);
        //fileContent[size + 1] = '\0'; ?
    }
    printf("FILE CONTENT: \n%s\n", fileContent);
}

void writeToFile(long targetLineNumber, char *text) {
    FILE *temp = fopen("temp", "w");
    if(temp == NULL) {
        perror("can't create temp");
        exit(-1);
    }
    char *fileContents = NULL;
    FILE *file = fopen(filename, "r");
    if(file != NULL) {
        size_t size = getFileSize(file);
        fileContents = malloc((size + 1) * sizeof(char));
        fread(fileContents, 1, size, file);
        fileContents[size] = '\0'; // tbh, I don't know whether I should do this or not.
        fclose(file);
    }
    char *fileContentsCpy = fileContents;

    printf("FILE CONTENT:\n %s\n", fileContents);

    printf("%d Text to save %s\n", nthFunctionCall, text);

    char *currentLineFromFile;
    size_t processedLineNumber;
    for (processedLineNumber = 1; (currentLineFromFile = strsep(&fileContents, "\n")) != NULL; processedLineNumber++) {
        printf("%d targetLineNumber %ld processedLineNumber %ld \n", nthFunctionCall, targetLineNumber, processedLineNumber);
        printf("%d copy the current line into temp: %s\n", nthFunctionCall, currentLineFromFile);
        fputs(currentLineFromFile, temp);
        if(processedLineNumber == targetLineNumber) {
            printf("%d add text to line %ld: %s\n", nthFunctionCall, processedLineNumber, text);
            fputs(text, temp);
        }
        fputs("\n", temp);
        fflush(temp);
    }

    printf("%d Finished loop with: targetLineNumber %ld processedLineNumber %ld \n", nthFunctionCall, targetLineNumber, processedLineNumber);

    if(targetLineNumber >= processedLineNumber) {
        for (int j = 0; j < (targetLineNumber - processedLineNumber); ++j) {
            fputs("\n", temp);
        }
        printf("%d added text: %s\n", nthFunctionCall, text);
        fputs(text, temp);
        fflush(temp);
    }
    fclose(temp);

    if(fileContentsCpy != NULL) free(fileContentsCpy);
    nthFunctionCall++;
    remove(filename);
    rename("temp", filename);
    printf("One iteration end\n");
}

int numberLength(size_t number) {
    int len = 0;
    while(number > 0) {
        number /= 10;
        len++;
    }
    return len;
}

int main(int argc, char **argv)
{
    if (argc < 4) {
        fprintf(stderr, "testConsument <fifo_path> <file_to_save_in> <chunk size>\n");
        exit(-1);
    }

    char *myfifo = argv[1];
    filename = argv[2];
    int numberToRead = atoi(argv[3]);

    int fd = open(myfifo, O_RDONLY);
    perror("sdada test consument");
    char *str1 = calloc(100, sizeof(char));
    while (read(fd, str1, numberToRead + 3) > 0) {
        long lineNumber;
        printf("length: %ld raw input: %s\n", strlen(str1), str1);
        sscanf(str1, "%ld", &lineNumber);
        char* content = str1 + numberLength(lineNumber) + 1; // lines should be of the format "<number> <chunk-sized-word>\0"
        printf("add to line %ld content : %s \n", lineNumber, content);
        writeToFile(lineNumber, content);
        sleep(1);
        free(str1);
        str1 = calloc(100, sizeof(char));
        printf("#################\n");
    }
    free(str1);
    close(fd);

    FILE *res = fopen(filename, "r");
    printFile(res);
    fclose(res);
    return 0;
}

Producer code:

#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>

size_t getFileSize(FILE *fp) {
    fseek(fp, 0, SEEK_END);
    size_t len = ftell(fp);
    rewind(fp);
    return len;
}

int main(int argc, char **argv) {
    if (argc < 5) {
        fprintf(stderr, "producer <fifo_path> <line_number_to_save_in> <input_file> <chunk_size>\n");
        exit(-1);
    }

    char *myfifo = argv[1];
    char *lineNumber = argv[2];
    int numberToRead = atoi(argv[4]);

    mkfifo(myfifo, 0666);
    int fd = open(myfifo, O_WRONLY);

    char *someFilePath = argv[3];
    FILE *somFile = fopen(someFilePath, "r");
    char *buf = calloc(numberToRead, sizeof(char));
    size_t size = 1;
    while ((fread(buf, size, numberToRead, somFile) > 0)) {
        char *buf2 = calloc((numberToRead + 3), sizeof(char));
        strcat(buf2, lineNumber); strcat(buf2, " "); strcat(buf2, buf); strcat(buf2, "\0");
        while (strstr(buf2, "\n")) {
            buf2[strcspn(buf2, "\n")] = ' ';
        }
        printf("SENDING: %s\n", buf2);
        fflush(stdout);
        write(fd, buf2, numberToRead + 3);
        sleep(2);
        free(buf);
        free(buf2);
        buf = calloc(numberToRead, sizeof(char));
    }
    write(fd, lineNumber, 2);
    close(fd);
    return 0;
}

After running both the producer and the consumer the communication should start working and after some time there should be an output file. After each such execution you have to manually remove the file, because I didn't really consider the situation where it has existed before.

Example start (each line should be in a different terminal):

./producer '/tmp/fifo3' 3 'file1' 10
./producer '/tmp/fifo3' 2 'file1' 10
./producer '/tmp/fifo3' 1 'file1' 10
./testConsument '/tmp/fifo3' 'output' 10

There are a lot of debug prints, I'm not sure if they are helpful or not but I'm leaving them in.

The problem laid with the line

    write(fd, lineNumber, 2);

near the end in the producer program. It sent unnecessary data which wasn't meaningful in any way and wasn't interpreted properly.

After removing it the program works as intended (except for the unexpected new lines, but they aren't that bad and they have happened before).

The problem you face is that having several producers attached to a shared resource (the fifo) you need to control how the accesses are done to be able to control that the consumer gets the data in the proper sequence. The only help you get from the kernel is at the write(2) system call level (the kernel locks the inode of the destination fifo during the time the system call is being executed) So, if you are making short writes, the easiest approach is to group together all the data you are going to put in the fifo and write(2) it all in one single call to write.

If you opt for a more complex solution, then you need to use some kind of mutex/semaphore/whatever to control who has exclusive access to the fifo for writting, as other processes must wait for it to release the lock before starting to write.

Also, don't try to use stdio if you are using this approach. The stdio package only writes data when flushing a buffer, and this happens differently for the output terminal than for a fifo, it depends on the actua buffer size it is using and you don't have an exact and clear idea on when it is happening. This means you cannot use fprintf(3) and friends.

Finally, if you use the atomicity of write(2) , then have in mind that a fifo is a limited resource, that can buffer data, and will break a write(2) call if you try to write a big amount of data in one shot (this meaning a single write(2) call) you can get a partial write and you will not be able to recover from this because in the mean time other producers can have access to the fifo and be writting on it (which will break your writting structure) As a rule of thumb, try to recude your messages to a small number of kilobytes (4kb or 8kb being a good upper limit, to be portable to different unices)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM