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

Часть 38. Работа с UNIX-сигналами

Comments

Серия контента:

Этот контент является частью # из серии # статей: Нестандартные сценарии использования модулей ядра

Следите за выходом новых статей этой серии.

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

Следите за выходом новых статей этой серии.

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

Сигналы UNIX

Сигналы одновременно являются одним из «краеугольных камней» и характерной особенностью операционных систем семейства UNIX. Сигналы UNIX (именно такое полное название применяется для них в литературе) часто незаметны для конечного пользователя, но играют значительную роль внутри системы, так как без помощи сигналов мы не смогли бы завершить ни один процесс в системе (нажимая ^C или выполняя команды kill, killall). Как обычно, мы не будем углубляться в подробное изучение сигналов, но сосредоточимся на их совместном использовании с модулями ядра.

В Linux существует возможность отправки из ядра сигнала любому процессу пространства пользователя или любому другому потоку пространства ядра. В ядре Linux (начиная с версии 2.5) может существовать много независимых потоков выполнения, более того, ядро Linux является вытесняющим (preemptive): код ядра в состоянии вытеснить другие выполняющиеся задания, даже когда те работают в режиме ядра. Кстати, только очень немногие коммерческие реализации UNIX обладают вытесняющим ядром, например, Solaris или AIX.

Другими словами, речь идёт о том, что в пользовательском пространстве обычно выполняется командой kill (или вызовом kill() из программного кода), но теперь аналогичные действия можно выполнить и из кода модуля ядра. Возможность организации взаимодействия в ядре с помощью сигналов UNIX становится очевидной при внимательном изучении вызовов API ядра, которые переводят текущий поток выполнения в блокированное состояние. Все эти вызовы присутствуют в двух формах:

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

Нам остается только выяснить: как реализовать такую возможность? Для решения этой задачи подготовим тестовый проект из архива signal.tgz, который можно найти в разделе "Материалы для скачивания". Наш пример будет видоизменённой реализацией идеи из упоминавшейся книги "Writing Linux Device Drivers" (см. раздел "Ресурсы"). Этот объёмный тест состоит из трёх программных компонентов:

  • пользовательское приложение sigreq, регистрирующее получаемые сигналы;
  • модуль ядра ioctl_signal.ko, который будет отправлять указанный сигнал определенному пользовательскому процессу;
  • диалоговый пользовательский процесс ioctl, который указывает модулю ядра ioctl_signal.ko, какой именно сигнал отсылать (по номеру) и какому процессу (в нашем случае — это sigreq).

Процесс ioctl будет передавать команды для ioctl_signal.ko посредством вызовов ioctl(), которые уже рассматривались в статьях, посвящённых драйверам символьных устройств. Общие определения, необходимые для команд ioctl(), вынесены в отдельный файл ioctl.h, приведенный в листинге 1.

Листинг 1. Определения кодов команд ioctl().
#define MYIOC_TYPE 'k' 
#define MYIOC_SETPID  _IO(MYIOC_TYPE,1) //установить PID процесса, 
			которому будет отправлен сигнал
#define MYIOC_SETSIG  _IO(MYIOC_TYPE,2) //установить номер отсылаемого сигнала
#define MYIOC_SENDSIG _IO(MYIOC_TYPE,3) //отправить сигнал
#define SIGDEFAULT SIGKILL

В листинге 2 приведен код модуля ioctl_signal.ko, выполняющего отправку сигналов.

Листинг 2. Модуль, посылающий сигнал UNIX приложениям.
#include <linux/module.h> 
#include "ioctl.h" 
#include "lab_miscdev.h" 

static int sig_pid = 0; 
static struct task_struct *sig_tsk = NULL; 
static int sig_tosend = SIGDEFAULT; 

static inline long mycdrv_unlocked_ioctl( struct file *fp, 
                                          unsigned int cmd, unsigned long arg ) { 
   int retval; 
   switch( cmd ) { 
      case MYIOC_SETPID: 
         sig_pid = (int)arg; 
         printk( KERN_INFO "Setting pid to send signals to, sigpid = %d\n", 
                 sig_pid ); 
         sig_tsk = pid_task( find_vpid( sig_pid ), PIDTYPE_PID ); 
         break; 
      case MYIOC_SETSIG: 
         sig_tosend = (int)arg; 
         printk( KERN_INFO "Setting signal to send as: %d \n", 
                 sig_tosend ); 
         break; 
      case MYIOC_SENDSIG: 
         if( !sig_tsk ) { 
            printk( KERN_INFO "You haven't set the pid; using current\n" ); 
            sig_tsk = current; 
            sig_pid = (int)current->pid; 
         } 
         printk( KERN_INFO "Sending signal %d to process ID %d\n",
                 sig_tosend, sig_pid ); 
         retval = send_sig( sig_tosend, sig_tsk, 0 ); 
         printk( KERN_INFO "retval = %d\n", retval ); 
         break; 
      default: 
         printk( KERN_INFO " got invalid case, CMD=%d\n", cmd ); 
         return -EINVAL; 
   } 
   return 0; 
} 

static const struct file_operations mycdrv_fops = { 
   .owner = THIS_MODULE, 
   .unlocked_ioctl = mycdrv_unlocked_ioctl, 
   .open = mycdrv_generic_open, 
   .release = mycdrv_generic_release 
}; 

module_init( my_generic_init ); 
module_exit( my_generic_exit );

В этом файле присутствует интересующий нас обработчик функций ioctl(), а все остальные операции модуля (создание символьного устройства /dev/mycdrv, open(), и т.д.) вынесены во включаемый файл miscdev.h, общий для многих примеров и подробно изученный ранее при рассмотрении операций с символьным устройством.

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

#include <linux/sched.h> 
struct task_struct *find_task_by_vpid( pid_t nr );
#include <linux/pid.h> 
struct pid *find_vpid( int nr );
struct task_struct *pid_task( struct pid *pid, enum pid_type );
enum pid_type { 
   PIDTYPE_PID, 
   PIDTYPE_PGID, 
   PIDTYPE_SID, 
   PIDTYPE_MAX 
};

В листинге 3 приведено приложение, получающее информацию от пользователя и выполняющее серию вызовов ioctl() для взаимодействия с модулем: установка PID процесса, установка номера сигнала и отправка сигнала:

Листинг 3. Программа для управления модулем
#include <stdio.h> 
#include <stdlib.h> 
#include <fcntl.h> 
#include <sys/ioctl.h> 
#include <signal.h> 
#include "ioctl.h" 

static void sig_handler( int signo ) { 
   printf( "---> signal %d\n", signo ); 
} 

int main( int argc, char *argv[] ) { 
   int fd, rc; 
   unsigned long pid, sig; 
   char *nodename = "/dev/mycdrv"; 
   pid = getpid(); 
   sig = SIGDEFAULT; 
   if( argc > 1 ) sig = atoi( argv[ 1 ] ); 
   if( argc > 2 ) pid = atoi( argv[ 2 ] ); 
   if( argc > 3 ) nodename = argv[ 3 ]; 
   if( SIG_ERR == signal( sig, sig_handler ) ) 
      printf( "set signal handler error\n" ); 
   /* открытие устройства */ 
   fd = open( nodename, O_RDWR ); 
   printf( "I opened the device node, file descriptor = %d\n", fd ); 
   /* вызов IOCTL для установки PID процесса */ 
   rc = ioctl( fd, MYIOC_SETPID, pid ); 
   printf("rc from ioctl setting pid is = %d\n", rc ); 
   /* вызов IOCTL для установки номера сигнала */ 
   rc = ioctl( fd, MYIOC_SETSIG, sig ); 
   printf("rc from ioctl setting signal is = %d\n", rc ); 
   /* вызов IOCTL для инициации отправки сигнала */ 
   rc = ioctl( fd, MYIOC_SENDSIG, "anything" ); 
   printf("rc from ioctl sending signal is = %d\n", rc ); 
   /* закрытие устройства */ 
   close( fd ); 
   printf( "FINISHED, TERMINATING NORMALLY\n"); 
   exit( 0 ); 
}

И, наконец, простое приложение, являющееся конечным приёмником отправляемых сигналов (файл sigreq.c в архиве signals.tgz):

Листинг 4. Процесс, регистрирующий получение сигнала.
#include <stdio.h> 
#include <stdlib.h> 
#include <signal.h> 
#include "ioctl.h" 

static void sig_handler( int signo ) { 
   printf( "---> signal %d\n", signo ); 
} 

int main( int argc, char *argv[] ) { 
   unsigned long sig = SIGDEFAULT; 
   printf( "my own PID is %d\n", getpid() );.. 
   sig = SIGDEFAULT; 
   if( argc > 1 ) sig = atoi( argv[ 1 ] ); 
   if( SIG_ERR == signal( sig, sig_handler ) ) 
      printf( "set signal handler error\n" ); 
   while( 1 ) pause(); 
   exit( 0 ); 
}

В этом приложении (как и в предыдущем) для установки обработчика сигнала используется старая, так называемая "ненадёжная модель" обработки сигналов c использованием вызова signal(), но в данном случае это никак не влияет на достоверность получаемых результатов. Для проверки функционирования можно воспользоваться (почти) любым из набора сигналов UNIX, поэтому мы выберем не имеющий специального предназначения сигнал SIGUSR1 (сигнал номер 10):

$ ./sigreq 10 
my own PID is 10737 
---> signal 10 
$ kill -n 10 10737

Как видно процесс-регистратор отреагировал на получение сигнала, т.е. сигнал был доставлен до приложения. А теперь выполним полноценное тестирование, заставив процесс ioctl выполнить серию вызовов ioctl() к загруженному модулю ядра, чтобы он, в свою очередь, отправил указанный сигнал процессу sigreq:

$ sudo insmod ioctl_signal.ko 
$ lsmod | head -n2 
Module                  Size  Used by 
ioctl_signal            2053  0. 
$ dmesg | tail -n2
Succeeded in registering character device mycdrv 
$ cat /sys/devices/virtual/misc/mycdrv/dev 
10:56 
$ ls -l /dev | grep my 
crw-rw----  1 root root     10,  56 Май  6 17:15 mycdrv 
$ ./ioctl 10 11684 
I opened the device node, file descriptor = 3 
rc from ioctl setting pid is = 0 
rc from ioctl setting signal is = 0 
rc from ioctl sending signal is = 0 
FINISHED, TERMINATING NORMALLY 
$ dmesg | tail -n14 
Succeeded in registering character device mycdrv 
 attempting to open device: mycdrv: 
  MAJOR number = 10, MINOR number = 56 
   successfully open  device: mycdrv: 
I have been opened  1 times since being loaded 
ref=1 
Setting pid to send signals to, sigpid = 11684 
Setting signal to send as: 10. 
Sending signal 10 to process ID 11684 
retval = 0 
closing character device: mycdrv: 
$ ./sigreq 10 
my own PID is 11684 
---> signal 10 
^C

Отправка сигнала в этой реализации выполняется при вызове send_sig(), который вместе с остальными функциями API, связанными с отправкой сигналов, определены в файле <linux/sched.h>:

int send_sig_info( int signal, struct siginfo *info, struct task_struct *task ); 
int send_sig( int signal, struct task_struct *task, int priv ); 
int kill_pid_info( int signal, struct siginfo *info, struct pid *pid );
...

Описание достаточно сложной структуры siginfo находится в заголовочном файле пространства пользователя </usr/include/asm-generic/siginfo.h>:

typedef struct siginfo { 
   int si_signo; 
   int si_errno; 
   int si_code;
...
}

Заключение

В данной статье были рассмотрены такие возможности как:

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

Показанный пример является ещё одной демонстрацией того, что вопреки непонятному предубеждению из кода ядра (модуля ядра) можно выполнить практически все действия, доступные из среды пользовательского приложения. В следующей статье мы продолжим изучение возможностей API ядра.


Ресурсы для скачивания


Похожие темы


Комментарии

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux, Open source
ArticleID=856060
ArticleTitle=Нестандартные сценарии использования модулей ядра: Часть 38. Работа с UNIX-сигналами
publish-date=01242013