线程
进程:
进程有独立的地址空间
Linux为每个进程创建task_struct
每个进程都参与内核调度,互不影响
线程:
进程在切换时候系统开销比较大
很多操作系统引入了轻量级LWP
同一个进程中的线程共享相同的地址空间
Linux不区分进程、线程
特点:
通常线程是指共享相同地址空间的多个任务
使用线程的好处
● 大大提高了任务切换效率
● 避免了额外的TLB & cache的刷新
一个进程中的线程共享以下的资源
● 可执行的指令
● 静态数据
● 进程中打开的文件描述符
● 当前工作目录
● 用户ID
● 用户组ID
Pthread线程库
1.pthread 线程库提供了如下基本操作
- 线程创建
- 线程回收
- 结束线程
2.同步和互斥机制
- 信号量
- 互斥锁
线程创建 - pthread_create
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*routine)(void *), void *arg);
● 成功时候返回0, 失败时返回错误码
● thread是线程对象
● attr线程属性,NULL代表默认属性
● routine线程执行的函数
● 传递给routine的参数
线程回收- pthread_join
int pthread_join(pthread_t thread, void **retval);
● 成功时候返回0, 失败时返回错误码
● thread是要回收的线程对象
● 调用线程阻塞知道thread结束
● *retval接收线程thread的返回值
线程结束- pthread_exit
void pthread_exit(void *retval);
● 结束当前线程
● retval可被其他线程通过pthread_join获取
● 线程私有资源被释放
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include <time.h> #include <sys/stat.h> #include <pthread.h> #include <string.h> char message[32] = "hello world"; void* thread_func(void *arg); int main() { pthread_t a; void *result; if(pthread_create(&a, NULL, thread_func, NULL) != 0) { perror("error"); return -1; } pthread_join(a, &result); printf("result is %s\n", result); printf("message is %s\n", message); } void* thread_func(void *arg) { sleep(1); strcpy(message, "marked by thread"); pthread_exit("aaa"); }
线程间通信
线程共享统一进程的地址空间
优点: 线程间通信很容易,通过全局变量交换数据
缺点:多个线程访问共享数据时需要同步或者互斥机制
线程通信 - 同步
● 同步指的是多个任务按照约定的先后顺序相互配合完成一件事情
● 信号量
● 信号量来决定线程是继续运行还是阻塞
信号量
信号量代表某一类资源,其值表示系统中该资源的数量
信号量是一个受保护的变量,只能通过三种操作来访问
● 初始化
● P操作(申请资源)
● V操作(释放资源)
Posix信号量
posix中定义了两类信号量
● 无名信号量(基于内存的信号量)
● 有名信号量(线程之间和进程之间)
pthread库常用的信号量操作函数如下:
#include <semaphore.h> int sem_init(sem_t *sem, int pshared, ussigned int value); int sem_wait(sem_t *sem); // p操作 int sem_post(sem_t *sem); // v操作
信号量初始化 - sem_init
int sem_init(sem_t *sem, int pshared, unsigned int val);
成功时候返回0, 失败时候返回EOF
sem 指向要初始化的信号量对象
pshared 0 - 线程间 1 - 进程间
信号量P/V操作
int sem_wait(sem_t *sem); // p操作 int sem_post(sem_t *sem); // v操作
成功时候返回0, 失败时候返回EOF
sem 指向要操作的信号量对象
信号量代码示例:
#include <fcntl.h> #include <unistd.h> #include <dirent.h> #include <stdio.h> #include <time.h> #include <sys/stat.h> #include <sys/types.h> #include <semaphore.h> #include <pthread.h> char buf[32]; sem_t sem; void* function(void *arg); int main() { pthread_t a_thread; if(sem_init(&sem, 0, 0) < 0) { perror("sem_init"); return -1; } if(pthread_create(&a_thread, NULL, function, NULL) < 0) { printf("failed to pthread_create"); return -1; } printf("input quit to exit"); do { fgets(buf , 32, stdin); sem_post(&sem); }while(strncmp(buf, "quit", 4) !=0 ); return 0; } void* function(void *arg) { while(1) { sem_wait(&sem); printf("you enter %d cahraters\n", strlen(buf)); fflush(stdout); } }
使用两个信号量做到数据严格同步:
#include <fcntl.h> #include <unistd.h> #include <dirent.h> #include <stdio.h> #include <time.h> #include <sys/stat.h> #include <sys/types.h> #include <semaphore.h> #include <pthread.h> char buf[32]; sem_t sem_r, sem_w; void* function(void *arg); int main() { pthread_t a_thread; if(sem_init(&sem_r, 0, 0) < 0) { perror("sem_init"); return -1; } if(sem_init(&sem_w, 0, 1) < 0) { perror("sem_init"); return -1; } if(pthread_create(&a_thread, NULL, function, NULL) < 0) { printf("failed to pthread_create"); return -1; } printf("input quit to exit"); do { sem_wait(&sem_w); fgets(buf , 32, stdin); sem_post(&sem_r); }while(strncmp(buf, "quit", 4) !=0 ); return 0; } void* function(void *arg) { while(1) { sem_wait(&sem_r); printf("you enter %d cahraters\n", strlen(buf)); fflush(stdout); sem_post(&sem_w); } }
线程通信 - 互斥
● 临界资源
一次只允许一个任务(进程、线程)访问的共享资源
● 临界区
访问临界区的代码
● 互斥机制
mutex互斥锁
任务在访问临界资源前申请锁,访问完之后释放锁
互斥锁
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr); int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex);
pthread_mutex_init:
成功时候返回0,失败时候返回错误码
mutex 指向要初始化的互斥锁对象
attr 互斥锁属性,NULL表示缺省属性
pthread_mutex_lock:
成功时候返回0,失败时候返回错误码
mutex 指向要初始化的互斥锁对象
如果无法获得锁,任务阻塞
pthread_mutex_unlock:
成功时候返回0,失败时候返回错误码
mutex 指向要初始化的互斥锁对象
执行完临界区要及时释放锁
#include <fcntl.h> #include <unistd.h> #include <dirent.h> #include <stdio.h> #include <time.h> #include <sys/stat.h> #include <sys/types.h> #include <semaphore.h> #include <pthread.h> unsigned int count, value1, value2; pthread_mutex_t lock; void* function(void *arg); #define LOCKM_ int main() { pthread_t a_thread; if(pthread_mutex_init(&lock, NULL) != 0) { perror("pthread_mutex_init"); return -1; } if(pthread_create(&a_thread, NULL, function, NULL) != 0) { perror("pthread_create"); return -1; } while(1) { count++; #ifdef LOCKM_ pthread_mutex_lock(&lock); #endif value1 = count; value2 = count; #ifdef LOCKM_ pthread_mutex_unlock(&lock); #endif } return 0; } void* function(void *arg) { while(1) { #ifdef LOCKM_ pthread_mutex_lock(&lock); #endif if(value1 != value2) { printf("value1 = %d , value2 = %d\n", value1, value2); fflush(stdout); usleep(1000); } #ifdef LOCKM_ pthread_mutex_unlock(&lock); #endif } }
进程间通信
● 早期UNIX进程间通信方式
无名管道(pipe)
有名管道(fifo)
信号(signal)
● System V IPC
共享内存(share memory)
消息队列(message queue)
信号灯集(semaphore set)
● 套接字(socket)
无名管道
● 只能用于具有亲缘关系的进程之间的通信
● 单工通信模式,具有固定的读端和写端
● 无名管道创建时候会返回两个文件描述符,分别用于读写管道
int pipe(int pfd[2]);
pipe:
成功时候返回0,失败时候返回EOF
pfd包含两个元素的整形数组,用于保存文件描述符
pfd[0]用于读管道,pfd[1]用于写管道
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> int main() { pid_t pid1, pid2; char buf[32]; int pfd[2]; if(pipe(pfd) < 0) { perror("pipe"); return -1; } if((pid1 = fork()) <0) { perror("fork"); return -1; } else if(pid1 == 0) { strcpy(buf, "im process1"); write(pfd[1], buf, 32); exit(0); } else { if((pid2 == fork()) < 0) { perror("fork2"); return -1; } else if(pid2 == 0) { sleep(1); strcpy(buf, "im process2"); write(pfd[1], buf, 32); } else { wait(NULL); read(pfd[0], buf, 32); printf("%s\n", buf); wait(NULL); read(pfd[0], buf, 32); printf("%s\n", buf); } } return 0; }
读无名管道
● 写端存在
有数据 – read返回实际读取的字节数量
无数据 – 进程读阻塞
● 写端不存在
有数据 – read返回实际读取的字节数量
无数据 – read返回0
写无名管道
● 读端存在
有空间 – write返回实际写入的字节数
无空间 – 空间不足时候不保证原子操作(有多少写多少,剩余等有空间再写),进程写阻塞
● 读端不存在不允许写入
管道断裂
有名管道
对应管道文件,可用于任意进程之间通信
打开管道时候可以指定读写方式
通过文件I/O操作,内容存放在内存中
int mkfifo(const char *path, mode_t mode);
mkfifo:
成功时候返回0,失败返回EOF
path 创建管道文件路径
mode 管道文件权限,如0666
信号
● 信号是在软件层次上对中断机制的一种模拟,是一种异步通信方式
● linux内核通过信号通知用户进程,不同的信号类型代表不同的事件
● Linux对早期unix信号机制进行了扩展
● 进程对信号有不同的响应方式
缺省方式
忽略信号
捕捉信号
常用的信号
信号名 | 含义 | 默认操作 |
SIGHUP | 该信号在用户终端关闭时候产生,通常是发送给和该终端关联的会话内所有的进程 | 终止 |
SIGINT | 该信号在用户键入INTR制度时候产生,内核发送此信号到当前终端的所有前台进程(ctrl+c) | 终止 |
SIGQUIT | 该信号和sigint类似,但是由于quit字符来产生(ctrl+\) | 终止 |
SIGLL | 该信号在一个进程企图执行一条非法指令时候产生 | 终止 |
SIGSEV | 该信号在非法访问内存时候产生,如野指针、缓冲区溢出 | 终止 |
SIGPIPE | 当进程往一个没有读端的管道中写入时产生,代表管道断裂 | 终止 |
SIGKILL | 该信号用来结束进程,并且不能被捕捉和忽略 | 终止 |
SIGSTOP | 该信号用于暂停继承,并且不能被捕捉和忽略 | 暂停进程 |
SIGTSTP | 该信号用于暂停继承,用户可以键入SUSP字符发出这个信号(ctrl+z) | 暂停进程 |
SIGCONT | 该信号让进程进入运行态 | 继续运行 |
SIGALRM | 该信号用于通知进程定时器时间已到 | 终止 |
SIGUSR1/2 | 该信号保留给用户程序使用 | 终止 |
信号发送
int kill(pid_t pid, int sig); int raise(int sig); int alarm(unsigned int seconds); // 创建一个定时器,如果为0取消当前定时器 int pause(void); // 进程会一直阻塞,知道被信号终端
设置信号的响应方式
void (*signal(int signo, void(*handker)(int)))(int);
System V IPC
包括共享内存,消息队列和信号灯集
每个IPC对象有唯一的ID
IPC对象创建后一直存在,知道被显式的删除
每一个IPC对象有一个关联的key
key_t ftok(const char *path, int proj_id);
ftok:
成功时候返回key,失败返回EOF
path是存在可访问的文件路径
proj_id是用于生成key的数字,不能是0
#include <stdio.h> #include <stdli.h> #include <sys/types.h> #include <sys/ipc.h> #include <unistd.h> int main() { key_t key; if((key=ftok(".", 'a')) == -1) { perror("ftok"); return -1; } }
共享内存
● 共享内存是一种最为高效的进程间通信方式,进程可以直接读写内存,而不需要任何数据拷贝
● 共享内存在内核空间创建,可以被进程映射到用户空间访问,使用灵活
● 由于多个进程可以同时访问共享内存,因此需要同步和互斥机制配合使用
使用步骤
1.创建/打开共享内存
2.映射共享内存,即把指定的共享内存映射到进程的地址空间用于访问
3.读写共享内存
4.撤销共享内存映射
5.删除共享内存对象
// 1. 创建/打开共享内存 int shmget(kety_t key, int size, int shmflg); // 2. 映射共享内存,即把指定的共享内存映射到进程的地址空间用于访问 void *shmat(int shmid, const void *shmaddr, int shmflg); // 3. 读写共享内存 ... // 4. 撤销共享内存映射 int shmdt(void *shmaddr); // 5. 设置共享内存, 删除共享内存对象 int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmget:
key 和共享内存关联的key, IPC_PRIVATE或者ftok生成
shmflg 共享内存标志位 IPC_CREATE | 0666
示例代码(不能直接运行)
// 创建私有 int main() { int shmid; if((shmid = shmget(IPC_PRIVATE, 512, 0666) < 0) { perror("shmget"); return -1; } } // 创建关联key的 int main() { key_t key; int shmid; if((key = ftok('.', 'm') == -1) { perror("ftok"); return -1; } if((shmid = shmget(key, 1024, IPC_CREEATE|0666) < 0) { perror("shmget"); return -1; } }
shmat:
shmid 要映射的共享内存ID
shmaddr 映射后的地址,NULL表示由系统自动映射
shmflg 标志位, 0表示可读可写,SHM_RDONLY表示只读
● 通过指针访问共享内存,指针类型取决于共享内存存放的数据类型
示例代码(不能直接运行)
char *addr; int shmid; ... if(addr = (char*)shmat(shmid, NULL, 0) == (cahr *)-1); { perror("shmat"); return -1; } fgets(addr , N , stdin);
shmdt:
不适用共享内存时候应该撤销映射关系
进程结束时候,操作系统会自动撤销
shmctl:
shmid 要操作的共享内存ID
cmd 要执行的操作 IPC_STAT(获取属性) IPC_SET(设置属性) IPC_RMID(删除ID)
struct shmid_ds *buf 表示共享内存属性
共享内存 – 注意事项
每块共享内存大小有限制
● ipcs -l
● cat /proc/sys/kernel/shmmax
共享内存删除的时间点
● shmct(shmid, IC_RMID, NULL) 添加删除标记
● nattach变成0时候真正删除
消息队列
● 消息队列是IPC对象的一种
● 消息队列由消息队列ID来唯一标识
● 消息队列就是一个消息列表,用户可以在消息队列中添加消息、读取消息
● 消息队列可以按照类型来发送、接收消息
使用步骤:
1.打开/创建见消息队列
2.向消息队列发送消息
3.从消息队列接收消息
4.控制消息队列
#include <sys/ipc.h #include <sys/msg.h> // 1. 打开/创建见消息队列 int msgget(key_t key, int msgflg); // 2. 向消息队列发送消息 int msgsnd(int msgid, const void *msgp, size_t size, int msgflg); // 3. 从消息队列接收消息 int msgrcv(int msgid, void *msgp, size_t size, long msgtype, int msgflg); // 4. 控制消息队列 int msgctl(int msgid, int cmd, struct msqid_df *buf);
msgget:
key 和消息队列关联的key IPC_PRIVATE或ftok
msgflg 标志位IPC_CREATE | 0666
示例代码(不能直接运行)
int main() { int msgid; key_t key; if((key = ftok(".", "q")) == -1) { perror("ftok"); return -1; } if((msgid = msgget(key, IPC_CREATE|0666)) < 0) { perror("msgget"); return -1; } return 0; }
msgsnd:
msgid 消息队列ID
msgp 消息缓冲区地址
size 发送的消息长度(正文长度)
msgflg 标志位0或者IPC_NOWAIT
示例代码(不能直接运行)
typedef struct { long mtype; char mtext[64]; }MSG #define LEN (sizeof(MSG) - sizeof(long)) int main() { MSG buf; buf.mtype = 100; fgets(buf.mtext, 64, stdin); msgsnd(msgid, &buf, LEN, 0); ... return 0; }
消息格式
● 通信双方首先定义好统一的消息格式
● 用户根据应用需求定义结构体类型
● 首成员为long,代表消息类型(正整数)
● 其他成员为消息正文
msgrcv:
msgid 消息队列id
msgp 消息缓冲区地址
size 接收的消息长度(正文长度)
msgtype 指定接收的消息类型
msgflg 标志位0或者IPC_NOWAIT
示例代码(不能直接运行)
typedef struct { long mtype; char mtext[64]; }MSG #define LEN (sizeof(MSG) - sizeof(long)) int main() { MSG buf; buf.mtype = 100; fgets(buf.mtext, 64, stdin); if(msgrcv(msgid, &buf, LEN, 100,0) < 0) { perror("msgrcv"); return -1; } ... return 0; }
msgctl:
msgid 消息队列ID
cmd 要执行的操作 IPC_STAT / IPC_SET / IPC_RMID
struct msqid_df*buf 表示消息队列属性地址
(消息被删除会立刻删除)
信号灯
信号灯又叫信号量,用于线程或者进程同步或者互斥的机制
信号灯类型
posix 无名信号灯
posix 有名信号灯
信号灯的含义 计数信号灯
system v 信号灯
● 是一个或者多个计数信号灯的集合
● 可以同时操作集合中的多个信号灯
● 申请多个资源时候避免死锁
使用步骤
1.打开/创建信号灯
2.信号灯初始化
3.PV操作
4.删除信号灯
// 1.打开/创建信号灯 int semget(key_t key, int nsems, int semflg); // 2. 信号灯初始化 int semctl(int semid, int semnum. int cmd, ...) // 3. PV操作 int semop(int semid, struct sembuf *sops, size_t nsops); // 4. 删除信号灯 semctl
semget:
key 和信号灯关联的key IPC_PRIVATE或ftok
nsems 集合中包括的信号灯的个数
semflg 标志位IPC_CREATE | 0666 IPC_EXCL
semctl:
semid 信号灯的ID
semnum 要操作的信号灯的编号
cmd 执行的操作 SETVAL IPC_RMID
union semun 取决于cmd
semop:
semid 要操作的信号灯集
sops 描述对信号灯操作的结构体
nsops 要操作的信号灯个数
struct sembuf { short semnum; // 信号灯编号 short sem_op; // -1 p操作 // 1 v操作 short sem_flg; // 0 IPC_NOWAIT }