Текст книги "Операционная система UNIX"
Автор книги: Андрей Робачевский
Жанр:
ОС и Сети
сообщить о нарушении
Текущая страница: 12 (всего у книги 39 страниц)
Системный вызов mmap(2) предоставляет механизм доступа к файлам, альтернативный вызовам read(2) и write(2). С помощью этого вызова процесс имеет возможность отобразить участки файла в собственное адресное пространство. После этого данные файла могут быть получены или записаны путем чтения или записи в память. Функция mmap(2) определяется следующим образом:
#include
#include
caddr_t mmap(caddr_t addr, size_t len, int prot,
int flags, int fildes, off_t off);
Этот вызов задает отображение len
байтов файла с дескриптором fildes
, начиная со смещения off
, в область памяти со стартовым адресом addr
. Разумеется, перед вызовом mmap(2) файл должен быть открыт с помощью функции open(2). Аргумент prot
определяет права доступа к области памяти, которые должны соответствовать правам доступа к файлу, указанным в системном вызове open(2). В табл. 2.12 приведены возможные значения аргумента prot и соответствующие им права доступа к файлу. Возможно логическое объединение отдельных значений prot
. Так значение PROT_READ | PROT_WRITE
соответствует доступу O_RDWR
к файлу.
Таблица 2.12. Права доступа к области памяти
PROT_READ | Область доступна для чтения | r |
PROT_WRITE | Область доступна для записи | w |
PROT_EXEC | Область доступна для исполнения | x |
PROT_NONE | Область недоступна | - |
Обычно значение addr
задается равным 0, что позволяет операционной системе самостоятельно выбрать виртуальный адрес начала области отображения. В любом случае, при успешном завершении возвращаемое системным вызовом значение определяет действительное расположение области памяти.
Операционная система округляет значение len до следующей страницы виртуальной памяти.[19]19
Организация виртуальной памяти подробно рассматривается в главе 3.
[Закрыть] Например, если размер файла 96 байтов, а размер страницы 4 Кбайт, то система все равно выделит область памяти размером 4096 байтов. При этом 96 байтов займут собственно данные файла, а остальные 4000 байтов будут заполнены нулями. Процесс может модифицировать и оставшиеся 4000 байтов, но эти изменения не отразятся на содержимом файла. При обращении к участку памяти, лежащему за пределами файла, ядро отправит процессу сигнал SIGBUS
[20]20
Если быть более точным, сигнал посылается процессу, когда происходит обращение к странице памяти, на которую не отображается ни один из участков файла. Таким образом, в приведенном примере сигнал процессу не будет отправлен.
[Закрыть]. Несмотря на то что область памяти может превышать фактический размер файла, процесс не имеет возможности изменить его размер.
Использование права на исполнение (prot = PROT_EXEC
) позволяет процессу определить собственный механизм загрузки кода. В частности, такой подход используется редактором динамических связей при загрузке динамических библиотек, когда библиотека отображается в адресное пространство процесса. Значение PROT_NONE
позволяет приложению определить собственные механизмы контроля доступа к разделяемым объектам (например, к разделяемой памяти), разрешая или запрещая доступ к области памяти.
Аргумент flags
определяет дополнительные особенности управления областью памяти. В табл. 2.13 приведены возможные типы отображения, определяемые аргументом flags
.
Таблица 2.13. Типы отображения
MAP SHARED | Область памяти может совместно использоваться несколькими процессами |
MAP PRIVATE | Область памяти используется только вызывающим процессом |
MAP_FIXED | Требует выделения памяти, начиная точно с адреса addr |
MAP_NORESERVE | He требует резервирования области свопинга |
В случае указания MAP_PRIVATE
, для процесса, определившего этот тип отображения, будет создана собственная копия страницы памяти, которую он пытается модифицировать. Заметим, что копия будет создана только при вызове операции записи, до этого остальные процессы, определившие тип отображения как MAP_SHARED
могут совместно использовать одну и ту же область памяти.
Не рекомендуется использовать флаг MAP_FIXED
, т.к. это не позволяет системе максимально эффективно распределить память. В случае отсутствия этого флага, ядро пытается выделить область памяти, начиная с адреса наиболее близкого к значению addr
. Если же значение addr
установлено равным 0, операционная система получает полную свободу в размещении области отображения.
Отображение автоматически снимается при завершении процесса. Процесс также может явно снять отображение с помощью вызова munmap(2). Закрытие файла не приводит к снятию отображения. Следует отметить, что снятие отображения непосредственно не влияет на отображаемый файл, т. е. содержимое страниц области отображения не будет немедленно записано на диск. Обновление файла производится ядром согласно алгоритмам управления виртуальной памятью. В то же время в ряде систем существует функция msync(3C), которая позволяет синхронизировать обновление памяти с обновлением файла на диске.[21]21
На самом деле msync(3C) синхронизирует обновление страниц памяти с вторичной памятью. Для областей типа MAP_SHARED вторичной памятью является сам файл на диске. Для областей типа MAP_PRIVATE вторичной памятью является область свопинга. Функция msync(3C) также позволяет принудительно обновить страницы, так что при следующем обращении к какой-либо из них ее содержимое будет загружено из вторичной памяти.
[Закрыть]
В качестве примера приведем упрощенную версию утилиты cp(1), копирующую один файл в другой с использованием отображения файла в память.
#include
#include
#include
#include
#include
main(int argc, char *argv[]) {
int fd_src, fd_dst;
caddr_t addr_src, addr_dst;
struct stat filestat;
/* Первый аргумент – исходный файл, второй – целевой */
fd_dst=open(argv[2], O_RDWR | O_CREAT);
/* Определим размер исходного файла */
fstat(fd_src, &filestat);
/* Сделаем размер целевого файла равным исходному */
lseek(fd_dst, filestat.st_size – 1, SEEK_SET);
/* Зададим отображение */
addr_src=mmap((caddr_t)0, filestat.st_size,
PROT_READ, MAP_SHARED, fd_src, 0);
addr_dst=mmap((caddr_t)0, filestat.st_size,
PROT_READ | PROT_WRITE, MAP_SHARED, fd_dst, 0);
/* Копируем области памяти */
memcpy(addr_dst, addr_src, filestat.st_size);
exit(0);
}
Поскольку, как обсуждалось выше, с помощью вызова mmap(2) нельзя изменить размер файла, это было сделано с помощью вызова lseek(2) с последующей записью одного байта так, что размер целевого файла стал равным размеру исходного. При этом в целевом файле образуется «дыра», которая, к счастью, сразу же заполняется содержимым копируемого файла.
Владение файламиВладелец-пользователь и владелец-группа файла могут быть изменены с помощью системных вызовов chown(2), fchown(2) и lchown(2):
#include
#include
int chown(const char *path, uid_t owner, gid_t group);
int fchown(int fildes, uid_t owner, gid_t group);
int lchown(const char *path, uid_t owner, gid_t group);
Все три вызова работают одинаково за исключением ситуации, когда адресуемый файл является символической связью. В последнем случае вызов lchown(2) действует на сам файл – символическую связь, а не на целевой файл (т.е. не следует символической связи). В функциях chown(2) и lchown(2) файл адресуется по имени, а в fchown(2) – по файловому дескриптору. Если значение owner
или group
установлено равным -1, соответствующий владелец файла не изменяется.
В версиях BSD UNIX только суперпользователь может изменить владение файлом. Это ограничение призвано, в первую очередь, не допустить "скрытие" файлов под именем другого пользователя, например, при установке квотирования ресурсов файловой системы. Владельца-группу можно изменить только для файлов, которыми вы владеете, причем им может стать одна из групп, членом которой вы являетесь. Эти же ограничения определены и стандартом POSIX.1.
В системах ветви System V эти ограничения являются конфигурируемыми, и в общем случае в UNIX System V пользователь может изменить владельца собственных файлов.
В случае успешного изменения владельцев файла биты SUID и SGID сбрасываются, если процесс, вызвавший chown(2) не обладает правами суперпользователя.
Права доступаКак уже обсуждалось в предыдущей главе, каждый процесс имеет четыре пользовательских идентификатора – UID, GID, EUID и EGID. В то время как UID и GID определяют реального владельца процесса, EUID и EGID определяют права доступа процесса к файлам в процессе выполнения. В общем случае реальные и эффективные идентификаторы эквивалентны. Это значит, что процесс имеет те же привилегии, что и пользователь, запустивший его. Однако, как уже обсуждалось выше, возникают ситуации, когда процесс должен получить дополнительные привилегии, чаще всего – привилегии суперпользователя. Это достигается установкой битов SUID и SGID. Примером такого процесса может служить утилита passwd(1), изменяющая пароль пользователя.
Права доступа к файлу могут быть изменены с помощью системных вызовов chmod(2) и fchmod(2):
#include
#include
int chmod(const char *path, mode_t mode);
int fchmod(int fildes, mode_t mode);
Значение аргумента mode определяет устанавливаемые права доступа и дополнительные атрибуты (такие как SUID, SGID и Sticky bit), и создается путем логического объединения различных флагов, представленных в табл. 2.14. Вторая колонка таблицы содержит восьмеричные значения для девяти битов прав доступа (чтение, запись и выполнение для трех классов доступа) и трех битов дополнительных атрибутов.
Таблица 2.14. Флаги аргумента mode
S_ISUID | 04000 | Установить бит SUID |
S_ISGID | 020#0 | Установить бит SGID, если # равно 7, 5, 3 или 1. Установить обязательное блокирование файла, если # равно 6, 4, 2 или 0 |
S_ISVTX | 01000 | Установить Sticky bit |
S_IRWXU | 00700 | Установить право на чтение, запись и выполнение для владельца-пользователя |
S_IRUSR | 00400 | Установить право на чтение для владельца-пользователя |
S_IWUSR | 00200 | Установить право на запись для владельца-пользователя |
S_IXUSR | 00100 | Установить право на выполнение для владельца-пользователя |
S_IRWXG | 00070 | Установить право на чтение, запись и выполнение для владельца-группы |
S_IRGRP | 00040 | Установить право на чтение для владельца-группы |
S_IWGRP | 00020 | Установить право на запись для владельца-группы |
S_IXGRP | 00010 | Установить право на выполнение для владельца-группы |
S_IRWXO | 00007 | Установить право на чтение, запись и выполнение для остальных пользователей |
S_IROTH | 00004 | Установить право на чтение для остальных пользователей |
S_IWOTH | 00002 | Установить право на запись для остальных пользователей |
S_IXOTH | 00001 | Установить право на выполнение для остальных пользователей |
Некоторые флаги, представленные в таблице, уже являются объединением нескольких флагов. Так, например, флаг S_RWXU
эквивалентен S_IRUSR | S_IWUSR | S_IXUSR
. Значение флага S_ISGID зависит от того, установлено или нет право на выполнение для группы (S_IXGRP). В первом случае, он будет означать установку SGID, а во втором – обязательное блокирование файла.
Для иллюстрации приведем небольшую программу, создающую файл с полными правами доступа для владельца, а затем изменяющую их. После каждой установки прав доступа в программе вызывается библиотечная функция system(3S), позволяющая запустить утилиту ls(1) и отобразить изменение прав доступа и дополнительных атрибутов.
#include
#include
#include
main() {
int fd;
/* Создадим файл с правами rwx– */
fd = creat(«my_file», S_IRUSR | S_IWUSR | S_IXUSR);
system(«ls -l my_file»);
/*Добавим флаг SUID */
fchmod(fd, S_IRWXU | S_ISUID);
/* Установим блокирование записей файла */
fchmod(fd, S_IRWXU | S_ISUID | S_ISGID);
system(«ls -l my_file»);
/* Теперь установим флаг SGID */
fchmod(fd, S_IRWXU | S_ISUID | S_ISGID | S_IXGRP);
system(«ls -l my_file»);
}
В результате запуска программы на выполнение, получим следующий вывод:
$ a.out
-rwx– 1 andy user 0 Jan 6 19:28 my_file
-rws– 1 andy user 0 Jan 6 19:28 my_file
-rws–1– 1 andy user 0 Jan 6 19:28 my_file
-rws–s– 1 andy user 0 Jan 6 19:28 my_file
Каждый процесс имеет два атрибута, связанных с файловой системой – корневой каталог (root directory) и текущий рабочий каталог (current working directory). Когда некоторый файл адресуется по имени (например, в системных вызовах open(2), creat(2) или readlink(2)), ядро системы производит поиск файла, начиная с корневого каталога, если имя файла задано как абсолютное, либо текущего каталога, если имя файла является относительным. Абсолютное имя файла начинается с символа '/', обозначающего корневой каталог. Все остальные имена файлов являются относительными. Например, имя /usr/bin/sh является абсолютным, в то время как mydir/test1.c или ../andy/mydir/test1.c – относительным, при котором фактическое расположение файла в файловой системе зависит от текущего каталога.
Процесс может изменить свой корневой каталог с помощью системного вызова chroot(2) или fchroot(2).
#include
int chroot (const char *path);
int fchroot(int fildes);
После этого поиск всех адресуемых файлов с абсолютными именами будет производиться, начиная с нового каталога, указанного аргументом path
. Например, после изменения корневого каталога на домашний каталог пользователя абсолютное имя инициализационного скрипта .profile станет /.profile.[22]22
Изменение корневого каталога разрешено только для администратора системы – суперпользователя. Эта операция таит в себе определенную опасность, т.к. часть утилит операционной системы (если не все) могут оказаться недоступными, в том числе и команда chroot(1M). Таким образом, последствия необдуманного изменения корневого каталога могут стать необратимыми.
[Закрыть]
Изменение корневого каталога может потребоваться, например, при распаковке архива, созданного с абсолютными именами файла, в другом месте файловой системы, либо при работе над большим программным проектом, затрагивающим существенную часть корневой файловой системы. В этом случае для отладочной версии удобно создать собственную корневую иерархию.
Процесс также может изменить и текущий каталог. Для этого используются системные вызовы chdir(2) или fchdir(2):
#include
int chdir(const char* path);
int fchdir(int fildes);
Например, внутренняя команда командного интерпретатора cd может быть реализована следующим кодом:
...
char newdir[PATH_MAX];
...
/* Предположим, что имя нового каталога,
введенного пользователем, уже находится
в переменной newdir*/
if (chdir(newdir) == -1) perror(«sh: cd»);
...
Как уже говорилось, каждый файл помимо собственно данных содержит метаданные, описывающие его характеристики, например, владельцев, права доступа, тип и размер файла, а также содержащие указатели на фактическое расположение данных файла. Метаданные файла хранятся в структуре inode. Часть полей этой структуры могут быть получены с помощью системных вызовов stat(2):
#include
#include
int stat(const char *path, struct stat *buf);
int lstat (const char *path, struct stat *buf);
int fstat(int fildes, struct stat *buf);
В качестве аргумента функции принимают имя файла или файловый дескриптор (fstat(2)) и возвращают заполненные поля структуры stat, которые приведены в табл. 2.15.
Таблица 2.15. Поля структуры stat
mode_t st_mode | Тип файла и права доступа |
ino_t st_ino | Номер inode. Поля st_ino и st_dev однозначно определяют обычные файлы |
dev_t st_dev | Номер устройства, на котором расположен файл (номер устройства файловой системы) |
dev_t st_rdev | Для специального файла устройства содержит номер устройства, адресуемого этим файлом |
nlink_t st_link | Число жестких связей |
uid_t st_uid | Идентификатор пользователя-владельца файла |
gid_t st_gid | Идентификатор группы-владельца файла |
off_t st_size | Размер файла в байтах. Для специальных файлов устройств это поле не определено |
time_t st_atime | Время последнего доступа к файлу |
time_t st_mtime | Время последней модификации данных файла |
time_t st_ctime | Время последней модификации метаданных файла |
long st_blksize | Оптимальный размер блока для операций ввода/вывода. Для специальных файлов устройств и каналов это поле не определено |
long st_blocks | Число размещенных 512-байтовых блоков хранения данных. Для специальных файлов устройств это поле не определено |
Для определения типа файла служат следующие макроопределения, описанные в файле
Таблица 2.16. Определение типа файла
S_ISFIFO(mode) | FIFO |
S_ISCHR(mode) | Специальный файл символьного устройства |
S_ISDIR(mode) | Каталог |
S_ISBLK(mode) | Специальный файл блочного устройства |
S_ISREG(mode) | Обычный файл |
S_ISLNK(mode) | Символическая связь |
S_ISSOCK(mode) | Сокет |
Все значения времени, связанные с файлом (время доступа, модификации данных и метаданных) хранятся в секундах, прошедших с 0 часов 1 января 1970 года. Заметим, что информация о времени создания файла отсутствует.
Приведенная ниже программа выводит информацию о файле, имя которого передается ей в качестве аргумента:
#include
#include
#include
main(int argc, char *argv[]) {
struct stat s;
char* ptype;
lstat(argv[1] , &s); /* Определим тип файла */
if (S_ISREG(s.st_mode)) ptype = «Обычный файл»;
else if (S_ISDIR(s.st_mode)) ptype = «Каталог»;
else if (S_ISLNK(s.st_mode)) ptype = «Симв. Связь»;
else if (S_ISCHR(s.st_mode)) ptype = «Симв. Устройство»;
else if (S_ISBLK(s.st_mode)) ptype = «Бл.устройство»;
else if (S_ISSOCK(s.st_mode)) ptype = «Сокет»;
else if (S_ISFIFO(s.st_mode)) ptype = «FIFO»;
else ptype = «Неизвестный тип»;
/* Выведем информацию о файле */
/* Его тип */
printf(«type = %sn», ptype);
/* Права доступа */
printf(«perm =%on», s.st_mode & S_IAMB);
/* Номер inode */
printf(«inode = %dn», s.st_ino);
/* Число связей */
printf(«nlink = %dn», s.st_nlink);
/* Устройство, на котором хранятся данные файла */
printf(«dev = (%d, %d)n», major(s.st_dev), minor(s.st_dev));
/* Владельцы файла */
printf(«UID = %dn», s.st_uid);
printf(«GID = %dn», s.st_gid);
/* Для специальных файлов устройств – номера устройства */
printf(«rdev = (%d, %d)n», major(s.st_rdev),
minor(s.st_rdev));
/* Размер файла */
printf(«size = %dn», s.st_size);
/* Время доступа, модификации и модификации метаданных */
printf(«atime = %s», ctime(&s.st_atime));
printf(«mtime = %s», ctime(&s.st_mtime));
printf(«ctime = %s», ctime(&s.st_ctime));
}
Программа использует библиотечные функции major(3C) и minor(3C), возвращающие, соответственно, старший и младший номера устройства. Функция ctime(3C) преобразует системное время в удобный формат.
Запуск программы на выполнение приведет к следующим результатам:
$ а.out ftype.c
type = Обычный файл
perm = 644
inode = 13
nlink = 1
dev = (1, 42)
UID = 286
GID = 100
rdev = (0, 0)
size = 1064
atime = Wed Jan 8 17:25:34 1997
mtime = Wed Jan 8 17:19:27 1997
ctime = Wed Jan 8 17:19:27 1997
$ ls -il /tmp/ftype.c
13 -rw-r–r– 1 andy user 1064 Jan 8 17:19 ftype.c
Процессы
В главе 1 уже упоминались процессы. Однако знакомство ограничивалось пользовательским, или командным интерфейсом операционной системы. В этом разделе попробуем взглянуть на них с точки зрения программиста.
Процессы являются основным двигателем операционной системы. Большинство функций выполняется ядром требованию того или иного процесса. Выполнение этих функций контролируется привилегиями процесса, которые соответствуют привилегиям пользователя, запустившего его.
В этом разделе рассматриваются:
□ Идентификаторы процесса
□ Программный интерфейс управления памятью: системные вызовы низкого уровня и библиотечные функции, позволяющие упростить управление динамической памятью процесса.
□ Важнейшие системные вызовы, обеспечивающие создание нового процесса и запуск новой программы. Именно с помощью этих вызовов создается существующая популяция процессов в операционной системе и ее функциональность.
□ Сигналы и способы управления ими. Сигналы можно рассматривать как элементарную форму межпроцессного взаимодействия, позволяющую процессам сообщать друг другу о наступлении некоторых событий. Более мощные средства будут рассмотрены в разделе "Взаимодействие между процессами" главы 3.
□ Группы и сеансы; взаимодействие процесса с пользователем.
□ Ограничения, накладываемые на процесс, и функции, которые позволяют управлять этими ограничениями.
Идентификаторы процессаВы уже знаете, что каждый процесс характеризуется набором атрибутов и идентификаторов, позволяющих системе управлять его работой. Важнейшими из них являются идентификатор процесса PID и идентификатор родительского процесса PPID. PID является именем процесса в операционной системе, по которому мы можем адресовать его, например, при отправлении сигнала. PPID указывает на родственные отношения между процессами, которые (как и в жизни) в значительной степени определяют его свойства и возможности.
Однако нельзя не отметить еще четыре идентификатора, играющие решающую роль при доступе к системным ресурсам: идентификатор пользователя UID, эффективный идентификатор пользователя EUID, идентификатор группы GID и эффективный идентификатор группы EGID. Эти идентификаторы определяют права процесса в файловой системе, и как следствие, в операционной системе в целом. Запуская различные команды и утилиты, можно заметить, что порожденные этими командами процессы полностью отражают права пользователя UNIX. Причина проста – все процессы, которые запускаются, имеют идентификатор пользователя и идентификатор группы. Исключение составляют процессы с установленными флагами SUID и SGID.
При регистрации пользователя в системе утилита login(1) запускает командный интерпретатор, – login shell, имя которого является одним из атрибутов пользователя. При этом идентификаторам UID (EUID) и GID (EGID) процесса shell присваиваются значения, полученные из записи пользователя в файле паролей /etc/passwd. Таким образом, командный интерпретатор обладает правами, определенными для данного пользователя.
При запуске программы командный интерпретатор порождает процесс, который наследует все четыре идентификатора и, следовательно, имеет те же права, что и shell. Поскольку в конкретном сеансе работы пользователя в системе прародителем всех процессов является login shell, то и их пользовательские идентификаторы будут идентичны.
Казалось бы, эту стройную систему могут "испортить" утилиты с установленными флагами SUID и SGID. Но не стоит волноваться – как правило, такие программы не позволяют порождать другие процессы, в противном случае, эти утилиты необходимо немедленно уничтожить!
На рис. 2.10. показан процесс наследования пользовательских идентификаторов в рамках одного сеанса работы.
Рис. 2.10. Наследование пользовательских идентификаторов
Для получения значений идентификаторов процесса используются следующие системные вызовы:
#include
#include
uid_t getuid(void);
uid_t geteuid(void);
gid_t getgid(void);
gid_t getegid(void);
Эти функции возвращают для сделавшего вызов процесса соответственно реальный и эффективный идентификаторы пользователя и реальный и эффективный идентификаторы группы.
Процесс также может изменить значения этих идентификаторов с помощью системных вызовов:
#include
#include
int setuid(uid_t uid);
int setegid(gid_t egid);
int seteuid(uid_t euid);
int setgid(gid_t gid);
Системные вызовы setuid(2) и setgid(2) устанавливают сразу реальный и эффективный идентификаторы, а системные вызовы seteuid(2) и setegid(2) – только эффективные.
Ниже приведен фрагмент программы login(1), изменяющей идентификаторы процесса на значения, полученные из записи файла паролей. В стандартной библиотеке имеется ряд функций работы с записями файла паролей, каждая из которых описывается структурой passwd
, определенной в файле
Таблица 2.17. Поля структуры passwd
char *pw_name | Имя пользователя |
char *pw_passwd | Строка, содержащая пароль в зашифрованном виде; из соображения безопасности в большинстве систем пароль хранится в файле /etc/shadow, а это поле не используется |
uid_t pw_uid | Идентификатор пользователя |
gid_t pw_gid | Идентификатор группы |
char *pw_gecos | Комментарий (поле GECOS), обычно реальное имя пользователя и дополнительная информация |
char *pw_dir | Домашний каталог пользователя |
char *pw_shell | Командный интерпретатор |
Функция, которая потребуется для нашего примера, позволяет получить запись файла паролей по имени пользователя. Она имеет следующий вид:
#include
struct passwd *getpwnam(const char *name);
Итак, перейдем к фрагменту программы:
...
struct passwd *pw;
char logname[MAXNAME];
/* Массив аргументов при запуске
командного интерпретатора */
char *arg[MAXARG];
/* Окружение командного интерпретатора */
char *envir[MAXENV];
...
/* Проведем поиск записи пользователя с именем logname,
которое было введено на приглашение «login:» */
pw = getpwnam(logname);
/* Если пользователь с таким именем не найден, повторить
приглашение */
if (pw == 0)
retry();
/* В противном случае установим идентификаторы процесса
равными значениям, полученным из файла паролей и запустим
командный интерпретатор */
else {
setuid(pw->pw_uid);
setgid(pw->pw_gid);
execve(pw->pw_shell, arg, envir);
}
...
Вызов execve(2) запускает на выполнение программу, указанную в первом аргументе. Мы рассмотрим эту функцию в разделе «Создание и управление процессами» далее в этой главе.