Splice Syscall Explained
Introduction
Yesterday, I was learning about the splice() syscalL. Splice is a Linux
system call which basically moves the data between two file descriptors
*without* copying it between kernel space and user space.
Normally, moving data between two file descriptors happens as follows:
[Source FD] -> Kernel Page Cache -> (copy_to_user) -> [User Space Buffer] ->
(copy_from_user) -> Kernel Page Cache -> [Destination FD]
copy_to_user and copy_from_user are kernel functions that move data between
kernel and user space. As you can see, this is quite a long process, and if the
data is not to be processed by the program, is a waste of resources.
Splice fixes this process by eliminating the transfer from the kernel to user space.
Syscall
The syscall has the following signature, from man splice:
1
2
3
4
5
6
7
#define _GNU_SOURCE /* See feature_test_macros(7) */
#define _FILE_OFFSET_BITS 64
#include <fcntl.h>
ssize_t splice(int fd_in, off_t *_Nullable off_in,
int fd_out, off_t *_Nullable off_out,
size_t size, unsigned int flags);
_GNU_SOURCEis a feature test macro, which exposes Linux specific stuff from the headers.
Upon successful completion, splice() returns the number of bytes moved. A
return value of 0 indicates the end of input stream. On error, -1 is
returned, and errno is set to indicate the exact cause of the failure (such
as EBADF for a bad file descriptor, or EINVAL for invalid arguments).
Code Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// Expose non-standard GNU-specific extensions, low-level Linux system
// functions, and all standard POSIX features.
#define _GNU_SOURCE
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
int main() {
printf("Beginning splicing\n");
int pipfd[2];
if(pipe(pipfd) == -1){
perror("Failed to create pipe");
return -1;
}; // Create the required pipe
int length = 100;
ssize_t status;
// Splice from standard input into the pipe
status = splice(STDIN_FILENO, NULL, pipfd[1], NULL, length, SPLICE_F_MOVE);
if (statues == -1){
perror("Splice-in failed");
return 1;
}
printf("[Spliced in %ld bytes]\n", status);
// Splice out of the pipe into standard output
status = splice(pipfd[0], NULL, STDOUT_FILENO, NULL, length, SPLICE_F_MOVE);
if (statues == -1){
perror("Splice-out failed");
return 1;
}
printf("[Spliced out %ld bytes]\n", status);
return 0;
}
An Interesting Behaviour
While testing the above code yesterday, I found that if more than the
length amount of input was provided, the remaining part ended up on the
prompt in the terminal after the program exited.
This happened because after reading from stdin, the remaining data was still
present in the stream. And since the program was started from the terminal
(fork and exec) that stream was shared with the terminal. So, after the
program ended, the shell read whatever residual data was left behind in that
shared stream, and treated it as a new command.