Portál AbcLinuxu, 27. dubna 2024 04:20

Linux: systémové volání splice()

14. 5. 2013 | Nikola Pajkovský
Články - Linux: systémové volání splice()  

Ukážeme si, jak elegantně kopírovat soubory mezi dvěma otevřenými souborovými deskriptory bez nutnosti kopírovat obsah z jádra do uživatelského prostoru a zpět.

Systémové volání splice() má dlouhou historii. splice() byl poprvé navržen Larrym McVoyem v roce 1998 jako způsob vylepšení I/O operací na serverech. Přestože se často v následujících letech zmiňovalo o splice(), žádná implementace nikdy nebyla vytvořena pro hlavní řadu linuxového jádra. Nicméně, situace se změnila těsně před uzavřením začleňovacího okna pro 2.6.17, kdy Jens Axboe zaslal sadu změn i s množstvím oprav, které byly začleněny.

Prototyp systémového volání splice() vypadá následovně:

	long splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out,
                    size_t len, unsigned int flags);

Při pohledu na koncept na vyšší úrovni se v jádře objevuje nový pojem "náhodný jaderný buffer" (random kernel buffer), který je vystaven do uživatelského prostoru. Jinými slovy, splice() pracuje na jaderném bufferu, nad kterým má uživatel kontrolu.

Volání splice() způsobí přesun dat mezi dvěma popisovači souboru (file descriptors), bez nutnosti přesunout data z jádra do uživatelského prostoru a zpět. Jádro přesune až len dat z deskriptoru souboru fd_in do deskriptoru souboru fd_out, kde jeden z deskriptorů musí být roura (pipe). Takže ve velmi realném (ale stále abstraktním) smyslu, splice() není nic jiného, než read()/write() do jaderného bufferu.

Dvě hodnoty offsetu (off_in a off_out) ukazují, na kterou pozici by měl každý deskriptor souboru být umístěný před začátkem přesunu dat. Všimněte si, že offsety se předávají pomocí ukazatelů, které jsou příslušným způsobem upraveny po čtení/zápisu z/do bufferu. Z uživatelského prostoru se může použít ukazatel NULL k indikaci, že se má použít stávající offset. Nicméně je chyba použít NULL ukazatel jako offset k přiřazené rouře (pipe).

Argument flags upravuje, jak se kopírování provádí:

A kde byste ve skutečnosti chtěli použít splice()? Normálně byste použili splice()tam, kde chcete kopírovat z jednoho zdroje do druhého, aniž byste chtěli vidět data, která se kopírují. Použití splice() vám nabízí efektivnější způsob, jak to udělat. Takto se vyhnete zbytečné alokaci paměti a memcpy() z/do bufferu v uživatelském prostoru.

Pokud byste chtěli kopírovat soubor, mohli byste to napsat tradičním způsobem v uživatelském prostoru:

	for (;;) {
		char *p;
		int ret = read(input, buffer, BUFSIZE);

		if (!ret)
			break;
		if (ret < 0) {
			if (errno == EINTR)
				continue;
			.. exit with an inpot error ..
		}

		p = buffer;
		do {
			int written = write(output, p, ret);
			if (!written)
				.. exit with filesystem full ..
			if (written < 0) {
				if (errno == EINTR)
					continue;
				.. exit with an output error ..
			}
			p += written;
			ret -= written;
		} while (ret);
	}

s tím rozdílem, že byste neměli buffer v uživatelském prostoru, a kde jsou systémová volání read() a write() nahrazena systémovým voláním splice() do/z roury (pipe). Takže jediné, co se změní je to, kde ve skutečnosti existuje buffer:

	int pipefd[2], r;

	r = pipe(pipefd);
	if (r < 0)
		die("pipe");

	for (;;) {
		int nr = splice(fd_in, NULL, pipefd[1], NULL, INT_MAX,
				SPLICE_F_MOVE | SPLICE_F_MORE);
		if (!nr)
			break;
		if (nr < 0) {
			if (errno == EINTR)
				continue;
			.. exit with an inpot error ..
		}

		do {
			int ret = splice(pipefd[0], NULL, fd_out, NULL, nr, SPLICE_F_MOVE);
			if (!ret)
				.. exit with filesystem full ..
			if (ret < 0) {
				if (errno == EINTR)
					continue;
				.. exit with an output error ..
			}

			nr -= ret;
		} while (nr);
	}

Ne každý deskriptor souboru se může použít se splice() a důvodem je to, že to ješte nikdo nepotřeboval, a tudíž ani nikdo nenapsal.

Kombinace možných souborových deskriptorů:

\out
inpipe reg chr unix tcp udp raw
pipe yes yes yes yes yes yes yes
reg yes no no no no no no
chr yes no no no no no no
unix no no no no no no no
tcp yes no no no no no no
udp no no no no no no no
raw no no no no no no no

Další články z této rubriky

Úvod do Dockeru (1)
Paralelizace běžných činností v konzoli pomocí GNU Parallel
Unixové nástroje – 26 (triky pro práci v Bashi)
Unixové nástroje – 25 ((s,c)fdisk, gdisk, parted a findmnt)
Linux: systémové volání splice()

ISSN 1214-1267, (c) 1999-2007 Stickfish s.r.o.