1、多线程及同步
linux多线程api:pthread_equal 、 pthread_self 、 pthread_create 、 pthread_exit 、pthread_join、pthread_cancel、pthread_cleanup_push、pthread_cleanup_pop、pthread_detach、pthread_mutex_init、pthread_mutex_destroy
pthread_join用于等待线程结束,常用于主线程结束时等待其他线程结束。
主线程或其他线程中往往还会创建线程,如果线程退出时一般发生的情况:
a、主线程通过pthread_join、sleep等待其他线程退出,然后程序正常结束。
b、exit(0)退出,进程退出,所有的线程也退出并释放资源。
c、主线程调用pthread_exit退出,主线程退出,其他线程依然运行。
线程同步互斥锁、读写锁、条件变量、信号量。
信号量是计数的方式,可以用于进程间、互斥锁是对变量的互斥保护。
条件变量是用于睡眠等待资源的,不加锁,一般条件变量与互斥锁配合使用。
互斥锁函数:pthread_mutex_lock/trylock/unlock。
条件变量:pthread_cond_wait/signal/broadcast
- #include <pthread.h>
- #include <unistd.h>
- #include "stdio.h"
- #include "stdlib.h"
- static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
- static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
- struct node
- {
- int n_number;
- struct node *n_next;
- }*head = NULL;
- static void cleanup_handler(void *arg)
- {
- printf("Cleanup handler of second thread./n");
- free(arg);
- (void)pthread_mutex_unlock(&mtx);
- }
- static void *thread_func(void *arg)
- {
- struct node *p = NULL;
- pthread_cleanup_push(cleanup_handler, p);
- while (1)
- {
- //这个mutex主要是用来保证pthread_cond_wait的并发性
- pthread_mutex_lock(&mtx);
- while (head == NULL)
- {
- //这个while要特别说明一下,单个pthread_cond_wait功能很完善,为何
- //这里要有一个while (head == NULL)呢?因为pthread_cond_wait里的线
- //程可能会被意外唤醒,如果这个时候head != NULL,则不是我们想要的情况。
- //这个时候,应该让线程继续进入pthread_cond_wait
- // pthread_cond_wait会先解除之前的pthread_mutex_lock锁定的mtx,
- //然后阻塞在等待对列里休眠,直到再次被唤醒(大多数情况下是等待的条件成立
- //而被唤醒,唤醒后,该进程会先锁定先pthread_mutex_lock(&mtx);,再读取资源
- //用这个流程是比较清楚的
- pthread_cond_wait(&cond, &mtx);
- p = head;
- head = head->n_next;
- printf("Got %d from front of queue/n", p->n_number);
- free(p);
- }
- pthread_mutex_unlock(&mtx); //临界区数据操作完毕,释放互斥锁
- }
- pthread_cleanup_pop(0);
- return 0;
- }
- int main(void)
- {
- pthread_t tid;
- int i;
- struct node *p;
- //子线程会一直等待资源,类似生产者和消费者,但是这里的消费者可以是多个消费者,而
- //不仅仅支持普通的单个消费者,这个模型虽然简单,但是很强大
- pthread_create(&tid, NULL, thread_func, NULL);
- sleep(1);
- for (i = 0; i < 10; i++)
- {
- p = (struct node*)malloc(sizeof(struct node));
- p->n_number = i;
- pthread_mutex_lock(&mtx); //需要操作head这个临界资源,先加锁,
- p->n_next = head;
- head = p;
- pthread_cond_signal(&cond);
- pthread_mutex_unlock(&mtx); //解锁
- sleep(1);
- }
- printf("thread 1 wanna end the line.So cancel thread 2./n");
- //关于pthread_cancel,有一点额外的说明,它是从外部终止子线程,子线程会在最近的取消点,退出
- //线程,而在我们的代码里,最近的取消点肯定就是pthread_cond_wait()了。
- pthread_cancel(tid);
- pthread_join(tid, NULL);
- printf("All done -- exiting/n");
- return 0;
- }
2、进程编程及进程间通讯
新建进程:
a、fork和exec实现,fork复制父进程,exec替换当前进程。
因此一般用法时,在父进程中调用fork,根据fork返回值确定执行代码是父进程还是子进程,父进程代码wait等待子进程执行完成,子进程代码通过exec开启一个进程替换当前进程。
fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,linux中引入了“写时复制“技术,也就是只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。于是起初我就感到奇怪,子进程的物理空间没有代码,怎么去取指令执行exec系统调用呢?!原来在fork之后exec之前两个进程用的是相同的物理空间(内存区),子进程的代码段、数据段、堆栈都是指向父进程的物理空间,也就是说,两者的虚拟空间不同,但其对应的物理空间是同一个。当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间,如果不是因为exec,内核会给子进程的数据段、堆栈段分配相应的物理空间(至此两者有各自的进程空间,互不影响),而代码段继续共享父进程的物理空间(两者的代码完全相同)。而如果是因为exec,由于两者执行的代码不同,子进程的代码段也会分配单独的物理空间。
- if (fork() == 0){
- //child process
- char * execv_str[] = {"echo", "executed by execv",NULL};
- if (execv("/usr/bin/echo",execv_str) <0 ){
- perror("error on exec");
- exit(0);
- }
- }else{
- //parent process
- wait();
- printf("execv done\n\n");
- }
fork()是全部复制,vfork()是共享内存,vfork时父进程阻塞,而clone()是则可以将父进程资源有选择地复制给子进程,
而没有复制的数据结构则通过指针的复制让子进程共享,具体要复制哪些资源给子进程,由参数列表中的clone_flags来决定。
另外,clone()返回的是子进程的pid。
fork的实现:
do_fork(CLONE_SIGCHLD,...)
clone的实现:
do_fork(CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGCHLD,...)
vfork的实现:
do_fork(CLONE_VFORK|CLONE_VM|CLONE_SIGCHLD,...)
为了优化那些:fork然后就是exec的程序,Linux提供了vfork。vfork时,父进程会被阻塞,直到子进程调用了exec或exit,因为此时不复制页表结构。
clone()系统调用是fork()的推广形式,它允许新进程共享父进程的存储空间、文件描述符和信号处理程序
fork, vfork and clone三者最终都会调用do_fork函数,三者的差别就是参数上的不同而已。
实际上产生效果的也是这些参数:
CLONE_VM标识:表示共享地址空间(变量等)
CLONE_FILES标志:表示共享文件描述符表
CLONE_VFORK标识:标识父进程会被阻塞,子进程会把父进程的地址空间锁住,直到子进程退出或执行exec时才释放该锁
SIGCHLD标识:共享信号
Linux使用copy on wirte的技术,Linux中的fork代价仅仅是创建子进程的页表结构和创建一个task_struct结构。
进程间通讯
a、signal
信号用于进程间交互,内核为每个进程维护一个信号掩码,类似信号的队列,但同类型的信号只能存储一个。
信号的来源:硬件事件(一些按键触发)、非法运算、系统函数调用(kill, raise, alarm、setitimer、sigqueue)等。
信号处理:忽略、系统默认处理、用户自定义处理。SIGKILL、SIGSTOP即不能被捕捉也不能被忽略。
b、管道
管道是环形队列,半双工模式,且存储缓冲区有限。半双工,读时要关闭写,写时要关闭读。管道只能应用于有亲缘关系的进程之间:
int pipe(int filedes[2]);
filedes[0]用于读,使用时要close(filedes[1])
filedes[1]用于写,使用时要close(filedes[0])
命名管道可以应用于不存在亲缘关系的进程之间:
int mkfifo(const char *filename, mode_t mode); int mknode(const char *filename, mode_t mode | S_IFIFO, (dev_t) 0 );c、socket
socket用于进程间通讯。netlink是一种基于socket实现的用户和进程间交互的方式。
d、消息队列
消息队列可以可以避免管道的同步和阻塞问题,不需要进程自己来管理同步问题。消息队列可以选择接收的消息类型。
通过ipcs -l可以查看到系统支持的ipc资源空间。
接收消息进程:
- #include <unistd.h>
- #include <stdlib.h>
- #include <stdio.h>
- #include <string.h>
- #include <sys/msg.h>
- #include <errno.h>
- #define MAX_TEXT 512
- struct msg_st
- {
- long int msg_type;
- char text[MAX_TEXT];
- };
- int main()
- {
- int running = 1;
- struct msg_st data;
- char buffer[BUFSIZ];
- int msgid = -1;
- //建立消息队列
- msgid = msgget((key_t)1234, 0666 | IPC_CREAT);
- if(msgid == -1)
- {
- fprintf(stderr, "msgget failed with error: %d\n", errno);
- exit(EXIT_FAILURE);
- }
- //向消息队列中写消息,直到写入end
- while(running)
- {
- //输入数据
- printf("Enter some text: ");
- fgets(buffer, BUFSIZ, stdin);
- data.msg_type = 1; //注意2
- strcpy(data.text, buffer);
- //向队列发送数据
- if(msgsnd(msgid, (void*)&data, MAX_TEXT, 0) == -1)
- {
- fprintf(stderr, "msgsnd failed\n");
- exit(EXIT_FAILURE);
- }
- //输入end结束输入
- if(strncmp(buffer, "end", 3) == 0)
- running = 0;
- sleep(1);
- }
- exit(EXIT_SUCCESS);
- }
e、共享内存
共享内存没有自动同步机制,一般要结合信号量实现同步。
共享内存的优点是使用方便,无进程关系限制,速度快。
共享内存读:
- #include <unistd.h>
- #include <stdlib.h>
- #include <stdio.h>
- #include <sys/shm.h>
- #include "shmdata.h"
- int main()
- {
- int running = 1;//程序是否继续运行的标志
- void *shm = NULL;//分配的共享内存的原始首地址
- struct shared_use_st *shared;//指向shm
- int shmid;//共享内存标识符
- //创建共享内存
- shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666|IPC_CREAT);
- if(shmid == -1)
- {
- fprintf(stderr, "shmget failed\n");
- exit(EXIT_FAILURE);
- }
- //将共享内存连接到当前进程的地址空间
- shm = shmat(shmid, 0, 0);
- if(shm == (void*)-1)
- {
- fprintf(stderr, "shmat failed\n");
- exit(EXIT_FAILURE);
- }
- printf("\nMemory attached at %X\n", (int)shm);
- //设置共享内存
- shared = (struct shared_use_st*)shm;
- shared->written = 0;
- while(running)//读取共享内存中的数据
- {
- //没有进程向共享内存定数据有数据可读取
- if(shared->written != 0)
- {
- printf("You wrote: %s", shared->text);
- sleep(rand() % 3);
- //读取完数据,设置written使共享内存段可写
- shared->written = 0;
- //输入了end,退出循环(程序)
- if(strncmp(shared->text, "end", 3) == 0)
- running = 0;
- }
- else//有其他进程在写数据,不能读取数据
- sleep(1);
- }
- //把共享内存从当前进程中分离
- if(shmdt(shm) == -1)
- {
- fprintf(stderr, "shmdt failed\n");
- exit(EXIT_FAILURE);
- }
- //删除共享内存
- if(shmctl(shmid, IPC_RMID, 0) == -1)
- {
- fprintf(stderr, "shmctl(IPC_RMID) failed\n");
- exit(EXIT_FAILURE);
- }
- exit(EXIT_SUCCESS);
- }
- #include <unistd.h>
- #include <stdlib.h>
- #include <stdio.h>
- #include <string.h>
- #include <sys/shm.h>
- #include "shmdata.h"
- int main()
- {
- int running = 1;
- void *shm = NULL;
- struct shared_use_st *shared = NULL;
- char buffer[BUFSIZ + 1];//用于保存输入的文本
- int shmid;
- //创建共享内存
- shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666|IPC_CREAT);
- if(shmid == -1)
- {
- fprintf(stderr, "shmget failed\n");
- exit(EXIT_FAILURE);
- }
- //将共享内存连接到当前进程的地址空间
- shm = shmat(shmid, (void*)0, 0);
- if(shm == (void*)-1)
- {
- fprintf(stderr, "shmat failed\n");
- exit(EXIT_FAILURE);
- }
- printf("Memory attached at %X\n", (int)shm);
- //设置共享内存
- shared = (struct shared_use_st*)shm;
- while(running)//向共享内存中写数据
- {
- //数据还没有被读取,则等待数据被读取,不能向共享内存中写入文本
- while(shared->written == 1)
- {
- sleep(1);
- printf("Waiting...\n");
- }
- //向共享内存中写入数据
- printf("Enter some text: ");
- fgets(buffer, BUFSIZ, stdin);
- strncpy(shared->text, buffer, TEXT_SZ);
- //写完数据,设置written使共享内存段可读
- shared->written = 1;
- //输入了end,退出循环(程序)
- if(strncmp(buffer, "end", 3) == 0)
- running = 0;
- }
- //把共享内存从当前进程中分离
- if(shmdt(shm) == -1)
- {
- fprintf(stderr, "shmdt failed\n");
- exit(EXIT_FAILURE);
- }
- sleep(2);
- exit(EXIT_SUCCESS);
- }
f、信号量
信号量是一个特殊的变量,程序对其访问都是原子操作,且只允许对它进行等待(即P(信号变量))和发送(即V(信号变量))信息操作。我们通常通过信号来解决多个进程对同一资源的访问竞争的问题,使在任一时刻只能有一个执行线程访问代码的临界区域,也可以说它是协调进程间的对同一资源的访问权,也就是用于同步进程的。
- #include <unistd.h>
- #include <sys/types.h>
- #include <sys/stat.h>
- #include <fcntl.h>
- #include <stdlib.h>
- #include <stdio.h>
- #include <string.h>
- #include <sys/sem.h>
- union semun
- {
- int val;
- struct semid_ds *buf;
- unsigned short *arry;
- };
- static int sem_id = 0;
- static int set_semvalue();
- static void del_semvalue();
- static int semaphore_p();
- static int semaphore_v();
- int main(int argc, char *argv[])
- {
- char message = 'X';
- int i = 0;
- //创建信号量
- sem_id = semget((key_t)1234, 1, 0666 | IPC_CREAT);
- if(argc > 1)
- {
- //程序第一次被调用,初始化信号量
- if(!set_semvalue())
- {
- fprintf(stderr, "Failed to initialize semaphore\n");
- exit(EXIT_FAILURE);
- }
- //设置要输出到屏幕中的信息,即其参数的第一个字符
- message = argv[1][0];
- sleep(2);
- }
- for(i = 0; i < 10; ++i)
- {
- //进入临界区
- if(!semaphore_p())
- exit(EXIT_FAILURE);
- //向屏幕中输出数据
- printf("%c", message);
- //清理缓冲区,然后休眠随机时间
- fflush(stdout);
- sleep(rand() % 3);
- //离开临界区前再一次向屏幕输出数据
- printf("%c", message);
- fflush(stdout);
- //离开临界区,休眠随机时间后继续循环
- if(!semaphore_v())
- exit(EXIT_FAILURE);
- sleep(rand() % 2);
- }
- sleep(10);
- printf("\n%d - finished\n", getpid());
- if(argc > 1)
- {
- //如果程序是第一次被调用,则在退出前删除信号量
- sleep(3);
- del_semvalue();
- }
- exit(EXIT_SUCCESS);
- }
- static int set_semvalue()
- {
- //用于初始化信号量,在使用信号量前必须这样做
- union semun sem_union;
- sem_union.val = 1;
- if(semctl(sem_id, 0, SETVAL, sem_union) == -1)
- return 0;
- return 1;
- }
- static void del_semvalue()
- {
- //删除信号量
- union semun sem_union;
- if(semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
- fprintf(stderr, "Failed to delete semaphore\n");
- }
- static int semaphore_p()
- {
- //对信号量做减1操作,即等待P(sv)
- struct sembuf sem_b;
- sem_b.sem_num = 0;
- sem_b.sem_op = -1;//P()
- sem_b.sem_flg = SEM_UNDO;
- if(semop(sem_id, &sem_b, 1) == -1)
- {
- fprintf(stderr, "semaphore_p failed\n");
- return 0;
- }
- return 1;
- }
- static int semaphore_v()
- {
- //这是一个释放操作,它使信号量变为可用,即发送信号V(sv)
- struct sembuf sem_b;
- sem_b.sem_num = 0;
- sem_b.sem_op = 1;//V()
- sem_b.sem_flg = SEM_UNDO;
- if(semop(sem_id, &sem_b, 1) == -1)
- {
- fprintf(stderr, "semaphore_v failed\n");
- return 0;
- }
- return 1;
- }
3、linux应用定时任务
a、精度要求不高的定时可以用sleep、usleep。
b、alarm、signal。精度不高。
c、setitimer,精度高,三种定时模式,配合信号使用。
4、多路复用技术
多路复用即同步I/O,select、poll、epoll是实现同步I/O的技术。
select:
- #include <sys/select.h>
- #include <stdio.h>
- #include <sys/types.h>
- #include <sys/stat.h>
- #include <fcntl.h>
- #include <unistd.h>
- int main(int argc, char *argv[])
- {
- fd_set rfds;
- struct timeval tv;
- int ret;
- int fd;
- ret = mkfifo("test_fifo", 0666); // 创建有名管道
- if(ret != 0){
- perror("mkfifo:");
- }
- fd = open("test_fifo", O_RDWR); // 读写方式打开管道
- if(fd < 0){
- perror("open fifo");
- return -1;
- }
- ret = 0;
- while(1){
- // 这部分内容,要放在while(1)里面
- FD_ZERO(&rfds); // 清空
- FD_SET(0, &rfds); // 标准输入描述符 0 加入集合
- FD_SET(fd, &rfds); // 有名管道描述符 fd 加入集合
- // 超时设置
- tv.tv_sec = 1;
- tv.tv_usec = 0;
- // 监视并等待多个文件(标准输入,有名管道)描述符的属性变化(是否可读)
- // 没有属性变化,这个函数会阻塞,直到有变化才往下执行,这里没有设置超时
- // FD_SETSIZE 为 <sys/select.h> 的宏定义,值为 1024
- ret = select(FD_SETSIZE, &rfds, NULL, NULL, NULL);
- //ret = select(FD_SETSIZE, &rfds, NULL, NULL, &tv);
- if(ret == -1){ // 出错
- perror("select()");
- }else if(ret > 0){ // 准备就绪的文件描述符
- char buf[100] = {0};
- if( FD_ISSET(0, &rfds) ){ // 标准输入
- read(0, buf, sizeof(buf));
- printf("stdin buf = %s\n", buf);
- }else if( FD_ISSET(fd, &rfds) ){ // 有名管道
- read(fd, buf, sizeof(buf));
- printf("fifo buf = %s\n", buf);
- }
- }else if(0 == ret){ // 超时
- printf("time out\n");
- }
- }
- return 0;
- }
poll:
epoll:
- #include <sys/epoll.h>
- #include <stdio.h>
- #include <sys/types.h>
- #include <sys/stat.h>
- #include <fcntl.h>
- #include <unistd.h>
- #include <stdlib.h>
- int main(int argc, char *argv[])
- {
- int ret;
- int fd;
- ret = mkfifo("test_fifo", 0666); // 创建有名管道
- if(ret != 0){
- perror("mkfifo:");
- }
- fd = open("test_fifo", O_RDWR); // 读写方式打开管道
- if(fd < 0){
- perror("open fifo");
- return -1;
- }
- ret = 0;
- struct epoll_event event; // 告诉内核要监听什么事件
- struct epoll_event wait_event;
- int epfd = epoll_create(10); // 创建一个 epoll 的句柄,参数要大于 0, 没有太大意义
- if( -1 == epfd ){
- perror ("epoll_create");
- return -1;
- }
- event.data.fd = 0; // 标准输入
- event.events = EPOLLIN; // 表示对应的文件描述符可以读
- // 事件注册函数,将标准输入描述符 0 加入监听事件
- ret = epoll_ctl(epfd, EPOLL_CTL_ADD, 0, &event);
- if(-1 == ret){
- perror("epoll_ctl");
- return -1;
- }
- event.data.fd = fd; // 有名管道
- event.events = EPOLLIN; // 表示对应的文件描述符可以读
- // 事件注册函数,将有名管道描述符 fd 加入监听事件
- ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
- if(-1 == ret){
- perror("epoll_ctl");
- return -1;
- }
- ret = 0;
- while(1){
- // 监视并等待多个文件(标准输入,有名管道)描述符的属性变化(是否可读)
- // 没有属性变化,这个函数会阻塞,直到有变化才往下执行,这里没有设置超时
- ret = epoll_wait(epfd, &wait_event, 2, -1);
- //ret = epoll_wait(epfd, &wait_event, 2, 1000);
- if(ret == -1){ // 出错
- close(epfd);
- perror("epoll");
- }else if(ret > 0){ // 准备就绪的文件描述符
- char buf[100] = {0};
- if( ( 0 == wait_event.data.fd )
- && ( EPOLLIN == wait_event.events & EPOLLIN ) ){ // 标准输入
- read(0, buf, sizeof(buf));
- printf("stdin buf = %s\n", buf);
- }else if( ( fd == wait_event.data.fd )
- && ( EPOLLIN == wait_event.events & EPOLLIN ) ){ // 有名管道
- read(fd, buf, sizeof(buf));
- printf("fifo buf = %s\n", buf);
- }
- }else if(0 == ret){ // 超时
- printf("time out\n");
- }
- }
- close(epfd);
- return 0;
- }
select跨平台,一般平台都支持select。
select系统开销大,每次执行时都要将fd集合从内核态拷贝到用户态。
select单个进程能够监视的fd有限,一般是1024。
poll机制跟select类似,所以系统开销也大,但poll的fd(文件描述符)没有数量限制。
epoll监视的fd没有数量限制,epoll的fd拷贝只需要一次。
select、poll在等待和就绪时都需要遍历描述符,而epoll只需要在等待时遍历描述符并存储就绪的回调,唤醒时只需要查询就绪列表的回调并执行。epoll效率更高。