Wer sich mit File-Locking beschäftigt, der wird vermutlich auf zwei Möglichkeiten stoßen: die Funktion flock und Locking mit fcntl. Es gibt zwei wichtige Unterschiede zwischen diesen beiden Funktionen.
Locks, die mit flock erstellt wurden, werden bei einem fork an den Kind-Prozess weitergegeben. Es wird jedoch nicht der Lock kopiert, wenn durch fork oder dup der Filedescriptor dupliziert wird, denn jeder Filedescriptor auf die gelockte Datei enthält nur eine Referenz auf den selben Lock. Der Lock bleibt bestehen bis entweder alle Filedeskriptoren geschlossen sind, oder explizit eine Unlock-Operation auf einen Filedescriptor mit diesem Lock, egal in welchem Prozess, ausgeführt wird.
Mit fcntl erstellte Locks werden bei einem fork hingegen gar nicht weitergegeben. Der Lock gilt immer nur für den Prozess, der ihn erstellt hat. Außerdem wird ein Lock entfernt, wenn auch nur ein Filedescriptor der gelockten Datei geschlossen wird.
Der andere große Unterschied ist, dass nur Locking mit fcntl Posix-spezifiziert ist. Die Funktion flock hingegen ist eine BSD-Erfindung, die jedoch auch von Linux übernommen wurde. Andere Unixe, wie z.B. Solaris, unterstützen flock gar nicht.
Außerdem unterscheidet sich auch das Interface der beiden Funktionen deutlich. Mit fcntl hat man ein bisschen mehr Schreibarbeit, dafür ist es auch möglich nur einen Bereich einer Datei zu locken, wärend flock etwas primitiver ist.
Locking mit fcntl:
int fd = open(path, O_RDWR);
struct flock lock;
memset(&lock, 0, sizeof(struct flock));
lock.l_type = F_WRLCK;
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0;
fcntl(fd, F_SETLK, &lock);
Locking mit flock:
int fd = open(path, O_RDWR);
flock(fd, LOCK_EX);
Die Verlockung ist vielleicht groß, flock zu nutzen, wenn man nur schnell und einfach eine ganze Datei locken möchte. Ich würde aber empfehlen, immer fcntl zu nutzen, außer man möchte wirklich Locks mit mehreren Prozessen teilen.
Es ist möglich, dass mehrere Prozesse Zugriff auf die gleichen File-Deskriptoren haben. Beispielsweise werden bei einem Fork alle File-Deskriptoren kopiert. Der Kind-Prozess kann dann auf die selben Dateien oder Sockets zugreifen.
Man kann auch nach einem Fork oder mit nicht-verwandten Prozessen File-Deskriptoren austauschen. Dies geht über Unix Domain Sockets mit den Funktionen sendmsg und recvmsg.
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
Die Nachricht vom Typ struct msghdr
hat so einige Felder, die man erstmal ausfüllen muss.
struct msghdr {
void *msg_name; /* optional address */
socklen_t msg_namelen; /* size of address */
struct iovec *msg_iov; /* scatter/gather array */
size_t msg_iovlen; /* # elements in msg_iov */
void *msg_control; /* ancillary data, see below */
size_t msg_controllen; /* ancillary data buffer len */
int msg_flags; /* flags on received message */
};
Das Feld msg_name
wird hier nicht benötigt, daher setzen wir msg_name
und msg_namelen
auf 0.
struct msghdr msg;
msg.msg_name = NULL;
msg.msg_namelen = 0;
Die eigentlichen Daten überträgt man mit dem Feld msg_iov
, ein Array an Buffern, wie man es von writev kennt. Man erstellt ein Array mit struct iovec
Elementen und gibt jeweils einen Pointer auf die Daten und die Länge an. Es reicht natürlich, nur einen Buffer anzugeben.
struct iovec iov[1] ;
iov[0].iov_base = (void*)buf;
iov[0].iov_len = len;
msg.msg_iov = iov;
msg.msg_iovlen = 1;
Jetzt wollen wir noch einen Filedescriptor übertragen. Für solche (und andere) Zwecke gibt es das Feld msg_control
für folgende struct:
struct cmsghdr {
size_t cmsg_len; /* Data byte count, including header
(type is socklen_t in POSIX) */
int cmsg_level; /* Originating protocol */
int cmsg_type; /* Protocol-specific type */
/* followed by
unsigned char cmsg_data[]; */
};
Nach den Feldern der struct müssen also noch zusätzliche Daten im Speicher liegen. Für den Fall, dass man Filedeskriptoren übertragen möchte, müssen ein oder mehrere Integer folgen. Es empfiehlt sich zunächst einen statischen Buffer mit der entsprechenden Größe anzulegen. Hierfür gibt es das Makro CMSG_SPACE
, was die Größe des Buffers liefert.
char cmsg_buf[CMSG_SPACE(sizeof(int))];
Anschließend setzen wir die Felder der struct.
struct cmsghdr *cmsg = (struct cmsghdr*)cmsg_buf;
cmsg->cmsg_len = CMSG_LEN(sizeof(int));
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS; // indicates fd payload data
Jetzt fehlt nur noch der Filedescriptor. Dieser muss, wie schon erwähnt, hinter der struct liegen. Mit dem Makro CMSG_DATA
erhält man einen Pointer auf die Payload-Data.
int *data = (int*)CMSG_DATA(cmsg);
*data = fd;
Jetzt setzen wir nur noch msg_control
und msg_controllen
, sowie msg_flags
. Danach kann sendmsg
aufgerufen werden.
msg.msg_control = cmsg;
msg.msg_controllen = CMSG_LEN(sizeof(int));
msg.msg_flags = 0;
sendmsg(sock, &msg, 0);
Um einen Filedescriptor zu empfangen nutzt man recvmsg
, was in der Benutzung ähnlich ist. Dort kommt die selbe struct für die Message zum Einsatz und man muss die Felder auch genauso setzen.
Ich hab ein kleines Beispielprogramm geschrieben, welches nach einem Fork eine Datei im Elternprozess öffnet und anschließend den Filedescriptor dieser Datei an den Kindprozess sendet. Das Kind empfängt dann den Filedescriptor und schreibt etwas in diese Datei.
Es ist möglich Programme einfach anzuhalten, und zu einem beliebigen Zeitpunkt fortzusetzen. Zum stoppen schickt man das SIGSTOP-Signal an den Prozess, zum fortsetzen SIGCONT.
Kleine Demonstration, in der der Sekundenzeiger von xclock angehalten wird.
$ xclock -update 1 &
$ kill -STOP $!
$ kill -CONT $!