如何在Android中创建命名管道(mkfifo)?

15

我在Android上创建命名管道时遇到了问题,下面的示例说明了我的困境:

res = mkfifo("/sdcard/fifo9000", S_IRWXO);
if (res != 0)
{
    LOG("Error while creating a pipe (return:%d, errno:%d)", res, errno);
}

这段代码总是打印出:

Error while creating a pipe (return:-1, errno:1)

我无法确定为什么这会失败。 应用程序具有android.permission.WRITE_EXTERNAL_STORAGE权限。 我可以在相同位置使用完全相同的名称创建普通文件,但管道创建失败。 相关的管道应该可以从多个应用程序访问。

  1. 我怀疑没有人能在 /sdcard 中创建管道。最好在哪里创建?
  2. 我应该设置什么模式(第二个参数)?
  3. 应用程序需要任何额外的权限吗?
5个回答

18

Roosmaa的回答是正确的- mkfifo()只调用mknod()来创建一个特殊文件,而FAT32不支持该功能。

作为替代方案,您可以考虑使用Linux的“抽象命名空间”UNIX域套接字。它们应该大致相当于命名管道。您可以通过名称访问它们,但它们不是文件系统的一部分,因此您不必处理各种权限问题。请注意,套接字是双向的。

由于这是一个套接字,您可能需要INTERNET权限。不确定。

以下是一个快速的客户端/服务器示例代码:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stddef.h>
#include <sys/socket.h>
#include <sys/un.h>

/*
 * Create a UNIX-domain socket address in the Linux "abstract namespace".
 *
 * The socket code doesn't require null termination on the filename, but
 * we do it anyway so string functions work.
 */
int makeAddr(const char* name, struct sockaddr_un* pAddr, socklen_t* pSockLen)
{
    int nameLen = strlen(name);
    if (nameLen >= (int) sizeof(pAddr->sun_path) -1)  /* too long? */
        return -1;
    pAddr->sun_path[0] = '\0';  /* abstract namespace */
    strcpy(pAddr->sun_path+1, name);
    pAddr->sun_family = AF_LOCAL;
    *pSockLen = 1 + nameLen + offsetof(struct sockaddr_un, sun_path);
    return 0;
}

int main(int argc, char** argv)
{
    static const char* message = "hello, world!";
    struct sockaddr_un sockAddr;
    socklen_t sockLen;
    int result = 1;

    if (argc != 2 || (argv[1][0] != 'c' && argv[1][0] != 's')) {
        printf("Usage: {c|s}\n");
        return 2;
    }

    if (makeAddr("com.whoever.xfer", &sockAddr, &sockLen) < 0)
        return 1;
    int fd = socket(AF_LOCAL, SOCK_STREAM, PF_UNIX);
    if (fd < 0) {
        perror("client socket()");
        return 1;
    }

    if (argv[1][0] == 'c') {
        printf("CLIENT %s\n", sockAddr.sun_path+1);

        if (connect(fd, (const struct sockaddr*) &sockAddr, sockLen) < 0) {
            perror("client connect()");
            goto bail;
        }
        if (write(fd, message, strlen(message)+1) < 0) {
            perror("client write()");
            goto bail;
        }
    } else if (argv[1][0] == 's') {
        printf("SERVER %s\n", sockAddr.sun_path+1);
        if (bind(fd, (const struct sockaddr*) &sockAddr, sockLen) < 0) {
            perror("server bind()");
            goto bail;
        }
        if (listen(fd, 5) < 0) {
            perror("server listen()");
            goto bail;
        }
        int clientSock = accept(fd, NULL, NULL);
        if (clientSock < 0) {
            perror("server accept");
            goto bail;
        }
        char buf[64];
        int count = read(clientSock, buf, sizeof(buf));
        close(clientSock);
        if (count < 0) {
            perror("server read");
            goto bail;
        }
        printf("GOT: '%s'\n", buf);
    }
    result = 0;

bail:
    close(fd);
    return result;
}

这可能很难在Java中复制 - 只是说 :) - KevinDTimm
1
没错,但原始代码片段也不是Java,所以我觉得这不是一个破坏者。套接字方法比文件系统中的命名管道更需要JNI。 - fadden
2
Berkus:套接字调用需要字符串长度,而不是以空字符结尾的字符串,因此您需要计算并传递它。 (使用空终止符和过长的长度可能会正常工作,但我认为上述方法是严谨正确的方法。) - fadden
这种方法在Android上可行,我刚试过了。您可以使用Java中的LocalSocketAddress和LocalSocket类来访问使用上述代码打开的套接字。 - Nurpax
1
自从Android 5.1以来,套接字存在一些问题。请参阅我的问题:https://dev59.com/I4zda4cB1Zd3GeqPlmP- - Fox
显示剩余4条评论

8

/sdcard的默认文件系统是FAT32,不支持命名管道。

在未root的设备上,您唯一可以尝试创建这些管道的地方是应用数据目录/data/data/com.example/。 注意:您不应硬编码该值,而应使用Context.getApplicationInfo().dataDir。

但请注意,每当用户使用Apps2SD或Google正式实现该支持时,您需要确保让用户知道该应用程序无法存储在VFAT文件系统中。


1
我希望您能附加到已接受的答案:
1)我能够使用此方法在Android应用程序的两个本地模块之间连接套接字。
2)由于write()可能无法一次性写入请求的全部内容,因此它应该在循环中。例如,应该像这样读取:
void *p = buffer;
count = 0;
while ((count += write(clientSock, buffer, num_bytes - count)) < num_bytes)
{
    if (count < 0)
    {
        close(clientSock);
        errCode = count;
        break;
    }
    p += count;
}

上面所示的错误处理不足,因为有几个错误代码只是表示需要重试。请参阅write的文档。

您可以使用TEMP_FAILURE_RETRY()在EINTR上进行重试。 - fadden

1

还有一个/sqlite_stmt_journals目录(我们用它进行测试,我不知道这个目录会在操作系统更新后存活多久)

如果您需要IPC,最佳实践是使用Binders

如果您只需要线程间通信,则可以通过JNI使用未命名管道(这很好用)


是的,这就是我最终使用的方法。关键在于让本地代码与本地代码进行交互。通过 Java 进行操作会使得问题更加复杂(需要 JNI 等),而且可能会降低速度。虽然 C++ 中可用 Binder 接口,但需要完整的 Android 源码,这是一个巨大的障碍。 - Ignas Limanauskas
我们在未命名管道上使用JNI,因为我们有Java代码异步发送数据到本地线程;但是如果您只需要使本地线程与同一进程中的其他本地线程通信,则不需要JNI。我应该补充说明的是,管道比套接字更快,更轻。 - Stéphane

-1

1
PipedInputStream和PipedOutputStream能否像C++中的命名管道一样跨多个应用程序使用,以供两个不同的应用程序进行读写操作? - Ahmed_Faraz
1
不行,它只能在进程中使用。 - Jesse Wilson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接