Правильное использование close в C

#c #pipe #system-calls

#c #труба #системные вызовы

Вопрос:

Я в замешательстве относительно того, как правильно использовать close для закрытия каналов в C. Я довольно новичок в C, поэтому прошу прощения, если это слишком элементарно, но я не могу найти никаких объяснений в другом месте.

 #include <stdio.h>

int main()
{
    int fd[2];
    pipe(fd);
    if(fork() == 0) {
        close(0);
        dup(fd[0]);
        close(fd[0]);
        close(fd[1]);
    } else {
        close(fd[0]);
        write(fd[1], "hi", 2);
        close(fd[1]);
    }
    wait((int *) 0);
    exit(0);

}
  

Мой первый вопрос: в приведенном выше коде дочерний процесс закроет сторону записи fd . Если мы сначала достигнем close(fd[1]) , то родительский процесс reach write(fd[1], "hi", 2) , разве fd[1] он уже не был бы закрыт?

 int main()
{
    char *receive;
    int[] fd;
    pipe(fd);
    if(fork() == 0) {
       while(read(fd[0], receive, 2) != 0){
            printf("got u!n");
       }
    } else {
        for(int i = 0; i < 2; i  ){
            write(fd[1], 'hi', 2);
        }
        close(fd[1]);
    }
    wait((int *) 0);
    exit(0);

}
  

Второй вопрос: в приведенном выше коде возможно ли, чтобы мы достигли close(fd[1]) родительского процесса до того, как дочерний процесс завершит получение всего содержимого? Если да, то каков правильный способ связи между родителем и дочерним элементом. Насколько я понимаю, если мы не закроем fd[1] родительский файл, чтение будет продолжать блокироваться, и программа также не завершится.

Комментарии:

1. Файл на самом деле не закрывается, пока все процессы, имеющие ссылку на него, не закроют свои дескрипторы. Поэтому не имеет значения, в каком порядке происходят закрытия, потому что другой процесс сохраняет его открытым.

2. Дочерний процесс может закрывать свои файловые дескрипторы, но это не отделяет файловые дескрипторы родительского файла от базового описания открытого файла в ядре. И наоборот .

3. Ваш дочерний процесс не считывает данные из канала, но может. Ваш родительский процесс выполняет запись в дочерний процесс. Закрытие выполняется правильно при всех нормальных обстоятельствах. В ненормальных обстоятельствах использование dup2() может быть безопаснее, но мы говорим об очень ненормальных обстоятельствах.

4. В вашей второй программе у вас есть неинициализированный указатель. Чтение этого приведет к несчастью.

Ответ №1:

Прежде всего, обратите внимание, что после fork() этого файловые дескрипторы fd также будут скопированы в дочерний процесс. Таким образом, по сути, a pipe действует как файл, где каждый процесс имеет свои собственные ссылки на конец чтения и записи pipe . По сути, существует 2 файловых дескриптора для чтения и 2 для записи, по одному для каждого процесса.

Мой первый вопрос: в приведенном выше коде дочерний процесс закроет сторону записи fd. Если мы сначала достигнем close(fd[1]), затем родительский процесс достигнет write(fd [1], «hi», 2), разве fd [1] уже не был бы закрыт?

Ответ: Нет. Родительский процесс fd[1] в родительском процессе — это конец записи родительского процесса. Дочерний элемент отказался от своего права на запись в канал, закрыв его fd[1] , что не мешает родительскому элементу записывать в него.

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

 int main()
{
    char receive[10];
    int fd[2];
    pipe(fd);
    if(fork() == 0) {
        close(fd[1]);      <-- Close UNUSED write end
       while(read(fd[0], receive, 2) != 0){
            printf("got u!n");
            receive[2] = '';
            printf("%sn", receive);
       }
       close(fd[0]);       <-- Close read end after reading
    } else {
        close(fd[0]);      <-- Close UNUSED read end
        for(int i = 0; i < 2; i  ){
            write(fd[1], "hi", 2);
        }
        close(fd[1]);      <-- Close write end after writing

        wait((int *) 0);
    }
    exit(0);

}
  

Результат:

 got u!
hi
got u!
hi
  

Примечание: Мы (по-видимому) потеряли hi его, потому что мы считываем его в тот же массив receive , который по существу переопределяет первый hi . Вы можете использовать 2D-массивы символов для сохранения обоих сообщений.

Второй вопрос: в приведенном выше коде возможно ли для нас достичь close(fd[1]) в родительском процессе до того, как дочерний процесс завершит получение всего содержимого?

Ответ: Да. Запись в a pipe() не блокируется (если не указано иное) до тех пор, пока буфер канала не будет заполнен.

Если да, то каков правильный способ связи между родителем и дочерним элементом. Я понимаю, что если мы не закроем fd[1] в родительском, то чтение будет продолжать блокироваться, и программа также не завершится.

Если мы закроем fd[1] в parent , это будет сигнализировать о том, что parent закрыл свой конец записи. Однако, если дочерний элемент не закрыл его fd[1] раньше, он будет заблокирован read() , поскольку канал не будет отправлять EOF , пока все концы записи не будут закрыты. Таким образом, дочерний элемент будет ожидать, что он будет записывать в pipe , одновременно читая из него!

Теперь, что произойдет, если родительский элемент не закроет свой неиспользуемый конец чтения? Если в файле был только один дескриптор чтения (скажем, с дочерним), то, как только дочерний элемент закроет его, родительский элемент получит некоторый сигнал или ошибку при попытке дальнейшей записи pipe в, поскольку считывателей нет.

Однако в этой ситуации у parent также открыт дескриптор чтения, и он сможет выполнять запись в буфер до тех пор, пока он не будет заполнен, что может вызвать проблемы при следующем write вызове, если таковые имеются.

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

Комментарии:

1. Большое вам спасибо, и я многому научился! Но, учитывая вашу новую версию кода, если родительский процесс сначала закроет fd [1], не сможет ли дочерний процесс не прочитать весь отправляемый контент? Я предполагаю, что мой вопрос таков: прекратит ли «чтение» чтение сразу после закрытия обоих каналов или будет продолжать считывать все содержимое, уже находящееся в канале, до закрытия канала?

2. Чтение из канала не остановится, пока буфер считывателя не будет заполнен (прочитано все из канала) или EOF не будет получено. Родительское закрытие конца записи не помешает дочернему элементу прочитать все, что уже присутствует в канале. Однако родительский элемент, не закрывающий свой конец записи, заблокирует дочерний элемент при read вызове, поскольку он не будет получать EOF и продолжать ожидать содержимого в канале.

3. Также обратите внимание, что EOF он отправляется через канал считывателям только тогда, когда все его доступные концы записи закрыты. Так что это также причина, по которой вы должны сначала закрыть неиспользуемый конец записи в дочернем элементе. Не стесняйтесь задавать любые дополнительные разъяснения.

Ответ №2:

каков правильный способ связи между родительским и дочерним [?]

Родительский элемент создает трубу перед разветвлением. После разветвления родительский и дочерний элементы закрывают конец канала, который они не используют (каналы следует считать однонаправленными; создайте два, если хотите двунаправленную связь). У каждого процесса есть своя собственная копия каждого файлового дескриптора на конце канала, поэтому эти закрытия не влияют на способность другого процесса использовать канал. Затем каждый процесс использует конец, который он держит открытым, соответствующим его направленности — запись в конец записи или чтение с конца чтения.

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

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