Linux - статьи

         

Реализация сети в операционной


Глеб Пахаренко ()

Нашу курсовую мы разделим на 4 части

общий обзор более специфическое исследование сетевой активности пройдемся через вызовы функций разберём основные структуры данных и участки кода

Рассмотрим подробнее что происходит с пакетом при попадании в нашу машину. Сначала он обрабатывается драйвером аппаратуры(сетевой карты и т.д) если пакет предназначен нам то он посылается на выше лежаций уровень - сетевой там определяется для кого он предназначен: нам или кому-то другому, для етого просматривается кэш маршрутизации,если там нет маршрута то Forwarding Information Base (FIB), если пакет предназначен другому компьютеру то ядро шлёт его на соответствующее устройство (сетевую карту) ,если нам ,то через транспортный и вышележашие уровни приложению. Обмен данными между приложением и ядром осуществляется через абстракцию сокета.В Линухе используется BSD сокеты.

Рассмотрим поподробнее структуру пакета

Ключ к быстрому обмену данными в использовании структуры sk_buf и передачи на вышестоящии уровни тобько указаттеля на неё

описание структуры лежит в linux/skbuff.h

её поля

struct sk_buff { /* These two members must be first. */ struct sk_buff * next; /* Next buffer in list */ struct sk_buff * prev; /* Previous buffer in list */

struct sk_buff_head * list; /* List we are on */ struct sock *sk; /* Socket we are owned by */ struct timeval stamp; /* Time we arrived */ struct net_device *dev; /* Device we arrived on/are leaving by */

/* Transport layer header */ union { struct tcphdr *th; struct udphdr *uh; struct icmphdr *icmph; struct igmphdr *igmph; struct iphdr *ipiph; struct spxhdr *spxh; unsigned char *raw; } h;

/* Network layer header */ union { struct iphdr *iph; struct ipv6hdr *ipv6h; struct arphdr *arph; struct ipxhdr *ipxh; unsigned char *raw; } nh;

/* Link layer header */ union { struct ethhdr *ethernet; unsigned char *raw; } mac;

struct dst_entry *dst;

/* * This is the control buffer. It is free to use for every * layer. Please put your private variables there. If you * want to keep them across layers you have to do a skb_clone() * first. This is owned by whoever has the skb queued ATM. */ char cb[48];


unsigned int len; /* Length of actual data */ unsigned int data_len; unsigned int csum; /* Checksum */ unsigned char __unused, /* Dead field, may be reused */ cloned, /* head may be cloned (check refcnt to be sure). */ pkt_type, /* Packet class */ ip_summed; /* Driver fed us an IP checksum */ __u32 priority; /* Packet queueing priority */ atomic_t users; /* User count - see datagram.c,tcp.c */ unsigned short protocol; /* Packet protocol from driver. */ unsigned short security; /* Security level of packet */ unsigned int truesize; /* Buffer size */

unsigned char *head; /* Head of buffer */ unsigned char *data; /* Data head pointer */ unsigned char *tail; /* Tail pointer */ unsigned char *end; /* End pointer */

void (*destructor)(struct sk_buff *); /* Destruct function */ #ifdef CONFIG_NETFILTER /* Can be used for communication between hooks. */ unsigned long nfmark; /* Cache info */ __u32 nfcache; /* Associated connection, if any */ struct nf_ct_info *nfct; #ifdef CONFIG_NETFILTER_DEBUG unsigned int nf_debug; #endif #endif /*CONFIG_NETFILTER*/

#if defined(CONFIG_HIPPI) union{ __u32 ifield; } private; #endif

#ifdef CONFIG_NET_SCHED __u32 tc_index; /* traffic control index */ #endif };

там же содержится масса полезных функций для работы с sk_buff. все протоколы используют ету структуру добавляя заголовки своего уровня



Маршрутизация

Уровень IP использует 3 структуры для маршрутизации FIB где хранятся все маршруты routing cache где находятся наиболее часто используемые neibour table список компьютеров физически соединенных с данным

FIB содержит 32 зоны по одной на каждый бит ip адреса каждая зона содержит точки входа для хостов и сетей которые задайтся данной маской подсети 255.0.0.0 имеет 8 значащих бит и поэтому в восьмой зоне 255.255.255.0 в 24 зоне

файл /proc/net/route содержит FIB

routing cache хэш-таблица которая содержит до 256 цепочек меаршрутов если подходящий маршрут не найден в кэше то он добавляется туда из FIB устаревшие записи по истечении некоторого времени удаляются содержимое кэша можно увидеть в /proc/net/rt_cache



Инициализация сети

главные настройки сети в дистрибутиве RedHat (Mandrake) лежат в /etc/sysconfig/network ,/etc/sysconfig/network-scripts/ifcfg-eth0 и тд...

содержимое моих файлов (не в virtual mashine редхате а на нормальной машине Mandrake-8. 2 где соответственно нет никаких сетевых карт)

/etc/sysconfig/network

NETWORKING=yes

FORWARD_IPV4=false

HOSTNAME=freeland.linux

DOMAINNAME=linux

/etc/sysconfig/network-scripts/ifcfg-lo

DEVICE=lo

IPADDR=127.0.0.1

NETMASK=255.0.0.0

NETWORK=127.0.0.0

# If you're having problems with gated making 127.0.0.0/8 a martian,

# you can change this to something else (255.255.255.255, for example)

BROADCAST=127.255.255.255

ONBOOT=yes

NAME=loopback

Очень полезной программой являеся ifconfig синтаксис которой подробно рассмотрен в мануале

[20:16][pts1]/etc/sysconfig/network-scripts [root] #ifconfig lo Link encap:Local Loopback inet addr:127.0.0.1 Mask:255.0.0.0 UP LOOPBACK RUNNING MTU:16436 Metric:1 RX packets:3242 errors:0 dropped:0 overruns:0 frame:0 TX packets:3242 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:0 RX bytes:227644 (222.3 Kb) TX bytes:227644 (222.3 Kb)

не менее полезна команда route

#route Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 127.0.0.0 * 255.0.0.0 U 0 0 0 lo

её назначение ,а также многих других описано в Linux Network Administrator Guide

Соединения

В этой части мы подробно рассмотрим сокеты и всё что с ними связано

Когда процесс создаёт сокет то он пустой потом система определяет маршрут к удалённому хосту и вносит ету информацию в сокет.После этого пакеты направляются на нужное устройство

Есть два типа сокетов BSD сокеты которые включают как член INET cокеты BSD сокеты описываются структурой struct socket в linux/net.h

struct socket

{ socket_state state;

unsigned long flags; struct proto_ops *ops; struct inode *inode; struct fasync_struct *fasync_list; /* Asynchronous wake up list */ struct file *file; /* File back pointer for gc */ struct sock *sk; wait_queue_head_t wait;



short type; unsigned char passcred; };

struct proto_ops { int family;

int (*release) (struct socket *sock); int (*bind) (struct socket *sock, struct sockaddr *umyaddr, int sockaddr_len); int (*connect) (struct socket *sock, struct sockaddr *uservaddr, int sockaddr_len, int flags); int (*socketpair) (struct socket *sock1, struct socket *sock2); int (*accept) (struct socket *sock, struct socket *newsock, int flags); int (*getname) (struct socket *sock, struct sockaddr *uaddr, int *usockaddr_len, int peer); unsigned int (*poll) (struct file *file, struct socket *sock, struct poll_table_struct *wait); int (*ioctl) (struct socket *sock, unsigned int cmd, unsigned long arg); int (*listen) (struct socket *sock, int len); int (*shutdown) (struct socket *sock, int flags); int (*setsockopt) (struct socket *sock, int level, int optname, char *optval, int optlen); int (*getsockopt) (struct socket *sock, int level, int optname, char *optval, int *optlen); int (*sendmsg) (struct socket *sock, struct msghdr *m, int total_len, struct scm_cookie *scm); int (*recvmsg) (struct socket *sock, struct msghdr *m, int total_len, int flags, struct scm_cookie *scm); int (*mmap) (struct file *file, struct socket *sock, struct vm_area_struct * vma); ssize_t (*sendpage) (struct socket *sock, struct page *page, int offset, size_t size, int flags); };

наиболее важные поля

* struct proto_ops *ops указывает на протокольно зависимые функции struct inode на inode файла сокета struct sock* на инет сокет

INET net/sock.h struct sock

struct sock { /* Socket demultiplex comparisons on incoming packets. */ __u32 daddr; /* Foreign IPv4 addr */ __u32 rcv_saddr; /* Bound local IPv4 addr */ __u16 dport; /* Destination port */ unsigned short num; /* Local port */ int bound_dev_if; /* Bound device index if != 0 */

/* Main hash linkage for various protocol lookup tables. */ struct sock *next; struct sock **pprev; struct sock *bind_next; struct sock **bind_pprev;

volatile unsigned char state, /* Connection state */ zapped; /* In ax25 & ipx means not linked */ __u16 sport; /* Source port */



unsigned short family; /* Address family */ unsigned char reuse; /* SO_REUSEADDR setting */ unsigned char shutdown; atomic_t refcnt; /* Reference count */

socket_lock_t lock; /* Synchronizer... */ int rcvbuf; /* Size of receive buffer in bytes */

wait_queue_head_t *sleep; /* Sock wait queue */ struct dst_entry *dst_cache; /* Destination cache */ rwlock_t dst_lock; atomic_t rmem_alloc; /* Receive queue bytes committed */ struct sk_buff_head receive_queue; /* Incoming packets */ atomic_t wmem_alloc; /* Transmit queue bytes committed */ struct sk_buff_head write_queue; /* Packet sending queue */ atomic_t omem_alloc; /* "o" is "option" or "other" */ int wmem_queued; /* Persistent queue size */ int forward_alloc; /* Space allocated forward. */ __u32 saddr; /* Sending source */ unsigned int allocation; /* Allocation mode */ int sndbuf; /* Size of send buffer in bytes */ struct sock *prev;

/* Not all are volatile, but some are, so we might as well say they all are. * XXX Make this a flag word -DaveM */ volatile char dead, done, urginline, keepopen, linger, destroy, no_check, broadcast, bsdism; unsigned char debug; unsigned char rcvtstamp; unsigned char use_write_queue; unsigned char userlocks; /* Hole of 3 bytes. Try to pack. */ int route_caps; int proc; unsigned long lingertime;

int hashent; struct sock *pair;

/* The backlog queue is special, it is always used with * the per-socket spinlock held and requires low latency * access. Therefore we special case it's implementation. */ struct { struct sk_buff *head; struct sk_buff *tail; } backlog;

rwlock_t callback_lock;

/* Error queue, rarely used. */ struct sk_buff_head error_queue;

struct proto *prot;

#if defined(CONFIG_IPV6) defined (CONFIG_IPV6_MODULE) union { struct ipv6_pinfo af_inet6; } net_pinfo; #endif

union { struct tcp_opt af_tcp; #if defined(CONFIG_INET) defined (CONFIG_INET_MODULE) struct raw_opt tp_raw4; #endif #if defined(CONFIG_IPV6) defined (CONFIG_IPV6_MODULE) struct raw6_opt tp_raw; #endif /* CONFIG_IPV6 */ #if defined(CONFIG_SPX) defined (CONFIG_SPX_MODULE) struct spx_opt af_spx; #endif /* CONFIG_SPX */



} tp_pinfo;

int err, err_soft; /* Soft holds errors that don' t cause failure but are the cause of a persistent failure not just 'timed out' */ unsigned short ack_backlog; unsigned short max_ack_backlog; __u32 priority; unsigned short type; unsigned char localroute; /* Route locally only */ unsigned char protocol; struct ucred peercred; int rcvlowat; long rcvtimeo; long sndtimeo;

#ifdef CONFIG_FILTER /* Socket Filtering Instructions */ struct sk_filter *filter; #endif /* CONFIG_FILTER */

/* This is where all the private (optional) areas that don't * overlap will eventually live. */ union { void *destruct_hook; struct unix_opt af_unix; #if defined(CONFIG_INET) defined (CONFIG_INET_MODULE) struct inet_opt af_inet; #endif #if defined(CONFIG_ATALK) defined(CONFIG_ATALK_MODULE) struct atalk_sock af_at; #endif #if defined(CONFIG_IPX) defined(CONFIG_IPX_MODULE) struct ipx_opt af_ipx; #endif #if defined (CONFIG_DECNET) defined(CONFIG_DECNET_MODULE) struct dn_scp dn; #endif #if defined (CONFIG_PACKET) defined(CONFIG_PACKET_MODULE) struct packet_opt *af_packet; #endif #if defined(CONFIG_X25) defined(CONFIG_X25_MODULE) x25_cb *x25; #endif #if defined(CONFIG_AX25) defined(CONFIG_AX25_MODULE) ax25_cb *ax25; #endif #if defined(CONFIG_NETROM) defined(CONFIG_NETROM_MODULE) nr_cb *nr; #endif #if defined(CONFIG_ROSE) defined(CONFIG_ROSE_MODULE) rose_cb *rose; #endif #if defined(CONFIG_PPPOE) defined(CONFIG_PPPOE_MODULE) struct pppox_opt *pppox; #endif #ifdef CONFIG_NETLINK struct netlink_opt *af_netlink; #endif #if defined(CONFIG_ECONET) defined(CONFIG_ECONET_MODULE) struct econet_opt *af_econet; #endif #if defined(CONFIG_ATM) defined(CONFIG_ATM_MODULE) struct atm_vcc *af_atm; #endif #if defined(CONFIG_IRDA) defined(CONFIG_IRDA_MODULE) struct irda_sock *irda; #endif #if defined(CONFIG_WAN_ROUTER) defined(CONFIG_WAN_ROUTER_MODULE) struct wanpipe_opt *af_wanpipe; #endif } protinfo;

  /* This part is used for the timeout functions. */ struct timer_list timer; /* This is the sock cleanup timer. */ struct timeval stamp;



/* Identd and reporting IO signals */ struct socket *socket;

/* RPC and TUX layer private data */ void *user_data;

/* Callbacks */ void (*state_change)(struct sock *sk); void (*data_ready)(struct sock *sk,int bytes); void (*write_space)(struct sock *sk); void (*error_report)(struct sock *sk);

int (*backlog_rcv) (struct sock *sk, struct sk_buff *skb); void (*destruct)(struct sock *sk); };

Эта структура очень широко используется и имеет много hacks зависящих от конфигурации как видим для каждого протокола здесь найдется местечко

Сокеты проходят через процесс маршрутизации только один раз для каждого маршрута.Они содержат указатель на маршрут struct sock- >dst_cache* и вызывают ip_route_connect (net/route.h) для нахождения маршрута информация записывается в dst_cache и сокет дальше использует её не повторяя операции поиска маршрута пока не случится что-то необычное в етом и есть смысл connect

Установление соединеиния

Рассмотрим стандартный пример

/* look up host */ server = gethostbyname(SERVER_NAME); /* get socket */ sockfd = socket(AF_INET, SOCK_STREAM, 0); /* set up address */ address.sin_family = AF_INET; address.sin_port = htons(PORT_NUM); memcpy(&address.sin_addr,server->h_addr,server->h_length); /* connect to server */ connect(sockfd, &address, sizeof(address));

socket создаёт обект сокета определенного типа и инициалищирует его также делает дефолтовские очереди (incoming,outgoing,error,backlog) и заголовок TCP

connect определяет маршруты вызывая протокольно зависимые функции (tcp_v4_connect(),udp_connect()) net/socket.c

asmlinkage long sys_connect(int fd, struct sockaddr *uservaddr, int addrlen) { ................................ err = sock->ops->connect(sock, (struct sockaddr *) address, addrlen, sock->file->f_flags); ..........................

}

int sock_create(int family, int type, int protocol, struct socket **res) { .....................................

//cоздаем протокольно зависимый сокет! //-------------------------------------- if ((i = net_families[family]->create(sock, protocol)) < 0) { sock_release(sock); goto out; } ................. }



Функции

Socket Проверяем ошибки Выделяем память Ложим сокет в список inode Устанавливаем указатели на протокольно зависмые части Сохраняем данные про тип и параметры сокета Устанавливаем сокет в положение закрыт Инициализируем очереди пакетов

Connect Проверяем ошибки Определяем Маршрут Проверяем кэш Смотрим в FIB Создаем новую запись в таблице маршрутизации Заполняем её и вощвращаем Сохраняем указатель на запись маршрутишации в сокете Вызываем протокольно зависимую функцию connect Устанавливаем сокет в соединенный

Также надо не забыть закрыть сокет

Close вызывает sock_close in socket.c void sock_release(struct socket *sock) { if (sock->ops) sock->ops->release(sock);

........................... }

а та через цепочку вызовов протокольнозависимую функцию

Дополнительные функции

void inet_sock_release(struct sock *sk) -net/ipv4/af_inet.c назвние говорит за себя + хороший комментарий Алана Коха fib_lookup() - include/net/ip_fib.h возвращает маршрут .Написана русским -Кузнецов! fn_hach_lookup net/fib_hash.c возвращает маршрут по адресу inet_create net/ipv4/af_inet.c создает сокет inet_release <> ip_route_connect вызывает ip_route_output для определени адреса назначения ip_route_output ip_route_output_slow rt_intern_hash полезные для маршрутизации функции sock_close() sock_create() sock_init_data net/core/sock.c инициализирует основные поля сокета sock_release net/socket.c sys_socket tcp_close net/ipv4/tcp.c устанавливает флаг FYN tpc_connect net/ipv4/tpc_output.c сохдает пакеты для соединения с установленым размером окна и соответствующими битами,ложит пакет в очередь и выpывает tcp_transmit_skb чтоб послать пакет tcp_transmit_skb -заполняет заголовок пакета и передает его на уроветь IP tcp_v4_connect() вызывает ip_route_connect создает соединительный пакет и вызывает tcp_connect udp_close udp_connect

Обмен данными

Эта часть описывает процесс обмена данными между различными уровнями ядра и сети Когда приложение отправляет данные то оно пишет в сокет тот в своб оуередь определяет свой тип и вызывает соответствующую функцию,та передает данные протоколу транспортного уровня(tcp,udp) функции етого уровня создают структуру sk_buff,копируют в неё данные заполняют заголовок своего уровня,считают контрольную сумму и шлют на уровень IP.Там дописывается заголовок ip,checksum,возможно пакет фраг менторуется и шлётся на xmit очередь сетевого девайса ,тот посылает пакет в сеть.



dev_queue_xmit() - net/core/dev.c spin_lock_bh() -блокируем девайс если у него есть очередь calls enqueue() добавляем пакет calls qdis() пробуждаем девайс else calls dev->hard_start_xmit() calls spin_unlock_bh() освобождаем девайс

DEVICE->hard_start_xmit() - зависит от девайса, drivers/net/DEVICE.c в общем проверяет открыто ли устройство посылает заголовок говорит системной шине послать пакет обновляет статус

inet_sendmsg() - net/ipv4/af_inet.c int inet_sendmsg(struct socket *sock, struct msghdr *msg, int size, struct scm_cookie *scm) { struct sock *sk = sock->sk;

/*биндим сокет. */ if (sk->num==0 && inet_autobind(sk) != 0) return -EAGAIN; вызываем функцию протокола чтоб послать данные return sk->prot->sendmsg(sk, msg, size); }

    ip_build_xmit - net/ipv4/ip_output.c (604) calls sock_alloc_send_skb() выделяем память

=заголовочек=

if(!sk->protinfo.af_inet.hdrincl) { iph->version=4; iph->ihl=5; iph->tos=sk->protinfo.af_inet.tos; iph->tot_len = htons(length); iph->frag_off = df; iph->ttl=sk->protinfo.af_inet.mc_ttl; ip_select_ident(iph, &rt->u.dst, sk); if (rt->rt_type != RTN_MULTICAST) iph->ttl=sk->protinfo.af_inet.ttl; iph->protocol=sk->protocol; iph->saddr=rt->rt_src; iph->daddr=rt->rt_dst; iph->check=0; iph->check = ip_fast_csum((unsigned char *)iph, iph->ihl); err = getfrag(frag, ((char *)iph)+iph->ihl*4,0, length-iph->ihl*4); }

  calls getfrag() копируем данные у юзера returns rt->u.dst.output() [= dev_queue_xmit()]

ip_queue_xmit() - net/ipv4/ip_output.c (234) cмотри маршрут достраиваем ip заголовок фрагментирум если надо adds IP checksum calls skb->dst->output() [= dev_queue_xmit()]

qdisc_restart() - net/sched/sch_generic.c (50) вырываем пакет из очереди calls dev->hard_start_xmit() обновляем статистику if если ошибка опять стввим пакет в очередь

sock_sendmsg() - net/socket.c (325) проверяем права и всё такое calls scm_sendmsg() [socket control message] шлёмс данные calls sock->ops[inet]->sendmsg() and destroys scm



>>> sock_write() - net/socket.c (399) calls socki_lookup() accоциируем сокет с inode заполняем заголовок сообщения returns sock_sendmsg()

tcp_sendmsg() - net/ipv4/tcp.c (755) ждемс соединения skb = tcp_alloc_pskb память calls csum_and_copy_from_user() делаем checksum & копируем calls tcp_send_skb()

tcp_send_skb() - net/ipv4/tcp_output.c (160) это главная routine посылки буфера мы ставим буфер в очередь и решаем оставить его там или послать calls __skb_queue_tail() добавляем в очередь calls tcp_transmit_skb() если может

tcp_transmit_skb() - net/ipv4/tcp_output.c (77) строим заголовок tcp и чексумму calls tcp_build_and_update_options() проверяем ACKs,SYN calls tp->af_specific[ip]->queue_xmit()

  udp_getfrag() - net/ipv4/udp.c копируем из адресного пространства пользователя и добавляем checksum

udp_sendmsg() - net/ipv4/udp.c проверяем флаги и тд заполняем заголовок проверяем мультикаст заполняем маршутную информацию calls ip_build_xmit() обновляем статистику udp returns err

Получение данных

Получение данных начинается с прерывания от сетевой карты.Драйвер девайса выделяет память и пересылает данные в то пространство.Потом передает пакет в связующий уровень который вызывает bottom-halv,которое обрабатывает событие вне прерывания пересылая данные на уровень выше -ip.Тот проверяет ошибки фрагменты,маршрутизирует пакет или отсылает на уровень выше(tcp udp) Этот уровень снова проверяет ошибки определяет сокет которому предназначен пакет и ложит его в очередь сокета.Тот в свою очередь будит пользовательский процесс и копирует данные в его буфер.

Чтение из сокета(1)

Пытаемся чтото прочитать(и засыпаем) Заполняем заголовок собщения указателем на буфер(сокет) проверяем простые ошибки передаем сообшение inet сокету

Получение пакета

Пробуждение устройства(прерывание) проверка девайса Получение заголовка выделение памяти ложим пакет в то место судя по всему используя DMA ставим пакет в очередь выставляем флаг запуска bottom-halv BottomHalv

Запуск сетевого боттом-халва Пересылка пакетов из девайса чтоб не было прерываний пересылка пакетов на уровень ip очистка очереди отсылки возврат



Уровень IP Проверка ошибок Дефрагментация если необходимо Определение маршрута(форвардить или нет) Отсылка пакета по назначению(TCPUDPforwarding)

Получение пакета в UDP Проверка ошибок проверка сокета назначения пересылка пакета в очередь сокета пробуждения ждущего процесса

Получение TCP Проверка флагов и ошибок а также не был ли получен пакет ранее Определение сокета пересылка пакета в очередь сокета пробуждения ждущего процесса

Чтение из сокета(2) Пробуждение процесса Вызов соответствуюшей функции доставки(udp tcp) в буфер пользователя Возврат

IP forwarding

Рассмотрим подробнее процесс форвардинга пакетов

Сначада идет проверка TTL и уменьшение его на 1 Проверка пакета на наличие действительного маршрута если такого нет то отсылается соответствующее icmp cообщение копирование пакета в новый буфер и освобождение старого Установка нужных ip опций фрагменторование если необходимо отправка пакета на нужный девайс

DEVICE_rx() девайсно зависимая функция, пример drivers/net/de600.c здесь я попытаюсь перевести замечательные комментарии автора

Linux driver for the D-Link DE-600 Ethernet pocket adapter. * * Portions (C) Copyright 1993, 1994 by Bjorn Ekwall * The Author may be reached as bj0rn@blox.se

/* * Если у нас хороший пакет то забираем его из адаптера */ static void de600_rx_intr(struct net_device *dev) { struct sk_buff *skb; unsigned long flags; int i; int read_from; int size; register unsigned char *buffer;

save_flags(flags); cli();

/* Определяем размер пакета */ size = de600_read_byte(RX_LEN, dev); /* нижния байт */ size += (de600_read_byte(RX_LEN, dev) << 8); /* верхний байт */ size -= 4; /* Ignore trailing 4 CRC-bytes */

/* Сообщаем адаптеру куда ложить следующий пакет и получаем */

read_from = rx_page_adr(); next_rx_page(); de600_put_command(RX_ENABLE);

restore_flags(flags);

if ((size < 32) (size > 1535)) { printk("%s: Bogus packet size %d.\n", dev->name, size); if (size > 10000) adapter_init(dev); return; }

skb = dev_alloc_skb(size+2); if (skb == NULL) { printk("%s: Couldn't allocate a sk_buff of size %d.\n", dev->name, size); return; } /* Иначе*/



skb->dev = dev; skb_reserve(skb,2); /* Align */

/* 'skb->data' указывет на начало буфера данных. */ buffer = skb_put(skb,size);

/* копируем пакет в буфер */ de600_setup_address(read_from, RW_ADDR); for (i = size; i > 0; --i, ++buffer) *buffer = de600_read_byte(READ_DATA, dev);

/* Определяем тип протоколa skb->protocol=eth_type_trans(skb,dev);

/*Передаем на верхний уровень см net/core/dev.c netif_rx(skb);

/* обновляем статистику */ dev->last_rx = jiffies; ((struct net_device_stats *)(dev->priv))->rx_packets++; /* количество получений */ ((struct net_device_stats *)(dev->priv))->rx_bytes += size; /* количество полученных байт */

/* * Если случится что-то плохое во время доставки, netif_rx() * сделало a mark_bh(INET_BH) для нас и будет работать * когда мы войдем в bottom-halv. */ }

  ip_finish_output() net/ipv4/ip_output определяет девайс для данного маршрута вызывает функцию девайса[=dev_queue_xmit] ip_forward -net/ipv4/ip_forward в этом файле хорошие комментарии проверяем роутер если пакет никому не предназначен то дропаем если плохой TTL аналогично если неможет пакет отфорвардится то отправляем icmp пакет ICMP_DEST_UNREACH если необходимо шлем пакет ICMP HOST REDIRECT копирум и уничтожаем старый пакет уменьшаем TTL если необходимо устанавливаем нужные опции ip_forward_options в ip_forward_finish

ip_rcv net/ipv4/ip_input.c главная функция плучения ip пакета проверяем ошибки плохая длина версия чексумма вызываем pskb_trim вызываем ip_route_input

Процесс маршрутизации

Как уже говорилось есть тоюлица соседей,FIB,routing cache Таблица соседей содержит адреса(mac) компьютеров которые фищически соединены с нами.Linux использует АRP для определения адресов ета таблица динамическая хотя администраторы могут задать статические записи. Стуктуры связанные с етой таблицей описаны в include/net/neighbour.h основные структуры. struct neigh_table -их целый связаный список struct neigh_parms -список содержит разнообразную статистику struct neighbour -hash таблица соседей ассоциированых с данной таблицей struct pneig_entry -hash всех девайсов



поля struct neighbour struct net_device -девайс hh_cache -указатель на аппаратный кэш sk_buff_head arp_queuq -очередь arp пакетов есть local -в ней находятся свои интерфейсы и main в ней наверное всё остальное

Forwarding Information Database

struct fib_table в include/net/ip_fib.h содержит указатели на различные функции tb_stamp tb_id -255 для local и 254 для main td_data -hash fib таблица

struct fn_hash -net/ipv4/fib_hash.c struct fn_zone *fn_zones[33] -указатели на зоны struct fn_zone *fn_zone_list указатель на первую не пустую зону struct fn_zone содержит унформацию про зону и маршруты для неё struct fib_node ** fz_hash -указывает на хэш записей этой зоны int fz_nent количество записей int fx_divisor числу бакетов для зоны (в основном 16 кроме зоны 0000 loopback девайса) int fz_order индекс зоны в родительской fn_hash

struct fib_node -содержит информацию по девайсу в fib_info(include/net/ip_fib.h) метрику ,протокол и т.д

Routing Cache

Это наиболее быстрый способ нахождения маршрута Когда ip нужен маршрут ,то он определяет ячейку в хэше,которая указывает на цепочку маршрутов и идёт по этой цепочке пока не найдет нужный маршруты имеют таймеры и частоту использования ,наиболее частые перемещаются в начало.

struct rtable -звено в цепочке содержит адреса отправителя и получателя входящий интерфейс адрес соседа или шлюза

struct dst_entry содержит спецефические для данного маршрута данные и функциии struct dev -понятно pmtu максимальная длина пакета для данного маршрута int (*input)(struct sk_buff) -указатель на функцию приема для данного маршрута часто ето tcp_rcv int (*output)(struct sk_buff) указатель на функцию отсылки (dev_queue_xmit) также разнообразные стстистические данные и опции

Таким образом нами было проведено исследование сетевой архитектуры операционной системы Линух на примере реализации стека протоколов tcp-ip версии 4 в ядре 2.4.7

Приложение

После длительных теотетических изысканий применим их на практике

Нашей целью будет создание удобного пользовательского интерфейса для указания в пакете подставного ip адреса(адреса которого нет у никакого нашего интерфейса) Я не буду показывать ,то как адреса выставляются в ядре.Замечу только то что, из сокетa семейства AF_INET и типа SOCK_RAW пакет с несвоим адресом отправить вроде бы можно (в ядре 2.2 ,насчет 2.4 неуверен -может там есть какие-то проверки). страницы мана говорят про опцию IP_HDRINCL .Их можно отправлять также через тип SOCK_PACKET. Но для всего этого знать код ядра не очень необходимо. Поэтому мы пойдём други путём.



Наиболее легкий путь(?) сделать это через интерфейс setsockopt. После внимательного изучения кода функции sys_setsockopt -net/socket.c находим строки if ((sock = sockfd_lookup(fd, &err))!=NULL) { if (level == SOL_SOCKET) err=sock_setsockopt(sock,level,optname,optval,optlen); else err=sock->ops->setsockopt(sock, level, optname, optval,optlen); sockfd_put(sock); } return err; }

значит нам надо искать функцию setsockopt в коде для реализации для типа sock_raw это файл net/ipv4/raw.c смотрим static int raw_setsockopt(struct sock *sk, int level, int optname, char *optval, int optlen) { if (level != SOL_RAW) return ip_setsockopt(sk, level, optname, optval, optlen);

...................................

}

функция ip_setsockopt лежит в net/ipv4/ip_sockglue.c в ней идет длинный перебор опций мы остановим свой выбор на уровне SOL_IP и добавим в перебор свои строки

/*HACK:>>>>>>>>>>>>>>>*/ #ifdef CONFIG_HACKIP case IP_HACKIP: printk("HACKIP:setsockopt flag %d\n",sk->hackflag); sk->hackflag=1; get_user(val,(int *) optval); printk("HACKIP:setsockopt val %d\n",val);

sk->hackf.src_addr=val; break; #endif case IP_HDRINCL:

подробнее опишем происходящие действия

printk -выводим отлабочные сообщения

Я не уверен ,но судя по всему при солздании сокета вся структура обнуляется поэтому мы можем не смотреть флаг .Я добавил ету строку ,чтоб посмотреть всегда ли он равет 0 при не установленой опции а после установки при повторе он равен 1. get_user забираем значение ,подробности include/asm/uaccess.h но для всего етого нам надо добавить соответствующие поля в struct sock

=======sock.h=============

......................... #ifdef CONFIG_HACKIP /*HACK:>>>>>>>>>>>>>>>>>>*/ struct ip_hack { __u32 src_addr; }; #endif struct sock { /* Socket demultiplex comparisons on incoming packets. */ .................................

#ifdef CONFIG_HACKIP /*HACK:>>>>>>>>>>>>>>>>>*/ struct ip_hack hackf; int hackflag; #endif



........................................

===========end======================

теперь нам надо перехватить отправку пакета

идем в файл net/ipv4/ip_output.c и после всех строк где есть 'iph->saddr=' вставляем наш код

#ifdef CONFIG_HACKIP if((sk->hackf.src_addr!=0)&&(sk->hackflag==1)) { iph->saddr=sk->hackf.src_addr;

printk("HACKIP:ip_build_and_send.. %d\n",iph->saddr); }

#endif

Осталось малое: в файл include/linux/in.h добавляем строку #define IP_HACKIP 16

в файл net/Config.in

bool 'HACKIP facilities' CONFIG_HACKIP

делаем cd /usr/src/linux make menuconfig make dep make bzImage cp arh/i386/boot/bzImage /boot/kursach

правим lilo.conf или /boot/grub/menu.lst соответствуюшая команда reboot....

теперь протестируем нашу программу извиняюсь за возможное наличие лишних include просто я переделал файл из друго-го проекта

============rel.c======================== /* Written by Gleb Paharenko <gleb@ptf.kiev.ua> 2003 */ /*Посвящяется Кевину Митнику */ /*и прекрасной весне в мае 2003-го*/

#include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<sys/socket.h> #include<resolv.h> #include<arpa/inet.h> #include<errno.h> #include<string.h> #include<linux/ip.h> #define IP_HACKIP 16

int main() { int sd,res; int value=1; int sval=0; int oval=1; char buffer[100]; struct sockaddr_in addr,raddr; bzero(buffer,sizeof(buffer)); if((sd=socket(PF_INET,SOCK_RAW,6))<0) { perror("Socket"); exit(errno); } bzero(&addr,sizeof(addr)); addr.sin_family=AF_INET; raddr.sin_family=AF_INET; addr.sin_port=0; raddr.sin_port=0; inet_aton("212.168.1.11",(struct sockaddr *)&(addr.sin_addr)); inet_aton("192.168.1.1",(struct sockaddr *)&(raddr.sin_addr)); sval=addr.sin_addr.s_addr; inet_aton("192.168.1.10",(struct sockaddr *)&(addr.sin_addr));

  if(bind(sd,(struct sockaddr *)&addr,sizeof(addr))<0) { perror("bind"); exit(errno); } if(connect(sd,(struct sockaddr *)&raddr,sizeof(raddr))!=0){ perror("connect");exit(errno);}



/* Вот ОНО!*/ if(setsockopt(sd,SOL_IP,IP_HACKIP,&sval,4)!=0) { perror("setsockopt"); exit(errno); }

send(sd,"Kursovaja",10,0);

  }

делаем # gcc rel.c #./a.out #tail /var/log/messages .................. .................. May 20 00:53:49 kursach -- root[863]: ROOT LOGIN ON tty1 May 20 00:53:51 kursach kernel: HACKIP:setsockopt flag 0 May 20 00:53:51 kursach kernel: HACKIP:setsockopt val 184658132 May 20 00:53:51 kursach kernel: HACKIP:ip_build_and_send.. 184658132

Обьясняю

дома у меня стоит vmware :host-only networking

host machine Windows2000 Professional 192.168.1.1/24

virtual Linux Red-Hat 7.2 "Enigma" 192.168.1.10/24

на 2000 запущен SpyNet

Ловим пакет и...

Работает!!!!!

Заключение

Благодарности

Моим родителям - за понимание и заботу

Glenn Herrin и Computer Science Department networking lab at the University of New Hampshire -за замечательную статью которую я использовал(kernelnewbies.org)

Ильину Коле и Арсену Бандуряну – за предоставленую документацию и ПО

Мише Тютину -за то, что познакомил меня с Линухом

Полонскому Лёше,Попову Денису и Корневу Олегу –за предоставленый интернет

Всем разработчикам Линуха -за замечательную операционную систему и комментарии к исходным текстам

Написано Глебом Пахаренко в мае 2003-го

пишите мне на


Механизм контроля версий


Модули, скомпилированные с одним ядром, могут не загружаться другим ядром, если в ядре включен механизм проверки версий модулей. В большинстве дистрибутивов ядро собирается с такой поддержкой. Мы не собираемся обсуждать проблему контроля версий в этой книге, поэтому нам остается порекомендовать, в случае возникновения проблем, пересобрать ядро без поддержки механизма контроля версий.



Работа в XWindow


Мы настоятельно рекомендуем скачать и опробовать все примеры, обсуждаемые в книге. Кроме того, мы настаиваем на том, чтобы всю работу, связанную с редактированием исходных текстов, компиляцией и запуском модулей, вы выполняли из текстовой консоли. Поверьте нашему опыту, XWindow не подходит для выполнения подобных задач.

Модули не могут использовать функцию printf() для вывода не экран, но они могут регистрировать сообщения об ошибках, которые в конечном итоге попадают на экран, но только в текстовой консоли. Если же модуль загружается из окна терминала, например xterm, то эти сообщения будут попадать только в системный журнал и не будут выводиться на экран. Чтобы видеть выводимые сообщения на экране, работайте в текстовой консоли (от переводчика: при опробовании примеров из книги мне не удалось вывести ни одного сообщения на экран, так что ищите ваши сообщения в системном журнале, в моем случае это был файл /var/log/kern.log).



Проблемы компиляции


Зачастую, дистрибутивостроители распространяют исходные тексты ядра, на которые уже наложены разные нестандартные заплаты. Это может породить определенные проблемы.

Не менее частый случай -- неполный набор заголовочных файлов ядра. Для сборки своих модулей вам потребуются многие заголовочные файлы ядра Linux. А закон Мэрфи гласит: "Отсутствовать будут как раз те файлы, в которых вы больше всего нуждаетесь".

Чтобы избежать этих двух проблем, мы рекомендуем собрать и установить наиболее свежее ядро. Скачать исходные тексты ядра вы сможете на любом из зеркал, распространяющих ядро Linux. За более подробной информацией обращайтесь к "Kernel HOWTO".

Как это ни покажется странным, но проблемы могут крыться и в компиляторе. По-умолчанию gcc

может искать заголовочные файлы ядра совсем не там, куда вы их установили (как правило это каталог /usr/src/. Эта проблема легко преодолевается заданием ключа компиляции -I.



Адресное пространство


Управление памятью - очень сложная тема, она достаточно полно освещается в книге "Understanding The Linux Kernel", выпущенной издательством O'Reilly. Мы не собираемся делать из вас экспертов в области управления памятью, но вам действительно необходимо знать некоторые факты.

Если вы никогда не задумывалесь над тем, что означает слово segfault, то для вас скорее всего окажется сюрпризом тот факт, что указатели фактически не указывают на какой-то реальный участок физической памяти. В любом случе, эти адреса не являются реальными. Когда запускается процесс, ядро выделяет под него кусок физической памяти и передает его процессу. Эта память используется для размещения исполняемого кода, стека, переменных, динамической "кучи" и других вещей, о чем наверняка знают компьютерные гении. [3] Эта память начинается с логического адреса #0 и простирается до того адреса, который необходим. Поскольку области памяти, выделенные для разных процессов, не пересекаются, то каждый из процессов, обратившись к ячейке памяти с адресом, скажем 0xbffff978, получит данные из различных областей физической памяти! В даннм случае число 0xbffff978 можно рассматривать как смещение относительно начала области памяти, выделенной процессу. Как правило программы, подобные нашей "Hello World", не могут обратиться к памяти, занимаемой другим процессом, хотя существуют обходные пути, позволяющие добиться этого, но оставим пока эту тему для более позднего обсуждения.

Ядро тоже имеет свое собственное адресное пространство. Поскольку модуль по сути является частью ядра, то он так же работает в адресном пространстве ядра. Если ошибка segmentation fault, возникающая в приложении может быть отслежена и устранена без особых проблем, то в модуле подобная ошибка может стать фатальной для всей системы. Из-за незначительной ошибки в модуле вы рискуете "затоптать" ядро. Результат может быть самым плачевным. Поэтому будьте предельно внимательны!

Хотелось бы заметить, что это справедливо для любой операционной системы, которая построена на монолитном ядре. [4]

Есть операционные системы, в основе которых лежит микроядро. В таких ОС каждый модуль получает свое адресное пространство. Примерами могут служить GNU Hurd и QNX Neutrino.



Благодарности


Ори Померанц выражает свою благодарность Yoav Weiss, за многочисленные предложения, обсуждение материала и внесение исправлений. Он так же хотел бы поблагодарить Фродо Лоойаарда из Нидердандов, Стефана Джадда из Новой Зеландии, Магнуса Альторпа из Швеции и Эммануэль Папиракис из Квебека, Канада.

Питер выражает свою благодарность Ори, за то что позволил ему принять участие в проекте LKMPG. А так же Джеффа Ньюмиллера, Ронду Франчес Бэйли (ныне Ронда Франчес Зальцман) и Марка Кима за науку и долготерпение. Он так же хотел бы поблагодарить Дэвида Портера, который оказал неоценимую помощь в переводе документа из формата LaTeX в формат docbook. Это была тяжелая и нудная работа, но ее необходимо было сделать.

Отдельное спасибо всем участникам проекта , и особенно Марку Маклолину и Джону Левону, которые, я уверен, могли бы найти много более интересное занятие, нежели "зависать" на kernelnewbies.org и обучать новичков. Если это руководство научит вас чему либо, знайте -- в этом есть доля и их "вины"!

И Ори и я хотели бы сказать слова благодарности в адрес Ричарда М. Столлмана и Линаса Торвальдса не только за эту превосходную операционную систему, но и за то, что позволили исследовать ее исходный код, чтобы разобраться в том, как она работает.

Хочется выразить благодарность тем, кто внес свои исправления в документ или внес свои предложения по улучшению. Это Игнасио Мартин, Дэвид Портер, Дэниэл Паоло Скарпацца и Димо Велев.



Chardev.c


Следующий пример создает устройство с именем chardev. Вы можете читать содержимое файла устройства с помощью команды cat или открывать его на чтение из программы (функцией open()). Посредством этого файла драйвер будет извещать о количестве попыток обращения к нему. Модуль не поддерживает операцию записи (типа: echo "hi" > /dev/chardev), но определяет такую попытку и сообщает пользователю о том, что операция записи не поддерживается.

Пример 4-1. chardev.c

/* * chardev.c: Создает символьное устройство, * доступное только для чтения * возвращает сообщение, с указанием количества произведенных * попыток чтения из файла устройства */

#include <linux/kernel.h> #include <linux/module.h> #include <linux/fs.h> #include <asm/uaccess.h> /* определение функции put_user */

/* * Прототипы функций, обычно их выносят в заголовочный файл (.h) */ int init_module(void); void cleanup_module(void); static int device_open(struct inode *, struct file *); static int device_release(struct inode *, struct file *); static ssize_t device_read(struct file *, char *, size_t, loff_t *); static ssize_t device_write(struct file *, const char *, size_t, loff_t *);

#define SUCCESS 0 /* Имя устройства, будет отображаться в /proc/devices */ #define DEVICE_NAME "chardev" #define BUF_LEN 80 /* Максимальная длина сообщения */

/* * Глобальные переменные, объявлены как static, * воизбежание конфликтов имен. */

/* Старший номер устройства нашего драйвера */ static int Major; /* Устройство открыто? static int Device_Open = 0; * используется для предотвращения одновременного * обращения из нескольких процессов */ /* Здесь будет собираться текст сообщения */ static char msg[BUF_LEN]; static char *msg_Ptr;

static struct file_operations fops = { .read = device_read, .write = device_write, .open = device_open, .release = device_release };

/* * Функции */

int init_module(void) { Major = register_chrdev(0, DEVICE_NAME, &fops);

if (Major < 0) { printk("Registering the character device failed with %d\n", Major); return Major; }


printk("<1> I was assigned major number %d. To talk to\n", Major); printk("<1>the driver, create a dev file with\n"); printk("'mknod /dev/chardev c %d 0'.\n", Major); printk("<1>Try various minor numbers. Try to cat and echo to\n"); printk("the device file.\n"); printk("<1>Remove the device file and module when done.\n");

return 0; }

void cleanup_module(void) { /* * Отключение устройства */ int ret = unregister_chrdev(Major, DEVICE_NAME); if (ret < 0) printk("Error in unregister_chrdev: %d\n", ret); }

/* * Обработчики */

/* * Вызывается, когда процесс пытается * открыть файл устройства, например командой * "cat /dev/chardev" */ static int device_open(struct inode *inode, struct file *file) { static int counter = 0; if (Device_Open) return -EBUSY; Device_Open++; sprintf(msg, "I already told you %d times Hello world!\n", counter++); msg_Ptr = msg; try_module_get(THIS_MODULE);

return SUCCESS; }

/* * Вызывается, когда процесс закрывает файл устройства. */ static int device_release(struct inode *inode, struct file *file) { Device_Open--; /* Теперь мы готовы обслужить другой процесс */

/* * Уменьшить счетчик обращений, иначе, после первой * же удачной попытки открыть файл устройства, * вы никогда не сможете выгрузить модуль. */ module_put(THIS_MODULE);

return 0; }

/* * Вызывается, когда процесс пытается * прочитать уже открытый файл устройства */ /* см. include/linux/fs.h */ static ssize_t device_read(struct file *filp, /* буфер, куда надо положить данные */ char *buffer, /* размер буфера */ size_t length, loff_t * offset) { /* * Количество байт, фактически записанных в буфер */ int bytes_read = 0;

/* * Если достигли конца сообщения, * вернуть 0, как признак конца файла */ if (*msg_Ptr == 0) return 0;

/* * Перемещение данных в буфер */ while (length && *msg_Ptr) {

/* * Буфер находится в пространстве * пользователя (в сегменте данных), * а не в пространстве ядра, поэтому * простое присваивание здесь недопустимо. * Для того, чтобы скопировать данные, * мы используем функцию put_user, * которая перенесет данные из пространства * ядра в пространство пользователя. */ put_user(*(msg_Ptr++), buffer++);

length--; bytes_read++; }

/* * В большинстве своем, функции чтения * возвращают количество байт, записанных в буфер. */ return bytes_read; }

/* * Вызывается, когда процесс пытается записать в устройство, * например так: echo "hi" > /dev/chardev */ static ssize_t device_write(struct file *filp, const char *buff, size_t len, loff_t * off) { printk("<1>Sorry, this operation isn't supported.\n"); return -EINVAL; }

Пример 4-2. Makefile

obj-m += chardev.o


Что такое "Модуль Ядра"?


Итак, Вы хотите писать модули ядра. Вы знакомы с языком C и у вас есть опыт создания обычных программ, а теперь вы хотите забраться туда, где свершается великое таинство. Туда, где один ошибочный указатель может "снести" файловую систему или "подвесить" компьютер.

Так что же такое "модуль ядра"? Модуль -- это некий код, который может быть загружен или выгружен ядром по мере необходимости. Модули расширяют функциональные возможности ядра без необходимости перезагрузки системы. Например, одна из разновидностей модулей ядра, драйверы устройств, позволяют ядру взаимодействовать с аппаратурой компьютера. При отсутствии поддержки модулей нам пришлось бы писать монолитные ядра и добавлять новые возможности прямо в ядро. При этом, после добавления в ядро новых возможностей, пришлось бы перезагружать систему.



Драйверы устройств


Драйверы устройств являются одной из разновидностей модулей ядра. Они играют особую роль. Это настоящие "черные ящики", которые полностью скрывают детали, касающиеся работы устройства, и предоставляют четкий программный интерфейс для работы с аппаратурой. В Unix каждое аппаратное устройство представлено псевдофайлом (файлом устройства) в каталоге /dev. Этот файл обеспечивает средства взаимодействия с аппаратурой. Так, например, драйвер звуковой платы es1370.ko связывает файл устройства /dev/sound со звуковой платой Ensoniq IS1370. Пользовательское приложение, например mp3blaster может использовать для своей работы /dev/sound, ничего не подозревая о типе установленной звуковой платы.



Файловая система /proc: создание файлов, доступных для чтения


Linux предоставляет ядру и модулям ядра дополнительный механизм передачи информации заинтересованным в ней процессам -- это файловая система /proc. Первоначально она создавалась с целью получения сведений о процессах (отсюда такое название). Теперь она интенсивно используется и самим ядром, которому есть что сообщить! Например, /proc/modules -- список загруженных модулей, /proc/meminfo -- статистика использования памяти.

Методика работы с файловой системой /proc очень похожа на работу драйверов с файлами устройств: вы создаете структуру со всей необходимой информацией, включая указатели на функции-обработчики (в нашем случае имеется только один обработчик, который обслуживает чтение файла в /proc). Функция init_module регистрирует структуру, а cleanup_module отменяет регистрацию.

Основная причина, по которой используется proc_register_dynamic [6]

состоит в том, что номер inode, для нашего файла, заранее неизвестен, поэтому мы даем возможность ядру определить его самостоятельно, чтобы предотвратить возможные конфликты. В обычных файловых системах, размещенных на диске, не в памяти, как /proc, inode указывает на то место в дисковом пространстве, где размещена индексная запись (index node, сокращенно -- inode) о файле. Inode содержит все необходимые сведения о файле, например права доступа, указатель на первый блок с содержимым файла.

Поскольку мы не предусматриваем обработку операций открытия/закрытия файла в файловой системе /proc, то нам некуда вставлять вызовы функций try_module_get и try_module_put. Если вдруг случится так, что модуль был выгружен в то время как соответствующий файл в /proc оставался открытым, к сожалению у нас не будет возможности избежать возможных последствий. В следующем разделе мы расскажем о довольно сложном, но достаточно гибком способе защиты от подобных ситуаций.

Пример 5-1. procfs.c

/* * procfs.c - пример создания "файла" в /proc */

/* Необходимо для любого модуля */ #include <linux/module.h> /* Все-таки мы работаем с ядром! */ #include <linux/kernel.h> /* Необходимо для работы с файловой системой /proc */ #include <linux/proc_fs.h>


Пока мы знаем о двух способах получения информации от драйвера устройства: можно зарегистрировать драйвер и создать файл устройства, и создать файл в файловой системе /proc. Единственная проблема -- мы пока ничего не можем передать модулю ядра. Для начала попробуем организовать передачу данных модулю ядра посредством файловой системы /proc.

Поскольку файловая система /proc

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

Linux предусматривает возможность регистрации файловой системы. Так как каждая файловая система должна иметь собственные функции, для обработки inode и выполнять файловые операции, [7] то имеется специальная структура, которая хранит указатели на все необходимые функции-обработчики -- struct inode_operations, которая включает указатель на struct file_operations. Файловая система /proc, всякий раз, когда мы регистрируем новый файл, позволяет указать -- какая struct inode_operations будет использоваться для доступа к нему. В свою очередь, в этой структуре имеется указатель struct file_operations, а в ней уже находятся указатели на наши функции-обработчики.

Обратите внимание: стандартные понятия "чтение" и "запись", в ядре имеют противоположный смысл. Функции чтения используются для записи в файл, в то время как функции записи используются для чтения из файла. Причина в том, что понятия "чтение" и "запись" рассматриваются здесь с точки зрения пользователя: если процесс читает что-то из ядра -- ядро должно записать эти данные, если процесс пишет -- ядро должно прочитать то, что записано.

Еще один интересный момент -- функция module_permission. Она вызывается всякий раз, когда процесс пытается обратиться к файлу в файловой системе /proc, и принимает решение -- разрешить доступ к файлу или нет. На сегодняшний день, решение принимается только на основе выполняемой операции и UID процесса, но в принципе возможна и иная организация принятия решения, например, разрешать ли одновременный доступ к файлу нескольким процессам и пр..




struct proc_dir_entry *Our_Proc_File;

/* Обработчик чтения из файла в /proc. * * Аргументы * ========= * 1. Буфер с данными. Как его заполнить -- вы решаете сами * 2. Указатель на указатель на строку символов. * Если вы не желаете использовать буфер * размещенный ядром. * 3. Текущая позиция в файле * 4. Размер буфера. * 5. Признак конца файла, "1" == EOF. * 6. Указатель на данные (необходим в случае единственного * обработчика на несколько файлов в /proc) * * Порядок использования и возвращаемое значение * ============================================= * Нулевое значение == "буфер пуст", т.е. "Конец файла". * Отрицательное значение == код ошибки. * * Дополнительные сведения * ======================= * Основные принципы реализации этой функции * я почерпнул не из документации, а из исходных текстов * модулей, выполняющих подобные действия. * Меня интересовало использование * поля get_info в структуре proc_dir_entry (Если вам это интересно * то для поиска я пользовался утилитами find и grep), * Интересующий меня пример я нашел в <kernel source * directory>/fs/proc/array.c. * * Когда вам что-то непонятно, то лучше всего * поискать примеры в исходных текстах ядра. В этом состоит * огромное преимущество Linux перед другими ОС, * так как нам доступны все исходные тексты, так что -- * пользуйтесь этим преимуществом! */ ssize_t procfile_read(char *buffer, char **buffer_location, off_t offset, int buffer_length, int *eof, void *data) { printk(KERN_INFO "inside /proc/test : procfile_read\n");

int len = 0; /* Фактическое число байт */ static int count = 1;

/* * Мы всегда должны выдавать имеющуюся информацию, * если пользователь спрашивает -- мы должны ответить. * * Это очень важно, поскольку библиотечная функция read * будет продолжать обращаться к системному вызову * read до тех пор, пока ядро не ответит, что сведений больше нет * или пока буфер не будет заполнен. */ if (offset > 0) { printk(KERN_INFO "offset %d : /proc/test : procfile_read, \ wrote %d Bytes\n", (int)(offset), len); *eof = 1; return len; }



/* * Заполнить буфер и получить его размер */ len = sprintf(buffer, "For the %d%s time, go away!\n", count, (count % 100 > 10 && count % 100 < 14) ? "th" : (count % 10 == 1) ? "st" : (count % 10 == 2) ? "nd" : (count % 10 == 3) ? "rd" : "th"); count++;

/* * Вернуть размер буфера */ printk(KERN_INFO "leaving /proc/test : procfile_read, wrote %d Bytes\n", len); return len; }

int init_module() { int rv = 0; Our_Proc_File = create_proc_entry("test", 0644, NULL); Our_Proc_File->read_proc = procfile_read; Our_Proc_File->owner = THIS_MODULE; Our_Proc_File->mode = S_IFREG | S_IRUGO; Our_Proc_File->uid = 0; Our_Proc_File->gid = 0; Our_Proc_File->size = 37;

printk(KERN_INFO "Trying to create /proc/test:\n");

if (Our_Proc_File == NULL) { rv = -ENOMEM; remove_proc_entry("test", &proc_root); printk(KERN_INFO "Error: Could not initialize /proc/test\n"); } else { printk(KERN_INFO "Success!\n"); }

return rv; }

void cleanup_module() { remove_proc_entry("test", &proc_root); printk(KERN_INFO "/proc/test removed\n"); }

Пример 5-2. Makefile

obj-m += procfs.o



Причина, по которой для копирования данных используются функции put_user и get_user, состоит в том, что процессы в Linux (по крайней мере в архитектуре Intel) исполняются в изолированных адресных пространствах, не пересекающихся с адресным пространством ядра. Это означает, что указатель, не содержит уникальный адрес физической памяти -- он хранит логический адрес в адресном пространстве процесса.

Единственное адресное пространство, доступное процессу -- это его собственное адресное пространство. Практически любой модуль ядра, должен иметь возможность обмена информацией с пользовательскими процессами. Однако, когда модуль ядра получает указатель на некий буфер, то адрес этого буфера находится в адресном пространстве процесса. Макрокоманды put_user и get_user позволяют обращаться к памяти процесса по указанному им адресу.

Пример 5-3. procfs.c

/* * procfs.c - Пример создания файла в /proc, * который доступен как на чтение, так и на запись. */ /* Необходимо для любого модуля */ #include <linux/module.h> /* Все-таки мы работаем с ядром! */ #include <linux/kernel.h> /* Необходимо для работы с файловой системой /proc */ #include <linux/proc_fs.h> /* определения функций get_user и put_user */ #include <asm/uaccess.h>

/* * Место хранения последнего принятого сообщения, * которое будет выводиться в файл, чтобы показать, что * модуль действительно может получать ввод от пользователя */ #define MESSAGE_LENGTH 80 static char Message[MESSAGE_LENGTH]; static struct proc_dir_entry *Our_Proc_File;

#define PROC_ENTRY_FILENAME "rw_test"

static ssize_t module_output(struct file *filp, /* см. include/linux/fs.h */ char *buffer, /* буфер с данными */ size_t length, /* размер буфера */ loff_t * offset) { static int finished = 0; int i; char message[MESSAGE_LENGTH + 30];

/* * Для индикации признака конца файла возвращается 0. * Если этого не сделать, процесс будет продолжать * пытаться читать из файла, * угодив в бесконечный цикл. */ if (finished) { finished = 0; return 0; }



/* * Для передачи данных из пространства ядра * в пространство пользователя * следует использовать put_user. * В обратном направлении -- get_user. */ sprintf(message, "Last input:%s", Message); for (i = 0; i < length && message[i]; i++) put_user(message[i], buffer + i);

/* * Обратите внимание: в данной ситуации мы исходим из предположения, * что размер сообщения меньше, чем len, * в противном случае сообщение будт обрезано. * В реальной ситуации, если длина сообщения больше чем * len, то возвращается len, а остаток сообщения возвращается * на последующих вызовах. */ finished = 1;

return i; /* Вернуть количество "прочитанных" байт */ }

static ssize_t module_input(struct file *filp, const char *buff, size_t len, loff_t * off) { int i; /* * Переместить данные, полученные от пользователя в буфер, * который позднее будет выведен функцией module_output. */ for (i = 0; i < MESSAGE_LENGTH - 1 && i < len; i++) get_user(Message[i], buff + i);

Message[i] = '\0'; /* Обычная строка, завершающаяся символом \0 */ return i; }

/* * Эта функция принимает решение о праве на выполнение операций с файлом * 0 -- разрешено, ненулеое значение -- запрещено. * * Операции с файлом могут быть: * 0 - Исполнениеe (не имеет смысла в нашей ситуации) * 2 - Запись (передача от пользователя к модулю ядра) * 4 - Чтение (передача от модуля ядра к пользователю) * * Эта функция проверяет права доступа к файлу * Права, выводимые командой ls -l * могут быть проигнорированы здесь. */

static int module_permission(struct inode *inode, int op, struct nameidata *foo) { /* * Позволим любому читать файл, но * писать -- только root-у (uid 0) */ if (op == 4 (op == 2 && current->euid == 0)) return 0;

/* * Если что-то иное -- запретить доступ */ return -EACCES; }

/* * Файл открыт -- пока нам нет нужды беспокоиться о чем-то * единственное, что нужно сделать -- это нарастить * счетчик обращений к модулю. */ int module_open(struct inode *inode, struct file *file) { try_module_get(THIS_MODULE); return 0; }



/* * Файл закрыт -- уменьшить счетчик обращений. */ int module_close(struct inode *inode, struct file *file) { module_put(THIS_MODULE); return 0; /* все нормально! */ }

static struct file_operations File_Ops_4_Our_Proc_File = { .read = module_output, .write = module_input, .open = module_open, .release = module_close, };

/* * Операции над индексной записью нашего файла. Необходима * для того, чтобы указать местоположение структуры * file_operations нашего файла, а так же, чтобы задать адрес * функции определения прав доступа к файлу. Здесь можно указать адреса * других функций-обработчиков, но нас они не интересуют. */

static struct inode_operations Inode_Ops_4_Our_Proc_File = { .permission = module_permission, /* проверка прав доступа */ };

/* * Начальная и конечная функции модуля */ int init_module() { int rv = 0; Our_Proc_File = create_proc_entry(PROC_ENTRY_FILENAME, 0644, NULL); Our_Proc_File->owner = THIS_MODULE; Our_Proc_File->proc_iops = &Inode_Ops_4_Our_Proc_File; Our_Proc_File->proc_fops = &File_Ops_4_Our_Proc_File; Our_Proc_File->mode = S_IFREG | S_IRUGO | S_IWUSR; Our_Proc_File->uid = 0; Our_Proc_File->gid = 0; Our_Proc_File->size = 80;

if (Our_Proc_File == NULL) { rv = -ENOMEM; remove_proc_entry(PROC_ENTRY_FILENAME, &proc_root); printk(KERN_INFO "Error: Could not initialize /proc/test\n"); }

return rv; }

void cleanup_module() { remove_proc_entry(PROC_ENTRY_FILENAME, &proc_root); }

Хотите еще примеры работы с файловой системой /proc? Хорошо, но имейте ввиду, ходят слухи, что /proc уходит в небытие и вместо нее следует использовать sysfs. Дополнительные сведения о файловой системе /proc вы найдете в linux/Documentation/DocBook/. Дайте команду make help, она выведет инструкции по созданию документации в различных форматах, например: make htmldocs.


Функции, которые доступны из модулей


Как программист, вы знаете, что приложение может вызывать функции, которые не определены в самой программе. На стадии связывания (линковки) разрешаются все внешние ссылки, уходящие во внешние библиотеки. Функция printf -- одна из таких функций, которая определена в библиотеке libc.

Модули ядра в этом плане сильно отличаются от прикладных программ. В примере "Hello World" мы использовали функцию printk(), но не подключали стандартную библиотеку ввода-вывода. Модули так же проходят стадию связывания, но только с ядром, и могут вызывать только те функции, которые экспортируются ядром. Разрешение ссылок на внешние символы производится утилитой insmod. Если у вас есть желание взглянуть на список имен, экспортируемых ядром, загляните в файл /proc/kallsyms.

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

Как в этом можно убедиться? Да очень просто! Скомпилируйте следующую программу:

#include <stdio.h> int main(void) { printf("hello"); return 0; }

с помощью команды gcc -Wall -o hello hello.c и запустите ее командой strace hello. Впечатляет? Каждая строка, выводимая на экран, соответствует системному вызову. strace -- незаменимый инструмент для того, чтобы выяснить -- куда программа, пытается обратиться, включая такие сведения, как имена системных вызовов, передаваемые им аргументы и возвращаемые значения. Здесь вы должны увидеть строку, которая выглядит примерно так: write(1, "hello", 5hello). Это и есть то, что мы ищем. Т.е. скрытая от нас сторона вызова функции printf(). Возможно вы не знакомы с вызовом write(), поскольку большинство программистов предпочитает пользоваться стандартными библиотечными функциями (такими как fopen(), fputs(), fclose()). Если это так, тогда загляните в man 2 write. Второй раздел справочного руководства содержит описания системных вызовов (таких как kill(), read() и т.п.). В третьем разделе описываются библиотечные вызовы (такие как cosh(), random() и пр.).

Вы можете даже написать модули, которые подменяют системные вызовы ядра, вскоре мы продемонстрируем это. Взломщики довольно часто используют эту возможность для создания "черного хода" в систему или "троянов", но вы можете использовать ее в менее вредоносных целях, например заставить ядро выводить строку "Tee hee, that tickles!" ("Хи-хи, щекотно!") каждый раз, когда кто нибудь пробует удалить файл.



Работа с файлами устройств


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

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

Ответ: в Unix следует использовать специальную функцию с именем ioctl (сокращенно от Input Output ConTroL). Любое устройство может иметь свои команды ioctl, которые могут читать (для передачи данных от процесса ядру), писать (для передачи данных от ядра к процессу), и писать и читать, и ни то ни другое, [8]

Функция ioctl вызывается с тремя параметрами: дескриптор файла устройства, номер ioctl и третий параметр, который имеет тип long, используется для передачи дополнительных аргументов. [9]

Номер ioctl содержит комбинацию бит, составляющих старший номер устройства, тип команды и тип дополнительного параметра. Обычно номер ioctl создается макроопределением (_IO, _IOR, _IOW или _IOWR, в зависимости от типа) в файле заголовка. Этот заголовочный должен подключаться директивой #include, к исходным файлам программы, которая использует ioctl для обмена данными с модулем. В примере, приводимом ниже, представлены файл заголовка chardev.h и программа, которая взаимодействует с модулем ioctl.c.


Если вы предполагаете использовать ioctl в ваших собственных модулях, то вам надлежит обратиться к файлу Documentation/ioctl-number.txt с тем, чтобы не "занять" зарегистрированные номера ioctl.
Пример 6-1. chardev.c
/* * chardev.c - Пример создания символьного устройства * доступного на запись/чтение */
#include <linux/module.h> /* Необходимо для любого модуля */ #include <linux/kernel.h> /* Все-таки мы работаем с ядром! */ #include <linux/fs.h> #include <asm/uaccess.h> /* определения функций get_user и put_user */
#include "chardev.h" #define SUCCESS 0 #define DEVICE_NAME "char_dev" #define BUF_LEN 80
/* * Устройство уже открыто? Используется для * предотвращения конкурирующих запросов к устройству */ static int Device_Open = 0;
/* * Ответ устройства на запрос */ static char Message[BUF_LEN];
/* * Позиция в буфере. * Используется в том случае, если сообщение оказывется длиннее * чем размер буфера. */ static char *Message_Ptr;
/* * Вызывается когда процесс пытается открыть файл устройства */ static int device_open(struct inode *inode, struct file *file) { #ifdef DEBUG printk("device_open(%p)\n", file); #endif
/* * В каждый конкретный момент времени только * один процесс может открыть файл устройства */ if (Device_Open) return -EBUSY;
Device_Open++; /* * Инициализация сообщения */ Message_Ptr = Message; try_module_get(THIS_MODULE); return SUCCESS; }
static int device_release(struct inode *inode, struct file *file) { #ifdef DEBUG printk("device_release(%p,%p)\n", inode, file); #endif
/* * Теперь мы готовы принять запрос от другого процесса */ Device_Open--;
module_put(THIS_MODULE); return SUCCESS; }
/* * Вызывается когда процесс, открывший файл устройства * пытается считать из него данные. */ static ssize_t device_read(struct file *file, /* см. include/linux/fs.h*/ char __user * buffer, /* буфер для сообщения */ size_t length, /* размер буфера */ loff_t * offset) { /* * Количество байт, фактически записанных в буфер */ int bytes_read = 0;


#ifdef DEBUG printk("device_read(%p,%p,%d)\n", file, buffer, length); #endif
/* * Если достигнут конец сообщения -- вернуть 0 * (признак конца файла) */ if (*Message_Ptr == 0) return 0;
/* * Собственно запись данных в буфер */ while (length && *Message_Ptr) {
/* * Поскольку буфер располагается в пространстве пользователя, * обычное присвоение не сработает. Поэтому * для записи данных используется put_user, * которая копирует данные из пространства ядра * в пространство пользователя. */ put_user(*(Message_Ptr++), buffer++); length--; bytes_read++; }
#ifdef DEBUG printk("Read %d bytes, %d left\n", bytes_read, length); #endif
/* * Вернуть количество байт, помещенных в буфер. */ return bytes_read; }
/* * Вызывается при попытке записи в файл устройства */ static ssize_t device_write(struct file *file, const char __user * buffer, size_t length, loff_t * offset) { int i;
#ifdef DEBUG printk("device_write(%p,%s,%d)", file, buffer, length); #endif
for (i = 0; i < length && i < BUF_LEN; i++) get_user(Message[i], buffer + i);
Message_Ptr = Message;
/* * Вернуть количество принятых байт */ return i; }
/* * Вызывается, когда процесс пытается * выполнить операцию ioctl над файлом устройства. * Кроме inode и структуры file функция получает * два дополнительных параметра: * номер ioctl и дополнительные аргументы. * */ int device_ioctl(struct inode *inode, /* см. include/linux/fs.h */ struct file *file, /* то же самое */ unsigned int ioctl_num, /* номер и аргументы ioctl */ unsigned long ioctl_param) { int i; char *temp; char ch;
/* * Реакция на различные команды ioctl */ switch (ioctl_num) { case IOCTL_SET_MSG: /* * Принять указатель на сообщение (в пространстве пользователя) * и переписать в буфер. * Адрес которого задан в дополнительно аргументе. */ temp = (char *)ioctl_param;
/* * Найти длину сообщения */ get_user(ch, temp); for (i = 0; ch && i < BUF_LEN; i++, temp++) get_user(ch, temp);
device_write(file, (char *)ioctl_param, i, 0); break;


case IOCTL_GET_MSG: /* * Передать текущее сообщение вызывающему процессу - * записать по указанному адресу. */ i = device_read(file, (char *)ioctl_param, 99, 0);
/* * Вставить в буфер завершающий символ \0 */ put_user('\0', (char *)ioctl_param + i); break;
case IOCTL_GET_NTH_BYTE: /* * Этот вызов является вводом (ioctl_param) и * выводом (возвращаемое значение функции) одновременно */ return Message[ioctl_param]; break; }
return SUCCESS; }
/* Объявлнеия */
/* * В этой структуре хранятся адреса функций-обработчиков * операций, производимых процессом над устройством. * Поскольку указатель на эту структуру хранится в таблице устройств, * она не может быть локальной для init_module. * Отсутствующие указатели в структуре забиваются значением NULL. */ struct file_operations Fops = { .read = device_read, .write = device_write, .ioctl = device_ioctl, .open = device_open, .release = device_release, /* оно же close */ };
/* * Инициализация модуля - Регистрация символьного устройства */ int init_module() { int ret_val; /* * Регистрация символьного устройства * (по крайней мере - попытка регистрации) */ ret_val = register_chrdev(MAJOR_NUM, DEVICE_NAME, &Fops);
/* * Отрицательное значение означает ошибку */ if (ret_val < 0) { printk("%s failed with %d\n", "Sorry, registering the character device ", ret_val); return ret_val; }
printk("%s The major device number is %d.\n", "Registeration is a success", MAJOR_NUM); printk("If you want to talk to the device driver,\n"); printk("you'll have to create a device file. \n"); printk("We suggest you use:\n"); printk("mknod %s c %d 0\n", DEVICE_FILE_NAME, MAJOR_NUM); printk("The device file name is important, because\n"); printk("the ioctl program assumes that's the\n"); printk("file you'll use.\n");
return 0; }
/* * Завершение работы модуля - дерегистрация файла в /proc */ void cleanup_module() { int ret;
/* * Дерегистрация устройства */ ret = unregister_chrdev(MAJOR_NUM, DEVICE_NAME);


/* * Если обнаружена ошибка -- вывести сообщение */ if (ret < 0) printk("Error in module_unregister_chrdev: %d\n", ret); }
Пример 6-2. chardev.h
/* * chardev.h - определения ioctl. * * Определения, которые здесь находятся, * должны помещаться в заголовочный файл потому, * что они потребуются как модулю ядра (chardev.c), так и * вызывающему процессу (ioctl.c) */
#ifndef CHARDEV_H #define CHARDEV_H
#include <linux/ioctl.h>
/* * Старший номер устройства. В случае использования ioctl, * мы уже лишены возможности воспользоваться динамическим номером, * поскольку он должен быть известен заранее. */ #define MAJOR_NUM 100
/* * Операция передачи сообщения драйверу устройства */ #define IOCTL_SET_MSG _IOR(MAJOR_NUM, 0, char *) /* * _IOR означает, что команда передает данные * от пользовательского процесса к модулю ядра * * Первый аргумент, MAJOR_NUM -- старший номер устройства. * * Второй аргумент -- код команды * (можно указать иное значение). * * Третий аргумент -- тип данных, передаваемых в ядро */
/* * Операция получения сообщения от драйвера устройства */ #define IOCTL_GET_MSG _IOR(MAJOR_NUM, 1, char *) /* * Эта команда IOCTL используется для вывода данных. * Нам по прежнему нужен буфер, размещенный в адресном пространстве * вызывающего процесса, куда это сообщение должно быть переписано. */
/* * Команда получения n-ного байта сообщения */ #define IOCTL_GET_NTH_BYTE _IOWR(MAJOR_NUM, 2, int) /* * Здесь команда IOCTL работает как на ввод, так и на вывод. * Она принимает от пользователя номер байта (n), * и возвращает n-ный байт сообщения (Message[n]). */
/* * Имя файла устройства */ #define DEVICE_FILE_NAME "char_dev"
#endif
Пример 6-3. ioctl.c
/* * ioctl.c - Пример программы, использующей * ioctl для управления модулем ядра * * До сих пор мы ползовались командой cat, * для передачи данных в/из модуля. * Теперь же мы должны написать свою программу, * которая использовала бы ioctl. */
/* * Определения старшего номера устройства и коды операций ioctl */ #include "chardev.h"


#include <fcntl.h> /* open */ #include <unistd.h> /* exit */ #include <sys/ioctl.h> /* ioctl */
/* * Функции работы с драйвером через ioctl */
ioctl_set_msg(int file_desc, char *message) { int ret_val;
ret_val = ioctl(file_desc, IOCTL_SET_MSG, message);
if (ret_val < 0) { printf("Ошибка при вызове ioctl_set_msg: %d\n", ret_val); exit(-1); } }
ioctl_get_msg(int file_desc) { int ret_val; char message[100];
/* * Внимание - ядро понятия не имеет -- какой длины буфер мы используем * поэтому возможна ошибка, связанная с переполнением буфера. * В реальных проектах вам необходимо предусмотреть * передачу в ioctl двух дополнительных параметров: * собственно буфера сообщения и его длину */ ret_val = ioctl(file_desc, IOCTL_GET_MSG, message);
if (ret_val < 0) { printf("Ошибка при вызове ioctl_get_msg: %d\n", ret_val); exit(-1); }
printf("Получено сообщение (get_msg): %s\n", message); }
ioctl_get_nth_byte(int file_desc) { int i; char c;
printf("N-ный байт в сообщении (get_nth_byte): ");
i = 0; while (c != 0) { c = ioctl(file_desc, IOCTL_GET_NTH_BYTE, i++);
if (c < 0) { printf ("Ошибка при вызове ioctl_get_nth_byte на %d-м байте.\n", i); exit(-1); }
putchar(c); } putchar('\n'); }
/* * Main - Проверка работоспособности функции ioctl */ main() { int file_desc, ret_val; char *msg = "Это сообщение передается через ioctl\n";
file_desc = open(DEVICE_FILE_NAME, 0); if (file_desc < 0) { printf("Невозможно открыть файл устройства: %s\n", DEVICE_FILE_NAME); exit(-1); }
ioctl_get_nth_byte(file_desc); ioctl_get_msg(file_desc); ioctl_set_msg(file_desc, msg);
close(file_desc); }
Пример 6-4. Makefile
obj-m += chardev.o
Для облегчения сборки примера, предлагается скрипт, который выполнит эту работу за вас:
Пример 6-5. build.sh
#/bin/sh
# сборка пользовательского приложения gcc -o ioctl ioctl.c
# создание файла устройства mknod char_dev c 100 0

Системные вызовы


До сих пор, все что мы делали, это использовали ранее определенные механизмы ядра для регистрации файлов в файловой системе /proc и файлов устройств. Это все замечательно, но это годится только для создания драйверов устройств. А что если вы хотите сделать действительно что-то необычное, например изменить реакцию системы на какое либо событие?

Мы как раз вступаем в ту область, где программирование действительно становится опасным. При разработке примера, приведенного ниже, я "уничтожил" системный вызов open. В результате система потеряла возможность открывать любые файлы, что равносильно отказу системы выполнять любые программы. Я не мог даже остановить систему командой shutdown. Из-за этого пришлось прибегнуть к "помощи" кнопки выключения питания. К счастью, ни один файл не был уничтожен. Чтобы обезопасить себя от потери данных, в аналогичных ситуациях, перед загрузкой подобных модулей всегда выполняйте резервное копирование.

Забудьте про /proc, забудьте и про файлы устройств. Реальный механизм взаимодействия процессов с ядром -- это системные вызовы. Когда процесс запрашивает какую-либо услугу ядра (например, открытие файла, запуск нового процесса или выделение дополнительной памяти), используется механизм системных вызовов. Если вы хотите изменить поведение ядра, то системные вызовы -- это как раз то место, куда можно приложить свои знания и умения. Между прочим, если вы захотите увидеть -- какие системные вызовы используются той или иной программой, запустите: strace <command> <arguments>.

Строго говоря, процесс не имеет доступа в пространство ядра. Он не может обращаться к памяти ядра и не может вызывать функции в ядре. Микропроцессор ограничивает такого рода доступ на аппаратном уровне (вот почему режим исполнения ядра называется защищенным, или привилегированным).

Системные вызовы являются исключением из этого правила. Чтобы исполнить системный вызов, процесс заполняет регистры микропроцессора соответствующими значениями и выполняет специальную инструкцию, которая производит переход в предопределенное место в пространстве ядра (разумеется, точка перехода доступна пользовательским процессам на чтение). Для платформы Intel -- это инструкция прерывания с вектором 0x80. Микропроцессор воспринимает это как переход из ограниченного пользовательского режима в защищенный режим ядра, где позволено делать все, что вам заблагорассудится.


Точка перехода, в ядре, называется system_call. Процедура, которая там находится, проверяет номер системного вызова, который сообщает ядру -- какую именно услугу запрашивает процесс. Затем, она просматривает таблицу системных вызовов (sys_call_table), отыскивает адрес функции ядра, которую следует вызвать, после чего вызывается нужная функция. По окончании работы системного вызова, выполняется ряд дополнительных проверок и лишь после этого управление возвращается вызывающему процессу (или другому процессу, если вызывающий процесс исчерпал свой квант времени). Код, выполняющий все вышеперечисленные действия, вы найдете в файле arch/<architecture>/kernel/entry.S, после строки ENTRY(system_call).
Итак, если вас одолевает желание изменить поведение некоторого системного вызова, то первое, что необходимо сделать -- это написать вашу собственную функцию, которая выполняла бы требуемые действия (обычно, после выполнения своих действий, в подобных случаях, вызывается первоначальная функция, реализующая системный вызов), затем -- изменить указатель в sys_call_table так, чтобы он указывал на вашу функцию. Поскольку ваш модуль впоследствии может быть выгружен, то следует предусмотреть восстановление системы в ее первоначальное состояние, чтобы не оставлять ее в нестабильном состоянии. Это делается в пределах функции cleanup_module.
Ниже приводится исходный текст такого модуля. Он "шпионит" за выбранным пользователем, и посылать через printk сообщение всякий раз, когда данный пользователь открывает какой-либо файл. Для этого, системный вызов open(), подменяется функцией с именем our_sys_open. Она проверяет UID (User ID) текущего процесса, и если он равен заданному, то вызывает printk, чтобы сообщить имя открываемого файла, и в заключение вызывает оригинальную функцию open() с теми же параметрами, которая открывает требуемый файл.
Функция init_module изменяет соответствующий указатель в sys_call_table и сохраняет его первоначальное значение в переменной. Функция cleanup_module восстанавливает указатель в sys_call_table, используя эту переменную. В данном подходе кроются свои "подводные камни" из-за возможности существования двух модулей, перекрывающих один и тот же системный вызов. Представьте себе: имеется два модуля, А и B. Пусть модуль A перекрывает системный вызов open, своей функцией A_open, а модуль B -- функцией B_open. Первым загружается модуль A, он заменяет системный вызов open на A_open. Затем загружается модуль B, который заменит системный вызов A_open на B_open. Модуль B полагает, что он подменил оригинальный системный вызов, хотя на самом деле был подменен вызов A_open.


Теперь, если модуль B выгрузить первым, то ничего страшного не произойдет -- он просто восстановит запись в таблице sys_call_table в значение A_open, который в свою очередь вызывает оригинальную функцию sys_open. Однако, если первым будет выгружен модуль А, а затем B, то система "рухнет". Модуль А восстановит адрес в sys_call_table, указывающий на оригинальную функцию sys_open, "отсекая" таким образом модуль B от обработки действий по открытию файлов. Затем, когда будет выгружен модуль B, он восстановит адрес в sys_call_table на тот, который запомнил сам, потому что он считает его оригинальным. Т.е. вызовы будут направлены в функцию A_open, которой уже нет в памяти. На первый взгляд, проблему можно решить, проверкой -- совпадает ли адрес в sys_call_table с адресом нашей функции open и если не совпадает, то не восстанавливать значение этого вызова (таким образом B не будет "восстанавливать" системный вызов), но это порождает другую проблему. Когда выгружается модуль А, он "видит", что системный вызов был изменен на B_open и "отказывается" от восстановления указателя на sys_open. Теперь, функция B_open будет по прежнему пытаться вызывать A_open, которой больше не существует в памяти, так что система "рухнет" еще раньше -- до удаления модуля B.
Обратите внимание: подобные проблемы делают такую "подмену" системных вызовов неприменимой для широкого распространения.С целью предотвращения потенциальной опасности, связанной с подменой адресов системных вызовов, ядро более не экспортирует sys_call_table. Поэтому, если вы желаете сделать нечто большее, чем просто пробежать глазами по тексту данного примера, вам надлежит наложить "заплату" на ядро. В каталоге с примерами вы найдете файл README и "заплату". Как вы наверняка понимаете, подобные модификации сопряжены с определенными трудностями, поэтому я не рекомендую производить их на системах, владельцем которых вы не являетесь или не в состоянии быстро восстановить. Если вас одолевают сомнения, то лучшим выбором будет отказ от прогона этого примера.


Пример 7-1. syscall.c
/* * syscall.c * * Пример "перехвата" системного вызова. */
/* * Copyright (C) 2001 by Peter Jay Salzman */
/* * Необходимые заголовочные файлы */
#include <linux/kernel.h> /* Все-таки мы работаем с ядром! */ #include <linux/module.h> /* Необходимо для любого модуля */ #include <linux/moduleparam.h> /* для передачи параметров модулю */ #include <linux/unistd.h> /* Список системных вызовов */
/* * Необходимо, чтобы уметь определять * user id вызвавшего процесса. */ #include <linux/sched.h> #include <asm/uaccess.h>
/* * Таблица системных вызовов (таблица адресов функций). * Просто определим ее как ссылку на внешнюю таблицу. * * sys_call_table больше не экспортируется ядрами 2.6.x. * Если вы намереваетесь опробовать этот ОПАСНЫЙ модуль, * вам следует наложить "заплату" на ядро и пересобрать его */ extern void *sys_call_table[];
/* * UID пользователя, за которым "шпионим", * принимается из командной строки */ static int uid; module_param(uid, int, 0644);
/* * Указатель на оригинальную функцию, выполняющую системный вызов. * Мы сохраняем ее, вместо того, чтобы напрямую вызывать оригинальную * функцию, для того, чтобы имелась возможность вызова * обработчиков, вставленных до нас * Это не гарантирует 100% безопасность, поскольку другой модуль, * замещающий sys_open может быть выгружен раньше нашего модуля. * * Другая причина -- мы не можем получить адрес оригинальной sys_open. * Этот адрес не экспортируется ядром. */ asmlinkage int (*original_call) (const char *, int, int);
/* * Функция, замещающая sys_open (вызывается * всякий раз, когда делается обращение к системному вызову open). * Прототип функции, количество аргументов и их тип * вы найдете в fs/open.c. * * Теоретически - мы "привязаны" к данной конкретной * версии ядра. Практически -- системные вызовы * очень редко подвергаются кардинальному изменению, * поскольку это сделало бы огромное количество программного обеспечения * несовместимым с ядром и потребовало бы их пересборки. */ asmlinkage int our_sys_open(const char *filename, int flags, int mode) { int i = 0; char ch;


/* * Проверить -- это искомый пользователь? */ if (uid == current->uid) { /* * Зафиксировать */ printk("Opened file by %d: ", uid); do { get_user(ch, filename + i); i++; printk("%c", ch); } while (ch != 0); printk("\n"); }
/* * Вызвать оригинальную версию системного вызова sys_open - иначе * система потеряет возможность открывать файлы */ return original_call(filename, flags, mode); }
/* * Инициализация модуля - подмена системного вызова */ int init_module() { /* * Внимание - предупреждение запоздало, но * может быть в следующий раз... */ printk("I'm dangerous. I hope you did a "); printk("sync before you insmod'ed me.\n"); printk("My counterpart, cleanup_module(), is even"); printk("more dangerous. If\n"); printk("you value your file system, it will "); printk("be \"sync; rmmod\" \n"); printk("when you remove this module.\n");
/* * Сохранить указатель на оригинальную функцию * в переменной original_call, и затем заменить указатель * в таблице системных вызовов */ original_call = sys_call_table[__NR_open]; sys_call_table[__NR_open] = our_sys_open;
/* * Чтобы получить адрес любого системного вызова * с именем foo, обращайтесь к записи sys_call_table[__NR_foo]. */
printk("Spying on UID:%d\n", uid);
return 0; }
/* * Завершение работы модуля - восстановление * указателя на оригинальный системный вызов */ void cleanup_module() { /* * Восстановить адрес системного вызова */ if (sys_call_table[__NR_open] != our_sys_open) { printk("Somebody else also played with the "); printk("open system call\n"); printk("The system may be left in "); printk("an unstable state.\n"); }
sys_call_table[__NR_open] = original_call; }
Пример 7-2. "Заплата" на ядро (export_sys_call_table_patch_for_linux_2.6.x)
--- kernel/kallsyms.c.orig 2003-12-30 07:07:17.000000000 +0000 +++ kernel/kallsyms.c 2003-12-30 07:43:43.000000000 +0000 @@ -184,7 +184,7 @@ iter->pos = pos; return get_ksymbol_mod(iter); } - + /* If we're past the desired position, reset to start. */ if (pos < iter->pos) reset_iter(iter); @@ -291,3 +291,11 @@


EXPORT_SYMBOL(kallsyms_lookup); EXPORT_SYMBOL(__print_symbol); +/* START OF DIRTY HACK: + * Purpose: enable interception of syscalls as shown in the + * Linux Kernel Module Programming Guide. */ +extern void *sys_call_table; +EXPORT_SYMBOL(sys_call_table); + /* see http://marc.free.net.ph/message/20030505.081945.fa640369.html + * for discussion why this is a BAD THING(tm) and no longer supported by 2.6.0 + * END OF DIRTY HACK: USE AT YOUR OWN RISK */
Пример 7-3. Makefile
obj-m += syscall.o
Пример 7-4. README.txt
Основная проблема, связанная с данным примером, состоит в невозможности определить адрес sys_call_table, поскольку он более не экспортируется ядрами 2.6.x. Возможность "перекрытия" системных вызовов через sys_call_table потенциально опасна поэтому, начиная с версии 2.5.41, она больше не поддерживается
Обсуждение проблемы вы найдете на:
http://www.ussg.iu.edu/hypermail/linux/kernel/0305.0/0711.html http://marc.free.net.ph/message/20030505.081945.fa640369.html http://marc.theaimsgroup.com/?l=linux-kernel&m=105212296015799&w=2
Чтобы иметь возможность опробовать данный пример на ядрах версии 2.5.41 и выше вам необходимо наложить заплату на ядро.
ВНИМАНИЕ: НЕ ИСПОЛЬЗУЙТЕ ЭТУ ЗАПЛАТУ НА ПРОМЫШЛЕННЫХ ИЛИ ИНЫХ СИСТЕМАХ, КОТОРЫЕ СОДЕРЖАТ ЦЕННУЮ ИНФОРМАЦИЮ.
Если бы я писал встроенную справку к этой заплате в Configure.help то я бы пометил ее как <dangerous> и дал бы следующее описание:
#######################################################################
Эта опция экспортирует sys_call_table, что делает возможным "перекрытие" (подмену) системных вызовов. Подмена системных вызовов потенциально опасна и может стать причиной потери даных или еще хуже.
Скажите Y, если желаете опробовать прилагаемый пример и вас не беспокоит возможная потеря данных.
Практически любой должен здесть сказать N.
#######################################################################
Если ваш старенький PC используется только как игрушка можете наложить эту заплату и опробовать пример.
Предполагается, что исходные тексты ядра 2.6.x находятся в каталоге /usr/src/linux/ (http://www.linuxmafia.com/faq/Kernel/usr-src-linux-symlink.html)
Ниже приводится текст сценария, выполняющий наложение заплаты.
Эта заплата протестирована с ядрами 2.6.[0123], и может накладываться или не накладываться на другие версии.
#!/bin/sh cp export_sys_call_table_patch_for_linux_2.6.x /usr/src/linux/ cd /usr/src/linux/ patch -p0 < export_sys_call_table_patch_for_linux_2.6.x

Блокировка процессов


Что вы делаете, когда кто-то просит вас о чем-то, а вы не можете сделать это немедленно? Пожалуй единственное, что вы можете ответить: "Пожалуйста, не сейчас, я пока занят.". А что должен делать модуль ядра? У него есть другая возможность. Он можете приостановить работу процесса до тех пор, пока не сможет обслужить его. В конечном итоге, ядро постоянно то приостанавливает, то вновь возобновляет работу процессов. Именно так обеспечивается возможность одновременного исполнения нескольких процессов на единственном процессоре.

Пример ниже демонстрирует такую возможность. Модуль создает файл /proc/sleep, который может быть открыт только одним процессом, в каждый конкретный момент времени. Если файл уже был открыт кем-нибудь, то модуль вызывает wait_event_interruptible. [10] Эта функция изменяет состояние "задачи" (здесь, под термином "задача" понимается структура данных в ядре, которая хранит информацию о процессе), присваивая ему значение TASK_INTERRUPTIBLE, это означает, что задача не будет выполняться до тех пор, пока не будет "разбужена" каким либо образом, и добавляет процесс в очередь ожидания WaitQ, куда помещаются все процессы, желающие открыть файл /proc/sleep. Затем функция передает управление планировщику, который в свою очередь предоставляет возможность поработать другому процессу.

Когда процесс закрывает файл, это приводит к вызову module_close. Она запускает все процессы, которые "сидят" в очереди WaitQ (к сожалению нет механизма, который позволил бы "разбудить" только один процесс). Затем управление возвращается процессу, который только что закрыл файл и он продолжает свою работу. После того, как данный процесс исчерпает свой квант времени, планировщик передаст управление другому процессу. Таким образом, один из процессов, ожидавших своей очереди доступа к файлу, в конечном итоге получит управление и продолжит исполнение с точки, следующей за вызовом wait_event_interruptible. [11] Он установит глобальную переменную, извещающую остальные процессы о том, что файл открыт и займется обработкой открытого файла. Когда другие процессы получат свой квант времени, они обнаружат, что файл все еще открыт и опять приостановят свою работу.


Чтобы как- то оживить повествование замечу, что module_close не обладает монопольным правом на возобновление работы ожидающих процессов. Сигнал Ctrl-C (SIGINT) также может "разбудить" процесс. [12] В этом случае процессу немедленно возвращается -EINTR. Таким образом пользователи могут, например, прервать процесс прежде, чем он получит доступ к файлу.
Тут есть еще один момент, о котором хотелось бы упомянуть. Некоторые процессы не желают быть заблокированными, такие процессы должны либо получить в свое распоряжение открытый файл немедленно, либо извещение о том, что их запрос не может быть удовлетворен в настоящий момент. Такие процессы используют флаг O_NONBLOCK при открытии файла. Если ядро не в состоянии немедленно удовлетворить запрос, оно отвечает кодом ошибки -EAGAIN.
Пример 8-1. sleep.c
/* * sleep.c - Создает файл в /proc, доступ к * которому может получить только один процесс, * все остальные будут приостановлены. */
#include <linux/kernel.h> /* Все-таки мы работаем с ядром! */ #include <linux/module.h> /* Необходимо для любого модуля */ #include <linux/proc_fs.h> /* Необходимо для работы с /proc */ #include <linux/sched.h> /* Взаимодействие с планировщиком */ #include <asm/uaccess.h> /* определение функций get_user и put_user */
/* * Место хранения последнего принятого сообщения, * которое будет выводиться в файл, чтобы показать, что * модуль действительно может получать ввод от пользователя */ #define MESSAGE_LENGTH 80 static char Message[MESSAGE_LENGTH];
static struct proc_dir_entry *Our_Proc_File; #define PROC_ENTRY_FILENAME "sleep"
/* см. include/linux/fs.h */ static ssize_t module_output(struct file *file, /* буфер с данными (в пространстве пользователя) */ char *buf, /* размер буфера */ size_t len, loff_t * offset) { static int finished = 0; int i; char message[MESSAGE_LENGTH + 30];
/* * Для индикации признака конца файла возвращается 0. * В противном случае процесс будет продолжать читать из файла * угодив в бесконечный цикл. */ if (finished) { finished = 0; return 0; }


/* * Для передачи данных из пространства ядра в пространство пользователя * следует использовать put_user. * В обратном направлении -- get_user. */ sprintf(message, "Last input:%s\n", Message); for (i = 0; i < len && message[i]; i++) put_user(message[i], buf + i);
finished = 1; return i; /* Вернуть количество "прочитанных" байт */ }
/* * Эта функция принимает введенное пользователем сообщение */ static ssize_t module_input(struct file *file, /* Собственно файл */ const char *buf, /* Буфер с сообщением */ size_t length, /* размер буфера */ loff_t * offset) { /* смещение в файле - игнорируется */ int i;
/* * Переместить данные, полученные от пользователя в буфер, * который позднее будет выведен йункцией module_output. */ for (i = 0; i < MESSAGE_LENGTH - 1 && i < length; i++) get_user(Message[i], buf + i);
/* Обычная строка, завершающаяся символом \0 */ Message[i] = '\0';
/* * Вернуть число принятых байт */ return i; }
/* * 1 -- если файл открыт */ int Already_Open = 0;
/* * Очередь ожидания */ DECLARE_WAIT_QUEUE_HEAD(WaitQ); /* * Вызывается при открытии файла в /proc */ static int module_open(struct inode *inode, struct file *file) { /* * Если установлен флаг O_NONBLOCK, * то процесс не должен приостанавливаться * В этом случае, если файл уже открыт, * необходимо вернуть код ошибки * -EAGAIN, что означает "попробуйте в другой раз" */ if ((file->f_flags & O_NONBLOCK) && Already_Open) return -EAGAIN;
/* * Нарастить счетчик обращений, * чтобы невозможно было выгрузить модуль */ try_module_get(THIS_MODULE);
/* * Если файл уже открыт -- приостановить процесс */
while (Already_Open) { int i, is_sig = 0;
/* * Эта функция приостановит процесс и поместит его в очередь ожидания. * Исполнение процесса будет продолжено с точки, следующей за вызовом * этой функции, когда кто нибудь сделает вызов * wake_up(&WaitQ) (это возможно только внутри module_close, когда * файл будет закрыт) или когда процессу поступит сигнал Ctrl-C */ wait_event_interruptible(WaitQ, !Already_Open);


for (i = 0; i < _NSIG_WORDS && !is_sig; i++) is_sig = current->pending.signal.sig[i] & ~current-> blocked.sig[i];
if (is_sig) { /* * Не забыть вызвать здесь module_put(THIS_MODULE), * поскольку процесс был прерван * и никогда не вызовет функцию close. * Если не уменьшить счетчик обращений, то он навсегда останется * больше нуля, в результате модуль можно будет * уничтожить только при перезагрузке системы */ module_put(THIS_MODULE); return -EINTR; } }
/* * В этой точке переменная Already_Open должна быть равна нулю */
/* * Открыть файл */ Already_Open = 1; return 0; }
/* * Вызывается при закрытии файла */ int module_close(struct inode *inode, struct file *file) { /* * Записать ноль в Already_Open, тогда один * из процессов из WaitQ * сможет записать туда единицу и открыть файл. * Все остальные процессы, ожидающие доступа * к файлу опять будут приостановлены */ Already_Open = 0;
/* * Возобновить работу процессов из WaitQ. */ wake_up(&WaitQ);
module_put(THIS_MODULE);
return 0; }
/* * Эта функция принимает решение о праве на выполнение операций с файлом * 0 -- разрешено, ненулеое значение -- запрещено. * * Операции с файлом могут быть: * 0 - Исполнениеe (не имеет смысла в нашей ситуации) * 2 - Запись (передача от пользователя к модулю ядра) * 4 - Чтение (передача от модуля ядра к пользователю) * * Эта функция проверяет права доступа к файлу * Права, выводимые командой ls -l * могут быть проигнорированы здесь. */ static int module_permission(struct inode *inode, int op, struct nameidata *nd) { /* * Позволим любому читать файл, но * писать -- только root-у (uid 0) */ if (op == 4 (op == 2 && current->euid == 0)) return 0;
/* * Если что-то иное -- запретить доступ */ return -EACCES; }
/* * Указатели на функции-обработчики для нашего файла. */ static struct file_operations File_Ops_4_Our_Proc_File = { .read = module_output, /* чтение из файла */ .write = module_input, /* запись в файл */ .open = module_open, /* открытие файла */ .release = module_close, /* закрытие файла */ };


/* * Операции над индексной записью нашего файла. Необходима * для того, чтобы указать местоположение структуры * file_operations нашего файла, а так же, чтобы задать * функцию определения прав доступа к файлу. Здесь можно указать адреса * других функций-обработчиков, но нас они не интересуют. */ static struct inode_operations Inode_Ops_4_Our_Proc_File = { .permission = module_permission, /* check for permissions */ };
/* * Начальная и конечная функции модуля */
/* * Инициализация модуля - регистрация файла в /proc */
int init_module() { int rv = 0; Our_Proc_File = create_proc_entry(PROC_ENTRY_FILENAME, 0644, NULL); Our_Proc_File->owner = THIS_MODULE; Our_Proc_File->proc_iops = &Inode_Ops_4_Our_Proc_File; Our_Proc_File->proc_fops = &File_Ops_4_Our_Proc_File; Our_Proc_File->mode = S_IFREG | S_IRUGO | S_IWUSR; Our_Proc_File->uid = 0; Our_Proc_File->gid = 0; Our_Proc_File->size = 80;
if (Our_Proc_File == NULL) { rv = -ENOMEM; remove_proc_entry(PROC_ENTRY_FILENAME, &proc_root); printk(KERN_INFO "Error: Could not initialize /proc/test\n"); }
return rv; }
/* * Завершение работы модуля - дерегистрация файла в /proc. * Чревато последствиями * если в WaitQ остаются процессы, ожидающие своей очереди, * поскольку точка их исполнения * практически находится в функции open, которая * будет выгружена при удалении модуля. * Позднее, в 9 главе, я опишу как воспрепятствовать * удалению модуля в таких случаях */ void cleanup_module() { remove_proc_entry(PROC_ENTRY_FILENAME, &proc_root); }

Планирование задач


Очень часто возникает необходимость запуска вспомогательных задач по расписанию. Если запускаемая задача -- обычный процесс, то помещаем ее в файл crontab. Если же задача является модулем ядра, то у нас есть две возможности. Первая состоит в том, чтобы поместить некую задачу в файл crontab, которая будет "будить" модуль системным вызовом в заданный момент времени, например, открывая файл. Это очень неэффективно, т.к. при запуске нового процесса из crontab приходится загружать программу в память и всем это только для того, чтобы "разбудить" модуль ядра, который уже находится в памяти.

Вместо этого мы попробуем создать функцию, которая будет вызываться по прерываниям от таймера. Для этого, создадим задачу struct work_struct. Эта структура будет хранить указатель на функцию, срабатывающую по таймеру. Затем, с помощью queue_delayed_work, поместим задачу в очередь tq_timer, где должны располагаться задачи, срабатывающие по таймеру. А так как предполагается срабатывание функции каждый раз, по истечении заданного интервала времени, мы должны всякий раз опять вставлять ее в очередь tq_timer.

И еще один немаловажный момент. Когда модуль выгружается командой rmmod, сначала проверяется счетчик обращений к модулю. Если он равен нулю, то вызывается module_cleanup. После чего модуль удаляется из памяти со всеми его функциями. И никто не проверяет -- содержит ли очередь задач таймера указатель на одну из удаляемых функций. По прошествии некоторого времени (с точки зрения человека -- практически мгновенно), ядро получит прерывание от таймера и попробует вызывать удаленную из очереди задачу. Но функции-то больше нет! В большинстве случаев страница памяти, где она была, будет рассматриваться как неиспользуемая и вы получите сообщение об ошибке. Но может случиться так, что на этом месте окажется некоторый другой код и тогда ваше дело -- табак. К сожалению, у нас нет достаточно простого способа удаления задачи из очереди таймера.

Так как cleanup_module не может вернуть код ошибки (она не имеет возвращаемого значения), то напрашивается решение -- приостановить процедуру завершения работы модуля. Вместо того, чтобы немедленно завершить работу функции cleanup_module, мы можем приостановить работу команды rmmod. Затем, установив глобальную переменную, сообщить функции, вызываемой по прерыванию таймера, чтобы она убрала себя из очереди (точнее -- чтобы она опять не вставляла себя в очередь). На ближайшем прерывании таймера, процесс rmmod будет "разбужен", когда функция удалит себя из очереди таймера и удаление модуля станет безопасным.


Пример 10-1. sched.c
/* * sched.c - реализация срабатывания по таймеру. * * Copyright (C) 2001 by Peter Jay Salzman */
/* * Необходимые заголовочные файлы */
/* * Обычные, для модулей ядра */ #include <linux/kernel.h> /* Все-таки мы работаем с ядром! */ #include <linux/module.h> /* Необходимо для любого модуля */ #include <linux/proc_fs.h> /* Необходимо для работы с /proc */ #include <linux/workqueue.h> /* очереди задач */ #include <linux/sched.h> /* Взаимодействие с планировщиком */ #include <linux/init.h> /* макросы __init и __exit */ #include <linux/interrupt.h> /* определение irqreturn_t */
struct proc_dir_entry *Our_Proc_File; #define PROC_ENTRY_FILENAME "sched" #define MY_WORK_QUEUE_NAME "WQsched.c"
/* * Счетчик срабатываний по таймеру */ static int TimerIntrpt = 0;
static void intrpt_routine(void *);
static int die = 0; /* 1 -- завершить работу */
/* * очередь задач, создается для того, * чтобы поместить в очередь таймера (workqueue.h) */ static struct workqueue_struct *my_workqueue;
static struct work_struct Task; static DECLARE_WORK(Task, intrpt_routine, NULL);
/* * Функция-обработчик прерывания от таймера. * Обратите внимание на аргумент типа void* * функция может получать дополнительные * аргументы посредством этого указателя. */ static void intrpt_routine(void *irrelevant) { /* * Нарастить счетчик */ TimerIntrpt++;
/* * Если признак завершения сброшен, * то опять вставить себя в очередь таймера */ if (die == 0) queue_delayed_work(my_workqueue, &Task, 100); }
/* * Запись данных в файл /proc. */ ssize_t procfile_read(char *buffer, char **buffer_location, off_t offset, int buffer_length, int *eof, void *data) { int len; /* Фактическое число записанных байт */
/* * Переменные объявлены как static, поэтому они располагаются не на стеке * функции, а в памяти модуля */ static char my_buffer[80];
static int count = 1;
/* * Все сведения выдаются за один присест, * поэтому, если смещение != 0, то значит * нам нечего больше сказать, поэтому возвращается * 0, в качестве признака конца файла. */ if (offset > 0) return 0;


/* * Заполнить буфер и получить его длину */ len = sprintf(my_buffer, "Timer called %d times so far\n", TimerIntrpt); count++;
/* * Указать адрес буфера */ *buffer_location = my_buffer;
/* * Вернуть длину буфера */ return len; }
/* * Функция инициализации - зарегистрировать файл в /proc */ int __init init_module() { int rv = 0; /* * Создать очередь задач с нашей задачей и поместить ее в очередь таймера */ my_workqueue = create_workqueue(MY_WORK_QUEUE_NAME); queue_delayed_work(my_workqueue, &Task, 100);
Our_Proc_File = create_proc_entry(PROC_ENTRY_FILENAME, 0644, NULL); Our_Proc_File->read_proc = procfile_read; Our_Proc_File->owner = THIS_MODULE; Our_Proc_File->mode = S_IFREG | S_IRUGO; Our_Proc_File->uid = 0; Our_Proc_File->gid = 0; Our_Proc_File->size = 80;
if (Our_Proc_File == NULL) { rv = -ENOMEM; remove_proc_entry(PROC_ENTRY_FILENAME, &proc_root); printk(KERN_INFO "Error: Could not initialize /proc/%s\n", PROC_ENTRY_FILENAME); }
return rv; }
/* * Завершение работы */ void __exit cleanup_module() { /* * Удалить файл из /proc */ remove_proc_entry(PROC_ENTRY_FILENAME, &proc_root); printk(KERN_INFO "/proc/%s removed\n", PROC_ENTRY_FILENAME); /* Известить функцию обработки прерываний о завершении работы */ die = 1; cancel_delayed_work(&Task); flush_workqueue(my_workqueue); /* ждать пока отработает таймер */ destroy_workqueue(my_workqueue);
/* * Приостановить работу, пока intrpt_routine не * отработает в последний раз. * Это необходимо, поскольку мы освобождаем память, * занимаемую этой функцией. */
}
/* * некоторые функции, относящиеся к work_queue * доступны только если модуль лицензирован под GPL */ MODULE_LICENSE("GPL");

Симметричная многопроцессорность


Один из самых простых и самых дешевых способов увеличения производительности -- это разместить на материнской плате несколько микропроцессоров. Каждый из процессоров может играть свою собственную роль (асимметричная многопроцессорная обработка -- ASMP) или же они все работают параллельно, организуя вычисления таким образом, при котором и операционная система, и приложения могут использовать любой доступный процессор (симметричная многопроцессорная обработка -- SMP). Примером асимметричной многопроцессорной ОС может служить NetWare SFT III, использующая зеркальное отображение серверов, в двухпроцессорном сервере первый процессор занимается предоставлением услуг, а второй - операциями ввода/вывода. Асимметричная многопроцессорноя обработка более эффективна, но она требует точного распределения ролей между процессорами, что практически невозможно на универсальных ОС, подобных Linux. С другой стороны, симметричная многопроцессорная обработка менее эффективна, но относительно проста в реализации (здесь, под словами "относительно проста", вовсе не подразумевается, что это действительно

просто).

В симметричной многопроцессорной среде микропроцессоры совместно используют одну и ту же память, в результате код, работающий на одном CPU может изменять содержимое памяти, используемой другим CPU. Здесь уже нет уверенности, что переменная, которой вы присвоили некоторое значение, в предыдущей строке программы, все еще имеет то же самое значение -- код, исполняемый на другом CPU может его поменять. Очевидно, что я привел невероятный пример.

На самом деле, в случае обычных процессов, такой проблемы не существует. Как правило, в каждый конкретный момент времени, процесс может исполняться только на одном CPU. [15] Но ядро может выполнять одновременно разные процессы на разных CPU.

В ядрах версии 2.0.x это не было большой проблемой, поскольку ядро работало под защитой одной большой спин-блокировки (spinlock). Это означает, что когда один процессор работает в привилегированном режиме, а другой собирается войти в этот режим (например в случае системного вызова), то он вынужден ждать, пока первый процессор не выйдет в пользовательский режим. Это делает SMP в Linux безопасной, но малоэффективной.

Начиная с ядра версии 2.2.x стал возможным одновременный выход нескольких процессоров в привилегированный режим. Это обстоятельство вы должны знать и помнить.



Заключение


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

Стандартные библиотеки. Вы не должны использовать функции из стандартных библиотек языка C. Используйте только те функции, которые предоставляются ядром (большинство из них вы найдете в /proc/kallsyms).

Запрет прерываний. Если вы запретите прерывания на короткий срок, то ничего страшного не произойдет. Но если вы забудете их разрешить, то система "зависнет" и перезагрузить ее можно будет только кнопкой выключения питания.

Не суйте голову в пасть тигру! Вероятно это предупреждение излишне, но тем не менее...



"Hello, World" (часть 1): Простейший модуль ядра.


Когда первый пещерный программист высекал свою первую программу для каменного компьютера, это была программа, которая рисовала "Hello, World!" поперек изображения антилопы. Древнеримские учебники по программированию начинались с программы "Salut, Mundi". Я не знаю -- что случается с людьми, которые порывают с этой традицией, но думаю, что лучше этого и не знать. Мы начнем с серии программ "Hello world", на которых разберем различные базовые аспекты создания модулей ядра.

Ниже приводится исходный текст самого простого модуля ядра, какой только возможен. Пока только ознакомьтесь с его содержимым, а компиляцию и запуск модуля мы обсудим в следующем разделе.

Пример 2-1. hello-1.c

/* * hello-1.c - Простейший модуль ядра. */ #include <linux/module.h> /*Необходим для любого модуля ядра */ #include <linux/kernel.h> /*Здесь находится определение KERN_ALERT */

int init_module(void) { printk("<1>Hello world 1.\n");

/* * Если вернуть ненулевое значение, то это * будет воспринято как признак ошибки, * возникшей в процессе работы init_module; * в результате модуль не будет загружен. */ return 0; }

void cleanup_module(void) { printk(KERN_ALERT "Goodbye world 1.\n"); }

Любой модуль ядра должен иметь по меньшей мере хотя бы две функции: функцию инициализации модуля -- init_module(), которую вызывает insmod во время загрузки модуля, и функцию завершения работы модуля -- cleanup_module(), которую вызывает rmmod. Начиная с ядра, версии 2.3.13, требования к именованию начальной и конечной функций были сняты. Теперь вы можете давать им свои имена. Как это сделать будет описано в разделе . Новый метод именования является более предпочтительным, однако многие по-прежнему продолжают использовать имена init_module() и cleanup_module().

Обычно функция init_module()

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

И наконец, любой модуль ядра должен подключать заголовочный файл linux/module.h. В нашем примере мы подключали еще один файл -- linux/kernel.h, но лишь для того, чтобы получить доступ к определению KERN_ALERT, которое более подробно будет обсуждаться в



Hello World (часть 2)


Как мы уже упоминали, начиная с ядра, версии 2.3.13, требования к именованию начальной и конечной функций модуля были сняты. Достигается это с помощью макроопределений module_init() и module_exit(). Они определены в файле linux/init.h. Единственное замечание: начальная и конечная функции должны быть определены выше строк, в которых вызываются эти макросы, в противном случае вы получите ошибку времени компиляции. Ниже приводится пример использования этих макроопределений:

Пример 2-3. hello-2.c

/* * hello-2.c - Демонстрация использования * макроопределений module_init() и module_exit(). */ #include <linux/module.h> /* Необходим для любого модуля ядра */ #include <linux/kernel.h> /* Здесь находится определение KERN_ALERT */ #include <linux/init.h> /* Здесь находятся определения макросов */

static int __init hello_2_init(void) { printk(KERN_ALERT "Hello, world 2\n"); return 0; }

static void __exit hello_2_exit(void) { printk(KERN_ALERT "Goodbye, world 2\n"); }

module_init(hello_2_init); module_exit(hello_2_exit);

Теперь мы имеем в своем багаже два настоящих модуля ядра. Добавить сборку второго модуля очень просто:

Пример 2-4. Makefile для сборки обоих модулей

obj-m += hello-1.o obj-m += hello-2.o

Теперь загляните в файл linux/drivers/char/Makefile. Он может рассматриваться как пример полноценного Makefile модуля ядра. Здесь видно, что ряд модулей жестко "зашиты" в ядро (obj-y), но нигде нет строки obj-m. Почему? Знакомые с языком сценариев командной оболочки легко найдут ответ. Все записи вида obj-$(CONFIG_FOO) будут заменены на obj-y или obj-m, в зависимости от значения переменных CONFIG_FOO. Эти переменные вы сможете найти в файле .config, который был создан во время конфигурирования ядра с помощью make menuconfig или что-то вроде этого.



Hello World (часть 3): Макроопределения __init и __exit


Это демонстрация особенностей ядра, появивишихся, начиная с версии 2.2. Обратите внимание на то, как изменились определения функций инициализации и завершения работы модуля. Макроопределение __init вынуждает ядро, после выполнения инициализации модуля, освободить память, занимаемую функцией, правда относится это только к встроенным модулям и не имеет никакого эффекта для загружаемых модулей. Если вы мысленно представите себе весь процесс инициализации встроенного модуля, то все встанет на свои места.

То же относится и к макросу __initdata, но только для переменных.

Макроопределение __exit вынуждает ядро освободить память, занимаемую функцией, но только для встроенных модулей, на загружаемые модули это макроопределение не оказывает эффекта. Опять же, если вы представите себе -- когда вызывается функция завершения работы модуля, то станет понятно, что для встроенных модулей она не нужна, в то время как для загружаемых модулей -- просто необходима.

Оба этих макроса определены в файле linux/init.h и отвечают за освобождение неиспользуемой памяти в ядре. Вам наверняка приходилось видеть на экране, во аремя загрузки, сообщение примерно такого содержания: Freeing unused kernel memory: 236k freed. Это как раз и есть результат работы данных макроопределений.

Пример 2-5. hello-3.c

/* * hello-3.c - Использование макроопределений __init, __initdata и __exit. */ #include <linux/module.h> /* Необходим для любого модуля ядра */ #include <linux/kernel.h> /* Здесь находится определение KERN_ALERT */ #include <linux/init.h> /* Здесь находятся определения макросов */

static int hello3_data __initdata = 3;

static int __init hello_3_init(void) { printk(KERN_ALERT "Hello, world %d\n", hello3_data); return 0; }

static void __exit hello_3_exit(void) { printk(KERN_ALERT "Goodbye, world 3\n"); }

module_init(hello_3_init); module_exit(hello_3_exit);



Hello World (часть 4): Вопросы лицензирования и документирования модулей


Если у вас установлено ядро 2.4 или более позднее, то наверняка, во время запуска примеров модулей, вам пришлось столкнуться с сообщениями вида:

# insmod hello-3.o Warning: loading hello-3.o will taint the kernel: no license See http://www.tux.org/lkml/#export-tainted for information about tainted modules Hello, world 3 Module hello-3 loaded, with warnings

В ядра версии 2.4 и выше был добавлен механизм контроля лицензий, чтобы иметь возможность предупреждать пользователя об использовании проприетарного (не свободного) кода. Задать условия лицензирования модуля можно с помощью макроопределения MODULE_LICENSE(). Ниже приводится выдержка из файла linux/module.h

(от переводчика: я взял на себя смелость перевести текст комментариев на русский язык):

/* * В настоящее время, для обозначения свободных лицензий, приняты следующие * идентификаторы * * "GPL" [GNU Public License v2 или выше] * "GPL v2" [GNU Public License v2] * "GPL and additional rights" [GNU Public License v2 с дополнительными правами] * "Dual BSD/GPL" [GNU Public License v2 * или BSD license] * "Dual MPL/GPL" [GNU Public License v2 * или Mozilla license] * * Кроме того, дополнительно имеются следующие идентификаторы * * "Proprietary" [проприетарный, не свободный продукт] * * Здесь присутствуют компоненты, подразумевающие двойное лицензирование, * однако, по отношению к Linux они приобретают значение GPL, как наиболее * уместное, так что это не является проблемой. * Подобно тому, как LGPL связана с GPL * * На это есть несколько причин * 1. modinfo может показать сведения о лицензировании * для тех пользователей, которые желают, чтобы их * набор программных компонент был свободным * 2. Сообщество может игнорировать отчеты об ошибках * (bug reports), относящиеся к проприетарным модулям * 3. Поставщики программных продуктов могут поступать * аналогичным образом, основываясь на своих * собственных правилах */

Точно так же, для описания модуля может использоваться макрос MODULE_DESCRIPTION(), для установления авторства -- MODULE_AUTHOR(), а для описания типов устройств, поддерживаемых модулем -- MODULE_SUPPORTED_DEVICE().


Все эти макроопределения описаны в файле linux/module.h. Они не используются ядром и служат лишь для описания модуля, которое может быть просмотрено с помощью objdump. Попробуйте с помощью утилиты grep

посмотреть, как авторы модулей используют эти макросы (в каталоге linux/drivers).

Пример 2-6. hello-4.c

/* * hello-4.c - Демонстрация описания модуля. */ #include <linux/module.h> #include <linux/kernel.h> #include <linux/init.h> #define DRIVER_AUTHOR "Peter Jay Salzman <p@dirac.org>" #define DRIVER_DESC "A sample driver"

static int __init init_hello_4(void) { printk(KERN_ALERT "Hello, world 4\n"); return 0; }

static void __exit cleanup_hello_4(void) { printk(KERN_ALERT "Goodbye, world 4\n"); }

module_init(init_hello_4); module_exit(cleanup_hello_4);

/* * Вы можете передавать в макросы строки, как это показано ниже: */

/* * Запретить вывод предупреждения о "загрязнении" ядра, * объявив код под GPL. */ MODULE_LICENSE("GPL");

/* * или определения: */ MODULE_AUTHOR(DRIVER_AUTHOR); /* Автор модуля */ MODULE_DESCRIPTION(DRIVER_DESC); /* Назначение модуля */

/* * Этот модуль использует устройство /dev/testdevice. * В будущих версиях ядра * макрос MODULE_SUPPORTED_DEVICE может быть использован * для автоматической настройки модуля, но пока * он служит исключительно в описательных целях. */ MODULE_SUPPORTED_DEVICE("testdevice");


Как модули попадают в ядро?


Вы можете просмотреть список загруженных модулей командой lsmod, которая в свою очередь обращается за необходимыми сведениями к файлу /proc/modules.

Как же модули загружаются ядром? Когда ядро обнаруживает необходимость в тех или иных функциональных возможностях, еще не загруженных в память, то демон kmod [1]

вызывает утилиту modprobe, передавая ей строку в виде:

Название модуля, например softdog или ppp.

Универсальный идентификатор, например char-major-10-30.


Если утилите modprobe

передается универсальный идентификатор, то она сначала пытается отыскать имя соответствующего модуля в файле /etc/modules.conf, где каждому универсальному идентификатору поставлено в соответствие имя модуля, например:

alias char-major-10-30 softdog

Это соответствует утверждению: "Данному универсальному идентификатору соответствует файл модуля softdog.ko".

Затем modprobe отыскивает файл /lib/modules/version/modules.dep, чтобы проверить -- не нужно ли загружать еще какие-либо модули, от которых может зависеть заданный модуль. Этот файл создается командой depmod -a и описывает зависимости модулей. Например, модуль msdos.ko требует, чтобы предварительно был загружен модуль fat.ko. Если модуль Б экспортирует ряд имен (имена функций, переменных и т.п.), которые используются модулем А, то говорят, что "Модуль А зависит от модуля Б".

И наконец modprobe вызывает insmod, чтобы сначала загрузить необходимые, для удовлетворения зависимостей, модули, а затем и запрошенный модуль. Вызывая insmod, утилита modprobe указывает ей каталог, /lib/modules/version/ [2] -- стандартный путь к модулям ядра. Утилита insmod ничего не "знает" о размещении модулей ядра, зато это "знает" утилита modprobe. Таким образом, если вам необходимо загрузить модуль msdos, то вам необходимо дать следующие команды:

insmod /lib/modules/2.6.0/kernel/fs/fat/fat.ko insmod /lib/modules/2.6.0/kernel/fs/msdos/msdos.ko

или просто:

modprobe -a msdos

В большинстве дистрибутивов Linux, утилиты modprobe, insmod, depmod входят в состав пакета modutils или mod-utils.


Прежде чем закончить эту главу, я предлагаю вкратце ознакомиться с содержимым файла /etc/modules.conf:

### This file is automatically generated by modules-update # # Please do not edit this file directly. If you want to change or add # anything please take a look at the files in /etc/modules.d and read # the manpage for modules-update. # ### modules-update: start processing /etc/modules.d/aliases # Aliases to tell insmod/modprobe which modules to use path[misc]=/lib/modules/2.6.?/local keep path[net]=~p/mymodules options mydriver irq=10 alias eth0 eepro

Строки, начинающиеся с символа "#" являются комментариями. Пустые строки игнорируются.

Строка path[misc] сообщает modprobe о том, что модули ядра из категории misc следует искать в каталоге /lib/modules/2.6.?/local. Как видите, здесь вполне допустимы шаблонные символы.

Строка path[net] задает каталог размещения модулей категории net, однако, директива keep, стоящая выше, сообщает, что каталог ~p/mymodules не замещает стандартный путь поиска модулей (как это происходит в случае с path[misc]), а лишь добавляется к нему.

Строка alias говорит о том, что если запрошена загрузка модуля по универсальному идентификатору eth0, то следует загружать модуль eepro.ko

Вы едва ли встретите в этом файле строки, подобные:

alias block-major-2 floppy

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


Клавиатура на архитектуре Intel


Материал, рассматриваемый в оставшейся части этой главы, может быть применим исключительно к архитектуре Intel. На других платформах код примера работать не будет.

Было очень трудно выбрать тип драйвера, который можно было бы использовать в качестве примера в этой главе. С одной стороны пример должен быть достаточно полезным, он должен работать на любом компьютере и быть достаточно выразительным. С другой стороны, в ядро уже включено огромное количество драйверов практически для всех общеизвестных и широкораспространенных устройств. Эти драйверы не смогли бы совместно работать с тем, что я собирался написать. Наконец я принял решение представить в качестве примера -- обработчик прерываний от клавиатуры, но для демонстрации работоспособности кода сначала придется отключить стандартный обработчик прерываний от клавиатуры, а так как этот символ объявлен как static (в файле drivers/char/keyboard.c), то нет никакого способа восстановить обработчик. Поэтому, прежде чем вы дадите команду insmod, перейдите в другую консоль и дайте команду sleep 120; reboot, если ваша файловая система представляет для вас хоть какую-нибудь ценность.

Этот пример захватывает обработку IRQ 1 -- прерывание от клавиатуры на архитектуре Intel. При получении прерывания обработчик читает состояние клавиатуры (inb(0x64)) и скан-код нажатой клавиши. Затем, как только ядро сочтет возможным, оно вызывает got_char (она играет роль "нижней половины"), которая выводит, через printk, код клавиши (младшие семь бит скан-кода) и признак "нажата/отпущена" (8-й бит скан-кода -- 0 или 1 соответственно).

Пример 11-1. intrpt.c

/* * intrpt.c - Обработчик прерываний. * * Copyright (C) 2001 by Peter Jay Salzman */

/* * Standard in kernel modules */ #include <linux/kernel.h> /* Все-таки мы работаем с ядром! */ #include <linux/module.h> /* Необходимо для любого модуля */ #include <linux/workqueue.h> /* очереди задач */ #include <linux/sched.h> /* Взаимодействие с планировщиком */ #include <linux/interrupt.h> /* определение irqreturn_t */ #include <asm/io.h>


#define MY_WORK_QUEUE_NAME "WQsched.c"

static struct workqueue_struct *my_workqueue;

/* * Эта функция вызывается ядром, поэтому в ней будут безопасны все действия * которые допустимы в модулях ядра. */ static void got_char(void *scancode) { printk("Scan Code %x %s.\n", (int)*((char *)scancode) & 0x7F, *((char *)scancode) & 0x80 ? "Released" : "Pressed"); }

/* * Обработчик прерываний от клавиатуры. * Он считывает информацию с клавиатуры * и передает ее менее критичной по * времени исполнения части, * которая будет запущена сразу же, * как только ядро сочтет это возможным. */ irqreturn_t irq_handler(int irq, void *dev_id, struct pt_regs *regs) { /* * Эти переменные объявлены статическими, чтобы имелась возможность * доступа к ним (посредством указателей) из "нижней половины". */ static int initialised = 0; static unsigned char scancode; static struct work_struct task; unsigned char status;

/* * Прочитать состояние клавиатуры */ status = inb(0x64); scancode = inb(0x60);

if (initialised == 0) { INIT_WORK(&task, got_char, &scancode); initialised = 1; } else { PREPARE_WORK(&task, got_char, &scancode); }

queue_work(my_workqueue, &task);

return IRQ_HANDLED; }

/* * Инициализация модуля - регистрация обработчика прерывания */ int init_module() { my_workqueue = create_workqueue(MY_WORK_QUEUE_NAME);

/* * Поскольку стандартный обработчик прерываний от клавиатуры не может * сосуществовать с таким как наш, то придется запретить его * (освободить IRQ) прежде, чем что либо сделать. * Но поскольку мы не знаем где он находится в ядре, то мы лишены * возможности переустановить его - поэтому * компьютер придется перезагрузить * после опробования этого примера. */ free_irq(1, NULL);

/* * Подставить свой обработчик (irq_handler) на IRQ 1. * SA_SHIRQ означает, что мы допускаем возможность совместного * обслуживания этого IRQ другими обработчиками. */ return request_irq(1, /* Номер IRQ */ irq_handler, /* наш обработчик */ SA_SHIRQ, "test_keyboard_irq_handler", (void *)(irq_handler)); }

/* * Завершение работы */ void cleanup_module() { /* * Эта функция добавлена лишь для полноты изложения. * Она вообще бессмысленна, поскольку я не вижу способа * восстановить стандартный обработчик прерываний от клавиатуры * поэтому необходимо выполнить перезагрузку системы. */ free_irq(1, NULL); }

/* * некоторые функции, относящиеся к work_queue * доступны только если модуль лицензирован под GPL */ MODULE_LICENSE("GPL");


Модули ядра и прикладные программы


Работа программы обычно начинается с исполнения функции main(). После выполнения всей последовательность команд программа завершает свою работу. Модули исполняются иначе. Они всегда начинают работу с исполнения функции init_module, или с функции, которую вы определили через вызов module_init. Это функция запуска модуля, которая подготавливает его для последующих вызовов. После завершения исполнения функции init_module модуль больше ничего не делает, он просто "сидит и ждет", когда ядро обратится к нему для выполнения специфических действий.

Вторая точка входа в модуль -- cleanup_module, вызывается непосредственно перед его выгрузкой. Она производит "откат" изменений, выполненных функцией init_module() и, как бы говорит ядру: "Я ухожу! Больше не проси меня ни о чем!".

Любой модуль обязательно должен иметь функцию инициализации и функцию завершения. Так как существует более чем один способ определить функции инициализации и завершения, я буду стараться использовать термины "начальная" и "конечная" функции, если я собьюсь и укажу названия init_module и cleanup_module, то думаю, что вы поймете меня правильно.



Модули, состоящие из нескольких файлов


Иногда возникает необходимость разместить исходные тексты модуля в нескольких файлах. В этом случае kbuild опять возьмет на себя всю "грязную" работу, а Makefile поможет сохранить наши руки чистыми, а голову светлой! Ниже приводится пример модуля, состоящего из двух файлов:

Пример 2-8. start.c

/* * start.c - Пример модуля, исходный * текст которого размещен в нескольких файлах */

#include <linux/kernel.h> /* Все-таки мы пишем код ядра! */ #include <linux/module.h> /* Необходим для любого модуля ядра */

int init_module(void) { printk("Hello, world - this is the kernel speaking\n"); return 0; }

Пример 2-9. stop.c

/* * stop.c - Пример модуля, исходный текст * которого размещен в нескольких файлах */

#include <linux/kernel.h> /* Все-таки мы пишем код ядра! */ #include <linux/module.h> /* Необходим для любого модуля ядра */

void cleanup_module() { printk("<1>Short is the life of a kernel module\n"); }

Пример 2-10. Makefile для сборки всех модулей

obj-m += hello-1.o obj-m += hello-2.o obj-m += hello-3.o obj-m += hello-4.o obj-m += hello-5.o obj-m += startstop.o startstop-objs := start.o stop.o



Нумерация версий и дополнительные примечания


Ядро Linux -- динамически развивающийся проект. И перед авторами книги всегда остро стоял вопрос -- удалять ли устаревшие сведения из книги или сохранять их, как историческую ценность. Майкл и я (Питер Зальцман) решили создать отдельную ветку документа для каждой стабильной серии ядер. Таким образом, LKMPG (Linux Kernel Module Programming Guide), имеющее версию 2.4.x относится к ядру 2.4.x, а LKMPG 2.6.x -- к ядру версии 2.6.x. Мы решили не сохранять устаревшие сведения в новых версиях документа. Желающие получить эту информацию должны обращаться к соответствующим версиям документа.

Исходный код и подаваемый материал не относятся к какой либо конкретной аппаратной архитектуре, но я ничего не могу гарантировать. Одно важное исключение -- , где весь обсуждаемый материал относится к архитектуре x86.



Об авторах


Эта книга изначально была написана Ори Померанцем (Ori Pomerantz) для ядра Linux версии 2.2. К сожалению у Ори не хватает времени на продолжение работы над этой книгой. В конце концов ядро Linux продолжает быстро развиваться. Питер Зальцман (Peter Jay Salzman) взял на себя труд по адаптации документа для ядра версии 2.4. К сожалению и Питер не нашел достаточно свободного времени, чтобы продолжить работу над книгой и дополнить ее материалами, касающимися ядра версии 2.6. Таким образом, майкл Бариан (Michael Burian) взялся за адаптацию материала книги для ядра 2.6.



Обработка прерываний


Везде, кроме предыдущей главы, все наши действия в ядре сводилось к ответам на разные запросы от процессов, к работе со специальными файлом, посылке команд ioctl или запуску системных вызовов. Однако работа ядра не может сводится только к обработке запросов. Еще одна немаловажная задача -- это работа с аппаратурой компьютера.

Существует два типа взаимодействий между CPU и остальной аппаратной частью компьютера. Первый -- передача команд аппаратным средствам, второй -- прием ответов от аппаратуры. Второй тип взаимодействия -- прерывания, является наиболее тяжелым в обработке, потому что прерывания возникают тогда, когда это удобно устройству, а не CPU. Аппаратные устройства обычно имеют весьма ограниченный объем ОЗУ, и если не считать поставляемую ими информацию немедленно, то она может потеряться.

В Linux аппаратные прерывания называются IRQ (сокращенно от Interrupt ReQuests -- Запросы на Прерывание). [14]

Имеется два типа IRQ: "короткие" и "длинные". "Короткие" IRQ занимают очень короткий период времени, в течение которого работа операционной системы будет заблокирована, а так же будет невозможна обработка других прерываний. "Длинные" IRQ могут занять довольно продолжительное время, в течение которого могут обрабатываться и другие прерывания (но не прерывания из того же самого устройства). Поэтому, иногда бывает благоразумным разбить выполнение работы на исполняемую внутри обработчика прерываний (т.е. подтверждение прерывания, изменение состояния и пр.) и работу, которая может быть отложена на некоторое время (например постобработка данных, активизация процессов, ожидающих эти данные и т.п.). Если это возможно, лучше объявлять обработчики прерывания "длинными".

Когда CPU получает прерывание, он останавливает любые процессы (если это не более приоритетное прерывание, тогда обработка пришедшего прерывания произойдет только тогда, когда более приоритетное будет завершено), сохраняет некоторые параметры в стеке и вызывает обработчик прерывания. Это означает, что не все действия допустимы внутри обработчика прерывания, потому что система находится в неизвестном состоянии. Решение проблемы: обработчик прерывания определяет -- что должно быть сделано немедленно (обычно что-то прочитать из устройства или что-то послать ему), а затем запланировать обработку поступившей информации на более позднее время (это называется "bottom halves" -- "нижние половины") и вернуть управление. Ядро гарантирует вызов "нижней половины" так быстро, насколько это возможно. Когда это произойдет, то наш обработчик -- "нижняя половина", уже не будет стеснен какими-то рамками и ему будет доступно все то, что доступно обычным модулям ядра.


Устанавливается обработчик прерывания вызовом request_irq. Ей передаются номер IRQ, имя функции-обработчика, флаги, имя для /proc/interrupts и дополнительный параметр для обработчика прерываний. Флаги могут включать SA_SHIRQ, чтобы указать, что прерывание может обслуживаться несколькими обработчиками (обычно, по той простой причине, что на одном IRQ может "сидеть" несколько устройств) и SA_INTERRUPT, чтобы указать, что это "короткое" прерывание. Эта функция установит обработчик только в том случае, если на заданном IRQ еще нет обработчика прерывания, или если существующий обработчик зарегистрировал совместную обработку прерывания флагом SA_SHIRQ.

Во время обработки прерывания, из функции-обработчика прерывания, мы можем получить данные от устройства и затем, с помощью queue_task_irq, tq_immediate и mark_bh(BH_IMMEDIATE), запланировать "нижнюю половину". В ранних версиях Linux имелся массив только из 32 "нижних половин", теперь же, одна из них (а именно BH_IMMEDIATE) используется для обслуживания целого списка "нижних половин" драйверов. Вызов mark_bh(BH_IMMEDIATE) как раз и вставляет "нижнюю половину" драйвера в этот список, планируя таким образом ее исполнение.


Отключение устройства


Мы не можем позволить выгружать модуль по прихоти суперпользователя. Если файл устройства удалить после того как он будет открыт процессом, то может возникнуть ситуация когда процесс попытается обратиться к выгруженному драйверу (в конце концов процесс даже не подозревает, что такое могло произойти). В результате произойдет попытка обращения к тому участку памяти, где ранее находилась функция обработки запроса. Если вам повезет, то этот участок памяти окажется не затертым ядром и вы получите сообщение об ошибке. Если не повезет -- то произойдет переход в середину "чужой" функции. Результат такого "вызова" трудно предугадать заранее

Обычно, если какая-то операция должна быть отвергнута, функция возвращает код ошибки (отрицательное число). В случае с функцией cleanup_module() это невозможно, поскольку она не имеет возвращаемого значения. Однако, для каждого модуля в системе имеется счетчик обращений, который хранит число процессов, использующих модуль. Вы можете увидеть это число в третьем поле, в файле /proc/devices. Если это поле не равно нулю, то rmmod не сможет выгрузить модуль. Обратите внимание: вам нет нужды следить за состоянием счетчика в cleanup_module(), это делает система, внутри системного вызова sys_delete_module (определение функции вы найдете в файле linux/module.c). Вы не должны изменять значение счетчика напрямую, тем не менее, ядро предоставляет в ваше распоряжение функции, которые увеличивают и уменьшают значение счетчика обращений:

try_module_get(THIS_MODULE): увеличивает счетчик обращений на 1.

try_module_put(THIS_MODULE): уменьшает счетчик обращений на 1.

Очень важно сохранять точное значение счетчика! Если Вы каким-либо образом потеряете действительное значение, то вы никогда не сможете выгрузить модуль. Тут, милые мои мальчики и девочки, поможет только перезагрузка! Это обязательно случиться с вами, рано или поздно, при разработке какого-либо модуля!



Передача модулю параметров командной строки


Имеется возможность передачи модулю дополнительных параметров командной строки, но делается это не с помощью argc/argv.

Для начала вам нужно объявить глобальные переменные, в которые будут записаны входные параметры, а затем вставить макрос MODULE_PARAM(), для запуска механизма приема внешних аргументов. Значения параметров могут быть переданы модулю с помощью команд insmod или modprobe. Например: insmod mymodule.ko myvariable=5. Для большей ясности, объявления переменных и вызовы макроопределений следует размещать в начале модуля. Пример кода прояснит мое, по общему признанию, довольно неудачное объяснение.

Макрос MODULE_PARAM() принимает 2 аргумента: имя переменной и ее тип. Поддерживаются следующие типы переменных

"b" -- byte (байт);

"h" -- short int (короткое целое);

"i" -- integer (целое, как со знаком, так и без знака);

"l" -- long int (длинное целое, как со знаком, так и без знака);

"s" -- string (строка, должна объявляться как char*).

Для переменных типа char *, insmod будет сама выделять необходимую память. Вы всегда должны инициализировать переменные значениями по-умолчанию, не забывайте -- это код ядра, здесь лучше лишний раз перестраховаться. Например:

int myint = 3; char *mystr;

MODULE_PARAM(myint, "i"); MODULE_PARAM(mystr, "s");

Параметры-массивы так же допустимы. Целое число, предшествующее символу типа аргумента, обозначает максимальный размер массива. Два числа, разделенные дефисом -- минимальное и максимальное количество значений. Например, массив целых, который должен иметь не менее 2-х и не более 4-х значений, может быть объявлен так:

int myintArray[4]; MODULE_PARAM(myintArray, "2-4i");

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


И, наконец, еще одно макроопределение -- MODULE_PARAM_DESC(). Оно используется для описания входных аргументов модуля. Принимает два параметра: имя переменной и строку описания, в свободной форме.

Пример 2-7. hello-5.c

/* * hello-5.c - Пример передачи модулю аргументов командной строки. */ #include <linux/module.h> #include <linux/moduleparam.h> #include <linux/kernel.h> #include <linux/init.h> #include <linux/stat.h>

MODULE_LICENSE("GPL"); MODULE_AUTHOR("Peter Jay Salzman");

static short int myshort = 1; static int myint = 420; static long int mylong = 9999; static char *mystring = "blah";

/* * module_param(foo, int, 0000) * Первый параметр -- имя переменной, * Второй -- тип, * Последний -- биты прав доступа * для того, чтобы выставить в sysfs * (если ненулевое значение) на более поздней стадии. */

module_param(myshort, short, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP); MODULE_PARM_DESC(myshort, "A short integer"); module_param(myint, int, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); MODULE_PARM_DESC(myint, "An integer"); module_param(mylong, long, S_IRUSR); MODULE_PARM_DESC(mylong, "A long integer"); module_param(mystring, charp, 0000); MODULE_PARM_DESC(mystring, "A character string");

static int __init hello_5_init(void) { printk(KERN_ALERT "Hello, world 5\n=============\n"); printk(KERN_ALERT "myshort is a short integer: %hd\n", myshort); printk(KERN_ALERT "myint is an integer: %d\n", myint); printk(KERN_ALERT "mylong is a long integer: %ld\n", mylong); printk(KERN_ALERT "mystring is a string: %s\n", mystring); return 0; }

static void __exit hello_5_exit(void) { printk(KERN_ALERT "Goodbye, world 5\n"); }

module_init(hello_5_init); module_exit(hello_5_exit);

Давайте немножко поэкспериментируем с этим модулем:

satan# insmod hello-5.o mystring="bebop" myshort=255 myshort is a short integer: 255 myint is an integer: 420 mylong is a long integer: 9999 mystring is a string: bebop

satan# rmmod hello-5 Goodbye, world 5

satan# insmod hello-5.o mystring="supercalifragilisticexpialidocious" myint=100 myshort is a short integer: 1 myint is an integer: 100 mylong is a long integer: 9999 mystring is a string: supercalifragilisticexpialidocious

satan# rmmod hello-5 Goodbye, world 5

satan# insmod hello-5.o mylong=hello hello-5.o: `hello' invalid for parameter mylong


Прежде, чем продолжить


Прежде, чем мы приступим к программированию, необходимо обсудить еще ряд моментов. Любая система имеет свои отличительные черты и каждый из нас имеет разный багаж знаний. Написать, скомпилировать и запустить свою первую программу "Hello World!" для многих может оказаться довольно сложной задачей. Однако, после преодоления этого начального препятствия, работа, как правило, продвигается без особых проблем.



В ранних версиях ядра он



[1]
В ранних версиях ядра он назывался kerneld.
[2]
Если вы предполагаете проводить эксперименты с ядром, то чтобы избежать перезаписи существующих файлов модулей вы можете изменить значение переменной EXTRAVERSION в Makefile ядра. В результате все модули ядра будут записываться в отдельный каталог.
[3]
Я не компьютерный гений, я простой физик!
[4]
Это - далеко не то же самое, что и "встраивание всех модулей в ядро", хотя идея та же.
[5]
Делается это в соответствии с принятыми соглашениями. Однако, при разработке драйвера устройства, на период отладки, размещать файл устройства в своем домашнем каталоге -- наверное не такая уж и плохая идея. Единственное -- не забудьте исправить место для размещения файла устройства после того, как отладка будет закончена.
[6]
В версиях ядра 2.0 и 2.2 это делалось автоматически, если в качестве номера inode передавалось нулевое значение.
[7]
Различие здесь состоит в том, что файловые операции работают с файлом непосредственно, а функции, работающие с inode, предоставляют способ сослаться на файл, например создание ссылок на файл.
[8]
Обратите внимание, здесь смысл терминов "чтение" и "запись" также имеют обратный смысл. Так операция чтения передает данные от процесса ядру, а операция записи -- в обратном направлении, от ядра к процессу.
[9]
Не совсем верно. Вы не сможете передать функции ioctl, например структуру, но передать указатель на структуру -- безусловно возможно.
[10]
Самый простой способ оставить файл открытым -- это дать команду tail -f
[11]
Это означает, что процесс продолжает свою работу в привилегированном режиме -- поскольку процесс был приостановлен во время работы системного вызова, который еще не закончил свою работу. Процесс даже не "подозревает" о том, что кто-то еще, кроме него, использовал процессор в промежутке между обращением к системному вызову и возвратом из него.
[12]
Именно по этой причине использовался вызов функции wait_event_interruptible. Можно было бы использовать wait_event, но тогда задачу невозможно будет прервать по Ctrl-C во время ожидания.
[13]
Tty (от англ. TeleTYpe - телетайп) -- первоначально обозначал комбинацию клавиатура-печатающее устройство предназначенное для взаимодействия с Unix-системой, на сегодняшний день -- это абстракция текстового потока, используемого программами Unix, независимо от того, является ли он физическим терминалом, окном xterm на дисплее, сетевым подключением или чем то иным.
[14]
Это стандартное понятие для архитектуры Intel, на которой начинал разрабатываться Linux.
[15]
За исключением многопоточных процессов. В этом случае разные потоки одного процесса могут исполняться одновременно на разных процессорах.

Пространство имен


Когда вы пишете небольшую программку на C, вы именуете свои функции и переменные так, как вам это удобно. С другой стороны, когда вы разрабатываете некую программную единицу, входящую в состав большого программного пакета, любые глобальные переменные, которые вы вводите, становятся частью всего набора глобальных переменных пакета. В этой ситуации могут возникнуть конфликты имен. Внедрение большого количества имен функций и переменных, с глобальной областью видимости, значение которых не интутивно и трудноразличимо, приводит к "загрязнению" пространства имен. Программист, который работает с такими приложениями, тратит огромное количество умственных сил на то, чтобы запомнить "зарезервированные" имена и придумать свои уникальные названия.

Модули ядра компонуются с огромным программным пакетом -- ядром, поэтому проблема "загрязнения" пространства имен становится достаточно острой. Коллизии имен могут породить трудноуловимые ошибки, начиная от того, что модуль просто отказывается загружаться, и заканчивая весьма причудливыми сообщениями. Лучший способ избежать "загрязнения" пространства имен -- это объявлять все имена как static и использовать префиксы для придания уникальности именам с глобальной областью видимости. По соглашению об именовании, желательно, в качестве префиксов, использовать символы нижнего регистра. Если вы не можете какие-то имена объявить как static, то разрешить проблему можно посредством создания symbol table и регистрации ее в ядре. Эту тему мы обсудим ниже.

Файл /proc/kallsyms содержит все имена в ядре, с глобальной областью видимости, которые доступны для ваших модулей.



Пространство пользователя и пространство ядра


За доступ к ресурсам системы отвечает ядро, будь то видеоплата, жесткий диск или даже память. Программы часто конкурируют между собой за доступ к тем или иным ресурсам. Например, при подготовке этого документа, я сохраняю файл с текстом на жесткий диск, тут же стартует updatedb, чтобы обновить локальную базу данных. В результате мой vim и updatedb начинают конкурировать за обладание жестким диском. Ядро должно обслужить конкурирующие запросы, и "выстроить" их в порядке очередности. К тому же сам центральный процессор может работать в различных режимах. Каждый из режимов имеет свою степень "свободы" действий. Микропроцессор Intel 80386 имеет четыре таких режима, которые часто называют "кольцами". Unix использует только два из них: наивысший (нулевое кольцо, известное так же под названием привилегированный режим) и низший (пользовательский режим).

Вернемся к обсуждению библиотечных функций и системных вызовов. Как правило, программа обращается к библиотечным функциям, находясь в пользовательском режиме. Затем библиотечные функции обращаются к системным вызовам. Системные вызовы выступают от имени библиотечных функций, но работают в привилегированном режиме, так как они являются непосредственной частью ядра. Как только системный вызов завершает свою работу, он возвращает управление библиотечной функции и происходит обратный переход в пользовательский режим.

Обычно, о режимах исполнения, мы говорим как о пространстве ядра и пространстве пользователя. Эти два понятия охватывают не только два режима исполнения, но так же и то, что каждый из режимов имеет свое собственное отображение памяти -- свое собственное адресное пространство.

Unix производит переключение из пространства пользователя в пространство ядра всякий раз, когда приложение делает системный вызов или приостанавливается аппаратным прерыванием. Код ядра, исполняющий системный вызов, работает в контексте процесса -- от имени вызвавшего процесса и имеет доступ к данным в адресном пространстве процесса. Код, который обрабатывает прерывание, наоборот, являясь асинхронным по своей природе, не относится ни к одному из процессов.

Основное назначение модулей -- расширение функциональности ядра. Код модуля исполняется в пространстве ядра. Обычно модуль реализует обе, рассмотренные выше задачи -- одни функции выполняются как часть системных вызовов, другие -- производят обработку прерываний.



Регистрация устройства


Как уже говорилось ранее, доступ к символьным устройствам осуществляется посредством файлов устройств, которые как правило располагаются в каталоге /dev. [5]

Старший номер устройства говорит о том, какой драйвер с каким файлом устройства связан. Младший номер используется самим драйвером для идентификации устройства, если он обслуживает несколько таких устройств.

Добавление драйвера в систему подразумевает его регистрацию в ядре. Это означает -- получение старшего номера в момент инициализации модуля. Получить его можно вызовом функции register_chrdev(), определенной в файле linux/fs.h:

int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);

где unsigned int major -- это запрашиваемый старший номер устройства, const char *name -- название устройства, которое будет отображаться в /proc/devices и struct file_operations *fops -- указатель на таблицу file_operations драйвера. В случае ошибки, функция register_chrdev() возвращает отрицательное число. Обратите внимание: функции регистрации драйвера не передается младший номер устройства. Все потому, что ядро не обслуживает его -- это прерогатива драйвера.

А теперь вопрос: Как получить старший номер для своего устройства, чтобы случайно не "занять" уже существующий? Самый простой способ -- заглянуть в файл Documentation/devices.txt и выбрать один из неиспользуемых. Но это не самый лучший выход, потому что вы никогда не будете уверены в том, что выбранный вами номер не будет позднее официально связан с каким-либо другим устройством. Правильный ответ -- "попросить" ядро выделить вам динамический номер устройства.

Если вы передадите функции register_chrdev(), в качестве старшего номера, число 0, то возвращаемое положительное значение будет представлять собой, динамически выделенный ядром, старший номер устройства. Один из неприятных моментов здесь состоит в том, что вы заранее не можете создать файл устройства, поскольку старший номер устройства вам заранее не известен. Тем не менее, можно предложить ряд способов решения этой проблемы.

Драйвер может выводить сообщение в системный журнал (как это делает модуль "Hello World"), а вы затем вручную создадите файл устройства.

Для вновь зарегистрированного устройства, в файле /proc/devices появится запись. Вы можете найти эту запись и вручную создать файл устройства или можно написать небольшой сценарий, который выполнит эту работу за вас.

Можно "заставить" сам драйвер создавать файл устройства, с помощью системного вызова mknod, после успешной регистрации. А внутри cleanup_module() предусмотреть возможность удаления файла устройства с помощью rm.



Сборка модулей ядра


Чтобы модуль был работоспособен, при компиляции необходимо передать gcc ряд опций. Кроме того, необходимо чтобы модули компилировались с предварительно определенными символами. Ранние версии ядра полностью полагались, в этом вопросе, на программиста и ему приходилось явно указывать требуемые определения в Makefile-ах. Несмотря на иерархическую организацию, в Makefile-ах, на вложенных уровнях, накапливалось такое огромное количество параметров настройки, что управление и сопровождение этих настроек стало довольно трудоемким делом. К счастью появился kbuild, в результате процесс сборки внешних загружаемых модулей теперь полностью интегрирован в механизм сборки ядра. Дополнительные сведения по сборке модулей, которые не являются частью официального ядра (как в нашем случае), вы найдете в файле linux/Documentation/kbuild/modules.txt.

А теперь попробуем собрать наш с вами модуль hello-1.c . Соответствующий Makefile содержит всего одну строку:

Пример 2-2. Makefile для модуля ядра

obj-m += hello-1.o

Для того, чтобы запустить процесс сборки модуля, дайте команду make -C /usr/src/linux-`uname -r` SUBDIRS=$PWD modules (от переводчика: если у вас в каталоге /usr/src присутствует символическая ссылка linux на каталог с исходными текстами ядра, то команда сборки может быть несколько упрощена: make -C /usr/src/linux SUBDIRS=$PWD modules). На экран должно быть выведено нечто подобное:

[root@pcsenonsrv test_module]# make -C / /usr/src/linux-`uname -r` SUBDIRS=$PWD modules make: Entering directory `/usr/src/linux-2.6.x CC [M] /root/test_module/hello-1.o Building modules, stage 2. MODPOST CC /root/test_module/hello-1.mod.o LD [M] /root/test_module/hello-1.ko make: Leaving directory `/usr/src/linux-2.6.x

Обратите внимание: в ядрах версии 2.6 введено новое соглашение по именованию объектных файлов модулей. Теперь, они имеют расширение .ko (взамен прежнего .o), что отличает их от обычных объектных файлов. Дополнительную информацию по оформлению Makefile-ов модулей вы найдете в linux/Documentation/kbuild/makefiles.txt. Обязательно прочтите этот документ прежде, чем начнете углубляться в изучение Makefile-ов.


Итак, настал торжественный момент -- теперь можно загрузить свежесобранный модуль! Дайте команду insmod ./hello-1.ko (появляющиеся сообщения о "загрязнении" ядра вы сейчас можете просто игнорировать, вскоре мы обсудим эту проблему).

Любой загруженный модуль ядра заносится в список /proc/modules, так что дружно идем туда и смотрим содержимое этого файла. как вы можете убедиться, наш модуль стал частью ядра. С чем вас и поздравляем, теперь вы стали одним из авторов кода ядра! Вдоволь насладившись ощущением новизны, выгрузите модуль командой rmmod hello-1 и загляните в файл /var/log/messages, здесь вы увидите сообщения, которые сгенерировал ваш модуль. (от переводчика: в моем случае на экран консоли сообщения не выводились, зато они появились в файле /var/log/kern.log).

А теперь небольшое упражнение: Измените содержимое файла hello-1.c так, чтобы функция init_module() возвращала бы какое либо ненулевое значение и проверьте -- что получится?


Сборка модулей под существующее ядро


Мы уже рекомендовали вам пересобрать свое ядро, включив некоторые полезные для отладки опции, например такие, как (MODULE_FORCE_UNLOAD) -- когда эта опция включена, то вы имеете возможность принудительной выгрузки модуля (посредством команды rmmod -f module_name), даже если ядро "считает" ваши действия небезопасными. Эта опция поможет вам сэкономить время на перезагрузках системы, в процессе отладки модуля.

Как бы то ни было, но ситуация может сложиться так, что вам потребуется загрузить модуль в ранее откомпилированное ядро, например, на другой системе, или в случае, когда вы не можете пересобрать ядро по каким либо соображениям. Если вы не предполагаете возникновение таких ситуаций, то можете просто пропустить эту часть главы.

Если вы лишь установили дерево с исходными текстами ядра и использовали их для сборки своего модуля, то в большинстве случаев, при попытке загрузить его в работающее ядро, вы получите следующее сообщение об ошибке:

insmod: error inserting 'your_module_name.ko': -1 Invalid module format

Более подробная информация будет помещена в файл /var/log/messages:

Jun 4 22:07:54 localhost kernel: your_module_name: version magic '2.6.5-1.358custom 686 REGPARM 4KSTACKS gcc-3.3' should be '2.6.5-1.358 686 REGPARM 4KSTACKS gcc-3.3'

Другими словами -- ваше ядро отказывается "принимать" ваш модуль из-за несоответствия версий (точнее -- из-за несоответствия сигнатур версий). Сигнатура версии сохраняется в объектном файле в виде статической строки, начинающейся со слова vermagic:. Эта строка вставляется во время компоновки модуля с файлом init/vermagic.o. Просмотреть сигнатуру версии (так же как и некоторые дополнительные сведения) можно посредством команды modinfo module.ko:

[root@pcsenonsrv 02-HelloWorld]# modinfo hello-4.ko license: GPL author: Peter Jay Salzman <p@dirac.org> description: A sample driver vermagic: 2.6.5-1.358 686 REGPARM 4KSTACKS gcc-3.3 depends:

Для преодоления этого препятствия можно воспользоваться ключом --force-vermagic


(команды modprobe, прим. перев.), но это решение потенциально опасно и совершенно неприменимо при распространении готовых модулей. Следовательно, вам придется пересобрать модуль в окружении идентичном тому, в котором было собрано целевое ядро. Вопрос: "Как это сделать?" и является темой для дальнейшего обсуждения в данной главе.

Прежде всего вам необходимо установить дерево с исходными текстами ядра той же версии, что и целевое ядро. Найдите файл конфигурации целевого ядра, как правило он располагается в каталоге /boot, под именем, что-то вроде config-2.6.x. Просто скопируйте его в каталог с исходными текстами ядра на своей машине.

Вернемся к сообщению об ошибке, которое было приведено выше, и еще раз внимательно прочитаем его. Как видите, версия ядра практически та же самая, но даже небольшого отличия хватило, чтобы ядро отказалось загружать модуль. Все различие заключается лишь в наличии слова custom в сигнатуре версии модуля. Теперь откройте Makefile ядра и удостоверьтесь, что информация о версии в точности соответствует целевому ядру. Например, в данном конкретном случае Makefile должен содержать строки

VERSION = 2 PATCHLEVEL = 6 SUBLEVEL = 5 EXTRAVERSION = -1.358custom ...

Теперь запустите make, чтобы обновить информацию о версии:

[root@pcsenonsrv linux-2.6.x]# make CHK include/linux/version.h UPD include/linux/version.h SYMLINK include/asm -> include/asm-i386 SPLIT include/linux/autoconf.h -> include/config/* HOSTCC scripts/basic/fixdep HOSTCC scripts/basic/split-include HOSTCC scripts/basic/docproc HOSTCC scripts/conmakehash HOSTCC scripts/kallsyms CC scripts/empty.o ...

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


Создание модулей для работы с разными версиями ядра


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

Версии ядра подразделяются на стабильные (n.<четное_число>.m) и нестабильные (n.<нечетное_число>.m). Нестабильные версии несут в себе самые новые наработки, включая те, которые будут считаться ошибкой и те, которые претерпят существенные изменения в следующей версии. В результате, вы не можете доверять тому или иному интерфейсу, поскольку он может еще измениться (по этой причине я не посчитал нужным описывать их в этой книге -- слишком много работы, к тому же изменения происходят слишком быстро). От стабильных версий мы можем ожидать, что интерфейсы останутся неизменными, независимо от версии релиза (последнее число в номере версии -- m).

Итак, мы уже поняли, что между разными версиями ядра могут существовать весьма существенные отличия. Если у вас появится необходимость в создании модуля, который мог бы работать с разными версиями ядра, то можете воспользоваться директивами условной компиляции, основываясь на сравнении макроопределений LINUX_VERSION_CODE и KERNEL_VERSION. Для версии a.b.c, макрос KERNEL_VERSION вернет код версии, вычисленный в соответствии с выражением: 2^{16}a+2^{8}b+c. Макрос LINUX_VERSION_CODE возвращает текущую версию ядра.

В предыдущих версиях данного руководства, довольно подробно описывалось, как писать обратно совместимый код, с использованием директив условной компиляции. Но, начиная с этой версии, мы решили порвать с устоявшейся традицией. Теперь, если вы желаете писать модули под определенные версии ядра, обращайтесь к соответствующей версии руководства (LKMPG). Мы решили выпускать этот документ под версиями (номер версии и номер подверсии), совпадающими с версиями обсуждаемого ядра. Таким образом, разработчики, работающие под ядро 2.4.x, должны обращаться к LKMPG версии 2.4.x, работающие под ядро 2.6.x -- к LKMPG версии 2.6.x и т.д.



Старший и младший номер устройства


Давайте взглянем на некоторые файлы устройств. Ниже перечислены те из них, которые представляют первые три раздела на первичном жестком диске:

# ls -l /dev/hda[1-3] brw-rw---- 1 root disk 3, 1 Jul 5 2000 /dev/hda1 brw-rw---- 1 root disk 3, 2 Jul 5 2000 /dev/hda2 brw-rw---- 1 root disk 3, 3 Jul 5 2000 /dev/hda3

Обратили внимание на столбец с числами, разделенными запятой? Первое число называют "Старшим номером" устройства. Второе -- "Младшим номером". Старший номер говорит о том, какой драйвер используется для обслуживания аппаратного обеспечения. Каждый драйвер имеет свой уникальный старший номер. Все файлы устройств с одинаковым старшим номером управляются одним и тем же драйвером. Все из выше перечисленных файлов устройств имеют старший номер, равный 3, потому что все они управляются одним и тем же драйвером.

Младший номер используется драйвером, для различения аппаратных средств, которыми он управляет. Возвращаясь к примеру выше, заметим, что хотя все три устройства обслуживаются одним и тем же драйвером, тем не менее каждое из них имеет уникальный младший номер, поэтому драйвер "видит" их как различные аппаратные устройства.

Устройства подразделяются на две большие группы -- блочные и символьные. Основное различие блочных и символьных устройств состоит в том, что обмен данными с блочным устройством производится порциями байт -- блоками. Они имеют внутренний буфер, благодаря чему повышается скорость обмена. В большинстве Unix-систем размер одного блока равен 1 килобайту или другому числу, являющемуся степенью числа 2. Символьные же устройства -- это лишь каналы передачи информации, по которым данные следуют последовательно, байт за байтом. Большинство устройств относятся к классу символьных, поскольку они не ограничены размером блока и не нуждаются в буферизации. Если первый символ в списке, полученном командой ls-l /dev, 'b', тогда это блочное устройство, если 'c', тогда -- символьное. Устройства, которые были приведены в примере выше -- блочные. Ниже приводится список некоторых символьных устройств (последовательные порты):


crw-rw---- 1 root dial 4, 64 Feb 18 23:34 /dev/ttyS0 crw-r----- 1 root dial 4, 65 Nov 17 10:26 /dev/ttyS1 crw-rw---- 1 root dial 4, 66 Jul 5 2000 /dev/ttyS2 crw-rw---- 1 root dial 4, 67 Jul 5 2000 /dev/ttyS3

Если вам интересно узнать, как назначаются старшие номера устройств, загляните в файл /usr/src/linux/documentation/devices.txt.

Все файлы устройств создаются в процессе установки системы с помощью утилиты mknod. Чтобы создать новое устройство, например с именем "coffee", со старшим номером 12 и младшим номером 2, нужно выполнить команду mknod /dev/coffee c 12 2. Вас никто не обязывает размещать файлы устройств в каталоге /dev, тем не менее, делается это в соответствии с принятыми соглашениями. Однако, при разработке драйвера устройства, на период отладки, размещать файл устройства в своем домашнем каталоге -- наверное не такая уж и плохая идея. Единственное -- не забудьте исправить место для размещения файла устройства после того, как отладка будет закончена.

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

Между прочим, когда я говорю "устройства", я подразумеваю нечто более абстрактное чем, скажем, PCI плата, которую вы можете подержать в руке. Взгляните на эти два файла устройств:

% ls -l /dev/fd0 /dev/fd0u1680 brwxrwxrwx 1 root floppy 2, 0 Jul 5 2000 /dev/fd0 brw-rw---- 1 root floppy 2, 44 Jul 5 2000 /dev/fd0u1680

К настоящему моменту вы можете сказать об этих файлах устройств, что оба они - блочные устройства, что обслуживаются одним и тем же драйвером (старший номер 2). Вы можете даже заявить, что они оба представляют ваш дисковод для гибких дисков, несмотря на то, что у вас стоит только один дисковод. Но почему два файла? А дело вот в чем, один из них представляет дисковод для дискет, емкостью 1.44 Мб. Другой -- тот же самый дисковод, но для дискет емкостью 1.68 Мб, и соответствует тому, что некоторые люди называют "суперотформатированным" диском ("superformatted" disk). Такие дискеты могут хранить больший объем данных, чем стандартно-отформатированная дискета. Вот тот случай, когда два файла устройства, с различным младшими номерами, фактически представляют одно и то же физическое устройство. Так что, слово "устройство", в нашем обсуждении, может означать нечто более абстрактное.


Структура file


Каждое устройство представлено в ядре структурой file, которая определена в файле linux/fs.h. Эта структура используется исключительно ядром и никогда не используется прикладными программами, работающими в пространстве пользователя. Это совершенно не то же самое, что и FILE, определяемое библиотекой glibc и которое в свою очередь в ядре нигде не используется. Имя структуры может ввести в заблуждение, поскольку она представляет абстракцию открытого файла, а не файла на диске, который представляет структура inode.

Как правило указатель на структуру file называют filp.

Загляните в заголовочный файл и посмотрите определение структуры file. Большинство имеющихся полей структуры, например struct dentry *f_dentry, не используются драйверами устройств, и вы можете игнорировать их. Драйверы не заполняют структуру file непосредственно, они только используют структуры, содержащиеся в ней.



Структура file_operations


Структура file_operations определена в файле linux/fs.h и содержит указатели на функции драйвера, которые отвечают за выполнение различных операций с устройством. Например, практически любой драйвер символьного устройства реализует функцию чтения данных из устройства. Адрес этой функции, среди всего прочего, хранится в структуре file_operations. Ниже приводится определение структуры, взятое из исходных текстов ядра 2.6.5:

struct file_operations { struct module *owner; loff_t(*llseek) (struct file *, loff_t, int); ssize_t(*read) (struct file *, char __user *, size_t, loff_t *); ssize_t(*aio_read) (struct kiocb *, char __user *, size_t, loff_t); ssize_t(*write) (struct file *, const char __user *, size_t, loff_t *); ssize_t(*aio_write) (struct kiocb *, const char __user *, size_t, loff_t); int (*readdir) (struct file *, void *, filldir_t); unsigned int (*poll) (struct file *, struct poll_table_struct *); int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); int (*mmap) (struct file *, struct vm_area_struct *); int (*open) (struct inode *, struct file *); int (*flush) (struct file *); int (*release) (struct inode *, struct file *); int (*fsync) (struct file *, struct dentry *, int datasync); int (*aio_fsync) (struct kiocb *, int datasync); int (*fasync) (int, struct file *, int); int (*lock) (struct file *, int, struct file_lock *); ssize_t(*readv) (struct file *, const struct iovec *, unsigned long, loff_t *); ssize_t(*writev) (struct file *, const struct iovec *, unsigned long, loff_t *); ssize_t(*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void __user *); ssize_t(*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); unsigned long (*get_unmapped_area) (struct file *, unsigned long, unsigned long, unsigned long, unsigned long); };

Драйвер зачастую реализует далеко не все функции, предусмотренные данной структурой. Например, драйвер, который обслуживает видеоплату, не обязан выполнять операцию чтения каталога (readdir). Поля структуры, соответствующие нереализованным функциям, заполняются "пустыми" указателями -- NULL.


Компилятор gcc

предоставляет программисту довольно удобный способ заполнения полей структуры в исходном тексте. Поэтому, если вы встретите подобный прием в современных драйверах, пусть это вас не удивляет. Ниже приводится пример подобного заполнения:

struct file_operations fops = { read: device_read, write: device_write, open: device_open, release: device_release };

Однако, существует еще один способ заполнения структур, который описывается стандартом C99. Причем этот способ более предпочтителен. gcc 2.95, который я использую, поддерживает синтаксис C99. Вам так же следует придерживаться этого синтаксиса, если вы желаете обеспечить переносимость своему драйверу:

struct file_operations fops = { .read = device_read, .write = device_write, .open = device_open, .release = device_release };

На мой взгляд все выглядит достаточно понятным. И еще, вы должны знать, что в любое поле структуры, которое вы явно не инициализируете, компилятор gcc запишет "пустой" указатель -- NULL. Указатель на struct file_operations обычно именуют как fops.


The Linux Kernel Module Programming Guide (Руководство по программированию модулей ядра Linux)


Peter Jay Salzman, Michael Burian, Ori Pomerantz

Copyright 2001, Peter Jay Salzman.

2004-05-16 ver 2.6.0

Перевод: Андрей Киселёв (kis_an [at] linuxgazette [dot] ru), www.linuxcenter.ru

Руководство посвящено написанию модулей ядра для Linux 2.6. Рассматриваются такие вопросы, как взаимодействие с пользовательскими процессами через файлы устройств и файловую систему /proc, а также реализация новых системных вызовов. Текст богато проиллюстрирован примерами.

Оригинальная версия была опубликована на сайте проекта .

Данная книга распространяется на условиях Open Software License, version 1.1. Полный текст лицензии вы сможете найти по адресу .

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

Авторы приветствуют широкое распространение этой книги как для персонального, так и для коммерческого пользования, при условии соблюдения вышеупомянутого примечания относительно авторских прав, а так же при условии, что распространитель твердо придерживается условий Open Software License. Вы можете копировать и распространять эту книгу как бесплатно, так и с целью получения прибыли. От авторов не требуется никакого явного разрешения для воспроизводства этой книги на любом носителе, будь то твердая копия или электронная.

Производные работы и переводы этого документа должны размещаться на условиях Open Software License, а первоначальное примечание об авторских правах должно остаться нетронутым. Если вы добавили новый материал в эту книгу, то вам следует сделать его общедоступным. Пожалуйста извещайте руководителя проекта (Peter Jay Salzman >) о внесенных изменениях и дополнениях. Он объединит модификации и обеспечит непротиворечивость изменений документа.

Если Вы планируете издавать и распространять эту книгу на коммерческой основе, пожертвования, лицензионные отчисления и/или печатные копии будут высоко оценены автором и . Таким образом вы окажете поддержку свободному программному обеспечению и LDP. Если у вас появятся вопросы или предложения, пожалуйста пишите руководителю проекта по адресу, указанному выше.

Содержание


Управление индикаторами на клавиатуре


При определенных условиях у вас может возникнуть желание дать вашему модулю более простой и более прямолинейный способ взаимодействия с внешним миром. Изменение состояния светодиодных индикаторов клавиатуры может быть одним из вариантов привлечения внимания пользователя или отображения некоторого состояния. Светодиодные индикаторы присутствуют на любой клавиатуре, они всегда находятся в поле зрения, они не нуждаются в установке, и их "подмаргивание" достаточно ненавязчиво, по сравнению с выводом на tty или в файл.

Следующий исходный код иллюстрирует модуль ядра, который после загрузки начинает мигать индикаторами клавиатуры.

Пример 9-2. kbleds.c

/* * kbleds.c - Мигание индикаторами на клавиатуре. */

#include <linux/module.h> #include <linux/config.h> #include <linux/init.h> #include <linux/tty.h> /* определение fg_console, MAX_NR_CONSOLES */ #include <linux/kd.h> /* определение KDSETLED */ #include <linux/console_struct.h> /* определение vc_cons */

MODULE_DESCRIPTION("Пример module illustrating the use of Keyboard LEDs."); MODULE_AUTHOR("Daniele Paolo Scarpazza"); MODULE_LICENSE("GPL");

struct timer_list my_timer; struct tty_driver *my_driver; char kbledstatus = 0;

#define BLINK_DELAY HZ/5 #define ALL_LEDS_ON 0x07 #define RESTORE_LEDS 0xFF

/* * Функция my_timer_func мигает индикаторами * на клавиатуре периодически вызывая * ioctl() драйвера клавиатуры с командой KDSETLED. * Дополнительную информацию, * по командам ioctl виртуального терминала, вы найдете в: * /usr/src/linux/drivers/char/vt_ioctl.c, function vt_ioctl(). * * Дополнительный аргумент команды KDSETLED -- значение 7 * (перевод в режим LED_SHOW_IOCTL -- управление * индикаторами через ioctl), значение 0xFF -- * (любое значение, большее 7, перевод в режим * LED_SHOW_FLAGS -- * отображение фактического состояния клавиатуры). * Дополнительная информация: * /usr/src/linux/drivers/char/keyboard.c, * function setledstate(). * */


static void my_timer_func(unsigned long ptr) { int *pstatus = (int *)ptr;

if (*pstatus == ALL_LEDS_ON) *pstatus = RESTORE_LEDS; else *pstatus = ALL_LEDS_ON;

(my_driver->ioctl) (vc_cons[fg_console].d->vc_tty, NULL, KDSETLED, *pstatus);

my_timer.expires = jiffies + BLINK_DELAY; add_timer(&my_timer); }

static int __init kbleds_init(void) { int i;

printk(KERN_INFO "kbleds: loading\n"); printk(KERN_INFO "kbleds: fgconsole is %x\n", fg_console); for (i = 0; i < MAX_NR_CONSOLES; i++) { if (!vc_cons[i].d) break; printk(KERN_INFO "poet_atkm: console[%i/%i] #%i, tty %lx\n", i, MAX_NR_CONSOLES, vc_cons[i].d->vc_num, (unsigned long)vc_cons[i].d->vc_tty); } printk(KERN_INFO "kbleds: finished scanning consoles\n");

my_driver = vc_cons[fg_console].d->vc_tty->driver; printk(KERN_INFO "kbleds: tty driver magic %x\n", my_driver->magic);

/* * Инициировать таймер */ init_timer(&my_timer); my_timer.function = my_timer_func; my_timer.data = (unsigned long)&kbledstatus; my_timer.expires = jiffies + BLINK_DELAY; add_timer(&my_timer);

return 0; }

static void __exit kbleds_cleanup(void) { printk(KERN_INFO "kbleds: unloading...\n"); del_timer(&my_timer); (my_driver->ioctl) (vc_cons[fg_console].d->vc_tty, NULL, KDSETLED, RESTORE_LEDS); }

module_init(kbleds_init); module_exit(kbleds_cleanup);

Если ни один из примеров данной главы вас не устраивает, можно попробовать другие хитрости, скрытые в ядре. Может быть вам подойдет опция CONFIG_LL_DEBUG в make menuconfig? Включив ее, вы получите низкоуровневый доступ к последовательному порту. Как бы страшно это ни прозвучало, но можете попробовать изменить реализацию kernel/printk.c или какого нибудь другого системного вызова для вывода ascii-строк, чтобы иметь возможность отслеживать действия вашего модуля через последовательную линию связи.

Несмотря на то, что вы уже встретили в этой книге намало наглядных приемов отладки, существует еще ряд моментов, которые вам необходимо знать. Отладка -- это всегда очень утомительный процесс и практически всегда он сопровождается внедрением значительного количества отладочного кода. Может сложиться так, что отладочный код не дает проявляться некоторым ошибкам. Поэтому, при выпуске вашего модуля, старайтесь свести отладочный код к минимуму и "прогнать" модуль еще раз, пытаясь обнаружить какие либо ошибки.


Замена printk


Ранее я уже говорил о том, что XWindow и разработка модулей ядра есть вещи несовместимые. Это все так, но иногда возникает необходимость выдачи сообщений от модуля на любой tty. [13]

В качестве одного из вариантов можно предложить следующее: используя указатель на текущий исполняемый процесс -- current, получить структуру tty. Затем извлечь из этой структуры указатель на функцию вывода строки и использовать ее для выдачи сообщений.

Пример 9-1. print_string.c

/* * print_string.c - отправляет вывод на * tty терминала, независимо от того * X11 это, или telnet, или что-то еще. * Делается это путем вывода строки на tty, * ассоциированный с текущим процессом. */ #include <linux/kernel.h> #include <linux/module.h> #include <linux/init.h> #include <linux/sched.h> /* определение current */ #include <linux/tty.h> /* определение tty */ #include <linux/version.h> /* макрос LINUX_VERSION_CODE */

MODULE_LICENSE("GPL"); MODULE_AUTHOR("Peter Jay Salzman");

static void print_string(char *str) { struct tty_struct *my_tty;

/* * Начиная с версии 2.6.6, структура tty перекочевала в структуру signal */ #if ( LINUX_VERSION_CODE <= KERNEL_VERSION(2,6,5) ) /* * tty текущего процесса */ my_tty = current->tty; #else /* * tty текущего процесса, для ядер 2.6.6 и выше */ my_tty = current->signal->tty; #endif

/* * если my_tty == NULL, то текущий процесс не имеет tty на который можно * было бы что нибудь вывести (например, демон). * В этом случае нам ничего не нужно делать. */ if (my_tty != NULL) {

/* * my_tty->driver -- структура, которая * хранит указатели на функции-обработчики, * одна из которых (write) используется * для вывода строк на tty. * * Первый параметр функции -- tty, на * который осуществляется вывод, * поскольку эта функция обычно * используется для вывода на все * tty одного и того же типа. * Второй параметр -- флаг расположения строки * если строка находится в пространстве ядра, * флаг равен false (0) * если в пространстве пользователя, то true (не ноль). * Третий параметр -- указатель на строку. * Четвертый параметр -- длина строки. */ ((my_tty->driver)->write) (my_tty, /* Собственно tty */ 0, /* Строка в пространстве ядра */ str, /* Сама строка */ strlen(str)); /* Длина строки */


/* * tty изначально был аппаратным устройством, который (обычно) * ограничивался стандартом ASCII, в котором перевод строки * включал в себя два символа -- "возврат каретки" и "перевод строки". * В Unix, символ ASCII -- "перевод строки" заменял оба этих символа, * поэтому нам придется использовать для перевода строки * оба символа. * * Это одна из причин различий между текстовыми файлами Unix и * MS Windows. CP/M и ее "наследницы", например MS-DOS и * MS Windows, строго придерживались стандарта ASCII. */ ((my_tty->driver)->write) (my_tty, 0, "\015\012", 2); } }

static int __init print_string_init(void) { print_string("The module has been inserted. Hello world!"); return 0; }

static void __exit print_string_exit(void) { print_string("The module has been removed. Farewell world!"); }

module_init(print_string_init); module_exit(print_string_exit);


Знакомство с printk()


Несмотря на столь красноречивое название, функция printk() вовсе не предназначена для вывода информации на экран, даже не смотря на то, что мы использовали ее в своем примере именно для этой цели! Основное назначение этой функции -- дать ядру механизм регистрации событий и предупреждений. Поэтому, каждый вызов printk() сопровождается указанием приоритета, в нашем примере это <1> и KERN_ALERT. Всего в ядре определено 8 различных уровней приоритета для функции printk() и каждый из них имеет свое макроопределение, таким образом нет необходимости писать числа, лишенные смысла (имена уровней приоритета и их числовые значения вы найдете в файле linux/kernel.h). Если уровень приоритета не указывается, то по-умолчанию он принимается равным DEFAULT_MESSAGE_LOGLEVEL.

Найдите время и просмотрите содержимое этого файла. Здесь вы найдете краткое описание значения каждого из уровней. На практике считается дурным тоном указание уровней приоритета числовым значением, например так: <4>. Для этих целей лучше пользоваться именами макроопределений, например: KERN_WARNING.

Если задан уровень ниже, чем int console_loglevel, то сообщение выводится на экран. Если запущены и syslog, и klogd, то сообщение попадет также и в системный журнал /var/log/messages, при этом оно может быть выведено на экран, а может и не выводиться. Мы использовали достаточно высокий уровень приоритета KERN_ALERT для того, чтобы гарантировать вывод сообщения на экран функцией printk(). Когда вы вплотную займетесь созданием модулей ядра, вы будете использовать уровни приоритета наиболее подходящие под конкретную ситуацию.



Mandriva Linux 2006 Story


Dr. Linux,

Многие из нас начинали свое первое знакомство с компьютером с самой известной операционной системы - с MS Windows. Только потом уже (где-то с 1997 года) пользователи узнали о других ОС. И, хотя, с одной стороны рынок ОС достаточно разнообразен, он очень консервативен с другой (десктопной) стороны. Только этим можно объяснить то, что при всем разнообразии вариантов систем люди в основном пользуются только одной из них. Новой операционной системе завоевать популярность не то чтобы сложно, скорее практически невозможно.

Однако одной из новых ОС все же удалось пробиться на рынок и завоевать популярность. Прецедент состоялся. Этой операционной системой оказалась Linux. Знакомство, установку и использование Linux обычно начинают с выбора его версии или дистрибутива.

Из всех существующих на сегодня Linux дистрибутивов самый простой и известный пользователям - это Mandrake. Недавно после слияния этой фирмы с Connectiva название сменилось на Mandriva. Дистрибутив распространяется в двух основных версиях: обычной, доступной для свободного скачивания и тиражирования, и "клубной", в укомплектованном виде предназначенный для членов Mandriva Club, и включающий дополнительный софт, в том числе и платный.

Но самое интересное, какие кардинальные изменения произошли в самом дистрибутиве, кроме смены названия и слияния\приобретения фирм? На этот вопрос уже сейчас можно ответить - наконец-то вышел долгожданный полный выпуск дистрибутива после слияния Mandrakesoft c Connectiva и приобретения компании Lycoris: Mandriva Linux 2006 сборки Санкт-Петербургского интернет-магазина Linux Center на основе пакетов с официального FTP-сервера компании Mandriva и пакетов из репозитария Penguin Liberation Front.

К покупке этого дистрибутива подвела необходимость перехода со всем известной ОС на одну из версий Linux. Я устанавливал достаточно много дистрибутивов, но ни на одном из них не смог остановиться. Близким к идеалу был Mandrake, но мелкие (или не очень) проблемы мешали переходу на его использование. Упорно не хотел работать ТВ-тюнер Aver Media AverTV 305 Studio, не ставился драйвер для видеокарты, плохо читались CD-ROM диски виндовой записи+ В принципе - это проблемы не дистрибутива, а моих кривых /dev/hands, но от осознания этого легче не становилось. Хотелось Linux, а работать с ним никак не получалось. Хотелось простоты и настроенности.


Поэтому я придумал для себя стандарты для выбора дистрибутива, т.к. мне нужен был непременно мультимедиа дистрибутив:

Ядро Linux 2.6.9.X, X.org 6.8.0

Файловая система - ReiserFS или производная журналируемая файловая система.

Единый менеджер устройств с поддержкой Plug' n Play и USB.

Менеджер сервиса, обслуживания и настройки (центр управления компьютером).

Единая инсталляционная программа.

Расширенная поддержка Transgaming WineX, а также системы портирования wine.

Красивый и удобный графический интерфейс.

Доработка программного обеспечения (только стабильные версии)

Концепция: удобный и эффективный инсталлятор.

Кросплатформенность, не требовательность к ресурсам.

Поддержка всех новейших технологий.

Использование гибкой системы обновления.

Возможность установки и использования с экзотическим оборудованием.

Поддержка win32 API (под win95/98). Качественная эмуляция.

POSIX совместимость.

User Friendly!

Широкий выбор мультимедийного набора программного обеспечения.

С этими представлениями об идеальном дистрибутиве я отправился на . На форуме посоветовали попробовать Mandriva, там должно было быть все проще. Сказано - сделано, отправился я на , там как раз только появилась Mandriva Linux 2006, заказал - осталось только ждать. Аккурат через две недели на почту пришла бандероль из Питера с Mandriva. Внутри бумажного свертка оказалась коробка с DVD диском. Осталось проверить, насколько этот дистрибутив подходит к моим стандартам мутимедийной ОС. Знакомство с дистрибутивами обычно начинается с установки, этот тоже не стал исключением. Красивая и удобная программа установки, а также ее простота сразу развенчивает миф о недружелюбности Linux. Установка прошла быстро и успешно. Пакеты на этапе установки можно выбирать по-разному - от полного доверия установщику до индивидуального выбора. Понравилось, что при выборе того или иного пакета система предупреждает, что потянет за собой еще что-то.

Все оборудование было найдено быстро и безошибочно. Виндовые (FAT и NTFS) разделы были подхвачены и смонтированы как /mnt/win_c и т.д., в Mandriva используется наряду с udev и devfs. CD - автоматически монтируется/демонтируется, когда вставляешь/вынимаешь диск. Программа установщик практически все сделала за меня, кроме создания разделов (заранее создал их сам, т.к. по опыту не доверяю DiskDrake).



После перезагрузки, Linux довольно резво загрузился (после отключения почти всех служб - стал еще быстрее). После установки система действительно полностью готова к работе. Первое что бросилось в глаза - необыкновенной красоты интерфейс, который нисколько не тормозил. В этой версии KDE стал еще лучше, еще быстрее, еще удобнее. В домашнем каталоге - появились папки: Музыка, Документы, Изображения, с соответствующим оформлением. Заметно, что русификация выполнена качественно, хотя присутствуют мелкие огрехи (например, в OpenOffice) и нет русификатора для Mozilla Thungerbird (лично меня этот факт не расстроил, т.к. пользуюсь Evolution).

Программное обеспечение удивляет правильным подбором - все, что нужно и на одном DVD. Основные компоненты дистрибутива достаточно свежие: мультимедийное ядро версии 2.6.12, компилятор GCC 4.0, X-сервер: X.Org 6.9, KDE 3.4, Gnome 2.10. Порадовал набор программного обеспечения дистрибутива: Open Office 1.1.5, Kolab2, Scribus, Firefox, Evolution, XMMS, Xine, SIM, Skype и даже набор игр (одна из самых интересных - вариация на тему Tuxracer, другая - что-то похожее на Mario, только вместо смешного человечка, не менее забавный пингвин - supertux). Звук определился сразу. CD-ROM диски - читались, DVD фильмы - воспроизводились. В дистрибутиве были по-умолчанию установлены mp3, wma и другие кодеки (кроме win-32). Кроме того, в состав дистрибутива входят плееры: Kaffeine и Amarok. Офис работает довольно шустро, все документы - открывает корректно (у меня не имелось в наличии документов со сложным форматированием и макросами). Еще одно новшество - это по умолчанию устанавливаемый в Mandriva антивирус KlamAV Anti-Virus Manager. А в состав автоматически загружаемых служб входит служба для обновления баз этого антивируса. В общем и целом повышение безопасности системы - налицо.

Если говорить о настройке дистрибутива, то обязательно следует упомянуть менеджер устройств, который появился еще в Mandrake - это HardDrake. В его новой версии он даже указывает некоторые возможные параметры настройки модулей-драйверов, отвечающих за работу железа. Например, он указал параметры моего ТВ-тюнера для: card, tuner и gbuffers, что существенно облегчило настройку девайса. Отдельных слов заслуживает Центр Управления Mandriva, который является ничем иным как DrakConf. Нельзя сказать, чтобы он справлялся на отлично со своими функциями, потому что очень часто требуется дополнительная ручная настройка параметров, а некоторые вещи он просто не в состоянии настроить, например, настроить с его помощью файервол и ТВ-тюнер не получилось (есть сведения и о невозможности корректной настройки bluetooth), зато с его помощью я смог доустановить пакеты с дистрибутивного диска, отключить неиспользуемые службы, перекинуть шрифты и др.



Тюнер, конечно, пришлось настраивать вручную, но впервые он у меня заработал! Все было достаточно просто и интуитивно понятно. С помощью tvtime я смог, наконец, смотреть телепрограммы (нашлись все!). Тут же заметил отличный звук, чистый и приятный (не было привычных шорохов и дребезжаний на высоких тонах). Действительно, слушать под Linux Audio CD - одно удовольствие (все дело в новых драйверах ALSA). Для записи видео можно использовать Mplayer - красота, да и только. Даже мой сотовый телефон - и тот был распознан в Linux в качестве gprs-модема. Локальный поисковик Kat, который впервые появился в Mandriva, заслуживает отдельного разговора. Сам по себе - обычный поисковик, но отечественному пользователю он, похоже, ни к чему, т.к. русский язык для него не родной (хотя в опциях его можно выбрать), поэтому он плохо ищет русскоязычные документы. Запись\воспроизведение дисков - тоже прошло успешно, как, впрочем, и сграббливание Audio CD, т.к. в дистрибутиве все параметры сразу были определены и корректно настроены.

Установка дополнительных программ, не вошедших в дистрибутив, из Сети тоже не составит труда. Подключить дополнительные репозитарии для установки и обновления программ можно, например, отсюда: . Для установки дополнительного ПО достаточно будет выбрать версию дистрибутива (Mandriva Linux 2006 Free), нужные источники, вид списка пакетов, и, наконец, ввести команды от лица суперпользователя, которые появятся в списке. Подробнее об этом можно прочитать в HOWTO, написанном Valerius: . Более - менее полная документация на русском языке по Mandriva находится здесь: . Хотя, практически во всех книгах о Linux очень часто помещают (кроме описания других дистрибутивов) информацию и про Mandrake/Mandriva, кроме того, много информации по этой теме можно найти с помощью: .

В общем и целом, Mandriva Linux 2006 - отличный мультимедиа дистрибутив. Таким образом, теперь мы имеем полноценную систему, как настоящую альтернативу платным ОС, при помощи которой можно делать все, что необходимо домашнему пользователю. Именно такой дистрибутив удобнее всего для установки на домашнем компьютере. В его пользу говорит и настроенность, и подборка разнообразного, по преимуществу офисно-мультимедийного программного обеспечения, и поддержка всего мыслимого оборудования, которое может использоваться на домашнем компьютере. Можно сказать - мультимедийный центр, основанный на Linux.



Теперь о грустном. Очень огорчила стабильность, вернее жуткая нестабильность в этом дистрибутиве KDE. Он все время сыпался по поводу и без повода. Хотя, возможно, это проявляется только у меня из-за проблем с железом (у этого дистрибутива случаются проблемы с железом, например, с материнскими платами от ECS), может первый раз, не совсем корректно произошла установка дистрибутива. Но факт остается фактом. Может быть это единичный случай (сейчас как будто все нормально). Это единственный существенный минус.

А в остальном, отличный User Friendly дистрибутив, который подошел мне почти по всем параметрам. Наверное, если переходить полностью с WinXP на Linux, то проще всего перейти именно на Mandriva Linux 2006 Linux Center Edition. Для начинающего пользователя Linux это самый лучший выбор. Так что если вы решились перейти на Linux - это самый комфортный способ перехода, без лишней суеты и проблем.

2005.12.09


MEPIS, или Linux под сенью пирамид


Солдаты! Сорок веков

смотрят на вас с вершин

этих пирамид.

Наполеоне Буонопарте

Популярность семейства дистрибутивов Ubuntu, о которых столько говорят и пишут в последнее время (в частности, немало материалов на эту тему можно найти ), несколько затмила многочисленные клоны Debian, также декларирующие свою нестандартную ориентацию - то есть повышенную любовь к конечному пользователю. Одним из таких клонов является MEPIS (), рассмотрение особенностей которого и составит предмет настоящей заметки.

Дистрибутив MEPIS достаточно молод - решение о его создании было принято Уорреном Вудфордом в ноябре 2002 года, а первые релизы датируются уже годом 2003-м. По свидетельству разработчика, название его ничего не значит, хотя и может быть расшифровано как аббревиатура, например: Моя Экстраординарная Персональная Информационная Система. Однако символика - пирамиды на фоне восходящего солнца - вызывает отчетливые ассоциации с древнеегипетским городом Мемфисом, близ развалин которого генерал Бонапарт одержал победу над армией мамлюков. Не забыв предварительно сказать исторические слова, приведенные в качестве эпиграфа...

Собственно говоря, дистрибутива с названием MEPIS не существует. А имеют место быть, с одной стороны, SimplyMEPIS, доступный для свободного скачивания с ряда публичных серверов (список их: ), а с другой - несколько коммерческих вариантов, как-то: MEPISLite 3.3.2, Desktop OnTheGo, SoHoServer, распространяемых исключительно за деньги. В настоящей заметке речь, по понятным причинам, пойдет только о первом варианте.

Итак, SimplyMEPIS: в настоящее время он переживает последние стадии тестирования перед выходом 6-й версии. В частности, я имел дело с SimplyMEPIS_6.0-rc2_i386.iso - образом объемом 666 Мбайт. Каковой, кроме установочных целей, выполняет и цели демонстрационные, будучи полноценными LiveCD с кучей приложений.

После "сболванивания" дистрибутивного образа и старта с него на фоне все тех же пирамид появляется начальное меню - еще не инсталлятора, а загрузчика. В нем предусмотрены пункты:


Boot normal - загрузка в режиме, соответствующем возможностям видеокарты;

Boot VESA - загрузка в стандартном VESA-режиме ( на случай, если видеосистема определилась некорректно);

Boot minimal - для старых машины;

Boot small - для очень старых машин.

Кроме того, по нажатию функциональных клавиш можно вызвать помощь (F1), изменить язык системы (F2) - в списке, кроме английского, имеются немецкий, испанский, французский, итальянский, некий нидерландский (я всегда полагал, что язык Королевства Нидерланды называется голландским - Dutch), бразильский португальский и шведский, - и автоматически установить разрешение (F3).

При выборе нормальной загрузки (скорее всего, это подойдет в большинстве случаев) происходит загрузка ядра, затем вызывается kdm, требующий авторизации через один из двух аккаунтов - пользовательский (demo) и административный (root), с одноименными паролями. Очевидно, что для ознакомления с возможностями LiveCD скорее подойдет первый, а для установки - второй. Впрочем, и в demo-режиме можно начать установку - пароль root'а (то есть root же) будет запрошен.

После авторизации происходит загрузка KDE с автоматической коррекцией определения оборудования: именно на этом этапе выставляется правильное разрешение для графического режима. У меня это было прямо визуально видно, как начальное разрешение (1024x768) скачком превратилось в 1280x800 (физическое разрешение матрицы моего ноута). Не было проблем и со звуком, о чем мне радостно отрапортовали динамики. В итоге получился довольно обычный рабочий стол со все теми же пирамидами (рис. 1).



Рис. 1. Вид рабочего стола SimplyMEPIS после загрузки с LiveCD

Возможностями SimplyMEPIS как LiveCD я особенно не интересовался за неактуальностью (для меня). И потому сразу приступил к установке, щелкнув на соответствующей пиктограмме рабочего стола. И вот тут-то и запускается главное меню инсталлятора (рис. 2).



Рис. 2. Начало установки: главное меню

Ознакомившись, при желании, с уведомлениями о копирайтах и выбрав в левом фрейме пункт Install MEPIS on Hard Drive... Впрочем, сначала стоит подумать о настройке сети (пункт Network Interfaces). Дело в том, что сетевой интерфейс в MEPIS определяется и настраивается автоматически - но лишь в предположении, что подключение к сети осуществляется через DHCP-сервер. Если это не так - то при выборе указанного выше пункта и наличного интерфейса (например, eth0) можно будет видеть надпись об отсутствии коннекта. Что легко поправимо руками - если вы знаете такие параметры, как свой IP, адрес шлюза и DNS-серверов, а также маску подсети. Впрочем, если провайдер использует VPN-авторизацию (что, похоже, становится нормой не только для домотканых, но и для вполне городских подключателей) - и это не поможет, средств установки VPN-соединения в дистрибутиве нет.



Тем или иным образом разобравшись с сетью (решение этих проблем можно и отложить), возвращаемся к пункту Install MEPIS on Hard Drive и жмем на кнопку Next - и оказываемся перед выбором раздела. Если на диске уже имеется подходящий раздел - можно выбрать его, определив заодно и файловую систему (рис. 3) - ext3fs или ReiserFS, на выбор. Если же наличная разметка диска чем-либо не устраивает - то нажатием на кнопку QTParted (она появляется при выборе диска целиком) вызывается соответствующая программа, являющая собой графическую оболочку для универсальной утилиты управления дисковыми разделами - parted. Впрочем, использование и той, и другой далеко выходит за рамки настоящей заметки (об утилите parted можно прочитать ).



Рис. 3. Установка MEPIS, первый этап: разметка диска

Дальнейшие события происходят в один присест: разметка диска (если таковая заказывалась), форматирование разделов, развертывание корневой файловой системы и собственно инсталляция программного обеспечения (рис. 4).



Рис. 4. Установка MEPIS: процесс пошел

По завершении процесса установки (он показался мне не очень быстрым) предлагается определить место для системного загрузчика (а таковым безальтернативно выступает GRUB): в главной загрузочном секторе диска (MBR) или в загрузочном секторе раздела, несущего корневую (root) файловую систему новоинсталлированного MEPIS'а (рис. 5).



Рис. 5. Выбор места для расположения загрузчика

Первая позиция (то есть в MBR) единственно приемлема, если MEPIS - единственная же ОС на данной машине, предпочтительна - если MEPIS сосуществует с более иной ОС (догадайтесь - какой?), или если в дальнейшем планируется установка еще многих и многих операционок.

Единственная разумная причина для записи GRUB в загрузочный сектор корневого раздела MEPIS - в том, если ранее уже была установлена какая-либо из BSD-систем с их собственным загрузчиком BSD Loader, который только и умеет, что передать управление на BR какого-либо первичного раздела (впрочем, если у вас именно такая ситуация - особенности ее вы знаете не хуже меня). Потому что если MEPIS ставится в дополнение к какому-либо иному дистрибутиву Linux, у вас уже имеет место быть мультисистемный загрузчик (тот же GRUB или Lilo), который, после соответствующей редактуры, легко справится с загрузкой еще одного брата "по крови упругой и густой". И в этом случае резонно отказаться от установки загрузчика вообще, сняв отметку в боксе Install GRUB for MEPIS and Windows.



Следующий шаг - это создание аккаунта обычного пользователя (с заданием его логина и пароля) и определение пароля администратора (рис. 6), до отказа от последнего MEPIS, в отличие от Ubuntu, еще не дошел.



Рис. 6. Создание пользовательского аккаунта и определение пароля администратора

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



Рис. 7. Определение сетевых атрибутов

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



Рис. 8. С позволения сказать, "локализация"

Зато тут же можно отрегулировать системное время, правда, средствами KDE (рис. 9).



Рис. 9. Коррекция системного времени

После чего остается завершающий штрих: просмотреть список загружаемых по умолчанию системных сервисов (рис. 10) и, при желании, отключить ненужные или, напротив, подключить дополнительные.



Рис. 10. Список системных сервисов

И, наконец, финальная информация (рис. 11), по прочтении коей остается только нажать кнопку Finish для перезагрузки машины - правда, извлечением CD из привода придется озаботиться самому.



Рис. 11. Финальная информация

Что же мы видим после перезагрузки? А видим мы рабочий стол KDE (в который попадаем после авторизации через kdm), оснащенный неким набором приложений, как KDE-специфических, так и общего назначения. Интересно, что подборка первых - не вполне стандартна: роль аудио-плейера по умолчанию будет выполнять amaroK, прокрутка видео возлагается на Kaffeine, и так далее. А приложения общего характера - достаточно типичны: OOo как офисный пакет, Firefox как браузер...

А вот чего не увидим - так это доведенной до ума локализации, каковая сводится к возможности просмотра кириллических текстов; уже ввод русских символов потребует некоторой ручной правки, а уж о русскоязычных меню и говорить не приходится. Что и не удивительно, если вспомнить слова главного разработчика, Уоррена:



Это просто факт из жизни, что американские покупатели предпочитают покупать американские продукты.

А поскольку MEPIS совершенно явно ориентирован на пользователя из соплеменной Америки - остается только удивляться наличию поддержки всяких разных языков на стадии установки.

Интересно, что, не смотря на свою американскую ориентацию, MEPIS "из коробки" поддерживает воспроизводство мультимедийных файлов в закрытых или "патентованных" форматах (Real, MPEG) - а ведь именно Америка возглавляет список слаборазвитых стран, признающих патенты на алгоритмы. Как это решено разработчиками дистрибутива юридически - могу только гадать (и был бы признателен за разъяснения от профессионалов-юристов).

В общем, после установки MEPIS пользователь, не испытывающий идиосинкразии к KDE, получает вполне приемлемый набор приложений. Однако индивидуализация системы (в частности, окончательная ее кириллизация) неизбежно потребует доустановки кое-какого софта. И как обстоит дело с этим?

Выше уже говорилось о том, что MEPIS основывается на Debian. От которого наследует формат пакетов (deb) и весь комплекс средств для работы с ними - от dpkg до apt-tools и aptitude. То есть сама по себе установка пакетов ни малейшей сложности не представляет - такие утилиты, как apt-get и aptitude легко справятся с этой задачей, включая разрешение зависимостей. Дело остается только за репозиторием, из которого эти пакеты выуживать.

И такой репозиторий имеется - в первых же строках файла /etc/apt/sources.list можно видеть:

# MEPIS improvements, overrides and updates--the MEPIS magic deb http://apt.mepis.org/6.0/ mepis main

Правда, впечатления излишнего богатства пакетами этот репозиторий не производит. Однако он по умолчанию дополнен репозиториями дистрибутива Ubuntu - специфика клонов Debian такова, что они сохраняют практически полную бинарную совместимость как друг с другом, так и с материнской системой. Так что, при необходимости в какой-либо экзотической софтине, можно воспользоваться и собственно Debian'овским репозиторием - а уж в нем можно найти все, что душе угодно.

В общем, можно подвести итог: MEPIS являет собой дистрибутив, вполне пригодный для исполнения декларируемой задачи - обеспечения быстрого и безболезненного вхождения новичка в мир Linux. Правда, не в наших условиях: для окончательной локализации потребуются дополнительные пакеты и дополнительные усилия. Не очень значительные - типа корректировки раскладки клавиатуры, доустановки кириллических шрифтов, обеспечения средств проверки орфографии, - но все же...

Преимуществ у MEPIS над Ubuntu/Kubuntu лично я не увидел. Конечно, возможность воспроизведения мультимедийных файлов - безусловный плюс первого. Однако он компенсируется существенно худшей локализацией. Ну и сам факт обращения к репозиториям Ubuntu не позволяет считать MEPIS самодостаточным дистрибутивом.