Добавил:
ivanov666
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз:
Предмет:
Файл:Системное программирование в среде Linux. Учебное пособие
.pdf
Однако мы не можем просто присвоить результат выполнения
dlsym() такому указателю, как показано ниже:
funcp = dlsym (handle, symbol);
Причина в том, что стандарт С99 запрещает операцию присваивания между указателем на функцию и void *. В качестве решения
можно воспользоваться (немного грубым) приведением типов:
* ( void ** ) (&funcp ) = dlsym (handle, symbol);
Получив указатель на функцию с помощью dlsym(), можно вызвать ее
res = ( *funcp ) (somearg);
путем обычной для языка С операции разыменовывания:
4.5.3. Выгрузка динамической библиотеки: dlclose( )
Выгружается библиотека функцией:
dlclose(void *handle);
При закрытии библиотеки динамический компоновщик проверяет
счетчик количества открытий библиотеки, и если она была открыта
несколькими программами одновременно, то она не выгружается до
тех пор, пока все программы не закроют эту библиотеку.
4.5.4. Пример применения
Для примера создадим программу, где в качестве параметров –
названия функций, которые она будет использовать в работе. Например, математические функции возведения в степень. Напишем текст
функций:
double power2(double x){return x*x;};
double power3(double x){ return x*x*x; };
Сохраняем его в файл lib.c (см. [6]) и создаем динамическую библиотеку libpowers.so следующими командами:
[gun]$ gcc -fPIC -c lib.c
[gun]$ gcc -shared lib.o -o libpowers.so
61

Теперь пишем использующую ее основную программу в файле
main1.c (см. [6]):
#include <stdio.h>
#include <dlfcn.h>
int main(int argc, char* argv[]){
void *ext_library;
double value=0;
double (*powerfunc)(double x); // Прототип функции
ext_library = dlopen ("/gun/libs/libpowers.so",
RTLD_LAZY);
if (!ext_library){
fprintf(stderr,"dlopen() error: %s\n", dlerror());
return 1; };
powerfunc = dlsym(ext_library, argv[1]); //Ошибка,
если –std=С99
value=3.0;
// Вызов функции по адресу
printf("%s(%f) = %f\n",argv[1],value,(*powerfunc)
(value));
dlclose(ext_library); };
Код главной программы готов. Требуется его откомпилировать с
использованием библиотеки dl:
[gun]$ gcc main1.c -o main1 -ldl
Получим программный файл main1, который можно тестировать.
4.5.5. Инициализация и деинициализация
динамических библиотек
Представим себе ситуацию, когда функции библиотеки для работы
требуют правильно инициализированные переменные, например если
для работы функции нужен буфер или массив. Для таких случаев в
библиотеках можно задавать инициализирующую и деинициализирующую функции:
void _init(); //инициализация
void _fini(); //деинициализация
62

Поясним их применение на примере. Введем в нашей библиотеке
lib.c глобальную переменную test и возвращающую ее функцию:
char *test; char *ret_test(){ return test; };
Перепишем основную программу main2.c (см. [6]):
#include <stdio.h>
#include <dlfcn.h>
int main(){
void *ext_library; double value=0;
char * (*ret_test)();
ext_library = dlopen ("libtest.so",RTLD_LAZY);
if (!ext_library){
fprintf(stderr,"dlopen() error: %s\n", dlerror());
return 1; };
ret_test = dlsym (ext_library,"ret_test");
printf("Return of ret_test:\"%s\" [%p]\n",
(*ret_test)(), (*ret_test)());
dlclose(ext_library); };
После компиляции этого примера мы получим результат:
[gun]$ gcc -c lib.c -fPIC
[gun]$ gcc -shared lib.o -o libtest.so
[gun]$ gcc -o main2 main2.c -ldl
[gun]$ ./main Return of ret_test: "(null)" [(nil)]
Как видим, переменная test оказалась равной NULL, а хотелось
бы иметь в ней значение.
Для этого посмотрим, как работают функции
_init() и _fini(). Создадим вторую библиотеку lib1.c (см. [6]):
#include <stdlib.h>
char *test; char *ret_test(){ return test; };
void _init(){
test=(char *)malloc(5);
if (test!=NULL){
*(test+0)=‘g'; *(test+1)=‘u'; *(test+2)=‘n';
*(test+3)=‘!'; *(test+4)=0;
};
printf("_init() executed...\n"); };
void _fini(){
if (test!=NULL) free(test);
printf("_fini() executed...\n"); };
63

Теперь пробуем откомпилировать:
[gun]$ gcc -c lib1.c -fPIC
[gun]$ gcc -shared lib1.o -o libtest.so
lib1.o: In function `_init': lib1.o(.text+0x24):
multiple definition of `_init'
/usr/lib/crti.o(.init+0x0): first defined here
lib1.o:
In function `_fini': lib1.o(.text+0xc0): multiple
definition of `_fini' /usr/lib/crti.o(.fini+0x0):
first defined here collect2: ld returned 1 exit
status
Программа не может скомпоноваться. Чтобы избавиться от мешающей библиотеки надо использовать ключ компилятора -nostdlib.
Попробуем пересобрать библиотеку:
[gun]$ gcc -shared -nostdlib lib1.o -o libtest.so
Теперь повторно запускаем main:
[gun]$ ./main2
_init() executed...
Return of ret_test: "gun!" [0x8049c20]
_fini() executed...
Как видим, глобальная (для библиотеки) переменная test была
проинициализирована при загрузке, ее значение выведено библиотечной функцией, а при выгрузке библиотеки динамически выделенная
для переменной память была освобождена.
Вопросы для самопроверки
1. В чем состоит необходимость организации библиотек объектов?
2. Дайте определение библиотеки объектных файлов.
3. Дайте определение статической и динамической библиотеки.
4. Опишите команду создания и редактирования статических биб-
лиотек.
5. Перечислите варианты подключения статических библиотек.
6. Каковы особенности разделяемых библиотек?
7. Опишите процесс создания разделяемых библиотек.
8. Что такое адресно-независимый код?
9. Каковы
особенности применения разделяемых библиотек?
64

10. Опишите команды objdump, readelf, nm.
11. Что такое динамически загружаемые библиотеки?
12. Перечислите функции интерфейса dlopen.
13. Каковы особенности применения функции dlopen()?
14. Опишите флаги, применяемые в функции dlopen().
15. Каковы особенности применения функции dlsym()?
Упражнения
Выполните лабораторную работу № 2 из лабораторного практикума.
65

Глава 5. Многозадачное программирование
в Linux
5.1. Основные системные вызовы
для реализации многозадачности
Основными элементами для реализации многозадачности являются
системные вызовы fork(), exit(), wait() и execve(). Каждый
из них будет детально рассмотрен далее, здесь же ознакомимся с кратким описанием этих четырех вызовов и их использования в связке друг
с другом.
Системный вызов fork() позволяет одному процессу, родителю,
создавать новый, дочерний процесс. Оба этих
чти) идентичными: потомок получает копии родительского стека, данных, кучи, копии родительских сегментов стека и текста. Термин
«fork» («вилка», «разветвление») стали применять потому, что родительский процесс как бы делится на две копии самого себя.
Библиотечная функция exit(status) завершает процесс, делая
все его ресурсы (память, дескрипторы открытых
ступными для последующего перераспределения ядром. Аргумент
status – целое число, которое определяет код завершения процесса.
Родительский процесс может извлечь этот код с помощью системного
вызова wait().
Системный вызов wait(&status) имеет два назначения. Вопервых, если работа потомка текущего процесса еще не была завершена путем вызова
нение родителя, пока не будет завершен один из его потомков.
Во-вторых, код завершения потомка возвращается через аргумент
функции wait().
exit(), функция wait() приостанавливает выпол-
процесса являются (по-
файлов и т. д.) до-
66

Системный вызов execve(pathname, argv, envp) загружает в память процесса новую программу (расположенную в pathname,
с аргументами argv и списком переменных среды envp). Текст существующей программы сбрасывается, а для новой программы заново
создаются сегменты со стеком, данными и кучей. Эту операцию часто
называют выполнением новой программы.
Рис. 5.1. Обзор вызовов fork(), exit(), wait() и execve()
На рис. 5.1 показано, как вызовы fork(), exit(), wait() и
execve() обычно используются вместе. Применение вызова
execve(), показанное на этой диаграмме, не является обязательным.
Иногда имеет смысл позволить потомку продолжить выполнение про-
67

граммы родителя. В любом случае выполнение дочернего процесса в
конечном счете завершается вызовом exit() (или передачей сигнала)
и возвращением кода завершения, доступным родителю через функцию wait(). Вызов wait() тоже необязателен. Родитель может просто игнорировать своего потомка и продолжать работу. Однако использование функции wait() обычно является желательным и вы
полняется внутри обработчика сигнала SIGCHLD, который генерируется ядром для родителя, когда один из его дочерних процессов завершается (по умолчанию сигнал SIGCHLD игнорируется, поэтому в диаграмме сказано, что его доставка является опциональной).
-
5.2. Идентификаторы процессов в Linux
Стандартная библиотека C (libc, реализованная в Linux в glibc) использует возможности многозадачности Unix System V (далее SysV).
В многозадачной ОС любой процесс обладает уникальным идентификатором процесса (PID, Process Identifier – идентификатор процесса),
представляющим собой целое положительное число, уникальность которого гарантируется системой.
В libc тип pid_t определен как целое, способное вместить в себе
PID. Рассмотрим функцию, которая сообщает идентификатор процесса, содержащего
unistd.h и sys/types.h):
pid_t getpid (void);
программу (она определена вместе с pid_t в файлах
Напишем программу, выводящую в стандартный вывод свой идентификатор:
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
int main() {
pid_t pid; pid = getpid();
printf("pid, присвоенный процессу - %d\n", pid);
return 0; }
Эта программа выведет свой PID, и при последующих запусках это
число будет постоянно увеличиваться, потому что в перерыве между
запусками может быть создан другой процесс. Это можно выяснить,
выполняя
утилиту ps между запусками примера.
68

5.3. Порождение процессов
Системный вызов fork() создает новый процесс – потомок,
который является почти полной копией вызывающего процесса, роди-
теля:
#include <unistd.h>
pid_t fork(void);
В родительском процессе функция возвращает идентификатор потомка при успешном завершении или –1 при ошибке, в успешно созданном потомке всегда возвращает 0.
Оба процесса выполняют один и тот же программный код, но обладают разными
потомка вначале полностью дублируют соответствующие части памяти своего родителя. Но после завершения вызова fork() каждый из
процессов может самостоятельно изменять переменные в своих сегментах, не влияя на другой процесс.
Ключевым моментом в понимании вызова fork() является тот
факт, что после завершения
каждый из которых продолжает выполнение с момента возврата из
этого вызова. В случае одного процессорного ядра решение, какой
процесс должен выполняться (родительский или дочерний), принимается диспетчером задач.
Распознавать процессы внутри кода программы можно с помощью значения, возвращенного функцией fork(). В случае с роди
телем это значение равно идентификатору только что созданного
потомка. Это полезно, поскольку родитель может создать несколько
дочерних процессов, которые ему придется отслеживать (с помощью вызова wait() или одной из его разновидностей). В случае с
потомком возвращается 0. При необходимости дочерний процесс
может получить свой собственный идентификатор или идентификатор своего
ственно getppid().
Если новый процесс не удается создать, вызов fork() возвращает –1. Одной из причин этого может быть превышение пользователем
или всей системой в целом ограничения на количество создаваемых
процессов.
копиями сегментов стека, данных и кучи. Сегменты
его работы мы получаем два процесса,
родителя, используя функции getpid() и соответ-
-
69

Следует понимать, что после вызова fork() невозможно сказать,
какой из двух процессов первым получит от планировщика ресурсы
процессора. В плохо написанных программах такая неопределенность
может привести к ошибке, известной под названием «состояние
гонки».
Оба процесса содержат коды, как родительские, так и дочерние,
однако оба они должны выполнить только свой набор
кодов. Чтобы
прояснить это, взглянем на алгоритм:
РАЗВЕТВИТЬ
ЕСЛИ ТЫ ДОЧЕРНИЙ ПРОЦЕСС ВЫПОЛНИТЬ (...)
ЕСЛИ ТЫ РОДИТЕЛЬСКИЙ ПРОЦЕСС ВЫПОЛНИТЬ (...)
который представляет собой код, написанный на некоем метаязыке.
На языке C получим:
int main() {
pid_t pid;
pid = fork();
if (pid == 0)
{ КОД ДОЧЕРНЕГО ПРОЦЕССА }
КОД РОДИТЕЛЬСКОГО ПРОЦЕССА
}
Когда выполняется вызов fork(), потомок получает копии всех
файловых дескрипторов родителя. Копии в
том смысле, что соответствующие дескрипторы в родительском и дочернем процессах указывают на один и тот же открытый файл. Дескриптор открытого файла
содержит его текущее смещение (образовавшееся во время редактирования с помощью функций read(), write() и lseek()) и флаги
состояния (установленные вызовом open() и измененные операцией
fcntl() F_SETFL).
Как следствие, эти атрибуты открытого файла
являются общими для родителя и потомка. Если, например, дочерний
процесс обновляет смещение в файле, то это изменение доступно его
родителю посредством соответствующего дескриптора.
Linux 3.2.0 дает возможность создавать новые процессы с помощью системного вызова clone. Это более универсальный вариант
функции fork, позволяющий вызывающему процессу определить,
что будет совместно использоваться дочерним и родительским процессами.
70
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]
