我们学习计算机需要明白抽象这个概念,因为计算机系统的许多功能的实现通过抽象再转化我们常常见到的概念,如文件(操作系统管理磁盘的抽象)、地址空间(内存管理的抽象),以及今天我们谈到的进程,它其实是应用程序的抽象,今天我们主要简述进程、线程以及相关相关的概念…
首先我们得清楚这是多个程序执行下的概念。
1、并发就是不管几个程序执行,CPU只会被一个程序占用,只不过每个程序占用的时间非常的短,使用者感受不到多个程序实在交替进行;
2、并行就是每个程序每时每刻都在执行,每时每刻都会有一个CPU为它们服务;
这两者都可以被操作系统用于同时执行多个程序,并发用于早期单个CPU时实现多程序执行,而并行却是在多CPU上才能实现。这两者在宏观上我们可以认为是相等的,但是我们得知道并发是伪并行。
举个栗子:电脑上运行QQ和微信,在同一天得15:06每个程序收到一条消息,在并发的系统上QQ可能是15:06:20(或者单位更小)收到消息,微信则不可能是在15:06:20(或者单位更小)收到消息,而并行的系统上则恰好相反,QQ可能是15:06:20(或者单位更小)收到消息,微信也可能是在15:06:20(或者单位更小)收到消息。
这是我们可能会有一个疑问——进程包含哪些东西嘞?
我们知道进程是应用程序的抽象,程序最后会被编译成二进制的指令最后被加载进内存然后执行,在执行过程中就是进程就是一个运行实例,它包括运行需要的所有东西。如:程序计数器、寄存器、堆栈空间以及程序指令等。我们还需要明白一个概念——一个程序可以对应一个进程或多个进程。下面是进程的虚拟地址空间图:
接下来说线程,线程是进程中的概念,一个进程包含地址空间和至少一个的控制线程,线程是一种迷你进程,当然和进程还是有本质上的区别的,下面我们再深入了解。
接下来我们来详谈进程…
进程有三种状态:运行态、就绪态以及阻塞态。
它们之间存在以下的转换关系:
对于转换1:运行的进程由于等待某些资源,并且获取资源需要的时间较长,此时可能发生转换1使进程进入阻塞状态,将CPU的执行权给其它的进程来提高并发。
对于转换2和转换3:是由于进程调度问题,我们知道一个进程执行的太久了就没有“并行”的“感觉”了,作为操作系统的一份子进程调度程序就不允许这种情况发生,就将进程按照特定的进程调度算法(下文再讲)执行.
对于转换4:就是再需要的资源准备好了,就进入就绪状态,如果此时没有其它的进程运行则进行转换3进入运行状态,否则就有调度程序调度。
操作系统有两个功能:管理资源和提高抽象。进程也是资源,那么操作系统是怎么对他进行管理的嘞?首先操作系统维护一张“进程表”,然后然后每个进程占用进程表中的一个表项,表项中包含描述进程的重要信息,如程序计数器,堆栈指针,内存分配情况、打开的文件指针、优先级以及调度信息。还有一个需要单独说明这是一个笼统的概念——进程状态变化需要保存的信息。
1、进程的创建;在通用系统中有4种情况会导致系统的创建,分别是:系统初始化、正在运行的程序执行了创建进程的系统调用、用户请求创建一个新进程以及一个批处理作业的初始化。
第一种情况,在系统初始化时会运行许多的系统服务进程,例如界面的进程。
第二种情况,在编程时调用API创建该进程的副本,例如Windows下的CreateProcesee,Linux下的fork。在这种情况下有一种重要的技术叫做“写时技术”,简单的说就是进程复制时两个进程共享读数据,当两个当中的另一个需要改变数据时就将数据复制到子进程。
第三种情况,例如在交互式系统中键入命令启动进程。
第四种情况,这种情况发生在大型的其处理系统上,当操作系统认为有资源可以运行作业时,就会创建进程。
2、进程的终止;进程退出有这么几种情况:正常退出、异常退出、严重错误、被其它进程杀死。前两种是自愿的,如程序执行完等。异常退出就是没有办法按照正常的程序设计执行,设计者让程序退出。严重错误的话,如:C语言中引用未分配地址的指针所发生的段错误。最后一种情况是其它进程通知操作系统将另外一个进程杀死,不过该进程需要获得确定的授权。
1、管道
管道主要用于linux下的两进程之间通信,举个栗子:netstat -nlp | grep tcp。在Linux下可以同此命令查看使用TCP协议的条目,其中“|”就是一个管道,称为“匿名管道”(它的通信方式为半双工),当然还有命名管道。以上是linux下shell环境下使用管道的方式,当然还有API创建管道的方式,如:
int pipe(int pipefd[2]);(匿名管道,半双工)
int mkfifo(const char *pathname, mode_t mode);(命名管道,全双工)
2、消息队列
消息队列的形式和管道的差不多,不同点在于消息队列存放在内核中,有内核管理,通过消息队列通信的双方规定消息的大小块以及消息格式等信息。
3、共享内存
当两个进程需要传输大量的数据时,就可以使用共享内存了,我们知道我们在进程中分配的或者使用的都是虚拟地址,在对数据更改或者读取时实际上是会映射到物理地址的,而共享内存的原理就是:申请一块虚拟地址,两个进程或者是更多进程的这块虚拟地址映射到同一块物理地址。通过这种方式可以在进程之间传输大量的数据。使用的API为:
int shmget(key_t key, int size, int flag);
4、信号量
接下来就是信号量,个人认为信号量算不上一种进程间通信的机制或者方式,应该算一种计数器,它存在的意义就是使多个进程间不能同时访问同一资源,就像共享内存一样,如果多个资源同时访问那么就存在“一方不知情的情况”。
信号量有P操作和V操作,P就是申请资源将计数器的值减M,V操作就是归还资源,当计数器没有资源是去申请肯定是申请不到的,这时就会阻塞或者其它设定的操作。
5、信号
信号还是比较好理解的,简单的描述就是一个进程发送信号,另一个进程接受信号,然后用相应的处理函数进行处理。如在Linux下Crtl+C就会发送SIGINT信号来终止当前线程。在linux shell下还可以通过kill向某进程发送信号。
6、套接字
熟悉网络编程的会比较熟悉,与上述通信方式不同的是,它可以用于不同主机的进程之间通信,通信的两个程序创建套接字,然后通过一系列的步骤(如:绑定端口、监听、连接、接受等)建立连接(通信的协议可以是TCP、UDP),之后就可以通过以太网通信了。
进程间通信就讲到这里了…
前面已经说过线程是进程中的概念,它们是包含关系,也就是一个进程可以包含许多的进程,多个线程共享进程的地址空间,同时线程也有自己的堆栈空间,其实进程和线程的概念是可以看成一样的,这也是为什么线程可以称为“轻量进程”,我们需要重点突出的是它们之间的区别。
区别1:线程之间是共用地址空间,而进程之间是有各自的地址空间。这就可以使得线程之间通信更加的简易和高效,但是也带来的问题,由于多个线程都可以对资源操作的所以就存在当线程A改变了它的值之后线程B也改变它一次,这是线程A还以为是它设置的值,但是已经被线程B给改变了,就会造成异常。这就引出一个主题“线程同步”。这就个编程带来了复杂性。
区别2:线程之间切换只需要更换运行栈和程序寄存器(切换代价比较小),而进程之间的切换需要重现加载内存的页面、寄存器等(开销比较大),这也是线程的另一个优势。
区别3:线程不能独立执行必须在进程的环境下执行。线程是CPU调度的最小单位,进程是资源分配的最小单位。
区别4:多个进程之间互不影响,多个线程之间密切相关,也就会出现这种情况,一个进程出问题挂掉,其它进程不会受到影响,而一个线程挂掉则整个进程都会挂掉。
可能有的人有接触过Windows下的API创建线程,同时也接触过Linux下的线程创建,这是候就会想怎么不一样?其实这是两种系统的接口,就好你家的门和我家的不一样,但是作用是一样的。而Linux下的这种线程规范就是posix线程规范。
现在posix线程规范可以说已经普遍所有的Unix和类Unix了,而且Windows都有支持posix线程的第三方库了,可见它的普遍性和重要地位,这组线程API遵循国际正式标准啥IEEE来着,就记不得了,个人感觉也不重要。
前面讲过线程通信比较容易,难得是它们之间得同步,是它们每时每刻所认为的值都一致或者说状态一致。
同步主要有三种方式:
1、互斥量(mutex)
互斥量提供给线程独占式的访问某一段代码,使得多个线程同时访问时阻塞或者使其获得互斥量失败而返回,从而避免多个线程之间“认为的值”不一样。
所定义的互斥量其实使进程的一种资源,使用之前需要初始化,使用完需要销毁:
int pthread_mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t *attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
那么它们使如何使得一个线程独占式的访问某一段代码嘞,这就的介绍一组API:
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
前面两个获得互斥量,第三个解锁互斥量。注:互斥量不可复制,一般定义未全局变量。
2、条件变量(cond)
条件变量用来通知共享数据的状态的,可用来提高通信的效率,常与互斥量用于生产者消费者模型,同样的,条件变量属于进程资源,需要初始化和销毁:
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *condattr);
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);阻塞一个条件变量。
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);在abstime规定的时间内等待条件变量。
int pthread_cond_signal(pthread_cond_t *cond); 唤醒一个阻塞在条件变量上的线程。
int pthread_cond_broadcast(pthread_cond_t *cond); 唤醒阻塞在条件变量的所有线程
3、信号量(sem)
信号量和互斥量非常的相似,要说不同的话只是信号量能给资源获取的一个限制(初始化的值),同样的信号量也是资源需要初始化和销毁:
sem_init(sem_t *sem, int pshared, unsigned int value);
sem_destroy(sem_t *sem);
和互斥量一样信号量也通过加锁和释放对资源进行保护:
sem_wait(sem_t *sem);
sem_trywait(sem_t *sem);
sem_post(sem_t *sem);
4、总结
严谨的人就会发现,当信号量或者互斥量在加锁或者释放资源的时候会不会发生其它的中断信号而导致两个线程获得互斥量超过信号量所规定的线程获得资源嘞,从理论上讲这是有可能的,但是嘞,我能提供的加锁和释放资源的API都是“原子操作”,所以就不一样了,可以通俗一点理解:原子操作就是是这个操作不可被中断,从而保证了资源的的一致,以上的API都属于原子操作。
进程和线程在操作系统中属于非常重要的概念,不管使面试还是实际开发都是必须掌握的概念,熟悉了它们在开发中也会轻车熟路,以上是进程线程概念的浅谈,希望对各位有所帮助,当然,实际开发的话这些永远是不够的,还需要看一些权威性的书籍,在这里给大家推荐《posix多线程程序设计》,本人有中文电子版,私聊免费分享给大家,谢谢观看!
同样的如有疑问欢迎提问,如有错误欢迎指正!