同时读/写同一个unix套接字?

Nar*_*asK 10 linux unix-sockets

两个或多个进程并发读/写是否可以unix socket

我做了一些测试。

这是 my sock_test.sh,它产生 50 个客户端,每个客户端同时写入 5K 消息:

#! /bin/bash --

SOC='/tmp/tst.socket'

test_fn() {
  soc=$1
  txt=$2
  for x in {1..5000}; do
    echo "${txt}" | socat - UNIX-CONNECT:"${soc}"
  done
}

for x in {01..50}; do
  test_fn "${SOC}" "Test_${x}" &
done
Run Code Online (Sandbox Code Playgroud)

然后我创建一个unix socket并将所有流量捕获到文件sock_test.txt

# netcat -klU /tmp/tst.socket | tee ./sock_test.txt
Run Code Online (Sandbox Code Playgroud)

最后,我运行我的测试脚本 ( sock_test.sh) 并在屏幕上监控所有 50 名工人的工作。最后,我检查所有消息是否都已到达目的地:

# ./sock_test.sh
# sort ./sock_test.txt | uniq -c
Run Code Online (Sandbox Code Playgroud)

令我惊讶的是,没有错误,所有 50 名工作人员都成功发送了所有 5K 消息。

我想我必须得出结论,同时写入unix sockets可以吗?

我的并发级别是否太低而无法看到冲突?

我的测试方法有问题吗?那我如何正确测试它?

编辑

在对这个问题的出色回答之后,对于那些更熟悉python我的测试台的人:

#! /usr/bin/python3 -u
# coding: utf-8

import socket
from concurrent import futures


pow_of_two = ['B','KB','MB','GB','TB']
bytes_dict = {x: 1024**pow_of_two.index(x) for x in pow_of_two}
SOC = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
SOC.connect('/tmp/tst.socket')


def write_buffer(
    char: 'default is a' = 'a',
    sock: 'default is /tmp/tst.socket' = SOC,
    step: 'default is 8KB' = 8 * bytes_dict['KB'],
    last: 'default is 2MB' = 2 * bytes_dict['MB']):

    print('## Dumping to the socket: {0}'.format(sock))
    while True:
        in_memory = bytearray([ord(char) for x in range(step)])
        msg = 'Dumping {0} bytes of {1}'
        print(msg.format(step, char))
        sock.sendall(bytes(str(step), 'utf8') + in_memory)
        step += step
        if last % step >= last:
            break


def workers(concurrency=5):
    chars = concurrency * ['a', 'b', 'c', 'd']
    with futures.ThreadPoolExecutor() as executor:
        for c in chars:
            executor.submit(write_buffer, c)


def parser(chars, file='./sock_test.txt'):
    with open(file=file, mode='rt', buffering=8192) as f:
        digits = set(str(d) for d in range(0, 10))
        def is_digit(d):
            return d in digits
        def printer(char, size, found, junk):
            msg = 'Checking {}, Expected {:8s}, Found {:8s}, Junk {:8s}, Does Match: {}'
            print(msg.format(char, size, str(found), str(junk), size == str(found)))
        char, size, found, junk = '', '', 0, 0
        prev = None
        for x in f.read():
            if is_digit(x):
                if not is_digit(prev) and prev is not None:
                    printer(char, size, found, junk)
                    size = x
                else:
                    size += x
            else:
                if is_digit(prev):
                    char, found, junk = x, 1, 0
                else:
                    if x==char:
                        found += 1
                    else:
                        junk += 1
            prev = x
        else:
            printer(char, size, found, junk)


if __name__ == "__main__":
    workers()
    parser(['a', 'b', 'c', 'd'])
Run Code Online (Sandbox Code Playgroud)

然后在输出中,您可能会观察到如下几行:

Checking b, Expected 131072  , Found 131072  , Junk 0       , Does Match: True
Checking d, Expected 262144  , Found 262144  , Junk 0       , Does Match: True
Checking b, Expected 524288  , Found 219258  , Junk 0       , Does Match: False
Checking d, Expected 524288  , Found 219258  , Junk 0       , Does Match: False
Checking c, Expected 8192    , Found 8192    , Junk 0       , Does Match: True
Checking c, Expected 16384   , Found 16384   , Junk 0       , Does Match: True
Checking c, Expected 32768   , Found 32768   , Junk 610060  , Does Match: True
Checking c, Expected 524288  , Found 524288  , Junk 0       , Does Match: True
Checking b, Expected 262144  , Found 262144  , Junk 0       , Does Match: True
Run Code Online (Sandbox Code Playgroud)

您可以看到某些情况下的有效负载 ( b, d) 是不完整的,但是稍后会收到丢失的片段 ( c)。简单的数学证明:

# Expected
b + d = 524288 + 524288 = 1048576
# Found b,d + extra fragment on the other check on c
b + d + c = 219258 + 219258 + 610060 = 1048576
Run Code Online (Sandbox Code Playgroud)

因此同时写入unix socketsOK NOT OK。

thr*_*rig 5

那是一条很短的测试线。尝试大于netcat或使用的缓冲区大小socat,并从多个测试实例多次发送该字符串;这是一个sender执行此操作的程序:

#!/usr/bin/env expect

package require Tcl 8.5

set socket    [lindex $argv 0]
set character [string index [lindex $argv 1] 0]
set length    [lindex $argv 2]
set repeat    [lindex $argv 3]

set fh [open "| socat - UNIX-CONNECT:$socket" w]
# avoid TCL buffering screwing with our results
chan configure $fh -buffering none

set teststr   [string repeat $character $length]

while {$repeat > 0} {
    puts -nonewline $fh $teststr
    incr repeat -1
}
Run Code Online (Sandbox Code Playgroud)

然后 alauncher使用很长的不同测试字符 (9999) 多次调用 (25) 多次 (100) 以希望很好地超过任何缓冲区边界:

#!/bin/sh

# NOTE this is a very bad idea on a shared system
SOCKET=/tmp/blabla

for char in a b c d e f g h i j k l m n o p q r s t u v w x y; do
    ./sender -- "$SOCKET" "$char" 9999 100 &
done

wait
Run Code Online (Sandbox Code Playgroud)

嗯,我没有netcat希望nc在 Centos 7 上就足够了:

$ nc -klU /tmp/blabla > /tmp/out
Run Code Online (Sandbox Code Playgroud)

然后在其他地方我们向其提供数据

$ ./launcher
Run Code Online (Sandbox Code Playgroud)

现在我们/tmp/out会很尴尬,因为没有换行符(某些内容基于换行符缓冲,因此换行符会影响测试结果,如果是这种情况,请参阅setbuf(3)基于行的缓冲的潜力)所以我们需要代码来寻找字符,并计算前一个相同字符序列的长度。

#include <stdio.h>

int main(int argc, char *argv[])
{
    int current, previous;
    unsigned long count = 1;

    previous = getchar();
    if (previous == EOF) return 1;

    while ((current = getchar()) != EOF) {
        if (current != previous) {
            printf("%lu %c\n", count, previous);
            count = 0;
            previous = current;
        }
        count++;
    }
    printf("%lu %c\n", count, previous);
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

哦,小C!让我们编译和解析我们的输出...

$ make parse
cc     parse.c   -o parse
$ ./parse < /tmp/out | head
49152 b
475136 a
57344 b
106496 a
49152 b
49152 a
38189 r
57344 b
57344 a
49152 b
$ 
Run Code Online (Sandbox Code Playgroud)

哦哦。那看起来不对。9999 * 100应该是连续单个字母的 999,900,而我们得到了……不是那样。a并且b很早就开始了,但看起来r不知何故得到了一些早期的机会。这就是你的工作安排。换句话说,输出已损坏。接近文件末尾怎么样?

$ ./parse < /tmp/out | tail
8192 l
8192 v
476 d
476 g
8192 l
8192 v
8192 l
8192 v
476 l
16860 v
$ echo $((9999 * 100 / 8192))
122
$ echo $((9999 * 100 - 8192 * 122))
476
$
Run Code Online (Sandbox Code Playgroud)

看起来 8192 是此系统上的缓冲区大小。无论如何!您的测试输入太短,无法超过缓冲区长度,并给人一种错误的印象,即多个客户端写入是可以的。增加来自客户端的数据量,您将看到混合并因此损坏的输出。