Купить
 
 
Жанр: Учеба

Хрестоматия по программированию на Си в Unix

страница №15

d = head-"prev;
if( head-"next )
head-"next-"prev = head-"next-"next;
}
}

5.12. Напишите программу, составлящую двунаправленный список букв, вводимых с клавиатуры.
Конец ввода - буква '\n'. После третьей буквы вставьте букву '+'. Удалите
пятую букву. Распечатайте список в обратном порядке. Оформите операции
вставки/удаления как функции. Элемент списка должен иметь вид:

struct elem{
char letter; /* буква */
char *word; /* слово */
struct elem *prev; /* ссылка назад */
struct elem *next; /* ссылка вперед */
};
struct elem *head, /* первый элемент списка */
*tail, /* последний элемент */
*ptr, /* рабочая переменная */
*prev; /* предыдущий элемент при просмотре */
int c, cmp;
while((c = getchar()) != '\n' )
Insert(c, tail);
for(ptr=head; ptr != NULL; ptr=ptr-"next)
printf("буква %c\n", ptr-"letter);

Память лучше отводить не из массива, а функцией calloc(), которая аналогична функции
malloc(), но дополнительно расписывает выделенную память байтом '\0' (0, NULL). Вот
функции вставки и удаления:

extern char *calloc();
/* создать новое звено списка для буквы c */
struct elem *NewElem© char c; {
struct elem *p = (struct elem *)
calloc(1, sizeof(struct elem));
/* calloc автоматически обнуляет все поля,
* в том числе prev и next
*/
p-"letter = c; return p;
}

А. Богатырев, 1992-95 - 180 - Си в UNIX

/* вставка после ptr (обычно - после tail) */
Insert(c, ptr) char c; struct elem *ptr;
{ struct elem *newelem = NewElem©, *right;
if(head == NULL){ /* список был пуст */
head=tail=newelem; return; }
right = ptr-"next; ptr-"next = newelem;
newelem-"prev = ptr; newelem-"next = right;
if( right ) right-"prev = newelem;
else tail = newelem;
}

/* удалить ptr из списка */
Delete( ptr ) struct elem *ptr; {
struct elem *left=ptr-"prev, *right=ptr-"next;
if( right ) right-"prev = left;
if( left ) left-"next = right;
if( tail == ptr ) tail = left;
if( head == ptr ) head = right;
free((char *) ptr);
}

Напишите аналогичную программу для списка слов.

struct elem *NewElem(char *s) {
struct elem *p = (struct elem *)
calloc(1, sizeof(struct elem));
p-"word = strdup(s);
return p;
}
void DeleteElem(struct elem *ptr){
free(ptr-"word);
free(ptr);
}

Усложнение: вставляйте слова в список в алфавитном порядке. Используйте для этого
функцию strcmp(), просматривайте список так:

struct elem *newelem;

if (head == NULL){ /* список пуст */
head = tail = NewElem(новое_слово);
return;
}
/* поиск места в списке */
for(cmp= -1, ptr=head, prev=NULL;
ptr;
prev=ptr, ptr=ptr-"next
)
if((cmp = strcmp(новое_слово, ptr-"word)) "= 0 )
break;

Если цикл окончился с cmp==0, то такое слово уже есть в списке. Если cmp " 0, то
такого слова не было и ptr указывает элемент, перед которым надо вставить слово
новое_слово, а prev - после которого (prev==NULL означает, что надо вставить в начало
списка); т.е. слово вставляется между prev и ptr. Если cmp " 0, то слово надо добавить
в конец списка (при этом ptr==NULL).

head ==" "a" ==" "b" ==" "d" ==" NULL
| |
prev "c" ptr

А. Богатырев, 1992-95 - 181 - Си в UNIX

if(cmp == 0) return; /* слово уже есть */
newelem = NewElem( новое_слово );
if(prev == NULL){ /* в начало */
newelem-"next = head;
newelem-"prev = NULL;
head-"prev = newelem;
head = newelem;
} else if(ptr == NULL){ /* в конец */
newelem-"next = NULL;
newelem-"prev = tail;
tail-"next = newelem;
tail = newelem;
} else { /* между prev и ptr */
newelem-"next = ptr;
newelem-"prev = prev;
prev-"next = newelem;
ptr -"prev = newelem;
}

5.13. Напишите функции для работы с комплексными числами

struct complex {
double re, im;
};

Например, сложение выглядит так:

struct complex add( c1, c2 )
struct complex c1, c2;
{
struct complex sum;
sum.re = c1.re + c2.re;
sum.im = c1.im + c2.im;
return sum;
}

struct complex a = { 12.0, 14.0 },
b = { 13.0, 2.0 };
main(){
struct complex c;
c = add( a, b );
printf( "(%g,%g)\n", c.re, c.im );
}

5.14. Массивы в Си нельзя присваивать целиком, зато структуры - можно. Иногда
используют такой трюк: структуру из единственного поля-массива

typedef struct {
int ai[5];
} intarray5;
intarray5 a, b = { 1, 2, 3, 4, 5 };

и теперь законно

a = b;

Зато доступ к ячейкам массива выглядит теперь менее изящно:

А. Богатырев, 1992-95 - 182 - Си в UNIX

a.ai[2] = 14;
for(i=0; i " 5; i++) printf( "%d\n", a.ai[i] );

Также невозможно передать копию массива в качестве фактического параметра функции.
Даже если мы напишем:

typedef int ARR16[16];
ARR16 d;
void f(ARR16 a){
printf( "%d %d\n", a[3], a[15]);
a[3] = 2345;
}
void main(void){
d[3] = 9; d[15] = 98;
f(d);
printf("Now it is %d\n", d[3]);
}

то последний printf напечатает "Now it is 2345", поскольку в f передается адрес массива,
но не его копия; поэтому оператор a[3]=2345 изменяет исходный массив. Обойти
это можно, использовав тот же трюк, поскольку при передаче структуры в качестве параметра
передается уже не ее адрес, а копия всей структуры (как это и принято в Си во
всех случаях, кроме массивов).

5.15. Напоследок упомянем про битовые поля - элементы структуры, занимающие только
часть машинного слова - только несколько битов в нем. Размер поля в битах задается
конструкцией :число_битов. Битовые поля используются для более компактного хранения
информации в структурах (для экономии места).

struct XYZ {
/* битовые поля должны быть unsigned */
unsigned x:2; /* 0 .. 2**2 - 1 */
unsigned y:5; /* 0 .. 2**5 - 1 */
unsigned z:1; /* YES=1 NO=0 */
} xyz;
main(){
printf("%u\n", sizeof(xyz)); /* == sizeof(int) */
xyz.z = 1; xyz.y = 21; xyz.x = 3;
printf("%u %u %u\n", xyz.x, ++xyz.y, xyz.z);

/* Значение битового поля берется по модулю
* максимально допустимого числа 2**число_битов - 1
*/
xyz.y = 32 /* максимум */ + 7; xyz.x = 16+2; xyz.z = 11;
printf("%u %u %u\n", xyz.x, xyz.y, xyz.z); /* 2 7 1 */
}

Поле ширины 1 часто используется в качестве битового флага: вместо

#define FLAG1 01
#define FLAG2 02
#define FLAG3 04
int x; /* слово для нескольких флагов */
x |= FLAG1; x &= ~FLAG2; if(x & FLAG3) ...;

используется

struct flags {
unsigned flag1:1, flag2:1, flag3:1;
} x;
x.flag1 = 1; x.flag2 = 0; if( x.flag3 ) ...;

А. Богатырев, 1992-95 - 183 - Си в UNIX

Следует однако учесть, что машинный код для работы с битовыми полями более сложен и
занимает больше команд (т.е. медленнее и длиннее).
К битовым полям нельзя применить операцию взятия адреса "&", у них нет адресов и
смещений!

5.16. Пример на использование структур с полем переменного размера. Часть переменной
длины может быть лишь одна и обязана быть последним полем структуры. Внимание:
это программистский трюк, использовать осторожно!

#include "stdio.h"
#define SZ 5
extern char *malloc();
#define VARTYPE char

struct obj {
struct header { /* постоянная часть */
int cls;
int size; /* размер переменной части */
} hdr;

VARTYPE body [1]; /* часть переменного размера:
в описании ровно ОДИН элемент массива */
} *items [SZ]; /* указатели на структуры */

#define OFFSET(field, ptr) ((char *) &ptr-"field - (char *)ptr)
int body_offset;

/* создание новой структуры */
struct obj *newObj( int cl, char *s )
{
char *ptr; struct obj *op;
int n = strlen(s); /* длина переменной части (штук VARTYPE) */
int newsize = sizeof(struct header) + n * sizeof(VARTYPE);

printf("[n=%d newsize=%d]\n", n, newsize);

/* newsize = (sizeof(struct obj) - sizeof(op-"body)) + n * sizeof(op-"body);

При использовании этого размера не учитывается, что struct(obj)
выровнена на границу sizeof(int).
Но в частности следует учитывать и то, на границу чего выровнено
начало поля op-"body. То есть самым правильным будет

newsize = body_offset + n * sizeof(op-"body);

*/

/* отвести массив байт без внутренней структуры */
ptr = (char *) malloc(newsize);

/* наложить поверх него структуру */
op = (struct obj *) ptr;

op-"hdr.cls = cl;
op-"hdr.size = n;

strncpy(op-"body, s, n);

return op;
}

А. Богатырев, 1992-95 - 184 - Си в UNIX

void printobj( struct obj *p )
{
register i;

printf( "OBJECT(cls=%d,size=%d)\n", p-"hdr.cls, p-"hdr.size);
for(i=0; i " p-"hdr.size; i++ )
putchar( p-"body[i] );
putchar( '\n' );
}

char *strs[] = { "a tree", "a maple", "an oak", "the birch", "the fir" };

int main(int ac, char *av[]){
int i;

printf("sizeof(struct header)=%d sizeof(struct obj)=%d\n",
sizeof(struct header), sizeof(struct obj));

{
struct obj *sample;
printf("offset(cls)=%d\n", OFFSET(hdr.cls, sample));
printf("offset(size)=%d\n", OFFSET(hdr.size, sample));
printf("offset(body)=%d\n", body_offset = OFFSET(body, sample));
}

for( i=0; i " SZ; i++ )
items[i] = newObj( i, strs[i] );

for( i=0; i " SZ; i++ ){
printobj( items[i] ); free( items[i] ); items[i] = NULL;
}
return 0;
}

5.17. Напишите программу, реализующую список со "старением". Элемент списка, к
которому обращались последним, находится в голове списка. Самый старый элемент
вытесняется к хвосту списка и в конечном счете из списка удаляется. Такой алгоритм
использует ядро UNIX для кэширования блоков файла в оперативной памяти: блоки, к
которым часто бывают обращения оседают в памяти (а не на диске).

/* Список строк, упорядоченных по времени их добавления в список,
* т.е. самая "свежая" строка - в начале, самая "древняя" - в конце.
* Строки при поступлении могут и повторяться! По подобному принципу
* можно организовать буферизацию блоков при обмене с диском.
*/

#include "stdio.h"
extern char *malloc(), *gets();
#define MAX 3 /* максимальная длина списка */
int nelems = 0; /* текущая длина списка */

struct elem { /* СТРУКТУРА ЭЛЕМЕНТА СПИСКА */
char *key; /* Для блоков - это целое - номер блока */
struct elem *next; /* следующий элемент списка */
/* ... и может что-то еще ... */
} *head; /* голова списка */

void printList(), addList(char *), forget();

А. Богатырев, 1992-95 - 185 - Си в UNIX

void main(){ /* Введите a b c d b a c */
char buf[128];
while(gets(buf)) addList(buf), printList();
}

/* Распечатка списка */
void printList(){ register struct elem *ptr;
printf( "В списке %d элементов\n", nelems );
for(ptr = head; ptr != NULL; ptr = ptr-"next )
printf( "\t\"%s\"\n", ptr-"key );
}

/* Добавление в начало списка */
void addList(char *s)
{ register struct elem *p, *new;
/* Анализ - нет ли уже в списке */
for(p = head; p != NULL; p = p-"next )
if( !strcmp(s, p-"key)){ /* Есть. Перенести в начало списка */
if( head == p ) return; /* Уже в начале */
/* Удаляем из середины списка */
new = p; /* Удаляемый элемент */
for(p = head; p-"next != new; p = p-"next );
/* p указывает на предшественника new */
p-"next = new-"next; goto Insert;
}
/* Нет в списке */
if( nelems "= MAX ) forget(); /* Забыть старейший */
if((new = (struct elem *) malloc(sizeof(struct elem)))==NULL) goto bad;
if((new-"key = malloc(strlen(s) + 1)) == NULL) goto bad;
strcpy(new-"key, s); nelems++;
Insert: new-"next = head; head = new; return;
bad: printf( "Нет памяти\n" ); exit(13);
}

/* Забыть хвост списка */
void forget(){ struct elem *prev = head, *tail;
if( head == NULL ) return; /* Список пуст */
/* Единственный элемент ? */
if((tail = head-"next) == NULL){ tail=head; head=NULL; goto Del; }
for( ; tail-"next != NULL; prev = tail, tail = tail-"next );
prev-"next = NULL;
Del: free(tail-"key); free(tail); nelems--;
}

А. Богатырев, 1992-95 - 186 - Си в UNIX

6. Системные вызовы и взаимодействие с UNIX.
В этой главе речь пойдет о процессах. Скомпилированная программа хранится на
диске как обычный нетекстовый файл. Когда она будет загружена в память компьютера и
начнет выполняться - она станет процессом.
UNIX - многозадачная система (мультипрограммная). Это означает, что одновременно
может быть запущено много процессов. Процессор выполняет их в режиме разделения
времени - выделяя по очереди квант времени одному процессу, затем другому,
третьему... В результате создается впечатление параллельного выполнения всех процессов
(на многопроцессорных машинах параллельность истинная). Процессам, ожидающим
некоторого события, время процессора не выделяется. Более того, "спящий" процесс
может быть временно откачан (т.е. скопирован из памяти машины) на диск, чтобы освободить
память для других процессов. Когда "спящий" процесс дождется события, он будет
"разбужен" системой, переведен в ранг "готовых к выполнению" и, если был откачан -
будет возвращен с диска в память (но, может быть, на другое место в памяти!). Эта
процедура носит название "своппинг" (swapping).
Можно запустить несколько процессов, выполняющих программу из одного и того же
файла; при этом все они будут (если только специально не было предусмотрено иначе)
независимыми друг от друга. Так, у каждого пользователя, работающего в системе, имеется
свой собственный процесс-интерпретатор команд (своя копия), выполняющий программу
из файла /bin/csh (или /bin/sh).
Процесс представляет собой изолированный "мир", общающийся с другими "мирами" во
Вселенной при помощи:
a) Аргументов функции main:
void main(int argc, char *argv[], char *envp[]);
Если мы наберем команду
$ a.out a1 a2 a3
то функция main программы из файла a.out вызовется с

argc = 4 /* количество аргументов */
argv[0] = "a.out" argv[1] = "a1"
argv[2] = "a2" argv[3] = "a3"
argv[4] = NULL

По соглашению argv[0] содержит имя выполняемого файла из которого загружена эта
программа[*].
b) Так называемого "окружения" (или "среды") char *envp[], продублированного также
в предопределенной переменной
extern char **environ;
Окружение состоит из строк вида
"ИМЯПЕРЕМЕННОЙ=значение"
Массив этих строк завершается NULL (как и argv). Для получения значения пере-
менной с именем ИМЯ существует стандартная функция
char *getenv( char *ИМЯ );
Она выдает либо значение, либо NULL если переменной с таким именем нет.
c) Открытых файлов. По умолчанию (неявно) всегда открыты 3 канала:
ВВОД В Ы В О Д
FILE * stdin stdout stderr
соответствует fd 0 1 2
связан с клавиатурой дисплеем

____________________
[*] Именно это имя показывает команда ps -ef

#include "stdio.h"
main(ac, av) char **av; {
execl("/bin/sleep", "Take it easy", "1000", NULL);
}

А. Богатырев, 1992-95 - 187 - Си в UNIX

Эти каналы достаются процессу "в наследство" от запускающего процесса и связаны
с дисплеем и клавиатурой, если только не были перенаправлены. Кроме того, прог-
рамма может сама явно открывать файлы (при помощи open, creat, pipe, fopen).

Всего программа может одновременно открыть до 20 файлов (считая стандартные
каналы), а в некоторых системах и больше (например, 64). В MS DOS есть еще 2
предопределенных канала вывода: stdaux - в последовательный коммуникационный
порт, stdprn - на принтер.
d) Процесс имеет уникальный номер, который он может узнать вызовом
int pid = getpid();
а также узнать номер "родителя" вызовом
int ppid = getppid();
Процессы могут по этому номеру посылать друг другу сигналы:
kill(pid /* кому */, sig /* номер сигнала */);
и реагировать на них
signal (sig /*по сигналу*/, f /*вызывать f(sig)*/);
e) Существуют и другие средства коммуникации процессов: семафоры, сообщения, общая
память, сетевые коммуникации.
f) Существуют некоторые другие параметры (контекст) процесса: например, его текущий
каталог, который достается в наследство от процесса-"родителя", и может быть
затем изменен системным вызовом
chdir(char *имя_нового_каталога);
У каждого процесса есть свой собственный текущий рабочий каталог (в отличие от
MS DOS, где текущий каталог одинаков для всех задач). К "прочим" характеристи-
кам отнесем также: управляющий терминал; группу процессов (pgrp); идентификатор
(номер) владельца процесса (uid), идентификатор группы владельца (gid), реакции
и маски, заданные на различные сигналы; и.т.п.
g) Издания других запросов (системных вызовов) к операционной системе ("богу") для
выполнения различных "внешних" операций.
h) Все остальные действия происходят внутри процесса и никак не влияют на другие
процессы и устройства ("миры"). В частности, один процесс НИКАК не может полу-
чить доступ к памяти другого процесса, если тот не позволил ему это явно (меха-
низм shared memory); адресные пространства процессов независимы и изолированы
(равно и пространство ядра изолировано от памяти процессов).

Операционная система выступает в качестве коммуникационной среды, связывающей
"миры"-процессы, "миры"-внешние устройства (включая терминал пользователя); а также в
качестве распорядителя ресурсов "Вселенной", в частности - времени (по очереди выделяемого
активным процессам) и пространства (в памяти компьютера и на дисках).
Мы уже неоднократно упоминали "системные вызовы". Что же это такое? С точки
зрения Си-программиста - это обычные функции. В них передают аргументы, они возвращают
значения. Внешне они ничем не отличаются от написанных нами или библиотечных
функций и вызываются из программ одинаковым с ними способом.
С точки же зрения реализации - есть глубокое различие. Тело функции-сисвызова
расположено не в нашей программе, а в резидентной (т.е. постоянно находящейся в
памяти компьютера) управляющей программе, называемой ядром операционной системы[*].
____________________
[*] Собственно, операционная система характеризуется набором предоставляемых ею системных
вызовов, поскольку все концепции, заложенные в системе, доступны нам только
через них. Если мы имеем две реализации системы с разным внутренним устройством
ядер, но предоставляющие одинаковый интерфейс системных вызовов (их набор, смысл и
поведение), то это все-таки одна и та же система! Ядра могут не просто отличаться,
но и быть построенными на совершенно различных принципах: так обстоит дело с UNIX-ами
на однопроцессорных и многопроцессорных машинах. Но для нас ядро - это "черный
ящик", полностью определяемый его поведением, т.е. своим интерфейсом с программами,
но не внутренним устройством. Вторым параметром, характеризующим ОС, являются форматы
данных, используемые системой: форматы данных для сисвызовов и формат информации
в различных файлах, в том числе формат оформления выполняемых файлов (формат данных в
физической памяти машины в этот список не входит - он зависим от реализации и от процессора).
Как правило, программа пишется так, чтобы использовать соглашения, принятые
в данной системе, для чего она просто включает ряд стандартных include-файлов с
описанием этих форматов. Имена этих файлов также можно отнести к интерфейсу системы.

А. Богатырев, 1992-95 - 188 - Си в UNIX

Сам термин "системный вызов" как раз означает "вызов системы для выполнения действия",
т.е. вызов функции в ядре системы. Ядро работает в привелегированном режиме,
в котором имеет доступ к некоторым системным таблицам[**], регистрам и портам внешних
устройств и диспетчера памяти, к которым обычным программам доступ аппаратно запрещен
(в отличие от MS DOS, где все таблицы ядра доступны пользовательским программам, что
создает раздолье для вирусов). Системный вызов происходит в 2 этапа: сначала в пользовательской
программе вызывается библиотечная функция-"корешок", тело которой написано
на ассемблере и содержит команду генерации программного прерывания. Это - главное
отличие от нормальных Си-функций - вызов по прерыванию. Вторым этапом является
реакция ядра на прерывание:
1. переход в привелегированный режим;
2. разбирательство, КТО обратился к ядру, и подключение u-area этого процесса к
адресному пространству ядра (context switching);
3. извлечение аргументов из памяти запросившего процесса;
4. выяснение, ЧТО же хотят от ядра (один из аргументов, невидимый нам - это номер
системного вызова);
5. проверка корректности остальных аргументов;
6. проверка прав процесса на допустимость выполнения такого запроса;
7. вызов тела требуемого системного вызова - это обычная Си-функция в ядре;
8. возврат ответа в память процесса;
9. выключение привелегированного режима;
10. возврат из прерывания.


Во время системного вызова (шаг 7) процесс может "заснуть", дожидаясь некоторого
события (например, нажатия кнопки на клавиатуре). В это время ядро передаст управление
другому процессу. Когда наш процесс будет "разбужен" (событие произошло) - он
продолжит выполнение шагов системного вызова.
Большинство системных вызовов возвращают в программу в качестве своего значения
признак успеха: 0 - все сделано, (-1) - сисвызов завершился неудачей; либо некоторое
содержательное значение при успехе (вроде дескриптора файла в open(), и (-1) при неудаче.
В случае неудачного завершения в предопределенную переменную errno заносится
номер ошибки, описывающий причину неудачи (коды ошибок предопределены, описаны в
include-файле "errno.h" и имеют вид Ечтото). Заметим, что при УДАЧЕ эта переменная
просто не изменяется и может содержать любой мусор, поэтому проверять ее имеет смысл
лишь в случае, если ошибка действительно произошла:

#include "errno.h" /* коды ошибок */
extern int errno;
extern char *sys_errlist[];
int value;
if((value = sys_call(...)) " 0 ){
printf("Error:%s(%d)\n", sys_errlist[errno],
errno );
exit(errno); /* принудительное завершение программы */
}

____________________
Поведение всех программ в системе вытекает из поведения системных вызовов, которыми
они пользуются. Даже то, что UNIX является многозадачной системой, непосредственно
вытекает из наличия системных вызовов fork, exec, wait и спецификации их функционирования!

То же можно сказать про язык Си - мобильность программы зависит в основном от
набора используемых в ней библиотечных функций (и, в меньшей степени, от диалекта самого
языка, который должен удовлетворять стандарту на язык Си). Если две разные системы
предоставляют все эти функции (которые могут быть по-разному реализованы, но
должны делать одно и то же), то программа будет компилироваться и работать в обоих
системах, более того, работать в них одинаково.
[**] Таким как таблица процессов, таблица открытых файлов (всех вместе и для каждого
процесса), и.т.п.

А. Богатырев, 1992-95 - 189 - Си в UNIX

Предопределенный массив sys_errlist, хранящийся в стандартной библиотеке, содержит
строки-расшифровку смысла ошибок (по-английски). Посмотрите описание функции perror().


6.1. Файлы и каталоги.

6.1.1. Используя системный вызов stat, напишите программу, определяющую тип файла:
обычный файл, каталог, устройство, FIFO-файл. Ответ:

#include "sys/types.h"
#include "sys/stat.h"

typeOf( name ) char *name;
{ int type; struct stat st;
if( stat( name, &st ) " 0 ){
printf( "%s не существует\n", name );
return 0;
}
printf("Файл имеет %d имен\n", st.st_nlink);

switch(type = (st.st_mode & S_IFMT)){
case S_IFREG:
printf( "Обычный файл размером %ld байт\n",
st.st_size ); break;
case S_IFDIR:
printf( "Каталог\n" ); break;
case S_IFCHR: /* байтоориентированное */
case S_IFBLK: /* блочноориентированное */
printf( "Устройство\n" ); break;
case S_IFIFO:
printf( "FIFO-файл\n" ); break;
default:
printf( "Другой тип\n" ); break;
} return type;
}

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

struct stat st; int used, fd;
for(fd=0; fd " NOFILE; fd++ ){
used = fstat(fd, &st) " 0 ? 0 : 1;
}

Программа может использовать дескрипторы файлов с номерами 0..NOFILE-1 (обычно
0..19). Если fstat для какого-то fd вернул код ошибки ("0), это означает, что данный
дескриптор не связан с открытым файлом (т.е. не используется). NOFILE определено в
include-файле "sys/param.h", содержащем разнообразные параметры данной системы.

6.1.3. Напишите упрощенный аналог команды ls, распечатывающий содержимое текущего
каталога (файла с именем ".") без сортировки имен по алфавиту. Предусмотрите чтение
каталога, чье имя задается как аргумент программы. Имена "." и ".." не выдавать.
Формат каталога описан в header-файле "sys/dir.h" и в "канонической" версии выглядит
так: ката

Список страниц

Закладка в соц.сетях

Купить

☏ Заказ рекламы: +3805044-68-872

© Ассоциация электронных библиотек Украины

☝ Все материалы сайта (включая статьи, изображения, рекламные объявления и пр.) предназначены только для предварительного ознакомления. Все права на публикации, представленные на сайте принадлежат их законным владельцам. Просим Вас не сохранять копии информации.