Linux应用层编程

[TOC]

一、多线程

资源分配的最小单位是进程,任务调度的最小单位是线程。进程包含线程集和资源集,进程内的所有线程共享资源集,因此在进行线程间任务调度时开销小,多线程共享数据简单(因为本来就是共享的)。线程占用资源少。在编译时需要链接pthread库。同一个进程的不同线程可以在多核处理器的不同核心执行。

多线程中随进程创建的线程为主线程,主线程退出时会关闭所有线程

1. 线程创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @brief 创建新线程,一般创建即运行
* @headerfile <pthread.h>
*
* @param thread:[OUT]线程ID
* @param attr:线程特性设置,一般可为空
* @param start_routine:线程执行函数
* @param arg:传递给线程执行函数的参数
* @return 线程创建成功返回0,线程创建失败返回错误码
*/
int pthread_create(
pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine) (void *),
void *arg);

通过pthread_create函数可创建一个线程,创建即可开始运行。arg参数会传递给start_routine。

2. 等待线程结束

1
2
3
4
5
6
7
8
9
/**
* @brief 当前线程等待指定的线程结束
* @headerfile <pthread.h>
*
* @param thread:线程ID
* @param retval:[OUT]用于接收线程执行函数的返回值
* @return 成功返回0,否则返回错误码
*/
int pthread_join(pthread_t thread, void **retval);

pthread_join对某个线程只允许调用一次,在调用后线程的全部资源全部释放。原使用的thread无效,重复调用返回错误码3,不修改retval中的值。

3. 线程自行退出

1
2
3
4
5
6
7
/**
* @brief 退出当前线程
* @headerfile <pthread.h>
*
* @param retval:线程返回值
*/
void pthread_exit(void *retval);

线程执行函数执行结束也会使线程退出,也可以调用pthread_exit退出。两种方式略有区别,使用return退出时仅在线程执行函数完全执行结束时会自行退出,当出现嵌套调用时会逐层退出。使用pthread_exit可以在嵌套调用时直接退出线程。在C++中使用pthread_exit退出不会调用析构函数。一般情况下不适用exit退出,exit用于退出进程,退出进程时杀死当前进程的所有线程。

4. 杀死指定线程

1
2
3
4
5
6
7
8
/**
* @brief 杀死指定线程(不会立即杀死,需等待下一次进程切换)
* @headerfile <pthread.h>
*
* @param thread:线程ID
* @return 成功返回0,否则返回错误码
*/
int pthread_cancel(pthread_t thread);

5. 线程分离

线程在执行结束后会释放大部分资源,但是仍会保留一部分信息(包括线程退出状态等),直到其他线程调用pthread_join获取线程的退出信息。如果不需要知道线程的退出信息希望线程执行结束后自行释放全部资源。则需要在创建时设置线程分离属性或者在创建完成后调用pthread_detach进行线程分离。

1
2
3
4
5
6
7
8
/**
* @brief 线程分离
* @headerfile <pthread.h>
*
* @param thread:线程ID
* @return 成功返回0,否则返回错误码
*/
int pthread_detach(pthread_t thread);

使用attr配置线程分离状态

1
2
3
4
pthread_attr_t attr;//定义attr结构体
pthread_attr_init(&attr);//进行默认初始化
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);//设置detached特性
pthread_create(&tid, &attr, tfn, NULL);//创建线程

对已分离的线程调用pthread_join出现错误码22

二、多进程

Linux中进程是资源分配的基本单位,不同进程间资源一般不共享(除非只用进程间通信的相关机制)。

如果一个进程A创建了一个新的进程B,则A是B的父进程,B是A的子进程。

每个可执行文件启动后会占用一个进程。

每个进程有全局唯一的进程号

1号进程(init进程)是所有进程的直接或间接父进程

如果子进程结束,父进程不等待子进程结束(等待函数会在子进程结束后释放子进程的全部资源,父进程不等待子进程结束则资源不释放)。在父进程未结束之前子进程结束后,子进程被称为僵尸进程。若父进程先与子进程结束,子进程由init托管称为孤儿进程。init托管孤儿进程后等孤儿进程结束释放孤儿进程资源。init托管到僵尸进程时会直接释放资源。

1. 子进程的创建

1
2
3
4
5
6
7
/**
* @brief 创建新进程
* @headerfile <unistd.h>
*
* @return 父进程检测返回值时是PID,子进程检测返回值是0,若返回值为-1则创建失败
*/
pid_t fork(void);

具体可参看示例

2. 获取进程号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @brief 获取当前进程号
* @headerfile <unistd.h>
*
* @return 当前进程号
*/
pid_t getpid(void);

/**
* @brief 获取父进程号
* @headerfile <unistd.h>
*
* @return 父进程号
*/
pid_t getppid(void);

3. 等待进程结束

假设有A、B两个进程。A进程等待B进程结束并不只是为了进行不同进程间的同步,A调用wait相关函数时,wait相关函数还会释放B结束后所占用的资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* @brief 阻塞当前进程,等待任意子进程退出
* @headerfile <sys/wait.h>
*
* @param wstatus:[OUT]进程状态
* @return 等待到的进程pid
*/
pid_t wait(int *wstatus);

/**
* @brief 等待指定进程进程状态改变
* @headerfile <sys/wait.h>
*
* @param pid:需要等待的进程id
* @param wstatus:[OUT]进程状态
* @param options:等待操作
* @return 等待到的进程pid,失败返回-1
*/
pid_t waitpid(pid_t pid, int *wstatus, int options);
//waitpid(-1, &wstatus, 0); 等效于 wait(&wstatus)
//WNOHANG 不阻塞,如果进程未结束直接退出

4. 进程退出

进程退出可以使用return、exit、_exit等方式退出进程。不同方式退出操作不同return退出最安全。在C++环境下不使用retrun不会调用析构函数。exit会执行提前注册的退出处理函数,随后清空IO缓冲,即将缓冲区的数据写入磁盘。_exit函数则会直接退出,不调用atexit注册的函数,也不保证数据完整性。

1
2
3
4
5
6
7
8
/**
* @brief 注册退出处理函数(退出时按注册的逆顺序调用)
* @headerfile <stdlib.h>
*
* @param function:处理函数
* @return 成功返回0,否则返回错误码
*/
int atexit(void (*function)(void));

5. 进程分离

进程其实并没有分离的概念,这里说进程分离只是沿用线程分离的概念,实则是通过异步的方式为子进程释放资源,看起来不影响父进程的执行,也不会产生僵尸进程。子进程结束时会向父进程发送SIGCHLD信号,父进程可以在该信号的处理函数中执行wait操作为子进程释放资源。

方法一:自己处理

1
2
3
4
5
6
7
8
9
10
11
12
13
void reap_zombie(int signo)
{
int errno_old = errno;
while (waitpid(-1, NULL, WNOHANG) > 0);
errno = errno_old;
}

int main()
{
...
signal(SIGCHLD,reap_zombie);
...
}

方法二:系统处理

1
2
3
4
5
6
int main()
{
...
signal(SIGCHLD,SIG_IGN);
...
}

三、进程间通信

1. 管道

(1)管道(无名管道)

管道:正如其名,就像水管、燃气管一样进行传输,但是它们传输的是水和燃气。Linux中的管道传输的是数据。

允许在一端写入并在另一端读出。写入端不可读取,读取端不可写入。只能用于父子进程之间。

使用read和write读写即可。

多个线程读取同一个管道时,管道中的数据在哪个线程被读出是不确定的

同一个管道同时只允许读或者写

1
2
3
4
5
6
7
8
/**
* @brief 创建一个管道
* @headerfile <unistd.h>
*
* @param pipefd:文件描述符数组,[0]是读取端,[1]是写入端
* @return 成功返回0,否则返回-1,errno被设置
*/
int pipe(int pipefd[2]);

(2)命名管道(fifo)

命名管道可以任何进程间实现通信。在创建命名管道时会在指定的路径创建管道文件,其他进程打开该文件即可进行读写。

由于管道文件是文件,因此也有读写权限控制。

调用函数创建管道文件时文件必须不存在,否则会产生错误

创建后使用open打开文件即可进行读写操作。

非阻塞写入打开、没人读:打开失败

阻塞写入打开、没人读:等有人读时打开

非阻塞读取打开、没人写:打开成功,read阻塞,有人写后反复读取管道,会读取到历史数据

阻塞读取打开、没人写:等待有人写时打开,正常读取数据、不会读取到历史数据

若在管道读写过程中,读取端先断开会导致写入端写入到无人读取的管道中,引起管道破裂,在写入端进程中产生SIGPIPE管道终止信号

1
2
3
4
5
6
7
8
9
 /**
* @brief 创建命名管道
* @headerfile <sys/stat.h>
*
* @param pathname:管道文件名
* @param mode:文件的权限(会和umask相与)
* @return 若成功则返回0,否则返回-1,错误原因存于errno中
*/
int mkfifo(const char *pathname, mode_t mode);

2. 信号

3. 消息队列

4. 共享内存

四、文件操作

五、多线程同步

六、网络编程

1. 互斥锁

2. 自旋锁

3. 信号量

参考文献

  1. Linux 线程 Thread

  2. Linux编程中的坑——C++中exit和return的区别

  3. LinuxC线程学习之pthread_exit函数和总结exit、return、pthread_exit,pthread_cancel各自退出效果和join,detach的作用

  4. 线程和进程的分离(detach)

  5. Linux多进程编程入门

  6. 孤儿进程和僵尸进程的区别

  7. 操作系统 — 进程的退出(exit)

  8. Linux进程间通信的几种方式总结

  9. fork()之后都会做哪些复制,什么是写时拷贝

  10. 【Linux】一篇文章彻底搞定信号!

  11. linux中实现线程同步的6种方法

0%