【Linux编程】学习笔记-进程与线程知识
文章目录
进程
相关函数
#include<unistd.h>
pid_ t fork (void) ; // 子进程创建
void exit(int status) ; // 进程退出。需要 include<stdlib.h>
void _exit(); // 在头文件 <unistd.h>中声明
/* Get the process ID of the calling process. */
extern __pid_t getpid (void) __THROW; // 获得当前进程的 Id
/* Get the process ID of the calling process's parent. */
extern __pid_t getppid (void) __THROW; // 获得父进程的 ID
/* Get the process group ID of the calling process. */
extern __pid_t getpgrp (void) __THROW; // 获得进程组的 ID
/* Return the foreground process group ID of FD. */
extern __pid_t tcgetpgrp (int __fd) __THROW; //返回前台进程组 ID, 这与在 __fd打开的终端相关
Note exit()函数与_exit()函数最大区别就在于exit()函数在调用exit 系统之前要检查文件的打开情况,把文件缓冲区的内容写回文件。
进程之间私有和共享的资源
- 私有:地址空间、堆、全局变量、栈、寄存器
- 共享:进程代码段;进程的公有数据(进程间通信);进程打开的文件描述符;信号的处理器;进程的当前目录和进程用户ID与进程组ID
进程间的通信(Interprocess Communication, IPC)
- 管道(pipe)
- 信号量(semophonre)
- 信号(signal)
- 消息队列(message queue)
- 共享内存(shared memory)
- 套接字(Socket)
管道
管道是一种两个进程间进行单向通信的机制。
特点:(1)数据只能由一个进程流向另一个进程(其中一个读管道, 一个写管道);如果要进行双工通信,则需要建立两个管道。
(2) 管道只能用于父子进程或者兄弟进程间通信,也就是说管道只能用于具有亲缘关系的进程间通信。
#include <unistd.h>
int pipe(int fd[2]); // 管道创建
// fd[O]为读而打开,fd[1]为写而打开。fd[1]的输出是fd[0]的输入
消息队列
int msgget(key_t key, int msgflg) ; // 创建消息队列
/** 从队列中取用消息 **/
ssize_t msgrcv(int msqid , void *msgp, size_t msgsz, long msgtyp, int msgflg);
/** 将数据放到消息队列中 **/
int msgsnd(int msqid ,const void *msgp, size_t msgsz, int msgflg);
/** 设置消息队列属性 **/
int msgctl(int msgqid, int cmd, struct msqid_ds *buf);
共享内存
共享内存就是允许两个不相关的进程访问同一个逻辑内存。共享内存是最快的一种IPC
#include <sys/shm.h>
/** 创建共享内存 **/
int shmget(key_t key, int size, int flag) ;
/** 其它进程调用 shmat 将其连接到自身的地址空间中 **/
void *shmat(int shmid, void *addr, int flag) ;
/** 将共享内存从当前进程中分离 **/
int shmdt(const void *shmaddr) ;
共享内存的优缺点:
- 优点:使用共享内存进行进程间的通信非常方便,而且函数的接口也简单,数据的共享还使进程间的数据不用传送,而是直接访问内存,也加快了程序的效率。同时,它也不像无名管道那样要求通信的进程有一定的父子关系。
- 缺点:共享内存没有提供同步的机制,这使得在使用共享内存进行进程间通信时,往往要借助其他的手段来进行进程间的同步工作。 使 用 信 号 量 可 以 解 决 \textcolor{red}{使用信号量可以解决} 使用信号量可以解决
信号量
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
/** 创建和打开信号量 **/
int semget(key_t key, int nsems, int semflg) ;
/** 用于改变信号量的值 **/
int semop(int semid, struct sembuf *sops, unsigned nsops) ;
/** 用于直接控制信号量信息 **/
int semctl(int semid, int semnum, int cmd, ... /* union semum arg */) ;
套字节(Sockets)
网络中的进程是通过套字节来通信的,不论他们是在同一计算机还是不同计算机之间。
以TCP协议通信的socket其交互过程大概如图:
一种在windows平台上的网络编程实现:【TCP】网络编程学习
特殊的进程
僵尸进程
当子进程比父进程先结束,而父进程又没有回收子进程(调用wait/waitpid),释放子进程占用的资源,此时子进程将成为一个僵尸进程。会造成内存泄漏。
僵 尸 进 程 如 何 避 免 ? \textcolor{red}{僵尸进程如何避免?} 僵尸进程如何避免?
1、让僵尸进程的父进程来回收,父进程每隔一段时间来查询子进程是否结束并回收,调用wait()或者waitpid(),通知内核释放僵尸进程 。
2、采用信号SIGCHLD通知处理,并在信号处理程序中调用wait函数 。
3、让僵尸进程变成孤儿进程,就是让他的父亲先死。
4、如果父进程很忙,那么可以用signal函数为SIGCHLD安装handler,因为子进程结束后, 父进程会收到该信号,可以在handler中调用wait回收。
孤儿进程
孤儿进程就是说一个父进程退出,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。孤儿进程将被 init
进程(进程ID
为1
的进程)所收养,并由 init
进程对它们完成状态收集工作。因为孤儿进程会被 init
进程收养,所以孤儿进程不会对系统造成危害。
孤儿进程和僵尸进程的区别:孤儿进程是父进程已退出,而子进程未退出;僵尸进程是父进程未退出,而子进程已退出。
守护进程
运行在后台的一种特殊进程,它是独立于控制终端的,并周期性地执行某些任务。
线程
相关函数
/* Create a new thread, starting with execution of START-ROUTINE
getting passed ARG. Creation attributed come from ATTR. The new
handle is stored in *NEWTHREAD. */
extern int pthread_create (pthread_t *__restrict __newthread,
const pthread_attr_t *__restrict __attr,
void *(*__start_routine) (void *),
void *__restrict __arg); // 创建新增线程。如果创建成功,返回0,否则返回错误编号
/* Obtain the identifier of the current thread. */
extern pthread_t pthread_self (void); // 获取自身线程 ID
/* Terminate calling thread.*/
extern void pthread_exit (void *__retval); // 结束线程,一般子线程调用
/*This function is a cancellation point and therefore not marked with __THROW.*/
extern int pthread_join (pthread_t __th, void **__thread_return); // 结束线程,一般主线程调用
线程之间私有和共享的资源
- 私有:线程栈,寄存器,程序计数器
- 共享:堆,地址空间,全局变量,静态变量
- fork时子进程获得父进程代码和数据段、共享库、堆和栈的复制,所以变量的地址(当然是虚拟地址)也是一样的
线程同步
-
锁机制:包括互斥锁/量(mutex)、读写锁(reader-writer lock)、自旋锁(spin lock)、条件变量(condition)
-
条件变量
-
信号量机制(Semaphore)
-
信号机制(Signal)
-
屏障(barrier)
互斥量
取保同一时间只有一个线程访问数据。从本质上说是一把锁,在访问共享资源之前进行加锁。访问完成之后释放(解锁)。
pthread_ mutex _lock()//加锁
... //共享的资源的操作
pthread_ mutex _unlock()
读写锁
读写锁有三种状态:读模式下的加锁状态、写模式下的加锁状态和不加锁状态。
一 次 只 有 一 个 线 程 可 以 占 有 写 模 式 的 读 写 锁 , 但 是 多 个 线 程 可 以 同 时 占 有 读 模 式 的 读 写 锁 。 写 操 作 是 排 它 性 的 , 独 占 的 , 读 操 作 是 共 享 的 , 允 许 多 个 线 程 同 时 去 访 问 同 一 资 源 \textcolor{red}{一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。写操作是排它性的,独占的,读操作是共享的,允许多个线程同时去访问同一资源} 一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。写操作是排它性的,独占的,读操作是共享的,允许多个线程同时去访问同一资源
与互斥量相比,读写锁允许更高的并行性。互斥锁只有两个状态(加锁状态、不加锁状态)。读写锁三种状态。
读写锁的三种状态:
-
当读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞。
-
当读写锁在读加锁状态时,所有试图以读模式对它进行加锁的线程都可以得到访问权,但是以写模式对它进行加锁的线程将会被阻塞。
-
当读写锁在读模式的锁状态时,如果有另外的线程试图以写模式加锁,读写锁通常会阻塞随后的读模式锁的请求, 这样可以避免读模式锁长期占用, 而等待的写模式锁请求则长期阻塞。
条件变量
通过允许线程阻塞和等待另一个线程发送信号的方法弥补互斥锁的不足,它常和互斥锁一起使用。
使用过程:创建 --> 注销 —> 等待 —> 激发
// 创建
pthread_cond_t cond=PTHREAD_COND_INITIALIZER; // 静态方式创建
int pthread_cond_init(pthread_cond_t *cond , pthread_condattr_t *cond_attr) // 动态方式创建
// int pthread_cond_destroy(pthread_cond_t *cond) // 注销
pthread_cond_wait(); // 等待
pthread_cond_signal(); // 激发