0%

线程

这部分APUE没有详细介绍,资料来自网络

线程生命周期

进程,线程,协程

  • 进程是资源分配的最小单位,例如全局变量、代码段、堆、栈等。

  • 线程是系统调度的最小单位,线程之间共享进程的资源。

  • 协程可以理解为用户层的线程,其调度不由操作系统进行而由用户应用进行,相比线程更加轻量。

pthread使用

编译使用了pthread的程序时,需要输入cc -pthread xxx.c

pthread_t

线程标识符,linux上实现为unsigned long,可以直接打印

pthread_create

创建一个线程,立即开始执行,参数分别为:

  • 线程id
  • 线程属性
  • 任务函数
  • 函数参数,可以传入结构体表示多个参数
    1
    2
    int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
    void *(*start_routine) (void *), void *arg);

    pthread_exit

    正常退出线程,也可以在线程里返回以终止线程
    1
    void pthread_exit(void *retval);

    线程同步机制

    pthread_mutex_t

    互斥锁,只能有一个线程获得互斥锁,若获取不到lock将阻塞,trylock返回 EBUSY
    1
    2
    3
    4
    5
    6
    7
    8
    9
    int pthread_mutex_init(pthread_mutex_t *restrict mutex,
    const pthread_mutexattr_t *restrict attr);

    // 如果不用堆内存,可以这样初始化
    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

    int pthread_mutex_lock(pthread_mutex_t *mutex);
    int pthread_mutex_trylock(pthread_mutex_t *mutex);
    int pthread_mutex_unlock(pthread_mutex_t *mutex);

pthread_rwlock_t

读写锁,读写之间互斥,写之间互斥,读之间不互斥。也有trylock 版本

1
2
3
4
5
6
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

pthread_cond_t

条件变量,有点类似进程控制中的sigsuspend,等待特定信号到达后继续运行。发送信号可以单播也可以广播

1
2
3
4
5
6
7
8
9
10
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);

// 线程开始等待条件到达
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);

// 发送信号,唤醒等待条件的线程
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);

条件变量需要用mutex保护,一个模板写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 全局mutex,用于同步各线程,保护条件
pthread_mutex_t mutex;

// 我是否可以行动的条件
pthread_cond_t cond;

// 模板
pthread_mutex_lock(&mutex);
while (/* 条件没达成吗 */)
pthread_cond_wait(&cond, &mutex);
/* 临界区代码在此 */
pthread_mutex_unlock(&mutex);
// 非临界区

这是一个等待条件的线程的模板,当线程发现还没轮到它运行,会挂起,直到有一个其他线程发出了pthread_cond_signal(&cond),并且mutex可以获取,线程才能从pthread_cond_wait返回。

这种方式避免了使用自旋锁。若使用自旋锁,CPU会因为重复判断条件而一直处于高负荷状态,对系统整体效率影响很大。

pthread_barrier_t

屏障,必须在指定个数线程到达屏障后,这些线程才能继续运行,count表示线程数

注意,屏障可以重复使用,每次都会等待count个线程到达后放行,不需要重复初始化

1
2
int pthread_barrier_init(pthread_barrier_t *restrict barrier,
const pthread_barrierattr_t *restrict attr, unsigned count);

可重入函数

某些库函数中调用了malloc等函数,若在其调用期间被信号中断,可能引起内存分配错误。

可重入函数是异步信号安全的,它们调用时会阻塞信号,不会引起因信号产生的安全问题。

signal相关函数

sigaction

检查、修改某个信号的动作,act为新动作,可为NULLoldact为已存在动作

1
2
3
4
5
6
7
8
9
10
11
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);

struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};

setjmp longjmp 与 sigsetjmp siglongjmp

这几个函数都用于非本地跳转,类似gotolong处的函数调用后,会从set处返回。

setjmp longjmp不恢复信号屏蔽字,而sigsetjmp siglongjmpset处先保存当前信号屏蔽字,long回来时,恢复之前保存的屏蔽字。

1
2
3
4
5
6
#include <setjmp.h>
int setjmp(jmp_buf env);
int sigsetjmp(sigjmp_buf env, int savesigs);

void longjmp(jmp_buf env, int val);
void siglongjmp(sigjmp_buf env, int val);

sigsuspend

原子操作,保存当前信号屏蔽字,设置信号屏蔽字maskpause,在接收到信号并处理好后,恢复保存的屏蔽字再返回。它不存在临界区内收到信号的问题

1
2
 #include <signal.h>
int sigsuspend(const sigset_t *mask);

system

执行shell命令,注意system的返回值不一定是命令返回值,其实返回值是shell返回值,在shell异常退出时,会返回128 + 信号值

信号机制

信号用于处理异步事件,例如用户输入ctrl + c,调用kill杀死一个进程等

流程

生成信号

生成信号的方法有许多种:

  • 用户在终端输入,例如ctrl + c向前台进程发送SIGINT
  • 硬件异常,由内核产生信号,例如无效内存访问SIGSEGV
  • kill(1)命令与kill(2)函数,在权限允许情况下可以向其他进程发送任意信号
  • 某些软件条件达成

接收与处理信号

进程收到信号时,必须选择以下三个行为之一:

  • 忽略信号。例如通过signal函数注册忽略SIGINTsignal(SIGINT, SIG_IGN);
  • 捕获信号。需要通过signal函数注册,可自定义处理行为
  • 默认动作。不注册行为,则在收到信号时执行系统默认动作。查看系统默认行为,Linux:man 7 signal,FreeBSD:man signal

有两个不能忽略也不能捕获的信号:

  • SIGKILL:直接杀死进程,进程无法选择安全退出,不能继续运行
  • SIGSTOP:停止运行进程,收到SIGCONT后继续运行

signal函数

1
2
3
4
5
6
// Linux
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

// FreeBSD中的定义相同,不过属于库函数,而Linux中属于系统调用

这个复杂的定义,有两行:

  1. typedef定义了sighandler_t类型,它是:
    • 一个函数指针类型
    • 它指向的函数返回void,有一个参数int
  2. 下一行定义了signal函数:
    • 返回sighandler_t类型,它就是传入的第二个参数,或者SIG_ERR
    • 两个参数,int signum代表希望处理的信号,sighandler_t handler是自定义的信号处理函数,这个函数必须是返回void,参数int

示例

kill命令默认会发出SIGTERM信号,如果不注册处理函数,则系统默认行为是:

1
15  SIGTERM  terminate process    software termination signal

一般情况下,用kill杀死进程的过程如下:

  1. kill向指定进程发出SIGTERM
  2. 进程接受到信号,寻找信号处理函数
  3. 执行系统默认行为,终止进程

如果捕获了SIGTERM,则可以在进程停止前进行保存工作,安全退出,甚至可以选择不退出,但这不符合SIGTERM的意义。

代码的含义是:进程在接受到两次SIGTERM后,才正常退出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

static int c = 2;
static void sigterm_handler(int signo) {
printf("handle signal: %d\n", signo);
c--;
}

int
main() {
signal(SIGTERM, &sigterm_handler);
while (c > 0)
pause();
printf("exit\n");
exit(0);
}

让进程后台运行,通过kill发送两次SIGTERM,等待其正常退出

1
2
3
4
5
6
7
8
9
$ ./a.out &
[1] 2587
$ kill 2587
$ handle signal: 15

$ kill 2587
handle signal: 15
exit
$

fork

定义

1
2
3
4
// Linux
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);

父进程与子进程的关系

  1. fork函数父进程返回子进程pid,子进程返回0
  2. 父进程与子进程的全局变量不共享。出于效率,变量是“写时复制”的,父子任意进程写入变量前,创建存储区副本
  3. 父进程与子进程的文件描述符也是复制的,在父进程打开文件后fork,则子进程也已打开了文件,需要同步措施防止它们同时读写文件
  4. 子进程继承了实际id、有效id,工作目录等等

exec

定义

1
2
3
4
5
6
7
8
9
10
11
12
#include <unistd.h>
extern char **environ;
int execl(const char *pathname, const char *arg, ...
/* (char *) NULL */);
int execlp(const char *file, const char *arg, ...
/* (char *) NULL */);
int execle(const char *pathname, const char *arg, ...
/*, (char *) NULL, char *const envp[] */);
int execv(const char *pathname, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],
char *const envp[]);

说明

  1. pathname为路径名,绝对相对均可;file为文件名,若含"/"字符,视为文件名,否则在PATH环境变量中路径搜索可执行文件或shell脚本
  2. fork不同,exec并不产生新进程,它将原始进程的代码段、全局变量数据段、堆、栈替换为新进程,新进程pid并不会改变

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// t18.c,可执行文件为mypid,由exec启动这个进程
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int
main(void)
{
printf("my pid: %d\n", getpid());
exit(0);
}

// t19.c,原始进程,将启动./mypid
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int
main(void)
{
printf("before exec, my pid: %d\n", getpid());
execl("./mypid", NULL);
printf("this line\n");
exit(0);
}

编译,运行:

1
2
3
4
5
$ cc t18.c -o mypid
$ cc t19.c
$ ./a.out
before exec, my pid: 1520
my pid: 1520

定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Linux
#include <stdlib.h>
void *malloc(size_t size);
void *calloc(size_t nmemb, size_t size);
void *realloc(void *ptr, size_t size);
void free(void *ptr);

// FreeBSD
#include <stdlib.h>
#include <malloc_np.h>
void *malloc(size_t size);
void *calloc(size_t number, size_t size);
void *realloc(void *ptr, size_t size);
void free(void *ptr);

区别

函数之间的区别

  1. malloc申请的区域值不被初始化,而calloc会将其初始化为0
  2. malloc输入为需要的字节数,calloc输入为对象数nmemb与每个对象字节数size
  3. realloc为已分配的地址重新分配大小,可增可减

三个函数均通过系统调用sbrk实现。

mallocnew运算符的区别

  1. mallocstdlib.h中的库函数,而new是C++运算符
  2. malloc需要指定所需字节数,new则由编译器推算,无需指定大小
  3. malloc返回void *,后续需要自己转换类型,new则直接返回申请对象类型的指针
  4. malloc申请失败时返回NULLnew则会抛出异常std::bad_alloc
  5. malloc直接在堆上分配内存,new则在自由存储区上分配。大部分编译器用堆实现自由存储区。堆是操作系统维护的内存区域,而自由存储区是C++概念

C程序中的存储空间布局

从内存高地址到低地址,可分为:

  • 未初始化数据段
  • 初始化数据段
  • 正文段

正文段

  1. 主要内容为机器指令
  2. 只读
  3. 只需要一个副本,多次执行共享

初始化数据段

  1. 全局变量
  2. 内核在程序开始执行之前从文件读取值为其初始化
1
int a = 5;

未初始化数据段(Block Started by Symbol, BSS)

  1. 全局变量
  2. 内核在程序开始执行之前将其初始化为0NULL
    1
    int a[100];
    注意指针a是已初始化的,而a数组的内容是未初始化的,一般内核将a数组内容置为全0

  1. 动态内存分配的变量

  1. 局部变量
  2. 函数调用时保存的上下文

软链接中的实际内容为指向文件的路径,而硬链接中包含原始文件的索引节点(inode)。也就是:软链接的inode与原始文件不同,而硬链接的inode与原始文件相同。

当访问软链接时,需要通过文件中的路径来访问原始文件,而硬链接直接通过inode访问即可。

在删除原始文件后,其硬链接链接计数器会减1,若减至0则删除文件数据块。软链接中指向路径不会发生变化,而原始文件路径已经不存在了,所以再次访问软链接会报错No such file or directory,而硬链接因为在创建时,向原始文件的硬链接计数器st_nlink加1了,因此删除原始文件也只是使其减1,并没有删除数据块,硬链接仍然能访问原始数据块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct stat {
dev_t st_dev; /* ID of device containing file */
ino_t st_ino; /* Inode number */
mode_t st_mode; /* File type and mode */
nlink_t st_nlink; /* Number of hard links 硬链接计数器在这里 */
uid_t st_uid; /* User ID of owner */
gid_t st_gid; /* Group ID of owner */
dev_t st_rdev; /* Device ID (if special file) */
off_t st_size; /* Total size, in bytes */
blksize_t st_blksize; /* Block size for filesystem I/O */
blkcnt_t st_blocks; /* Number of 512B blocks allocated */
struct timespec st_atim; /* Time of last access */
struct timespec st_mtim; /* Time of last modification */
struct timespec st_ctim; /* Time of last status change */
};

在shell中测试删除效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
$ echo "something" > data
$ ln data hardlink_data # 硬链接
$ ln -s data softlink_data # 软链接

# data的inode值与hardlink_data值相同,links = 2
$ stat data
File: data
Size: 10 Blocks: 8 IO Block: 4096 regular file
Device: 830h/2096d Inode: 11481 Links: 2
Access: (0644/-rw-r--r--) Uid: ( 1000/scarecrow) Gid: ( 1000/scarecrow)
Access: 2021-06-03 11:41:38.998538700 +0800
Modify: 2021-06-03 11:41:38.998538700 +0800
Change: 2021-06-03 11:42:27.628538700 +0800
Birth: -
$ stat hardlink_data
File: hardlink_data
Size: 10 Blocks: 8 IO Block: 4096 regular file
Device: 830h/2096d Inode: 11481 Links: 2
Access: (0644/-rw-r--r--) Uid: ( 1000/scarecrow) Gid: ( 1000/scarecrow)
Access: 2021-06-03 11:41:38.998538700 +0800
Modify: 2021-06-03 11:41:38.998538700 +0800
Change: 2021-06-03 11:42:27.628538700 +0800
Birth: -

# softlink_data的inode值与data不同
$ stat softlink_data
File: softlink_data -> data
Size: 4 Blocks: 0 IO Block: 4096 symbolic link
Device: 830h/2096d Inode: 42305 Links: 1
Access: (0777/lrwxrwxrwx) Uid: ( 1000/scarecrow) Gid: ( 1000/scarecrow)
Access: 2021-06-03 11:42:59.968538700 +0800
Modify: 2021-06-03 11:42:42.768538700 +0800
Change: 2021-06-03 11:42:42.768538700 +0800
Birth: -

$ rm data
$ cat softlink_data
cat: softlink_data: No such file or directory
$ cat hardlink_data
something

# links 减至1了
$ stat hardlink_data
File: hardlink_data
Size: 10 Blocks: 8 IO Block: 4096 regular file
Device: 830h/2096d Inode: 11481 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 1000/scarecrow) Gid: ( 1000/scarecrow)
Access: 2021-06-03 11:47:07.268538700 +0800
Modify: 2021-06-03 11:41:38.998538700 +0800
Change: 2021-06-03 11:46:50.228538700 +0800
Birth: -

access open的区别

摘自Linux Programmer’s Manual中access函数DESCRIPTION节

The check is done using the calling process’s real UID and GID, rather than the effective IDs as is done when
actually attempting an operation (e.g., open(2)) on the file. Similarly, for the root user, the check uses
the set of permitted capabilities rather than the set of effective capabilities; and for non-root users, the
check uses an empty set of capabilities.

可以看到access系统调用在检测进程对文件的访问权限时,使用的是进程实际用户id和组id。而open系统调用使用的是有效id,大部分情况下它们的表现没有区别,而若有效id与实际id不同时,则会出现访问权限区别。

APUE书中给了一个例子,略作修改后如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int
main(void)
{
// 通过实际id访问文件
if (-1 == access("file.txt", R_OK))
perror("access");
else
printf("access成功\n");

// 通过有效id访问文件
if (-1 == open("file.txt", O_RDONLY))
perror("open");
else
printf("open读成功\n");
exit(0);
}

在shell中测试它的行为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 编译完成的后的文件为a.out,目录下存在一个file.txt文件用于测试

$ ls -l file.txt
-r-------- 1 root root 0 Jun 1 2021 file.txt
# file.txt文件属于root用户,仅root拥有读权限

$ ls -l a.out
-rwxr-xr-x 1 root root 16864 Jun 1 2021 a.out
# a.out可执行文件属于root用户,所有用户均可执行它

$ ./a.out
access: Permission denied
open: Permission denied
# 以普通用户身份执行a.out,access open均无法访问file.txt
# 因为此时进程的 有效id、实际id 均为 "scarecrow"

$ sudo chmod u+s a.out
$ ls -l a.out
-rwsr-xr-x 1 root root 16864 Jun 1 2021 a.out
# 给a.out设置上"S_ISUID"位,在执行a.out时,有效id会被设置成a.out的所有者,本例中为root

$ ./a.out
access: Permission denied
open读成功
# access 使用实际id (scarecrow) 访问,依然无法读取
# open 使用有效id (root) 访问,能读取

实际用户id 有效用户id 保存的设置用户id

与进程关联的id有至少6个:

名称 说明
read id 实际用户id,进程的运行者
read group id 实际组id,进程的运行者所在组
effective user id 有效用户id,进程文件访问权限
effective group id 有效组id,进程文件访问权限
saved user id 有效用户id的副本
saved group id 有效组id的副本

通常实际id不会改变,有效id等于实际id,但在某些情况下可能有效id会改变,例如运行passwd

保存的设置用户id

某些程序,例如at,普通用户能够执行at,但at有时需要超级用户权限,有时候则必须以普通用户权限运行。保存的设置用户id在这时发挥效果,它可以用来判断at是否能将其有效用户id设置为超级用户。

at的所有者是超级用户root,并且其文件权限设置了setuid位,因此普通用户它运行它时:

  • 实际用户id:普通用户
  • 有效用户id:root
  • 保存的设置用户id:root

在需要降低权限时:

  • 实际用户id:普通用户
  • 有效用户id:普通用户
  • 保存的设置用户id:root

需要恢复特权时,保存的设置用户id指出,这个进程是能够恢复成root的,因为它的保存的设置用户id等于rootseteuid函数允许输入的uid等于保存的设置用户id,或者实际用户id:

  • 实际用户id:普通用户
  • 有效用户id:root,能够设置!
  • 保存的设置用户id:root

文件访问权限

每个文件有9个访问权限位,从左至右:

st_mode掩码 含义
S_IRUSR 拥有者读
S_IWUSR 拥有者写
S_IXUSR 拥有者执行
S_IRGRP 组内用户读
S_IWGRP 组内用户写
S_IXGRP 组内用户执行
S_IROTH 其他用户读
S_IWOTH 其他用户写
S_IXOTH 其他用户执行

另外包含3位特殊位,在ls -l中不显示,从左至右为:

st_mode掩码 含义
S_ISUID 执行时设置有效用户id位为拥有者
S_ISGID 执行时设置有效组id位为拥有者所在组
S_ISVTX 拥有者才能删除、重命名

由于/usr/bin/passwd在运行时能够修改用户密码等内容,也就是修改/etc/passwd中内容,但是/etc/passwd只有root用户才能修改。因此普通用户若需要运行passwd命令,就需要将effective id设为超级用户,于是/usr/bin/passwd文件设置了S_ISUID位:

1
2
3
4
5
6
7
8
9
$ stat /usr/bin/passwd
File: /usr/bin/passwd
Size: 68208 Blocks: 136 IO Block: 4096 regular file
Device: 830h/2096d Inode: 2102 Links: 1
Access: (4755/-rwsr-xr-x) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2021-03-25 15:43:40.040000000 +0800
Modify: 2020-05-28 14:37:47.000000000 +0800
Change: 2021-03-25 15:43:25.060000000 +0800
Birth: -

注意Access: (4755/-rwsr-xr-x)4755二进制表示为

1
100 111 101 101

左边3位对应特殊位,右边9位为读写执行权限位,passwd程序S_ISUID = 1

stat fstat lstat

man 2 stat中对这三个函数定义:

1
2
3
4
5
6
7
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

int stat(const char *pathname, struct stat *statbuf);
int fstat(int fd, struct stat *statbuf);
int lstat(const char *pathname, struct stat *statbuf);

关于struct stat结构体可以直接运行man 2 stat查看。三个函数可用于查看文件属性,查询结果保存于statbuff中,statlstat的区别在于lstat不会解析链接而stat会。

放一个用fstat查看/usr/bin/passwd的设置有效用户id位代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int
main(void)
{
int fd_passwd = open("/usr/bin/passwd", O_RDONLY);
struct stat stat_passwd;
if (-1 == fstat(fd_passwd, &stat_passwd)) {
perror("fstat /usr/bin/passwd error");
exit(1);
}
printf("/usr/bin/passwd st_mode: %x, S_ISUID: %x\n",
stat_passwd.st_mode, S_ISUID);

printf("/usr/bin/passwd st_uid: %s\n",
(stat_passwd.st_mode & S_ISUID) ? "true" : "false");

exit(0);
}

输出:

1
2
3
$ ./a.out 
/usr/bin/passwd st_mode: 89ed, S_ISUID: 800
/usr/bin/passwd st_uid: true

包含于fcntl.h

1
2
3
4
5
6
7
8
// open通过绝对路径或当前目录的相对路径打开文件,返回fd
open(const char *__path, int __oflag, ...)

// openat 可以将__fd视为当前目录,__path为相对路径,也可令__fd = AT_FDCWD 使其为当前目录
// 若__path是绝对路径,__fd无效
openat(int __fd, const char *__path, int __oflag, ...)

// 两个函数出错均返回-1,errno设为错误值

使用例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>

// 推荐buf大小为4096字节,因为ext4文件系统中一个块大小为4096字节
#define READ_BUFSIZE 256

int
main(void)
{
int dir_fd = open(".", O_RDONLY);
if (dir_fd < 0) {
perror("dir open failed");
exit(1);
}
int data_fd = openat(dir_fd, "data.txt", O_RDONLY);
if (data_fd < 0) {
perror("data.txt open failed");
close(dir_fd);
exit(1);
}

printf("dir_fd: %d, data_fd: %d\n", dir_fd, data_fd);

char buf[READ_BUFSIZE];
ssize_t bytes, total = 0;
while ((bytes = read(data_fd, buf, READ_BUFSIZE)) > 0) {
printf("byted read: %d\n", (int)bytes);
total += bytes;
}

close(data_fd);
close(dir_fd);
if (bytes == -1) {
perror("read error");
exit(1);
}

printf("read from data.txt: %d bytes\n", (int)total);

int not_exist_fd = openat(AT_FDCWD, "no_such_file.txt", O_RDONLY);
if (not_exist_fd == -1) {
perror("open \"no_such_file.txt\"");
} else {
close(not_exist_fd);
}
exit(0);
}

输出

1
2
3
4
5
6
$ ./a.out
dir_fd: 3, data_fd: 4
byted read: 256
byted read: 97
read from data.txt: 353 bytes
open "no_such_file.txt": No such file or directory