// 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 <stdio.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <time.h>
#include <signal.h>

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;
lock.l_len = (size_t) SIZE;

if(fcntl(fd, F_SETLKW, &lock) <0)
perror("F_SETLKW");
printf("\twrite:%s locked\n", myname);

sprintf(buffer, "%s #%02d", myname, trial);
printf ("\twrite:%s \"%s\"\n", myname, buffer);

lseek (fd, (off_t) OFFSET, SEEK_SET);
write (fd, buffer, SIZE);

sleep (PAUSE);

lock.l_type = F_UNLCK;
if(fcntl(fd, F_SETLKW, &lock) <0)
perror("F_SETLKW");

printf("\twrite:%s unlocked\n", myname);

trial++;
}


void readAccess(){
flock_t lock;

printf("Read:%s #%d\n", myname, trial);

lock.l_type = F_RDLCK;
lock.l_whence = SEEK_SET;
lock.l_start = (off_t) OFFSET;
lock.l_len = (size_t) SIZE;

if(fcntl(fd, F_SETLKW, &lock) <0)
perror("F_SETLKW");
printf("\tread:%s locked\n", myname);

lseek(fd, (off_t) OFFSET, SEEK_SET);
read (fd, buffer, SIZE);

printf("\tcontents:%s \"%*.*s\"\n", myname, SIZE, SIZE, buffer);
sleep (PAUSE);

lock.l_type = F_UNLCK;
if(fcntl(fd, F_SETLKW, &lock) <0)
perror("F_SETLKW");

printf("\tread:%s unlocked\n", myname);

trial++;
}




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

Исследуя выдачу этой программы, вы можете обнаружить, что READ-области могут перекры-
ваться; но что никогда не перекрываются области READ и WRITE ни в какой комбинации.
Если идет чтение процессом A - то запись процессом B дождется разблокировки A (чтение
- не будет дожидаться). Если идет запись процессом A - то и чтение процессом B и
запись процессом B дождутся разблокировки A.

6.9.2.
UNIX SVR4 имеет еще один интерфейс для блокировки файлов: функцию lockf.

#include <unistd.h>

int lockf(int fd, int operation, size_t size);

Операция operation:
F_ULOCK
Разблокировать указанный сегмент файла (это может снимать один или несколько
замков).
F_LOCK
F_TLOCK
Установить замок. При этом, если уже имеется чужой замок на запрашиваемую
область, F_LOCK блокирует процесс, F_TLOCK - просто выдает ошибку (функция возв-
ращает -1, errno устанавливается в EAGAIN).
- Ожидание отпирания/запирания замка может быть прервано сигналом.
- Замок устанавливается следующим образом: от текущей позиции указателя чтения-
записи в файле fd (что не похоже на fcntl, где позиция задается явно как пара-
метр в структуре); длиной size. Отрицательное значение size означает отсчет от
текущей позиции к началу файла. Нулевое значение - означает "от текущей позиции
до конца файла". При этом "конец файла" понимается именно как конец, а не как
текущий размер файла. Если файл изменит размер, запертая область все равно
будет простираться до конца файла (уже нового).
- Замки, установленные процессом, автоматически отпираются при завершении про-
цесса.
F_TEST
Проверить наличие замка. Функция возвращает 0, если замка нет; -1 в противном
случае (заперто).
Если устанавливается замок, перекрывающийся с уже установленным, то замки объединя-
ются.

было: ___________#######____######__________

запрошено:______________##########______________

стало: ___________#################__________

Если снимается замок с области, покрывающей только часть заблокированной прежде,
остаток области остается как отдельный замок.

было: ___________#################__________

запрошено:______________XXXXXXXXXX______________

стало: ___________###__________####__________


    6.10. Файлы устройств.


Пространство дисковой памяти может состоять из нескольких файловых систем
дальнейшем FS), т.е. логических и/или физических дисков. Каждая файловая система
имеет древовидную логическую структуру (каталоги, подкаталоги и файлы) и имеет свой
корневой каталог. Файлы в каждой FS имеют свои собственные I-узлы и собственную их
нумерацию с 1. В начале каждой FS зарезервированы:




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

- блок для загрузчика - программы, вызываемой аппаратно при включении машины (заг-
рузчик записывает с диска в память машины программу /boot, которая в свою оче-
редь загружает в память ядро /unix);
- суперблок - блок заголовка файловой системы, хранящий размер файловой системы (в
блоках), размер блока (512, 1024, ...), количество I-узлов, начало списка сво-
бодных блоков, и другие сведения об FS;
- некоторая непрерывная область диска для хранения I-узлов - "I-файл".

Файловые системы объединяются в единую древовидную иерархию операцией монтирования -
подключения корня файловой системы к какому-то из каталогов-"листьев" дерева другой
FS.
Файлы в объединенной иерархии адресуются при помощи двух способов:
- имен, задающих путь в дереве каталогов:

/usr/abs/bin/hackIt
bin/hackIt
./../../bin/vi

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

Поскольку в каждой FS имеется собственная нумерация I-узлов, то файл в объединенной
иерархии должен адресоваться ДВУМЯ параметрами:
- номером (кодом) устройства, содержащего файловую систему, в которой находится
искомый файл: dev_t i_dev;
- номером I-узла файла в этой файловой системе: ino_t i_number;

Преобразование имени файла в объединенной файловой иерархии в такую адресную пару
выполняет в ядре уже упоминавшаяся выше функция namei (при помощи просмотра катало-
гов):

struct inode *ip = namei(...);

Создаваемая ею копия I-узла в памяти ядра содержит поля i_dev и i_number (которые на
самом диске не хранятся!).
Рассмотрим некоторые алгоритмы работы ядра с файлами. Ниже они приведены чисто
схематично и в сильном упрощении. Форматы вызова (и оформление) функций не соот-
ветствуют форматам, используемым на самом деле в ядре; верны лишь названия функций.
Опущены проверки на корректность, подсчет ссылок на структуры file и inode, блоки-
ровка I-узлов и кэш-буферов от одновременного доступа, и многое другое.
Пусть мы хотим открыть файл для чтения и прочитать из него некоторую информацию.
Вызовы открытия и закрытия файла имеют схему (часть ее будет объяснена позже):

#include <sys/types.h>
#include <sys/inode.h>
#include <sys/file.h>
int fd_read = open(имяФайла, O_RDONLY){

int fd; struct inode *ip; struct file *fp; dev_t dev;

u_error = 0; /* errno в программе */
// Найти файл по имени. Создается копия I-узла в памяти:
ip = namei(имяФайла, LOOKUP);
// namei может выдать ошибку, если нет такого файла
if(u_error) return(-1); // ошибка

// Выделяется структура "открытый файл":
fp = falloc(ip, FREAD);
// fp->f_flag = FREAD; открыт на чтение



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

// fp->f_offset = 0; RWptr
// fp->f_inode = ip; ссылка на I-узел

// Выделить новый дескриптор
for(fd=0; fd < NOFILE; fd++)
if(u_ofile[fd] == NULL ) // свободен
goto done;
u_error = EMFILE; return (-1);
done:
u_ofile[fd] = fp;

// Если это устройство - инициализировать его.
// Это функция openi(ip, fp->f_flag);
dev = ip->i_rdev;
if((ip->i_mode & IFMT) == IFCHR)
(*cdevsw[major(dev)].d_open)(minor(dev),fp->f_flag);
else if((ip->i_mode & IFMT) == IFBLK)
(*bdevsw[major(dev)].d_open)(minor(dev),fp->f_flag);
return fd; // через u_rval1
}


close(fd){
struct file *fp = u_ofile[fd];
struct inode *ip = fp->f_inode;
dev_t dev = ip->i_rdev;

if((ip->i_mode & IFMT) == IFCHR)
(*cdevsw[major(dev)].d_close)(minor(dev),fp->f_flag);
else if((ip->i_mode & IFMT) == IFBLK)
(*bdevsw[major(dev)].d_close)(minor(dev),fp->f_flag);

u_ofile[fd] = NULL;
// и удалить ненужные структуры из ядра.
}

Теперь рассмотрим функцию преобразования логических блоков файла в номера физических
блоков в файловой системе. Для этого преобразования в I-узле файла содержится таблица
адресов блоков. Она устроена довольно сложно - ее начало находится в узле, а продол-
жение - в нескольких блоках в самой файловой системе (устройство это можно увидеть в
примере "Фрагментированность файловой системы" в приложении). Мы для простоты будем
предполагать, что это просто линейный массив i_addr[], в котором n-ому логическому
блоку файла отвечает bno-тый физический блок файловой системы:

bno = ip->i_addr[n];

Если файл является интерфейсом устройства, то этот файл не хранит информации в логи-
ческой файловой системе. Поэтому у устройств нет таблицы адресов блоков. Вместо
этого, поле i_addr[0] используется для хранения кода устройства, к которому приводит
этот специальный файл. Это поле носит название i_rdev, т.е. как бы сделано

#define i_rdev i_addr[0]

(на самом деле используется union). Устройства бывают байто-ориентированные, обмен с
которыми производится по одному байту (как с терминалом или с коммуникационным пор-
том); и блочно-ориентированные, обмен с которыми возможен только большими порциями -
блоками (пример - диск). То, что файл является устройством, помечено в поле тип
файла

ip->i_mode & IFMT




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

одним из значений: IFCHR - байтовое; или IFBLK - блочное. Алгоритм вычисления номера
блока:

ushort u_pboff; // смещение от начала блока
ushort u_pbsize; // сколько байт надо использовать
// ushort - это unsigned short, смотри <sys/types.h>
// daddr_t - это long (disk address)

daddr_t bmap(struct inode *ip,
off_t offset, unsigned count){
int sz, rem;

// вычислить логический номер блока по позиции RWptr.
// BSIZE - это размер блока файловой системы,
// эта константа определена в <sys/param.h>
daddr_t bno = offset / BSIZE;
// если BSIZE == 1 Кб, то можно offset >> 10

u_pboff = offset % BSIZE;
// это можно записать как offset & 01777

sz = BSIZE - u_pboff;
// столько байт надо взять из этого блока,
// начиная с позиции u_pboff.

if(count < sz) sz = count;
u_pbsize = sz;

Если файл представляет собой устройство, то трансляция логических блоков в физические
не производится - устройство представляет собой "сырой" диск без файлов и каталогов,
т.е. обращение происходит сразу по физическому номеру блока:

if((ip->i_mode & IFMT) == IFBLK) // block device
return bno; // raw disk
// иначе провести пересчет:

rem = ip->i_size /*длина файла*/ - offset;
// это остаток файла.
if( rem < 0 ) rem = 0;
// файл короче, чем заказано нами:
if( rem < sz ) sz = rem;
if((u_pbsize = sz) == 0) return (-1); // EOF

// и, собственно, замена логич. номера на физич.
return ip->i_addr[bno];
}

Теперь рассмотрим алгоритм read. Параметры, начинающиеся с u_..., на самом деле пере-
даются как статические через вспомогательные переменные в u-area процесса.

read(int fd, char *u_base, unsigned u_count){
unsigned srccount = u_count;
struct file *fp = u_ofile[fd];
struct inode *ip = fp->f_inode;
struct buf *bp;
daddr_t bno; // очередной блок файла

// dev - устройство,
// интерфейсом которого является файл-устройство,
// или на котором расположен обычный файл.
dev_t dev = (ip->i_mode & (IFCHR|IFBLK)) ?



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

ip->i_rdev : ip->i_dev;

switch( ip->i_mode & IFMT ){

case IFCHR: // байто-ориентированное устройство
(*cdevsw[major(dev)].d_read)(minor(dev));
// прочие параметры передаются через u-area
break;

case IFREG: // обычный файл
case IFDIR: // каталог
case IFBLK: // блочно-ориентированное устройство
do{
bno = bmap(ip, fp->f_offset /*RWptr*/, u_count);
if(u_pbsize==0 || (long)bno < 0) break; // EOF
bp = bread(dev, bno); // block read

iomove(bp->b_addr + u_pboff, u_pbsize, B_READ);

Функция iomove копирует данные

bp->b_addr[ u_pboff..u_pboff+u_pbsize-1 ]

из адресного пространства ядра (из буфера в ядре) в адресное пространство процесса по
адресам

u_base[ 0..u_pbsize-1 ]

то есть пересылает u_pbsize байт между ядром и процессом (u_base попадает в iomove
через статическую переменную). При записи вызовом write(), iomove с флагом B_WRITE
производит обратное копирование - из памяти процесса в память ядра. Продолжим:

// продвинуть счетчики и указатели:
u_count -= u_pbsize;
u_base += u_pbsize;
fp->f_offset += u_pbsize; // RWptr
} while( u_count != 0 );
break;
...
return( srccount - u_count );
} // end read

Теперь обсудим некоторые места этого алгоритма. Сначала посмотрим, как происходит
обращение к байтовому устройству. Вместо адресов блоков мы получаем код устройства
i_rdev. Коды устройств в UNIX (тип dev_t) представляют собой пару двух чисел, назы-
ваемых мажор и минор, хранимых в старшем и младшем байтах кода устройства:

#define major(dev) ((dev >> 8) & 0x7F)
#define minor(dev) ( dev & 0xFF)

Мажор обозначает тип устройства (диск, терминал, и.т.п.) и приводит к одному из драй-
веров (если у нас есть 8 терминалов, то их обслуживает один и тот же драйвер); а
минор обозначает номер устройства данного типа (... каждый из терминалов имеет миноры
0..7). Миноры обычно служат индексами в некоторой таблице структур внутри выбранного
драйвера. Мажор же служит индексом в переключательной таблице устройств. При этом
блочно-ориентированные устройства выбираются в одной таблице - bdevsw[], а байто-
ориентированные - в другой - cdevsw[] (см. <sys/conf.h>; имена таблиц означают
block/character device switch). Каждая строка таблицы содержит адреса функций,
выполняющих некоторые предопределенные операции способом, зависимым от устройства.
Сами эти функции реализованы в драйверах устройств. Аргументом для этих функций
обычно служит минор устройства, к которому производится обращение. Функция в



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

драйвере использует этот минор как индекс для выбора конкретного экземпляра уст-
ройства данного типа; как индекс в массиве управляющих структур (содержащих текущее
состояние, режимы работы, адреса функций прерываний, адреса очередей данных и.т.п.
каждого конкретного устройства) для данного типа устройств. Эти управляющие структуры
различны для разных типов устройств (и их драйверов).
Каждая строка переключательной таблицы содержит адреса функций, выполняющих опе-
рации open, close, read, write, ioctl, select. open служит для инициализации уст-
ройства при первом его открытии (++ip->i_count==1) - например, для включения мотора;
close - для выключения при последнем закрытии (--ip->i_count==0). У блочных уст-
ройств поля для read и write объединены в функцию strategy, вызываемую с параметром
B_READ или B_WRITE. Вызов ioctl предназначен для управления параметрами работы уст-
ройства. Операция select - для опроса: есть ли поступившие в устройство данные (нап-
ример, есть ли в clist-е ввода с клавиатуры байты? см. главу "Экранные библиотеки").
Вызов select применим только к некоторым байтоориентированным устройствам и сетевым
портам (socket-ам). Если данное устройство не умеет выполнять такую операцию, то
есть запрос к этой операции должен вернуть в программу ошибку (например, операция
read неприменима к принтеру), то в переключательной таблице содержится специальное
имя функции nodev; если же операция допустима, но является фиктивной (как write для
/dev/null) - имя nulldev. Обе эти функции-заглушки представляют собой "пустышки":
{}.
Теперь обратимся к блочно-ориентированным устройствам. UNIX использует внутри
ядра дополнительную буферизацию при обменах с такими устройствами|-. Использованная
нами выше функция bp=bread(dev,bno); производит чтение физического блока номер bno с
устройства dev. Эта операция обращается к драйверу конкретного устройства и вызывает
чтение блока в некоторую область памяти в ядре ОС: в один из кэш-буферов (cache,
"запасать"). Заголовки кэш-буферов (struct buf) организованы в список и имеют поля
(см. файл <sys/buf.h>):
b_dev
код устройства, с которого прочитан блок;
b_blkno
номер физического блока, хранящегося в буфере в данный момент;
b_flags
флаги блока (см. ниже);
b_addr
адрес участка памяти (как правило в самом ядре), в котором собственно и хранится
содержимое блока.

Буферизация блоков позволяет системе экономить число обращений к диску. При обраще-
нии к bread() сначала происходит поиск блока (dev,bno) в таблице кэш-буферов. Если
блок уже был ранее прочитан в кэш, то обращения к диску не происходит, поскольку
копия содержимого дискового блока уже есть в памяти ядра. Если же блока еще нет в
кэш-буферах, то в ядре выделяется чистый буфер, в заголовке ему прописываются нужные
значения полей b_dev и b_blkno, и блок считывается в буфер с диска вызовом функции

bp->b_flags |= B_READ; // род работы: прочитать
(*bdevsw[major(dev)].d_startegy)(bp);
// bno и минор - берутся из полей *bp

из драйвера конкретного устройства.
Когда мы что-то изменяем в файле вызовом write(), то изменения на самом деле
происходят в кэш-буферах в памяти ядра, а не сразу на диске. При записи в блок буфер
помечается как измененный:

b_flags |= B_DELWRI; // отложенная запись

____________________
|- Следует отличать эту системную буферизацию от буферизации при помощи библиотеки
stdio. Библиотека создает буфер в самом процессе, тогда как системные вызовы имеют