Enviar descriptor de archivo por socket Linux
sockets file-descriptor (1)
Estoy tratando de enviar un descriptor de archivo por linux socket, pero no funciona. ¿Qué estoy haciendo mal? ¿Cómo se supone que uno debe depurar algo como esto? Intenté poner perror () en todas partes es posible, pero dijeron que todo está bien. Aquí está lo que he escrito:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <fcntl.h>
void wyslij(int socket, int fd) // send fd by socket
{
struct msghdr msg = {0};
char buf[CMSG_SPACE(sizeof fd)];
msg.msg_control = buf;
msg.msg_controllen = sizeof buf;
struct cmsghdr * cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
cmsg->cmsg_len = CMSG_LEN(sizeof fd);
*((int *) CMSG_DATA(cmsg)) = fd;
msg.msg_controllen = cmsg->cmsg_len; // why does example from man need it? isn''t it redundant?
sendmsg(socket, &msg, 0);
}
int odbierz(int socket) // receive fd from socket
{
struct msghdr msg = {0};
recvmsg(socket, &msg, 0);
struct cmsghdr * cmsg = CMSG_FIRSTHDR(&msg);
unsigned char * data = CMSG_DATA(cmsg);
int fd = *((int*) data); // here program stops, probably with segfault
return fd;
}
int main()
{
int sv[2];
socketpair(AF_UNIX, SOCK_DGRAM, 0, sv);
int pid = fork();
if (pid > 0) // in parent
{
close(sv[1]);
int sock = sv[0];
int fd = open("./z7.c", O_RDONLY);
wyslij(sock, fd);
close(fd);
}
else // in child
{
close(sv[0]);
int sock = sv[1];
sleep(0.5);
int fd = odbierz(sock);
}
}
Stevens (et al)
Programación de redes UNIX®, Vol. 1: La API de Sockets Networking
describe el proceso de transferencia de descriptores de archivos entre procesos en el Capítulo 15
Protocolos de dominio Unix
y específicamente §15.7
Descriptores de paso
.
Es complicado describirlo en su totalidad, pero debe hacerse en un socket de dominio Unix (
AF_UNIX
o
AF_LOCAL
), y el proceso del remitente usa
sendmsg()
mientras que el receptor usa
recvmsg()
.
Obtuve esta versión ligeramente modificada (e instrumentada) del código de la pregunta para que funcione en Mac OS X 10.10.1 Yosemite con GCC 4.9.1:
#include "stderr.h"
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
static
void wyslij(int socket, int fd) // send fd by socket
{
struct msghdr msg = { 0 };
char buf[CMSG_SPACE(sizeof(fd))];
memset(buf, ''/0'', sizeof(buf));
struct iovec io = { .iov_base = "ABC", .iov_len = 3 };
msg.msg_iov = &io;
msg.msg_iovlen = 1;
msg.msg_control = buf;
msg.msg_controllen = sizeof(buf);
struct cmsghdr * cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
cmsg->cmsg_len = CMSG_LEN(sizeof(fd));
*((int *) CMSG_DATA(cmsg)) = fd;
msg.msg_controllen = CMSG_SPACE(sizeof(fd));
if (sendmsg(socket, &msg, 0) < 0)
err_syserr("Failed to send message/n");
}
static
int odbierz(int socket) // receive fd from socket
{
struct msghdr msg = {0};
char m_buffer[256];
struct iovec io = { .iov_base = m_buffer, .iov_len = sizeof(m_buffer) };
msg.msg_iov = &io;
msg.msg_iovlen = 1;
char c_buffer[256];
msg.msg_control = c_buffer;
msg.msg_controllen = sizeof(c_buffer);
if (recvmsg(socket, &msg, 0) < 0)
err_syserr("Failed to receive message/n");
struct cmsghdr * cmsg = CMSG_FIRSTHDR(&msg);
unsigned char * data = CMSG_DATA(cmsg);
err_remark("About to extract fd/n");
int fd = *((int*) data);
err_remark("Extracted fd %d/n", fd);
return fd;
}
int main(int argc, char **argv)
{
const char *filename = "./z7.c";
err_setarg0(argv[0]);
err_setlogopts(ERR_PID);
if (argc > 1)
filename = argv[1];
int sv[2];
if (socketpair(AF_UNIX, SOCK_DGRAM, 0, sv) != 0)
err_syserr("Failed to create Unix-domain socket pair/n");
int pid = fork();
if (pid > 0) // in parent
{
err_remark("Parent at work/n");
close(sv[1]);
int sock = sv[0];
int fd = open(filename, O_RDONLY);
if (fd < 0)
err_syserr("Failed to open file %s for reading/n", filename);
wyslij(sock, fd);
close(fd);
nanosleep(&(struct timespec){ .tv_sec = 1, .tv_nsec = 500000000}, 0);
err_remark("Parent exits/n");
}
else // in child
{
err_remark("Child at play/n");
close(sv[0]);
int sock = sv[1];
nanosleep(&(struct timespec){ .tv_sec = 0, .tv_nsec = 500000000}, 0);
int fd = odbierz(sock);
printf("Read %d!/n", fd);
char buffer[256];
ssize_t nbytes;
while ((nbytes = read(fd, buffer, sizeof(buffer))) > 0)
write(1, buffer, nbytes);
printf("Done!/n");
close(fd);
}
return 0;
}
El resultado de la versión instrumentada pero no fija del código original fue:
$ ./fd-passing
fd-passing: pid=1391: Parent at work
fd-passing: pid=1391: Failed to send message
error (40) Message too long
fd-passing: pid=1392: Child at play
$ fd-passing: pid=1392: Failed to receive message
error (40) Message too long
Tenga en cuenta que el padre finalizó antes que el hijo, por lo que el mensaje apareció en el medio de la salida.
El resultado del código ''fijo'' fue:
$ ./fd-passing
fd-passing: pid=1046: Parent at work
fd-passing: pid=1048: Child at play
fd-passing: pid=1048: About to extract fd
fd-passing: pid=1048: Extracted fd 3
Read 3!
This is the file z7.c.
It isn''t very interesting.
It isn''t even C code.
But it is used by the fd-passing program to demonstrate that file
descriptors can indeed be passed between sockets on occasion.
Done!
fd-passing: pid=1046: Parent exits
$
Los principales cambios significativos fueron agregar la
struct iovec
a los datos en la
struct msghdr
en ambas funciones y proporcionar espacio en la función de recepción (
odbierz()
) para el mensaje de control.
struct iovec
un paso intermedio en la depuración donde proporcioné la
struct iovec
al padre y se eliminó el error "mensaje demasiado largo" del padre.
Para demostrar que estaba funcionando (se pasó un descriptor de archivo), agregué código para leer e imprimir el archivo desde el descriptor de archivo pasado.
El código original tenía
sleep(0.5)
pero dado que
sleep()
toma un entero sin signo, esto era equivalente a no dormir.
Usé literales compuestos C99 para que el niño durmiera durante 0.5 segundos.
El padre duerme durante 1,5 segundos para que la salida del niño se complete antes de que el padre salga.
También podía usar
wait()
o
waitpid()
, pero era demasiado vago para hacerlo.
No he vuelto y comprobé que todas las adiciones fueran necesarias.
El
"stderr.h"
declara las funciones
err_*()
.
Es el código que escribí (primera versión antes de 1987) para informar errores de manera sucinta.
La
err_setlogopts(ERR_PID)
antepone todos los mensajes con el PID.
Para las marcas de tiempo también,
err_setlogopts(ERR_PID|ERR_STAMP)
haría el trabajo.
Problemas de alineación
Nominal Animal sugiere en un comment :
¿Puedo sugerirle que modifique el código para copiar el descriptor
int
usandomemcpy()
lugar de acceder a los datos directamente? No está necesariamente alineado correctamente, razón por la cual el ejemplo de la página de manual también usamemcpy()
, y hay muchas arquitecturas de Linux en las que el accesoint
no alineado causa problemas (hasta la señal SIGBUS mata el proceso).
Y no solo las arquitecturas de Linux: tanto SPARC como Power requieren datos alineados y, a menudo, ejecutan Solaris y AIX respectivamente. Érase una vez, DEC Alpha también lo requería, pero rara vez se los ve en el campo en estos días.
El código en la página del manual
cmsg(3)
relacionado con esto es:
struct msghdr msg = {0};
struct cmsghdr *cmsg;
int myfds[NUM_FD]; /* Contains the file descriptors to pass. */
char buf[CMSG_SPACE(sizeof myfds)]; /* ancillary data buffer */
int *fdptr;
msg.msg_control = buf;
msg.msg_controllen = sizeof buf;
cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
cmsg->cmsg_len = CMSG_LEN(sizeof(int) * NUM_FD);
/* Initialize the payload: */
fdptr = (int *) CMSG_DATA(cmsg);
memcpy(fdptr, myfds, NUM_FD * sizeof(int));
/* Sum of the length of all control messages in the buffer: */
msg.msg_controllen = CMSG_SPACE(sizeof(int) * NUM_FD);
La asignación a
fdptr
parece suponer que
CMSG_DATA(cmsg)
está lo suficientemente bien alineado para convertirse a
int *
y
memcpy()
se supone que
NUM_FD
no es solo 1. Dicho esto, se supone que apunta en la matriz
buf
, y eso podría no estar lo suficientemente bien alineado como lo sugiere Nominal Animal, por lo que me parece que
fdptr
es solo un intruso y sería mejor si el ejemplo utilizado:
memcpy(CMSG_DATA(cmsg), myfds, NUM_FD * sizeof(int));
Y el proceso inverso en el extremo receptor sería apropiado. Este programa solo pasa un descriptor de archivo único, por lo que el código se puede modificar para:
memmove(CMSG_DATA(cmsg), &fd, sizeof(fd)); // Send
memmove(&fd, CMSG_DATA(cmsg), sizeof(fd)); // Receive
También parece recordar problemas históricos en varios sistemas operativos con datos auxiliares sin datos normales de carga útil, evitado al enviar también al menos un byte ficticio, pero no puedo encontrar ninguna referencia para verificar, por lo que puedo recordar mal.
Dado que Mac OS X (que tiene una base Darwin / BSD) requiere al menos una
struct iovec
, incluso si eso describe un mensaje de longitud cero, estoy dispuesto a creer que el código que se muestra arriba, que incluye un mensaje de 3 bytes , es un buen paso en la dirección general correcta.
El mensaje tal vez debería ser un solo byte nulo en lugar de 3 letras.
Revisé el código para leer como se muestra a continuación.
Utiliza
memmove()
para copiar el descriptor de archivo
memmove()
desde el búfer
cmsg
.
Transfiere un solo byte de mensaje, que es un byte nulo.
También tiene el proceso primario leído (hasta) 32 bytes del archivo antes de pasar el descriptor de archivo al secundario. El niño continúa leyendo donde lo dejó el padre. Esto demuestra que el descriptor de archivo transferido incluye el desplazamiento del archivo.
El receptor debe hacer más validación en el
cmsg
antes de tratarlo como un descriptor de archivo que pasa un mensaje.
#include "stderr.h"
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
static
void wyslij(int socket, int fd) // send fd by socket
{
struct msghdr msg = { 0 };
char buf[CMSG_SPACE(sizeof(fd))];
memset(buf, ''/0'', sizeof(buf));
/* On Mac OS X, the struct iovec is needed, even if it points to minimal data */
struct iovec io = { .iov_base = "", .iov_len = 1 };
msg.msg_iov = &io;
msg.msg_iovlen = 1;
msg.msg_control = buf;
msg.msg_controllen = sizeof(buf);
struct cmsghdr * cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
cmsg->cmsg_len = CMSG_LEN(sizeof(fd));
memmove(CMSG_DATA(cmsg), &fd, sizeof(fd));
msg.msg_controllen = CMSG_SPACE(sizeof(fd));
if (sendmsg(socket, &msg, 0) < 0)
err_syserr("Failed to send message/n");
}
static
int odbierz(int socket) // receive fd from socket
{
struct msghdr msg = {0};
/* On Mac OS X, the struct iovec is needed, even if it points to minimal data */
char m_buffer[1];
struct iovec io = { .iov_base = m_buffer, .iov_len = sizeof(m_buffer) };
msg.msg_iov = &io;
msg.msg_iovlen = 1;
char c_buffer[256];
msg.msg_control = c_buffer;
msg.msg_controllen = sizeof(c_buffer);
if (recvmsg(socket, &msg, 0) < 0)
err_syserr("Failed to receive message/n");
struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
err_remark("About to extract fd/n");
int fd;
memmove(&fd, CMSG_DATA(cmsg), sizeof(fd));
err_remark("Extracted fd %d/n", fd);
return fd;
}
int main(int argc, char **argv)
{
const char *filename = "./z7.c";
err_setarg0(argv[0]);
err_setlogopts(ERR_PID);
if (argc > 1)
filename = argv[1];
int sv[2];
if (socketpair(AF_UNIX, SOCK_DGRAM, 0, sv) != 0)
err_syserr("Failed to create Unix-domain socket pair/n");
int pid = fork();
if (pid > 0) // in parent
{
err_remark("Parent at work/n");
close(sv[1]);
int sock = sv[0];
int fd = open(filename, O_RDONLY);
if (fd < 0)
err_syserr("Failed to open file %s for reading/n", filename);
/* Read some data to demonstrate that file offset is passed */
char buffer[32];
int nbytes = read(fd, buffer, sizeof(buffer));
if (nbytes > 0)
err_remark("Parent read: [[%.*s]]/n", nbytes, buffer);
wyslij(sock, fd);
close(fd);
nanosleep(&(struct timespec){ .tv_sec = 1, .tv_nsec = 500000000}, 0);
err_remark("Parent exits/n");
}
else // in child
{
err_remark("Child at play/n");
close(sv[0]);
int sock = sv[1];
nanosleep(&(struct timespec){ .tv_sec = 0, .tv_nsec = 500000000}, 0);
int fd = odbierz(sock);
printf("Read %d!/n", fd);
char buffer[256];
ssize_t nbytes;
while ((nbytes = read(fd, buffer, sizeof(buffer))) > 0)
write(1, buffer, nbytes);
printf("Done!/n");
close(fd);
}
return 0;
}
Y una muestra de ejecución:
$ ./fd-passing
fd-passing: pid=8000: Parent at work
fd-passing: pid=8000: Parent read: [[This is the file z7.c.
It isn''t ]]
fd-passing: pid=8001: Child at play
fd-passing: pid=8001: About to extract fd
fd-passing: pid=8001: Extracted fd 3
Read 3!
very interesting.
It isn''t even C code.
But it is used by the fd-passing program to demonstrate that file
descriptors can indeed be passed between sockets on occasion.
And, with the fully working code, it does indeed seem to work.
Extended testing would have the parent code read part of the file, and
then demonstrate that the child codecontinues where the parent left off.
That has not been coded, though.
Done!
fd-passing: pid=8000: Parent exits
$