Текст книги "Linux программирование в примерах"
Автор книги: Арнольд Роббинс
Жанры:
Программирование
,сообщить о нарушении
Текущая страница: 33 (всего у книги 55 страниц)
«Наша история до настоящего времени, эпизод III»
– Арнольд Роббинс (Arnold Robbins) -
• Интерфейсы обработки сигналов развились от простых, но подверженных состояниям гонок, до сложных, но надежных. К сожалению, множественность интерфейсов затрудняет их изучение по сравнению с другими API Linux/Unix.
• У каждого сигнала есть связанное с ним действие. Действие может быть одним из следующих: игнорирование сигнала; выполнение действия системы по умолчанию или вызов предоставленного пользователем обработчика. Действие системы по умолчанию, в свою очередь, является одним из следующих: игнорирование сигнала, завершение процесса; завершение процесса с созданием его образа; остановка процесса или возобновление процесса, если он остановлен.
• signal()
и raise()
стандартизованы ISO С. signal()
управляет действиями для определенных сигналов; raise()
посылает сигнал текущему процессу. Остаются ли обработчики сигналов установленными после вызова или сбрасываются для действия по умолчанию, зависит от реализации, signal()
и raise()
являются простейшими интерфейсами, для многих приложений их достаточно.
• POSIX определяет функцию bsd_signal()
, которая подобна signal()
, но гарантирует, что обработчик остается установленным.
• Действия, происходящие после возвращения из обработчика, варьируют в зависимости от системы. Традиционные системы (V7, Solaris, возможно, и другие) восстанавливают действие сигнала по умолчанию. На этих системах прерванный системный вызов возвращает -1, устанавливая в errno
значение EINTR
. Системы BSD оставляют обработчик установленным и возвращают -1 с errno
, содержащим EINTR
, лишь в случае, когда не было перемещения данных; в противном случае, системный вызов запускается повторно.
• GNU/Linux придерживается POSIX, который похож, но не идентичен с BSD. Если не было перемещения данных, системный вызов возвращает -1/EINTR
. В противном случае он возвращает объем перемещенных данных. Поведение BSD «всегда повторный запуск» доступно через интерфейс sigaction()
, но он не является действием по умолчанию.
• Обработчики сигналов, используемые с signal()
, подвержены состояниям гонок. Внутри обработчиков сигналов должны использоваться исключительно переменные типа volatile sig_atomic_t
. (В целях упрощения в некоторых из наших примеров мы не всегда следовали этому правилу.) Таким же образом, для вызова из обработчика сигналов безопасными являются лишь функции из табл. 10.2.
• Первоначальной попыткой создания надежных сигналов был API сигналов System V Release 3 (скопированный из BSD 4.0). Не используйте его в новом коде.
• POSIX API содержит множество компонентов:
• маску сигналов процесса, перечисляющую текущие заблокированные сигналы;
• тип sigset_t
для представления масок сигналов, и функции sigfillset()
, sigemptyset()
, sigaddset()
, sigdelset()
и sigismember()
для работы с ними;
• функцию sigprocmask()
для установки и получения маски сигналов процесса,
• функцию sigpending()
для получения набора ожидающих сигналов;
• API sigaction()
и struct sigaction
во всем их великолепии.
Все эти возможности вместе используют блокирование сигналов и маску сигналов процесса для предоставления надежных сигналов. Более того, через различные флаги можно получить повторно запускаемые системные вызовы и более подходящие обработчики сигналов, которые получают большую информацию о причине, вызвавшей определенный сигнал (структура siginfo_t
).
• Механизмами POSIX для посылки сигналов являются kill()
и killpg()
. Они отличаются от raise()
в двух отношениях: (1) одни процесс может послать сигнал другому процессу или целой группе процессов (конечно, с проверкой прав доступа), и (2) посылка сигнала 0 ничего не посылает, но осуществляет проверку. Таким образом, эти функции предоставляют способ проверки наличия определенного процесса или группы процессов и возможность посылки ему (им) сигнала.
• Сигналы могут использоваться в качестве механизма IPC, хотя такой способ является плохим способом структурирования приложения, подверженным состояниям гонок. Если кто-то держит приставленным к вашей голове ружье, чтобы заставить вас работать таким способом, для правильной работы используйте тщательное блокирование сигналов и интерфейс sigaction()
.
• SIGALARM
и системный вызов alarm()
предоставляют низкоуровневый механизм для уведомления о прошествии определенного числа секунд, pause()
приостанавливает процесс, пока не появятся какие-нибудь сигналы, sleep()
использует их для помещения процесса в спящее состояние на заданный период времени: sleep()
и alarm()
не должны использоваться вместе. Сама pause()
создает состояние гонки; вместо этого нужно использовать блокирование сигналов и sigsuspend()
.
• Сигналы управления заданиями реализуют управление заданиями для оболочки. Большую часть времени следует оставлять их с установленными действиями по умолчанию, но полезно понимать, что иногда имеет смысл их перехватывать.
• Перехват SIGCHLD
позволяет родителю узнать, что делает порожденный им процесс. Использование 'signal(SIGCHLD, SIG_IGN)
' (или sigaction()
с SA_NOCLDWAIT
) вообще игнорирует потомков. Использование sigaction()
с SA_NOCLDSTOP
предоставляет уведомления лишь о завершении. В последнем случае, независимо от того, заблокирован SIGCHLD
или нет, обработчики сигналов для SIGCHLD
должны быть готовы немедленно обработать несколько потомков. Наконец, использование sigaction()
без SA_NOCLDSTOP
с обработчиком сигналов с тремя аргументами дает вам причину получения сигнала.
• После fork()
положение сигналов в порожденном процессе остается тем же самым, за исключением сброса ожидающих сигналов и установленных интервалов таймера. После exec()
положение несколько более сложно – в сущности, все, что может быть оставлено, остается; для всего остального восстанавливаются значения по умолчанию.
1. Реализуйте bsd_signal()
с использованием sigaction()
.
2. Если у вас не установлен GNU/Linux, запустите на своей системе ch10-catchint
. Является ли ваша система традиционной или BSD?
3. Реализуйте функции System V Release 3 sighold()
, sigrelse()
, sigignore()
, sigpause()
и sigset()
, использовав sigaction()
и другие подходящие функции из POSIX API.
4. Потренируйте свои навыки в жонглировании битами. В предположении, что сигнал 0 отсутствует и что имеется не более 31 сигналов, предусмотрите typedef
для sigset_t
и напишите sigemptyset()
, sigfillset()
, sigaddset()
, sigdelset()
и sigismember()
.
5. Еще немного потренируйте свои навыки жонглирования битами. Повторите предыдущее упражнение, на этот раз предположив, что наибольшим сигналом является 42.
6. Теперь, когда вы сделали предыдущие два упражнения, найдите sigemptyset()
и др. в своем заголовочном файле
. (Может потребоваться поискать их; они могут быть в #include
файлах, указанных в
.) Являются ли они макросами или функциями?
7. В разделе 10.7 «Сигналы для межпроцессного взаимодействия» мы упомянули, что код изделия должен работать с начальной маской сигналов процесса, добавляя и удаляя блокируемые сигналы в вызове sigsuspend()
. Перепишите пример, используя для этого соответствующие вызовы.
8. Напишите свою собственную версию команды kill
. Интерфейс должен быть таким:
kill [-s имя-сигнала] pid ...
Если сигнал не указан, программа должна посылать SIGTERM
.
9. Как вы думаете, почему в современных оболочках, таких, как Bash и ksh93, kill
является встроенной командой?
10. (Трудное) Реализуйте sleep()
, используя alarm()
, signal()
и pause()
. Что случится, если обработчик сигнала для SIGALRM
уже установлен?
11. Поэкспериментируйте с ch10-reap.c
, изменяя интервал времени, на который засыпает каждый потомок, и организуя достаточное число вызовов sigsuspend()
для сбора сведений о всех потомках.
12. Попробуйте заставить ch10-reap2.c
испортить информацию в kids
, nkids
и kidsleft
. Теперь добавьте вокруг критического раздела блокирование/разблокирование и посмотрите, есть ли разница.
Глава 11
Права доступа и ID пользователей и групп
Linux, вслед за Unix, является многопользовательской системой. В отличие от большинства операционных систем для персональных компьютеров,[114]114
MacOS X и Windows XP обе являются многопользовательскими системами, но это довольно недавние разработки – Примеч. автора.
[Закрыть] в которых имеется лишь один пользователь и в которых, кто бы ни находился перед компьютером, он имеет полный контроль, Linux и Unix различают файлы и процессы по владельцам и группам, которым они принадлежат. В данной главе мы исследуем проверку прав доступа и рассмотрим API для получения и установки идентификаторов владельцев и групп.
Как мы видели в разделе 5.4.2 «Получение информации о файлах», файловая система хранит идентификаторы владельца и группы файла в виде числовых значений; это типы uid_t
и gid_t
соответственно. Для краткости мы используем для «идентификатора владельца (пользователя)» и «идентификатора группы» сокращения UID и GID соответственно.
У каждого процесса есть несколько связанных с ним идентификаторов пользователя и группы. Для проверки прав доступа в качестве упрощения используется один определенный UID и GID; когда UID процесса совпадает с UID файла, биты прав доступа пользователя файла диктуют, что может сделать процесс с файлом. Если они не совпадают, система проверяет GID процесса с GID файла; при совпадении используются права доступа группы; в противном случае, используются права доступа для «остальных».
Помимо файлов, UID определяет, как один процесс может повлиять на другой путем посылки сигналов. Сигналы описаны в главе 10 «Сигналы».
Наконец, особым случаем является суперпользователь, root
. root
идентифицируется по UID, равным 0. Когда у процесса UID равен 0, ядро позволяет ему делать все, что он захочет: читать, записывать или удалять файлы, посылать сигналы произвольным процессам и т.д. (POSIX в этом отношении более непонятный, ссылаясь на процессы с «соответствующими привилегиями». Этот язык, в свою очередь, просочился в справочные страницы GNU/Linux и справочное руководство GLIBC online Info manual. Некоторые операционные системы действительно разделяют привилегии пользователей, и Linux также движется в этом направлении. Тем не менее, в настоящее время «соответствующие привилегии» означает просто процессы с UID, равным 0.)
Номера UID и GID подобны персональным удостоверениям личности. Иногда вам может понадобиться более одного удостоверяющего документа. Например, у вас могут быть водительские права или правительственное удостоверение личности[115]115
Хотя в Соединенных Штатах нет официальных удостоверений личности, во многих странах они имеются – Примеч. автора.
[Закрыть]. Вдобавок, ваш университет или компания могли выдать вам свои удостоверения личности. То же самое относится и к процессам; они имеют при себе множество следующих номеров UID и GID:
Действительный ID пользователя
UID пользователя, породившего процесс.
Эффективный ID пользователя
UID, использующийся при большинстве проверок прав доступа. В большинстве случаев эффективный и действительный UID являются одним и тем же. Эффективный UID может отличаться от действительного при запуске, если установлен бит setuid файла исполняемой программы и файл не принадлежит пользователю, запускающему программу. (Вскоре будут дополнительные сведения.)
Сохраненный set-user ID
Первоначальный эффективный UID при запуске программы (после выполнения exec.) Имеет значение при проверке прав доступа, когда процессу нужно менять действительный и эффективный UID в ходе работы. Эта концепция пришла из System V.
Действительный ID группы
GID пользователя, создавшего процесс, аналогично действительному UID.
Эффективный ID группы
GID, использующийся для проверки прав доступа, аналогично эффективному GID.
Сохраненный set-group ID
Первоначальный эффективный GID при запуске программы, аналогично сохраненному set-user ID.
Набор дополнительных групп
4.2 BSD ввело понятие набора групп. Помимо действительного и эффективного GID. у каждого процесса есть некоторый набор дополнительных групп, которым он принадлежит в одно и то же время. Таким образом, когда проверка прав доступа осуществляется для группы файла, ядро проверяет не только эффективный GID, но также и все GID в наборе групп.
Каждый процесс может получить все из этих значений. Обычный (не принадлежащий суперпользователю) процесс может переключать свои действительные и эффективные ID пользователя и группы. Процесс root
(с эффективным UID, равным 0) может также устанавливать значения таким образом, как ему нужно (хотя это может оказаться односторонней операцией)
Биты setuid и setgid[116]116
Денис Ричи (Dennis Ritchie), создатель С и соавтор Unix, получил патент для бита setuid: Protection of Data File Contents (Защита содержимого файла данных), номер патента США 4135240. См. http://www.delphion.com/details?pn=US04135240__
, а также http://www.uspco.gov
. AT&T передала патент общественности, разрешив всем использовать свою технологию – Примеч. автора.
[Закрыть] в правах доступа к файлу заставляют процесс принять эффективный UID или GID, который отличается от действительного. Эти биты накладываются на файл вручную с помощью команды chmod
:
$ chmod u+s myprogram /* Добавить бит setuid */
$ chmod g+s myprogram /* Добавить бит setgid */
$ ls -l myprogram
-rwsr-sr-x 1 arnold devel 4573 Oct 9 18:17 myprogram
Наличие символа s в месте, где обычно находится символ x, указывает на присутствие битов setuid/setgid.
Как упоминалось в разделе 8.2.1 «Использование опций монтирования», опция nosuid
команды mount для файловой системы предотвращает обращение ядра к битам setuid и setgid. Это мера безопасности; например, пользователь с домашней системой GNU/Linux мог бы вручную изготовить гибкий диск с копией исполняемого файла оболочки с setuid, устанавливающей в root
. Но если система GNU/Linux в офисе или лаборатории монтирует файловые системы с гибкими дисками с опцией nosuid
, запуск этой оболочки не предоставит доступа с правами root
[117]117
Безопасность для систем GNU/Linux и Unix является глубокой темой сама по себе. Это просто пример. см. раздел 11.9 «Рекомендуемая литература» – Примеч. автора.
[Закрыть].
Каноническим (и возможно, злоупотребляемым) примером программы с setuid является игровая программа. Представьте, что вы написали по-настоящему крутую игру и хотите позволить пользователям системы играть в нее. Игра содержит файл счета, в котором перечислены высшие достижения.
Если вы не являетесь системным администратором, вы не можете создать отдельную группу только для тех пользователей, которым разрешено играть в игру и тем самым записывать в файл счета. Но если вы сделаете файл доступным для записи любому, чтобы каждый смог поиграть в игру, тогда каждый сможет также сжульничать и поместить наверх любое имя.
Однако, заставив программу устанавливать setuid на вас, пользователи, запускающие игру, получат ваш UID в качестве своего эффективного UID. Игровая программа сможет при этом открывать и обновлять файл счета по мере необходимости, но произвольные пользователи не смогут прийти и отредактировать его. (Вы подвергаете себя также большинству опасностей при программировании setuid; например, если в игровой программе есть дыра, которую можно использовать для запуска оболочки, действующей от вашего имени, все ваши файлы оказываются доступными для удаления или изменения. Это действительно устрашающая мысль.)
Та же логика применяется к программам setgid, хотя на практике программы с setgid используются гораздо реже, чем с setuid (Это также плохо; многие вещи, которые делаются программами с setuid root
, легко могут быть сделаны программами с setgid или программами, которые вместо этого устанавливают setuid на обычного пользователя[118]118
Одной из программ, разработанных с этой целью, является GNU userv (ftp://ftp.gnu.org/gnu/userv/
) – Примеч. автора.
[Закрыть]).
Получение от системы сведений о UID и GID просто. Функции следующие:
#include
uid_t getuid(void); /* Действительный и эффективный UID */
uid_t geteuid(void);
gid_t getgid(void); /* Действительный и эффективный GID */
gid_t getegid(void);
int getgroups(int size, gid_t list[]); /* Список дополнительных групп*/
Функции:
uid_t getuid(void)
Возвращает действительный UID.
uid_t geteuid(void)
Возвращает эффективный UID.
gid_t getgid(void)
Возвращает действительный GID.
gid_t getegid(void)
Возвращает эффективный GID.
int getgroups(int size, gid_t list[])
Заполняет до size
элементов массива list
из набора дополнительных групп процесса. Возвращаемое значение является числом заполненных элементов или -1 при ошибке. Включается ли в набор также эффективный GID, зависит от реализации. На системах, совместимых с POSIX, можно передать в size нулевое значение; в этом случае getgroups()
возвращает число групп в наборе групп процесса. Затем можно использовать это значение для динамического выделения массива достаточного размера. На не-POSIX системах константа NGROUPS_MAX
определяет максимально допустимый размер для массива list
. Эту константу можно найти в современных системах в
, а в старых системах в
. Вскоре мы представим пример.
Возможно, вы заметили, что для получения сохраненных значений set-user ID или set-group ID нет вызовов. Это просто первоначальные значения эффективных UID и GID. Таким образом, для получения шести значений в начале программы вы можете использовать код наподобие этого:
uid_t ruid, euid, saved_uid;
gid_t rgid, egid, saved_gid;
int main(int argc, char **argv) {
ruid = getuid();
euid = saved_uid = geteuid();
rgid = getgid();
egid = saved_gid = getegid();
/* ...оставшаяся программа... */
}
Вот пример получения набора групп. В качестве расширения gawk
предоставляет доступ на уровне awk
к значениям действительных и эффективных UID и GID и дополнительному набору групп. Для этого он должен получить набор групп. Следующая функция из main.c
в дистрибутиве gawk
3.1.3:
1080 /* init_groupset – инициализация набора групп */
1081
1082 static void
1083 init_groupset()
1084 {
1085 #if defined(HAVE_GETGROUPS) && defined(NGROUPS_MAX) && NGROUPS_MAX > 0
1086 #ifdef GETGROUPS_NOT_STANDARD
1087 /* Для систем, которые не отвечают стандарту, используйте старый способ */
1088 ngroups = NGROUPS_MAX;
1089 #else
1090 /*
1091 * Если оба аргумента при вызове равны 0, возвращаемое
1092 * значение является общим числом групп.
1093 */
1094 ngroups = getgroups(0, NULL);
1095 #endif
1096 if (ngroups == -1)
1097 fatal(_("could not find groups: %s"), strerror(errno));
1098 else if (ngroups == 0)
1099 return;
1100
1101 /* заполнить группы */
1102 emalloc(groupset, GETGROUPS_T*, ngroups * sizeof(GETGROUPS_T), "init_groupset");
1103
1104 ngroups = getgroups(ngroups, groupset);
1105 if (ngroups == -1)
1106 fatal(_("could not find groups: %s"), strerror(errno));
1107 #endif
1108 }
Переменные ngroups
и groupset
глобальные; их объявления не показаны. Макрос GETGROUPS_T
(строка 1102) является типом для использования со вторым аргументом: на системе POSIX это gid_t
, в противном случае int
.
Строки 1085 и 1107 заключают в скобки все тело функции; на древних системах, в которых вообще нет наборов групп, тело функции пустое.
Строки 1086–1088 обрабатывают не-POSIX системы; до компиляции программы механизмом конфигурации определяется GETGROUPS_NOT_STANDARD
. В этом случае код использует NGROUPS_MAX
, как описано выше. (Даже а 2004 г. такие системы все еще существуют и используются; хотя, слава богу, число их уменьшается.)
Строки 1089–1094 для систем POSIX, причем нулевой параметр size
используется для получения числа групп.
Строки 1096–1099 осуществляют проверку ошибок. Если возвращаемое значение 0, дополнительных групп нет, поэтому init_groupset()
просто сразу возвращается.
Наконец, строка 1102 для выделения массива достаточного размера использует malloc()
(посредством проверяющего ошибки макроса-оболочки, см. раздел 3.2.1.8 «Пример: чтение строк произвольной длины»). Затем строка 1104 заполняет этот массив.
access()
В большинстве случаев значения эффективного и действительного UID и GID являются одними и теми же. Таким образом, не имеет значения, что проверка прав доступа к файлу осуществляется по эффективному ID, а не по действительному.
Однако, при написании приложения с setuid или setgid вы можете иногда захотеть проверить, является ли операция, разрешенная для эффективных UID и GID, также разрешенной для действительных UID и GID. В этом заключается задача функции access()
:
#include
int access(const char *path, int amode);
Аргумент path
является путем к файлу для проверки действительных UID и GID. amode
содержит объединение побитовым ИЛИ одного или нескольких из следующих значений:
R_OK
Действительный UID/GID разрешает чтение файла.
W_OK
Действительный UID/GID разрешает запись в файл.
X_OK
Действительный UID/GID разрешает исполнение файла или, в случае каталога, поиск в каталоге.
F_OK
Проверка существования файла.
Проверяется каждый компонент в имени пути, а на некоторых реализациях при проверке для root access()
может действовать, как если бы был установлен X_OK
, даже если в правах доступа к файлу не установлены биты, разрешающие исполнение. (Странно, но верно: в этом случае предупрежденный вооружен.) В Linux нет такой проблемы.
Если path
является символической ссылкой, access()
проверяет файл, на который указывает символическая ссылка.
Возвращаемое значение равно 0, если операция для действительных UID и GID разрешена, и -1 в противном случае. Соответственно, если access()
возвращает -1, программа с setuid может запретить доступ к файлу, с которым в противном случае эффективный UID/GID смог бы работать:
if (access("/some/special/file", R_OK|W_OK) < 0) {
fprintf(stderr, "Sorry: /some/special/file: %sn",
strerror(errno));
exit(1);
}
По крайней мере для серии ядра Linux 2.4, когда тест X_OK применяется к файловой системе, смонтированной с опцией noexec
(см. раздел 8.2.1 «Использование опций монтирования»), тест успешно проходится, если права доступа к файлу имеют разрешение на исполнение. Это верно, несмотря на то, что попытка выполнить файл завершилась бы неудачей.
ЗАМЕЧАНИЕ. Хотя использование
access()
перед открытием файла является обычной практикой, существует состояние гонки открываемый файл может быть сброшен при подкачке между проверкой функциейaccess()
и вызовомopen()
. Необходимо осмотрительное программирование, такое, как проверка владельца и прав доступа с помощьюstat()
иfstat()
до и после вызововaccess()
иopen()
.
Например, программа pathchk
проверяет действительность имен путей. GNU версия использует access()
для проверки того, что компоненты каталога данного пути действительны. Из Coreutils pathchk.c
:
244 /* Возвращает 1, если PATH является годным к использованию
245 каталогом, 0 если нет, 2 если он не существует. */
246
247 static int
248 dir_ok(const char *path)
249 {
250 struct stat stats;
251
252 if (stat (path, &stats)) /* Nonzero return = failure */
253 return 2;
254
255 if (!S_ISDIR(stats.st_mode))
256 {
257 error(0, 0, _("'%s" is not a directory"), path);
258 return 0;
259 }
260
261 /* Используйте access для проверки прав доступа на поиск,
262 поскольку при проверке битов прав доступа st_mode они могут
263 потеряться новыми механизмами управления доступом. Конечно,
264 доступ теряется, если вы используете setuid. */
265 if (access (path, X_OK) != 0)
266 {
267 if (errno == EACCES)
268 error (0, 0, _("directory '%s' is not searchable"), path);
269 else
270 error(0, errno, "%s", path);
271 return 0;
272 }
273
274 return 1;
275 }
Код прост. Строки 252–253 проверяют, существует ли файл. Если stat()
завершится неудачей, файл не существует. Строки 255–259 удостоверяют, что файл в самом деле является каталогом.
Комментарий в строках 261–264 объясняет использование access()
. Проверки битов st_mode
недостаточно: файл может находиться в файловой системе, которая смонтирована только для чтения, в удаленной файловой системе или в файловой системе, не принадлежащей Linux или Unix, или у файла могут быть атрибуты, предотвращающие доступ. Таким образом, лишь ядро может в действительности сказать, будет ли работать access
. Строки 265–272 осуществляют проверку, выдавая сообщение об ошибке, определяемое значением errno
(строки 267–270).