раммы "общего пользования", обращающиеся к этим файлам, должны иметь бит set-uid.
Откуда же изначально берутся значения uid и ruid (а также gid и rgid) у про-
цесса? Они берутся из процесса регистрации пользователя в системе: /etc/getty. Этот
процесс запускается на каждом терминале как процесс, принадлежащий суперпользователю
(u_uid==0). Сначала он запрашивает имя и пароль пользователя:
#include /* cc -lc_s */
#include
#include
struct passwd *p;
char userName[80], *pass, *crpass;
extern char *getpass(), *crypt();
...
/* Не прерываться по сигналам с клавиатуры */
signal (SIGINT, SIG_IGN);
for(;;){
/* Запросить имя пользователя: */
printf("Login: "); gets(userName);
/* Запросить пароль (без эха): */
pass = getpass("Password: ");
/* Проверить имя: */
if(p = getpwnam(userName)){
/* есть такой пользователь */
crpass = (p->pw_passwd[0]) ? /* если есть пароль */
crypt(pass, p->pw_passwd) : pass;
if( !strcmp( crpass, p->pw_passwd))
break; /* верный пароль */
}
printf("Login incorrect.\a\n");
}
signal (SIGINT, SIG_DFL);
А. Богатырев, 1992-95 - 239 - Си в UNIX
Затем он выполняет:
// ... запись информации о входе пользователя в систему
// в файлы /etc/utmp (кто работает в системе сейчас)
// и /etc/wtmp (список всех входов в систему)
...
setuid( p->pw_uid ); setgid( p->pw_gid );
chdir ( p->pw_dir ); /* GO HOME! */
// эти параметры будут унаследованы
// интерпретатором команд.
...
// настройка некоторых переменных окружения envp:
// HOME = p->pw_dir
// SHELL = p->pw_shell
// PATH = нечто по умолчанию, вроде :/bin:/usr/bin
// LOGNAME (USER) = p->pw_name
// TERM = считывается из файла
// /etc/ttytype по имени устройства av[1]
// Делается это как-то подобно
// char *envp[MAXENV], buffer[512]; int envc = 0;
// ...
// sprintf(buffer, "HOME=%s", p->pw_dir);
// envp[envc++] = strdup(buffer);
// ...
// envp[envc] = NULL;
...
// настройка кодов доступа к терминалу. Имя устройства
// содержится в параметре av[1] функции main.
chown (av[1], p->pw_uid, p->pw_gid);
chmod (av[1], 0600 ); /* -rw------- */
// теперь доступ к данному терминалу имеют только
// вошедший в систему пользователь и суперпользователь.
// В случае смерти интерпретатора команд,
// которым заменится getty, процесс init сойдет
// с системного вызова ожидания wait() и выполнит
// chown ( этот_терминал, 2 /*bin*/, 15 /*terminal*/ );
// chmod ( этот_терминал, 0600 );
// и, если терминал числится в файле описания линий
// связи /etc/inittab как активный (метка respawn), то
// init перезапустит на этом_терминале новый
// процесс getty при помощи пары вызовов fork() и exec().
...
// запуск интерпретатора команд:
execle( *p->pw_shell ? p->pw_shell : "/bin/sh",
"-", NULL, envp );
В результате он становится процессом пользователя, вошедшего в систему. Таковым же
после exec-а, выполняемого getty, остается и интерпретатор команд p->pw_shell (обычно
/bin/sh или /bin/csh) и все его потомки.
На самом деле, в описании регистрации пользователя при входе в систему, созна-
тельно было допущено упрощение. Дело в том, что все то, что мы приписали процессу
getty, в действительности выполняется двумя программами: /etc/getty и /bin/login.
Сначала процесс getty занимается настройкой параметров линии связи (т.е. терми-
нала) в соответствии с ее описанием в файле /etc/gettydefs. Затем он запрашивает имя
пользователя и заменяет себя (при помощи сисвызова exec) процессом login, передавая
ему в качестве одного из аргументов полученное имя пользователя.
Затем login запрашивает пароль, настраивает окружение, и.т.п., то есть именно он
производит все операции, приведенные выше на схеме. В конце концов он заменяет себя
интерпретатором команд.
Такое разделение делается, в частности, для того, чтобы считанный пароль в слу-
чае опечатки не хранился бы в памяти процесса getty, а уничтожался бы при очистке
А. Богатырев, 1992-95 - 240 - Си в UNIX
памяти завершившегося процесса login. Таким образом пароль в истинном, незашифрован-
ном виде хранится в системе минимальное время, что затрудняет его подсматривание
средствами электронного или программного шпионажа. Кроме того, это позволяет изме-
нять систему проверки паролей не изменяя программу инициализации терминала getty.
Имя, под которым пользователь вошел в систему на данном терминале, можно узнать
вызовом стандартной функции
char *getlogin();
Эта функция не проверяет uid процесса, а просто извлекает запись про данный терминал
из файла /etc/utmp.
Наконец отметим, что владелец файла устанавливается при создании этого файла
(вызовами creat или mknod), и полагается равным эффективному идентификатору создаю-
щего процесса.
di_uid = u_uid; di_gid = u_gid;
6.8.4. Напишите программу, узнающую у системы и распечатывающую: номер процесса,
номер и имя своего владельца, номер группы, название и тип терминала на котором она
работает (из переменной окружения TERM).
6.9. Блокировка доступа к файлам.
В базах данных нередко встречается ситуация одновременного доступа к одним и тем
же данным. Допустим, что в некотором файле хранятся данные, которые могут читаться и
записываться произвольным числом процессов.
- Допустим, что процесс A изменяет некоторую область файла, в то время как процесс
B пытается прочесть ту же область. Итогом такого соревнования может быть то,
что процесс B прочтет неверные данные.
- Допустим, что процесс A изменяет некоторую область файла, в то время как процесс
C также изменяет ту же самую область. В итоге эта область может содержать
неверные данные (часть - от процесса A, часть - от C).
Ясно, что требуется механизм синхронизации процессов, позволяющий не пускать
другой процесс (процессы) читать и/или записывать данные в указанной области. Меха-
низмов синхронизации в UNIX существует множество: от семафоров до блокировок областей
файла. О последних мы и будем тут говорить.
Прежде всего отметим, что блокировки файла носят в UNIX необязательный характер.
То есть, программа не использующая вызовов синхронизации, будет иметь доступ к данным
без каких либо ограничений. Увы. Таким образом, программы, собирающиеся корректно
пользоваться общими данными, должны все использовать - и при том один и тот же -
механизм синхронизации: заключить между собой "джентльменское соглашение".
6.9.1. Блокировка устанавливается при помощи вызова
flock_t lock;
fcntl(fd, operation, &lock);
Здесь operation может быть одним из трех:
F_SETLK
Устанавливает или снимает замок, описываемый структурой lock. Структура flock_t
имеет такие поля:
short l_type;
short l_whence;
off_t l_start;
size_t l_len;
long l_sysid;
pid_t l_pid;
l_type
тип блокировки:
А. Богатырев, 1992-95 - 241 - Си в UNIX
F_RDLCK - на чтение;
F_WRLCK - на запись;
F_UNLCK - снять все замки.
l_whence, l_start, l_len
описывают сегмент файла, на который ставится замок: от точки
lseek(fd,l_start,l_whence); длиной l_len байт. Здесь l_whence может быть:
SEEK_SET, SEEK_CUR, SEEK_END. l_len равное нулю означает "до конца файла". Так
если все три параметра равны 0, то будет заблокирован весь файл.
F_SETLKW
Устанавливает или снимает замок, описываемый структурой lock. При этом, если
замок на область, пересекающуюся с указанной уже кем-то установлен, то сперва
дождаться снятия этого замка.
Пытаемся | Нет Уже есть уже есть
поставить | чужих замок замок
замок на | замков на READ на WRITE
-----------|---------------------------------------------------------------
READ | читать читать ждать;запереть;читать
WRITE | записать ждать;запереть;записать ждать;запереть;записать
UNLOCK | отпереть отпереть отпереть
- Если кто-то читает сегмент файла, то другие тоже могут его читать свободно, ибо
чтение не изменяет файла.
- Если же кто-то записывает файл - то все остальные должны дождаться окончания
записи и разблокировки.
- Если кто-то читает сегмент, а другой процесс собрался изменить (записать) этот
сегмент, то этот другой процесс обязан дождаться окончания чтения первым.
- В момент, обозначенный как отпереть - будятся процессы, ждущие разблокировки, и
ровно один из них получает доступ (может установить свою блокировку). Порядок -
кто из них будет первым - вообще говоря не определен.
F_GETLK
Запрашиваем возможность установить замок, описанный в lock.
- Если мы можем установить такой замок (не заперто никем), то в структуре lock
поле l_type становится равным F_UNLCK и поле l_whence равным SEEK_SET.
- Если замок уже кем-то установлен (и вызов F_SETLKW заблокировал бы наш процесс,
привел бы к ожиданию), мы получаем информацию о чужом замке в структуру lock.
При этом в поле l_pid заносится идентификатор процесса, создавшего этот замок, а
в поле l_sysid - идентификатор машины (поскольку блокировка файлов поддержива-
ется через сетевые файловые системы).
Замки автоматически снимаются при закрытии дескриптора файла. Замки не наследу-
ются порожденным процессом при вызове fork.
#include
#include
#include
#include
#include
#include
char DataFile [] = "data.xxx";
char info [] = "abcdefghijklmnopqrstuvwxyz";
#define OFFSET 5
#define SIZE 12
#define PAUSE 2
int trial = 1;
int fd, pid;
char buffer[120], myname[20];
void writeAccess(), readAccess();
А. Богатырев, 1992-95 - 242 - Си в UNIX
void fcleanup(int nsig){
unlink(DataFile);
printf("cleanup:%s\n", myname);
if(nsig) exit(0);
}
int main(){
int i;
fd = creat(DataFile, 0644);
write(fd, info, strlen(info));
close(fd);
signal(SIGINT, fcleanup);
sprintf(myname, fork() ? "B-%06d" : "A-%06d", pid = getpid());
srand(time(NULL)+pid);
printf("%s:started\n", myname);
fd = open(DataFile, O_RDWR|O_EXCL);
printf("%s:opened %s\n", myname, DataFile);
for(i=0; i < 30; i++){
if(rand()%2) readAccess();
else writeAccess();
}
close(fd);
printf("%s:finished\n", myname);
wait(NULL);
fcleanup(0);
return 0;
}
А. Богатырев, 1992-95 - 243 - Си в UNIX
void writeAccess(){
flock_t lock;
printf("Write:%s #%d\n", myname, trial);
lock.l_type = F_WRLCK;
lock.l_whence = SEEK_SET;
lock.l_start = (off_t) OFFSET;