Nikita Shirikov Ed blog

Mmap and Dynamic Linkage (and time)

Сначала немного поговорим про время и работу с ним:

UTC: atomic clocks + leap seconds

Unix timestamp (Epoch time, POSIX time): number of seconds since Jan 1, 1970, 00:00:00 UTC, excluding leap seconds

Получить timestamp можно с помощью:

time_t time(time_t *t);

Раньше time_t был 32-битным, но это вызывает проблему (2038 года). Сейчас 64 бита.

Но мы хотим считать время поточнее, для этого есть сискол gettimeofday() - возвращает секунды и микросекунды.

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

Например, функция localtime(&timestamp). Результат данной функции - структурка tm с годом, месяцом, днем, временем (исходя из локальных настроек машины). Можем удобно получить строку со временем с помощью функции strftime();

Можем из tm получить timestamp с помощью функции mktime() (вызов портит саму tm).

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

С помощью этого вызова можем попросить ОС создать нам отображение начиная с опредленного адресса.

Пример использования:

#include <sys/mman.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>

#include <stdio.h>


int main(int argc, char* argv[]) {
    if (argc != 2) {
        fprintf(stderr, "Usage: %s FILE\n", argv[0]);
        return 1;
    }

    int fd = open(argv[1], O_RDWR);
    off_t filesize = lseek(fd, 0, SEEK_END);
    printf("filesize: %llu\n", (unsigned long long)filesize);

    printf("pid: %d\n", getpid());
    char *ptr = mmap(0, filesize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    printf("some file contents: %.12s\n", ptr);

    char buf[10];
    read(STDIN_FILENO, buf, sizeof(buf));

    puts("writing to the mapping");
    ptr[10] = buf[0];

    read(STDIN_FILENO, buf, sizeof(buf));
}

Параметры:

PROT_READ | PROT_WRITE говорят сами за себя - разрешаем ли мы читать, писать или исполнять выделенный участок памяти.

MAP_SHARED / MAP_PRIVATE будут ли изменения действительно вноситься на диск -> будут ли изменения для всех процессов.

mmap(0, filesize, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0) - получаем участок памяти, не связанный ни с каким файлом. (Кусок памяти изначально занулен). fd = -1 по традиции

С помощью pid и /proc/pid можем посмотреть все отображения для процесса.

mmap может вернуть NULL по этому в случае ошибки возвращается (void *)(-1)

В mmap память выделяется лениво, по мере необходимости (то есть чтения или изменения). Так как мы можем мапить достаточно большие файлы. Для ос было бы странно все сразу записывать в оперативную память. Поэтому когда мы просим создать отображание, оно может быть вообще без отображения на физическую памяти. Только когда мы начинаем читать, то случается pagefault и система идет все копировать. Система может сама решать, когда синхронизировать изменения с физической памятью. Вызов msync() принуждает ОС все записать в подлежащий файл. Можно пользоваться флагом MAP_SYNC.

Есть вызовы: mlock() и mlockall() - попросить не выгружать данные из оперативной памяти.

Динамическая компановка

Мы привыкли, что весь код, что мы пишем, собираем в один исполняемый файл.

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

Мы привыкли к статичекой компановке, когда компановщик разрешает все используемые символы в адресса. Написали мы функцию fun в файле fun.h. И хотим ее использовать из файла static.c:

// fun.h
#pragma once
int fun(int arg);
// fun.c
#include "fun.h"
int fun(int arg) {
    return arg * 2;
}
// static.c
#include "fun.h"
#include <stdio.h>
int main() {
    printf("fun(42): %d\n", fun(42));
    fun();
    x = 1;
}

Обычно мы собираем все так: gcc static.c fun.c -o static. Все единицы трансляции переделываются в объектные файлы. На символ fun будет неразрешенная зависимость. Компановщик ищет и удовлетворяет его.

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

Попросить компилятор сделать из файла динамическую библиотеку:

gcc -shared fun.c -o libfun.so

Теперь использовать динамическую библиотеку:

gcc static.c -o static -L. -lfun

В бинарнике останется неудовлетворительная зависимости. Просто ./static не запустится. Но мы можем сказать ОС, чтобы она искала дин. библы и все заработает:

LD_LIBRARY_PATH=. ./static

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

Зачем мы это делаем? Чтоб экономить память, не загружать одно и тоже кучу раз.

#include "fun.h"
#include <stdio.h>
int main() {
    printf("fun(42): %d\n", fun(42));
    fun(); // call fun@plt
    x = 1; // добываем сначала адресс, потом записываем
}

// Procedure linkage table (PLT):
// fun@plt

// Gloval offset table (GOT): (для динамических переменных)
// x:

Мы можем обратиться к динамическому загрузчику сами:

#include <dlfcn.h>
#include <stdlib.h>
#include <stdio.h>

int main() {
    void *dso = dlopen("libfun.so", RTLD_LAZY);
    if (dso == NULL) {
        fprintf(stderr, "dlopen: %s\n", dlerror());
        return EXIT_FAILURE;
    }
    void *sym = dlsym(dso, "fun");
    if (sym == NULL) {
        fprintf(stderr, "dlsym: %s\n", dlerror());
        return EXIT_FAILURE;
    }
    int (*fun)(int) = sym;
    printf("fun(42) == %d\n", fun(42));
}
gcc dlopen.c -o dlopen -ldl # добавляем динамический загрузчик

#Os