diff --git a/doc/services/portability/posix/option_groups/index.rst b/doc/services/portability/posix/option_groups/index.rst index e67523ae3d7c..6af6625d594e 100644 --- a/doc/services/portability/posix/option_groups/index.rst +++ b/doc/services/portability/posix/option_groups/index.rst @@ -564,13 +564,13 @@ This table lists service support status in Zephyr for `POSIX_FD_MGMT`: :header: API, Supported :widths: 50,10 - flockfile(), - ftrylockfile(), - funlockfile(), - getc_unlocked(), - getchar_unlocked(), - putc_unlocked(), - putchar_unlocked(), + flockfile(), yes + ftrylockfile(), yes + funlockfile(), yes + getc_unlocked(), yes :ref:`†` + getchar_unlocked(), yes :ref:`†` + putc_unlocked(), yes + putchar_unlocked(), yes .. _posix_option_group_memory_protection: @@ -901,11 +901,11 @@ _POSIX_THREAD_SAFE_FUNCTIONS asctime_r(), ctime_r(), - flockfile(), - ftrylockfile(), - funlockfile(), - getc_unlocked(), - getchar_unlocked(), + flockfile(), yes + ftrylockfile(), yes + funlockfile(), yes + getc_unlocked(), yes :ref:`†` + getchar_unlocked(), yes :ref:`†` getgrgid_r(), getgrnam_r(), getpwnam_r(), diff --git a/lib/libc/Kconfig b/lib/libc/Kconfig index 6607f62c521f..94d7bc14e7cd 100644 --- a/lib/libc/Kconfig +++ b/lib/libc/Kconfig @@ -75,6 +75,9 @@ config MINIMAL_LIBC imply COMMON_LIBC_MALLOC imply COMMON_LIBC_CALLOC imply COMMON_LIBC_REALLOCARRAY + select POSIX_FILE_LOCKING if POSIX_THREAD_SAFE_FUNCTIONS + select COMMON_LIBC_PUTC_UNLOCKED if POSIX_THREAD_SAFE_FUNCTIONS + select COMMON_LIBC_GETC_UNLOCKED if POSIX_THREAD_SAFE_FUNCTIONS help Build with minimal C library. diff --git a/lib/libc/common/CMakeLists.txt b/lib/libc/common/CMakeLists.txt index 64fe33d1c4ad..1a822a96d6c2 100644 --- a/lib/libc/common/CMakeLists.txt +++ b/lib/libc/common/CMakeLists.txt @@ -17,5 +17,7 @@ zephyr_library_sources_ifdef(CONFIG_COMMON_LIBC_THRD source/thrd/tss.c ) +add_subdirectory(source) + # Prevent compiler from optimizing calloc into an infinite recursive call zephyr_library_compile_options($) diff --git a/lib/libc/common/Kconfig b/lib/libc/common/Kconfig index ea0d6869435c..253876999419 100644 --- a/lib/libc/common/Kconfig +++ b/lib/libc/common/Kconfig @@ -82,3 +82,5 @@ config COMMON_LIBC_THRD default y help Common implementation of C11 API. + +rsource "source/Kconfig" diff --git a/lib/libc/common/source/CMakeLists.txt b/lib/libc/common/source/CMakeLists.txt new file mode 100644 index 000000000000..d80868a73ee2 --- /dev/null +++ b/lib/libc/common/source/CMakeLists.txt @@ -0,0 +1,4 @@ +# Copyright (c) 2024 Meta Platforms +# SPDX-License-Identifier: Apache-2.0 + +add_subdirectory_ifdef(CONFIG_COMMON_LIBC_STDIO stdio) diff --git a/lib/libc/common/source/Kconfig b/lib/libc/common/source/Kconfig new file mode 100644 index 000000000000..4bc4ea4d1851 --- /dev/null +++ b/lib/libc/common/source/Kconfig @@ -0,0 +1,4 @@ +# Copyright (c) 2024 Meta Platforms +# SPDX-License-Identifier: Apache-2.0 + +rsource "stdio/Kconfig" diff --git a/lib/libc/common/source/stdio/CMakeLists.txt b/lib/libc/common/source/stdio/CMakeLists.txt new file mode 100644 index 000000000000..922c590e81a5 --- /dev/null +++ b/lib/libc/common/source/stdio/CMakeLists.txt @@ -0,0 +1,5 @@ +# Copyright (c) 2024 Meta Platforms +# SPDX-License-Identifier: Apache-2.0 + +zephyr_library_sources_ifdef(CONFIG_COMMON_LIBC_GETC_UNLOCKED getc_unlocked.c) +zephyr_library_sources_ifdef(CONFIG_COMMON_LIBC_PUTC_UNLOCKED putc_unlocked.c) diff --git a/lib/libc/common/source/stdio/Kconfig b/lib/libc/common/source/stdio/Kconfig new file mode 100644 index 000000000000..fe21143e9122 --- /dev/null +++ b/lib/libc/common/source/stdio/Kconfig @@ -0,0 +1,19 @@ +# Copyright (c) 2024 Meta Platforms +# SPDX-License-Identifier: Apache-2.0 + +config COMMON_LIBC_STDIO + bool + help + common function implementation in stdio.h + +config COMMON_LIBC_GETC_UNLOCKED + bool + select COMMON_LIBC_STDIO + help + common implementation of getc_unlocked() & getchar_unlocked(). + +config COMMON_LIBC_PUTC_UNLOCKED + bool + select COMMON_LIBC_STDIO + help + common implementation of putc_unlocked() & putchar_unlocked(). diff --git a/lib/libc/common/source/stdio/getc_unlocked.c b/lib/libc/common/source/stdio/getc_unlocked.c new file mode 100644 index 000000000000..94d97e70102e --- /dev/null +++ b/lib/libc/common/source/stdio/getc_unlocked.c @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2024 Meta Platforms + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include + +int getc_unlocked(FILE *stream) +{ + ARG_UNUSED(stream); + + errno = ENOSYS; + + return EOF; +} + +int getchar_unlocked(void) +{ + return getc_unlocked(stdin); +} diff --git a/lib/libc/common/source/stdio/putc_unlocked.c b/lib/libc/common/source/stdio/putc_unlocked.c new file mode 100644 index 000000000000..5bd6ecf6cd44 --- /dev/null +++ b/lib/libc/common/source/stdio/putc_unlocked.c @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2024 Meta Platforms + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include + +int putc_unlocked(int c, FILE *stream) +{ + return zephyr_fputc(c, stream); +} + +int putchar_unlocked(int c) +{ + return putc_unlocked(c, stdout); +} diff --git a/lib/libc/minimal/include/stdio.h b/lib/libc/minimal/include/stdio.h index db3c89790c42..75767738af9e 100644 --- a/lib/libc/minimal/include/stdio.h +++ b/lib/libc/minimal/include/stdio.h @@ -63,6 +63,22 @@ size_t fwrite(const void *ZRESTRICT ptr, size_t size, size_t nitems, #define putc(c, stream) fputc(c, stream) #define putchar(c) putc(c, stdout) +#if defined(CONFIG_COMMON_LIBC_PUTC_UNLOCKED) || defined(__DOXYGEN__) +int putc_unlocked(int c, FILE *stream); +int putchar_unlocked(int c); +#endif /* CONFIG_COMMON_LIBC_PUTC_UNLOCKED || __DOXYGEN__ */ + +#if defined(CONFIG_COMMON_LIBC_GETC_UNLOCKED) || defined(__DOXYGEN__) +int getc_unlocked(FILE *stream); +int getchar_unlocked(void); +#endif /* CONFIG_COMMON_LIBC_GETC_UNLOCKED || __DOXYGEN__ */ + +#if defined(CONFIG_POSIX_FILE_LOCKING) || defined(__DOXYGEN__) +void flockfile(FILE *file); +int ftrylockfile(FILE *file); +void funlockfile(FILE *file); +#endif /* CONFIG_POSIX_FILE_LOCKING || __DOXYGEN__ */ + #ifdef __cplusplus } #endif diff --git a/lib/os/fdtable.c b/lib/os/fdtable.c index 534e212aa646..b4536370ddaf 100644 --- a/lib/os/fdtable.c +++ b/lib/os/fdtable.c @@ -392,6 +392,35 @@ int zvfs_fsync(int fd) return z_fdtable_call_ioctl(fdtable[fd].vtable, fdtable[fd].obj, ZFD_IOCTL_FSYNC); } +#if defined(CONFIG_POSIX_FILE_LOCKING) +void zvfs_flockfile(int fd) +{ + if (_check_fd(fd) < 0) { + return; + } + + (void)k_mutex_lock(&fdtable[fd].lock, K_FOREVER); +} + +int zvfs_ftrylockfile(int fd) +{ + if (_check_fd(fd) < 0) { + return -1; + } + + return k_mutex_lock(&fdtable[fd].lock, K_NO_WAIT); +} + +void zvfs_funlockfile(int fd) +{ + if (_check_fd(fd) < 0) { + return; + } + + (void)k_mutex_unlock(&fdtable[fd].lock); +} +#endif /* CONFIG_POSIX_FILE_LOCKING */ + static inline off_t zvfs_lseek_wrap(int fd, int cmd, ...) { off_t res; diff --git a/lib/posix/options/CMakeLists.txt b/lib/posix/options/CMakeLists.txt index f7da027223d4..6fddb9ed8ec7 100644 --- a/lib/posix/options/CMakeLists.txt +++ b/lib/posix/options/CMakeLists.txt @@ -55,6 +55,7 @@ zephyr_library_sources_ifdef(CONFIG_POSIX_MEMLOCK mlockall.c) zephyr_library_sources_ifdef(CONFIG_POSIX_MEMLOCK_RANGE mlock.c) zephyr_library_sources_ifdef(CONFIG_POSIX_MEMORY_PROTECTION mprotect.c) zephyr_library_sources_ifdef(CONFIG_POSIX_MAPPED_FILES mmap.c) +zephyr_library_sources_ifdef(CONFIG_POSIX_FILE_LOCKING file_locking.c) zephyr_library_sources_ifdef(CONFIG_POSIX_MESSAGE_PASSING mqueue.c) zephyr_library_sources_ifdef(CONFIG_POSIX_MULTI_PROCESS sleep.c diff --git a/lib/posix/options/Kconfig b/lib/posix/options/Kconfig index bdf0ed4901f0..07b76410a03c 100644 --- a/lib/posix/options/Kconfig +++ b/lib/posix/options/Kconfig @@ -13,6 +13,7 @@ rsource "Kconfig.barrier" rsource "Kconfig.c_lib_ext" rsource "Kconfig.device_io" rsource "Kconfig.fd_mgmt" +rsource "Kconfig.file_locking" rsource "Kconfig.fs" rsource "Kconfig.mem" rsource "Kconfig.mqueue" diff --git a/lib/posix/options/Kconfig.file_locking b/lib/posix/options/Kconfig.file_locking new file mode 100644 index 000000000000..7a731a592ce6 --- /dev/null +++ b/lib/posix/options/Kconfig.file_locking @@ -0,0 +1,38 @@ +# Copyright (c) 2024 Meta Platforms +# +# SPDX-License-Identifier: Apache-2.0 + +config POSIX_FILE_LOCKING + bool "POSIX file locking [EXPERIMENTAL]" + select EXPERIMENTAL + select FDTABLE + help + Select 'y' here and Zephyr will provide implementations for the POSIX_FILE_LOCKING Option + Group. + This includes support for flockfile(), ftrylockfile(), funlockfile(), getc_unlocked(), + getchar_unlocked(), putc_unlocked() and putchar_unlocked(). + + For more information, please see + https://pubs.opengroup.org/onlinepubs/9699919799/xrat/V4_subprofiles.html + +if POSIX_FILE_LOCKING + +# These options are intended to be used for compatibility with external POSIX +# implementations such as those in Newlib or Picolibc. + +config POSIX_FD_MGMT_ALIAS_FLOCKFILE + bool + help + Select 'y' here and Zephyr will provide an alias for flockfile() as _flockfile(). + +config POSIX_FD_MGMT_ALIAS_FTRYLOCKFILE + bool + help + Select 'y' here and Zephyr will provide an alias for ftrylockfile() as _ftrylockfile(). + +config POSIX_FD_MGMT_ALIAS_FUNLOCKFILE + bool + help + Select 'y' here and Zephyr will provide an alias for funlockfile() as _funlockfile(). + +endif # POSIX_FILE_LOCKING diff --git a/lib/posix/options/Kconfig.pthread b/lib/posix/options/Kconfig.pthread index ec38286ff49a..c779d2f2c8ac 100644 --- a/lib/posix/options/Kconfig.pthread +++ b/lib/posix/options/Kconfig.pthread @@ -156,6 +156,7 @@ config POSIX_THREAD_PRIO_PROTECT config POSIX_THREAD_SAFE_FUNCTIONS bool "POSIX thread-safe functions" + select POSIX_FILE_LOCKING help Select 'y' here to enable POSIX thread-safe functions including asctime_r(), ctime_r(), flockfile(), ftrylockfile(), funlockfile(), getc_unlocked(), getchar_unlocked(), diff --git a/lib/posix/options/file_locking.c b/lib/posix/options/file_locking.c new file mode 100644 index 000000000000..27a0e0fff5ce --- /dev/null +++ b/lib/posix/options/file_locking.c @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2024 Meta Platforms + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include +#include + +void zvfs_flockfile(int fd); +int zvfs_ftrylockfile(int fd); +void zvfs_funlockfile(int fd); + +void flockfile(FILE *file) +{ + zvfs_flockfile(POINTER_TO_INT(file)); +} +#ifdef CONFIG_POSIX_FD_MGMT_ALIAS_FLOCKFILE +FUNC_ALIAS(flockfile, _flockfile, void); +#endif /* CONFIG_POSIX_FD_MGMT_ALIAS_FLOCKFILE */ + +int ftrylockfile(FILE *file) +{ + return zvfs_ftrylockfile(POINTER_TO_INT(file)); +} +#ifdef CONFIG_POSIX_FD_MGMT_ALIAS_FTRYLOCKFILE +FUNC_ALIAS(ftrylockfile, _ftrylockfile, int); +#endif /* CONFIG_POSIX_FD_MGMT_ALIAS_FTRYLOCKFILE */ + +void funlockfile(FILE *file) +{ + zvfs_funlockfile(POINTER_TO_INT(file)); +} +#ifdef CONFIG_POSIX_FD_MGMT_ALIAS_FUNLOCKFILE +FUNC_ALIAS(funlockfile, _funlockfile, void); +#endif /* CONFIG_POSIX_FD_MGMT_ALIAS_FUNLOCKFILE */ diff --git a/tests/posix/common/src/file_locking.c b/tests/posix/common/src/file_locking.c new file mode 100644 index 000000000000..c0b24025a925 --- /dev/null +++ b/tests/posix/common/src/file_locking.c @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2024, Meta Platforms + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include +#include + +#ifndef CONFIG_PICOLIBC + +K_THREAD_STACK_DEFINE(test_stack, 1024); + +#define LOCK_SHOULD_PASS (void *)true +#define LOCK_SHOULD_FAIL (void *)false +#define UNLOCK_FILE (void *)true +#define NO_UNLOCK_FILE (void *)false + +void ftrylockfile_thread(void *p1, void *p2, void *p3) +{ + int ret; + FILE *file = p1; + bool success = (bool)p2; + bool unlock = (bool)p3; + + if (success) { + ret = ftrylockfile(file); + zassert_ok(ret, "Expected ftrylockfile to succeed but it failed: %d", ret); + if (unlock) { + funlockfile(file); + } + } else { + zassert_not_ok(ftrylockfile(file), + "Expected ftrylockfile to failed but it succeeded"); + } +} + +void flockfile_thread(void *p1, void *p2, void *p3) +{ + FILE *file = p1; + bool success = (bool)p2; + bool unlock = (bool)p3; + + flockfile(file); + + if (!success) { + /* Shouldn't be here if it supposed to fail */ + ztest_test_fail(); + } + + if (unlock) { + funlockfile(file); + } +} + +ZTEST(file_locking, test_file_locking) +{ + FILE *file = INT_TO_POINTER(z_alloc_fd(NULL, NULL)); + int priority = k_thread_priority_get(k_current_get()); + struct k_thread test_thread; + + /* Lock 5 times with flockfile */ + for (int i = 0; i < 5; i++) { + flockfile(file); + } + + /* Lock 5 times with ftrylockfile */ + for (int i = 0; i < 5; i++) { + zassert_ok(ftrylockfile(file)); + } + + /* Spawn a thread that uses ftrylockfile(), it should fail immediately */ + k_thread_create(&test_thread, test_stack, K_THREAD_STACK_SIZEOF(test_stack), + ftrylockfile_thread, file, LOCK_SHOULD_FAIL, NO_UNLOCK_FILE, priority, 0, + K_NO_WAIT); + /* The thread should terminate immediately */ + zassert_ok(k_thread_join(&test_thread, K_MSEC(100))); + + /* Try agian with flockfile(), it should block forever */ + k_thread_create(&test_thread, test_stack, K_THREAD_STACK_SIZEOF(test_stack), + flockfile_thread, file, LOCK_SHOULD_FAIL, NO_UNLOCK_FILE, priority, 0, + K_NO_WAIT); + /* We expect the flockfile() call to block forever, so this will timeout */ + zassert_equal(k_thread_join(&test_thread, K_MSEC(500)), -EAGAIN); + /* Abort the test thread */ + k_thread_abort(&test_thread); + + /* Unlock the file completely in this thread */ + for (int i = 0; i < 10; i++) { + funlockfile(file); + } + + /* Spawn the thread again, which should be able to lock the file now with ftrylockfile() */ + k_thread_create(&test_thread, test_stack, K_THREAD_STACK_SIZEOF(test_stack), + ftrylockfile_thread, file, LOCK_SHOULD_PASS, UNLOCK_FILE, priority, 0, + K_NO_WAIT); + zassert_ok(k_thread_join(&test_thread, K_MSEC(100))); + + z_free_fd(POINTER_TO_INT(file)); +} + +void put_thread(void *p1, void *p2, void *p3) +{ + FILE *file = p1; + + /* Lock held in main thread */ + zassert_not_ok(ftrylockfile(file)); + + /* Wait for the lock */ + flockfile(file); + zassert_equal(putc_unlocked('S', file), 'S'); + zassert_equal(putchar('T'), 'T'); + funlockfile(file); +} + +ZTEST(file_locking, test_stdio) +{ + FILE *file = INT_TO_POINTER(z_alloc_fd(NULL, NULL)); + struct k_thread test_thread; + int priority = k_thread_priority_get(k_current_get()); + + /* Lock the file before creating the test thread */ + flockfile(file); + + k_thread_create(&test_thread, test_stack, K_THREAD_STACK_SIZEOF(test_stack), + put_thread, file, INT_TO_POINTER(true), NULL, priority, 0, + K_NO_WAIT); + + /* Allow the test thread to run */ + k_msleep(100); + /* The test thread should be waiting for the lock */ + zassert_equal(k_thread_join(&test_thread, K_MSEC(10)), -EAGAIN); + + /* Main thread has the lock, either version should work */ + zassert_equal(putc('T', file), 'T'); + zassert_equal(putchar_unlocked('E'), 'E'); + + /* We are done with the file here, unlock it so that test thread can run */ + funlockfile(file); + zassert_equal(k_thread_join(&test_thread, K_MSEC(100)), 0); + + z_free_fd(POINTER_TO_INT(file)); +} + +/** + * @brief Existence test for the stubs + */ +ZTEST(file_locking, test_stubs) +{ +#ifndef CONFIG_NEWLIB_LIBC + errno = 0; + zassert_equal(getchar_unlocked(), EOF); + zassert_equal(errno, ENOSYS); + + errno = 0; + zassert_equal(getc_unlocked(stdin), EOF); + zassert_equal(errno, ENOSYS); +#else + ztest_test_skip(); +#endif /* !CONFIG_NEWLIB_LIBC */ +} + +#else +/** + * PicoLIBC doesn't support these functions in its header + * Skip the tests for now. + */ +ZTEST(file_locking, test_file_locking) +{ + ztest_test_skip(); +} +#endif /* CONFIG_PICOLIBC */ + +ZTEST_SUITE(file_locking, NULL, NULL, NULL, NULL, NULL);