1、如何判定线程是否安全?
判断一个线程是否是安全的,就要看这个线程是否做到了线程间的同步与互斥,要实现线程间的同步与互斥,主要由以下几个方法:
1.1 互斥量(mutex)
Linux环境下:
mutex互斥量的引入主要是因为,大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个进程,其他线程无法获得这种变量。但有时候,很多变量都需要在线程间共享,这样的变量成为共享变量,可以通过数据的共享,完成线程间的交互。多个线程并发的操作共享变量,可能会产生一些无法预期的错误,例如线程A要用的共享变量可能刚刚被线程B修改了。
引入互斥锁,可以解决上述问题:
- 代码必须有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
- 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
- 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
相关头文件和API:
#include<pthread.h>
#include<errno.h>
//初始化信号量接口,如果使用默认的属性初始化互斥量, 只需把attr设为NULL.
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restric attr);
//销毁信号量对象接口
int pthread_mutex_destroy(pthread_mutex_t *mutex);
//互斥量加锁接口--阻塞式
//说明:对共享资源的访问, 要对互斥量进行加锁, 如果互斥量已经上了锁, 调用线程会阻塞, 直到互斥量被解锁。在完成了对共享资源的访问后, 要对互斥量进行解锁。
int pthread_mutex_lock(pthread_mutex_t *mutex);
//互斥量加锁接口--非阻塞式
//说明: 这个函数是非阻塞调用模式, 也就是说, 如果互斥量没被锁住, trylock函数将把互斥量加锁, 并获得对共享资源的访问权限; 如果互斥量被锁住了, trylock函数将不会阻塞等待而直接返回EBUSY,表示共享资源处于忙状态。
int pthread_mutex_trylock(pthread_mutex_t *mutex);
//互斥量解锁接口
int pthread_mutex_unlock(pthread_mutex_t *mutex);
必须初始化互斥量:
- 方法一,静态分配: pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
- 方法二,动态分配:使用pthread_mutex_init 函数初始化(attr一般置NULL);
销毁互斥量需要注意:
- 使用PTHREAD_MUTEX_INITIALIZER初始化的互斥量不需要销毁;
- 不要销毁一个已经加锁的互斥量;
- 已经销毁的互斥量,要确保后面不会有线程在尝试加锁;
总体来讲, 有几个不成文的基本原则:
- 对共享资源操作前一定要获得锁。
- 尽量短时间地占用锁。
- 如果有多锁, 如获得顺序是ABC连环扣, 释放顺序也应该是ABC。
- 线程错误返回时应该释放它所获得的锁。
各种Mutex的区别:
锁类型 | 初始化方式 | 加锁特征 | 调度特征 |
---|---|---|---|
普通锁 | PTHREAD_MUTEX_INITIALIZER | 同一线程可重复加锁,解锁一次释放锁 | 先等待锁的进程先获得锁 |
嵌套锁 | PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP | 同一线程可重复加锁,解锁同样次数才可释放锁 | 先等待锁的进程先获得锁 |
纠错锁 | PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP | 同一线程不能重复加锁,加上的锁只能由本线程解锁 | 先等待锁的进程先获得锁 |
自适应锁 | PTHREAD_ADAPTIVE_MUTEX_INITIALIZER_NP | 同一线程可重加锁,解锁一次生效 | 所有等待锁的线程自由竞争 |
1.2 条件变量
条件变量:
条件变量使我们可以睡眠等待某种条件出现。
条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。
条件变量类型为pthread_cond_;
初始化:
int pthreade_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *rest rict attr);
//cond:要初始化的变量
//arrt:NULL
销毁:
int pthread_cond_destroy(pthread_cond_t *cond);
等待条件满足:
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *testrict mutex);
//cond:要在这个条件变量上等待
//mutex:互斥量
唤醒等待:
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
1.3 POSIX信号量
POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源的目的。但POSIX可以用于线程间同步。
初始化信号量:
#include<semaphore.h>
int sem_init(sem_t *sem,int pshared,unsigned int value);
//pshared:0表示线程间共享,非零表示进程间共享
//value:信号量初始值
销毁信号量:
int sem_destroy(sem_t *sem);
等待信号量:
//功能:等待信号量,会将信号的值减一
int sem_wait(sem_t *sem);
发布信号量:
//发布信号量,表示资源使用完毕,可以归还资源了,将信号量值加一
int sem_post(sem_t *sem);
1.4 读写锁
在编写多线程的时候,有一种情况是十分常见的。那就是有些公共资源更多的是被读,很少被改写。通常来说,在读的过程中,往往需要加上查找操作,会十分耗时,给这种代码段加锁,会极大地降低程序的效率。
而读写锁的引入,就是为了解决这种多读少写的情况。
读写锁的行为:
当前锁状态 | 读锁请求 | 写锁请求 |
---|---|---|
无锁 | 可以 | 可以 |
读锁 | 可以 | 阻塞 |
写锁 | 阻塞 | 阻塞 |
初始化:
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);
销毁:
int pthread_rwlock_destory(pthread_rwlock_t *rwlock);
加锁和解锁:
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
2、如何实现一个守护进程
2.1 守护进程概念
守护进程也称精灵进程(Daemon),是运行在后台的一种特殊进程。它独立于控制终端并且周期性的执行某种任务或等待处理某些发生的事件。守护进程是一种很有用的进程,Linux的大多数服务器就是用守护进程实现的,例如,ftp服务器,Web服务器,ssh服务器,httpd等。同时,守护进程完成许多系统任务(例如作业规划进程crond等)。
有的进程都是在用户登录或运行程序时创建,在运行结束或用户注销时终止,但系统服务进程(守护进程)不受用户登录注销的影响,他们一直在运行着。
2.2 创建守护进程
setsid函数:
#include<unistd.h>
pid_t setsid(void);
//该函数调用成功时返回新创建的Session的id(其实也是当前进程的id),出错返回-1。
【作用】:创建一个新的Session,并且成为Session Leader.
【注意】:调用这个函数之前,当前进程不允许是进程组的Leader,否则该函数返回-1。要保证当前进程不是进程组的Leader也很容易,只要先fork在调用sersid就行了(因为进程组的Leader是该进程组的第一个进程)。
【成功调用该函数的结果】:
- 创建一个新的Session,当前进程或成为Session Leader,当前进程的ID就是Session的ID。
- 创建一个新的进程组,当前进程成为进程组的Leader,当前进程的ID就是进程组的ID。
- 如果当前进程原本有一个控制终端,则它失去这个控制终端,成为一个没有控制终端的进程。所为失去控制终端是指,原来的控制终端仍然是打开的,仍然可以读写,但只是一个普通的打开文件而不是控制终端了。
2.3 简单的守护进程代码
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
#include<stdlib.h>
#include<fcntl.h>
#include<sys/stat.h>
void mydaemon(void){
int fd0;
pid_t pid;
struct sigaction sa;
umask(0);
//1. 调用umask将文件模式创建屏蔽字设置为0
//2.调用fork,父进程退出(exit)
//如果该守护进程是作为一条简单的shell命令启动的,那么父进程终止使得shell认为该命令已经执行完了
//保证子进程不是一个进程组的组长进程
if((pid = fork())<0){
perror("fork");
}else if(pid > 0){
//父进程
exit(0);
//终止父进程
}else{
//子进程
//调用setsid创建一个新会话
setsid();
//忽略SIGCHLD信号
sa.sa_handler = SIG_IGN;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
if(sigaction(SIGCHLD,&sa,NULL)<0){
//注册子进程退出忽略信号
return;
}
//在此fork,终止父进程,保持子进程不是话首进程,从而保证后续不会在和其他终端关联
//这部分不是必须的
if((pid = fork())<0){
printf("fork error!\n");
return;
}
else if(pid != 0){
//father
exit(0);
}
//将当前工作目录更改为根目录
if(chdir("/")<0){
printf("child dir error\n");
return;
}
//关闭不再需要的文件描述符,或者重定向到 /dev/null
close(0);
fd0 = open("/dev/null",O_RDWR);
dup2(fd0,1);
dup2(fd0,2);
}
}
int main(){
mydaemon();
while(1){
sleep(1);
}
return 0;
}
3、32位系统一个进程最多有多少堆内存
理论上是2^32,也就是4G内存。
Linux实现的是 虚拟地址的前3G供给用户态的进程,后1G是内核的部分, 也就是用户态的进程不能访问0xc0000000以上的虚拟地址。
4、exit()和_exit()区别
_exit终止调用进程,但不关闭文件,不清除输出缓存,也不调用出口函数。
exit函数将终止调用进程。在退出程序之前,所有文件关闭,缓冲输出内容将刷新定义,并调用所有已刷新的“出口函数”(由atexit定义)。
‘exit()’与‘_exit()’有不少区别在使用‘fork()’,特别是‘vfork()’时变得很突出。
‘exit()’与‘_exit()’的基本区别在于前一个调用实施与调用库里用户状态结构(user-mode constructs)有关的清除工作(clean-up),而且调用用户自定义的清除程序
5、几种常见信号
SIGINT: 按下“Ctrl-C”后产生的硬件异常中断,终端驱动程序会发送此信号给该进程(记录在该进程的PCB中)。只有前台进程才能接收到这种控制键产生的信号。
**SIGQUIT:**和SIGINT类似, 但由QUIT字符(通常是Ctrl-\ )来控制. 进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。
**SIGTERM:**程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出,shell命令kill缺省产生这个信号。如果进程终止不了,我们才会尝试SIGKILL。
**SIGSTOP:**停止(stopped)进程的执行. 注意它和terminate以及interrupt的区别:该进程还未结束, 只是暂停执行. 本信号不能被阻塞, 处理或忽略。
**SIGTSTP:**可使前台进程停止。由“Ctrl-Z”来控制。
**SIGFPE:**除零异常信号。
**SIGSEGV:**对于不正确的内存处理(见段错误)。SIGSEGV通常由操作系统生成,但是有适当权限的用户可以在需要时使用kill系统调用或kill命令(一个用户级程序,或者一个shell内建命令)来向一个进程发送信号SIGSEGV。
**SIGALRM:**闹钟超时信号。
**SIGPIPE:**向读端已关闭的管道写数据时,产生此信号。