如何在后台开启轻量级线程来定时更新共享内存优缺点

posts - 0,&
comments - 3,&
trackbacks - 0
&&&&&&&&共享内存区域是被多个进程共享的一部分物理内存。如果多个进程都把该内存区域映射到自己的虚拟地址空间,则这些进程就都可以直接访问该共享内存区域,从而可以通过该区域进行通信。共享内存是进程间共享数据的一种最快的方法,一个进程向共享内存区域写入了数据,共享这个内存区域的所有进程就可以立刻看到其中的内容。这块共享虚拟内存的页面,出现在每一个共享该页面的进程的页表中。但是它不需要在所有进程的虚拟内存中都有相同的虚拟地址。
图 共享内存映射图&&
&&&&&&&&&&&&&&&&&&&&&&&&&
&&&&&&&&&象所有的 System V IPC对象一样,对于共享内存对象的获取是由key控制。内存共享之后,对进程如何使用这块内存就不再做检查。它们必须依赖于其它机制,比如System V的信号灯来同步对于共享内存区域的访问(信号灯如何控制对临界代码的访问另起一篇说话)。
&&&&&&&&每一个新创建的共享内存对象都用一个shmid_kernel数据结构来表达。系统中所有的shmid_kernel数据结构都保存在shm_segs向量表中,该向量表的每一个元素都是一个指向shmid_kernel数据结构的指针。
shm_segs向量表的定义如下:
struct shmid_kernel *shm_segs[SHMMNI];
&&& SHMMNI为128,表示系统中最多可以有128个共享内存对象。
&&& 数据结构shmid_kernel的定义如下:
&&&&struct shmid_kernel
&&&&&&&&struct shmid_&&&&&&&& /* the following are private */
&&&&&&&&unsigned long shm_& /* size of segment (pages) */
&&&&&&&&unsigned long *shm_& /* array of ptrs to frames -& SHMMAX */&
&&&&&&&&struct vm_area_struct *& /* descriptors for attaches */
&&& 其中:
&&&&shm_pages代表该共享内存对象的所占据的内存页面数组,数组里面的每个元素当然是每个内存页面的起始地址.
&&& shm_npages则是该共享内存对象占用内存页面的个数,以页为单位。这个数量当然涵盖了申请空间的最小整数倍.
&&&&(A new shared memory segment,& with size& equal to the value of size rounded up to a multiple of PAGE_SIZE)
&&& shmid_ds是一个数据结构,它描述了这个共享内存区的认证信息,字节大小,最后一次粘附时间、分离时间、改变时间,创建该共享区域的进程,最后一次对它操作的进程,当前有多少个进程在使用它等信息。
&&&&其定义如下:
&&&&struct shmid_ds {
&&&&&&&&struct ipc_perm shm_&& /* operation perms */
&&&&&&&&int shm_&&&&&&&&&&&&& /* size of segment (bytes) */
&&&&&&&&__kernel_time_t shm_& /* last attach time */
&&&&&&&&__kernel_time_t shm_& /* last detach time */
&&&&&&&&__kernel_time_t shm_& /* last change time */
&&&&&&&&__kernel_ipc_pid_t shm_ /* pid of creator */
&&&&&&&&__kernel_ipc_pid_t shm_ /* pid of last operator */
&&&&&&&&unsigned short shm_&& /* no. of current attaches */
&&&&&&&&unsigned short shm_&& /* compatibility */
&&&&&&&&void *shm_unused2;&&&&&&&&&& /* ditto - used by DIPC */
&&&&&&&&void *shm_unused3;&&&&&&&&&& /* unused */
&&&&&&& attaches描述被共享的物理内存对象所映射的各进程的虚拟内存区域。每一个希望共享这块内存的进程都必须通过系统调用将其关联(attach)到它的虚拟内存中。这一过程将为该进程创建了一个新的描述这块共享内存的vm_area_struct数据结构。创建时可以指定共享内存在它的虚拟地址空间的位置,也可以让Linux自己为它选择一块足够的空闲区域。
&&&&&&&&这个新的vm_area_struct结构是维系共享内存和使用它的进程之间的关系的,所以除了要关联进程信息外,还要指明这个共享内存数据结构shmid_kernel所在位置; 另外,便于管理这些经常变化的vm_area_struct,所以采取了链表形式组织这些数据结构,链表由attaches指向,同时 vm_area_struct数据结构中专门提供了两个指针:vm_next_shared和 vm_prev_shared,用于连接该共享区域在使用它的各进程中所对应的vm_area_struct数据结构。&
图 System V IPC 机制 - 共享内存&
&&&&&&&&&&&& &&
&&&&&&&&Linux为共享内存提供了四种操作。
&&&&&&&&1. 共享内存对象的创建或获得。与其它两种IPC机制一样,进程在使用共享内存区域以前,必须通过系统调用sys_ipc (call值为SHMGET)创建一个键值为key的共享内存对象,或获得已经存在的键值为key的某共享内存对象的引用标识符。以后对共享内存对象的访问都通过该引用标识符进行。对共享内存对象的创建或获得由函数sys_shmget完成,其定义如下:
int sys_shmget (key_t key, int size, int shmflg)
&&& 这里key是表示该共享内存对象的键值,size是该共享内存区域的大小(以字节为单位),shmflg是标志(对该共享内存对象的特殊要求)。
&&& 它所做的工作如下:
&&& 1) 如果key == IPC_PRIVATE,则总是会创建一个新的共享内存对象。
&但是& (The name choice IPC_PRIVATE was perhaps unfortunate, IPC_NEW would more&clearly show its function)
&&& * 算出size要占用的页数,检查其合法性。
&&& * 申请一块内存用于建立shmid_kernel数据结构,注意这里申请的内存区域大小不包括真正的共享内存区,实际上,要等到第一个进程试图访问它的时候才真正创建共享内存区。
&&& * 根据该共享内存区所占用的页数,为其申请一块空间用于建立页表(每页4个字节),将页表清0。
&&& * 搜索向量表shm_segs,为新创建的共享内存对象找一个空位置。
&&& * 填写shmid_kernel数据结构,将其加入到向量表shm_segs中为其找到的空位置。
&&& * 返回该共享内存对象的引用标识符。
&&& 2) 在向量表shm_segs中查找键值为key的共享内存对象,结果有三:
&&& * 如果没有找到,而且在操作标志shmflg中没有指明要创建新共享内存,则错误返回,否则创建一个新的共享内存对象。
&&& * 如果找到了,但该次操作要求必须创建一个键值为key的新对象,那么错误返回。
&&& * 否则,合法性、认证检查,如有错,则错误返回;否则,返回该内存对象的引用标识符。
&&& 共享内存对象的创建者可以控制对于这块内存的访问权限和它的key是公开还是私有。如果有足够的权限,它也可以把共享内存锁定在物理内存中。
&&& 参见include/linux/shm.h
&&& 2. 关联。在创建或获得某个共享内存区域的引用标识符后,还必须将共享内存区域映射(粘附)到进程的虚拟地址空间,然后才能使用该共享内存区域。系统调用 sys_ipc(call值为SHMAT)用于共享内存区到进程虚拟地址空间的映射,而真正完成粘附动作的是函数sys_shmat,
其定义如下:&&&
&&&&&&&#include &sys/types.h&
&&&&&& #include &sys/shm.h&
&&&&&& void *shmat(int shmid, const void *shmaddr, int shmflg);
&&& 其中:
&&&& shmid是shmget返回的共享内存对象的引用标识符;
&&& shmaddr用来指定该共享内存区域在进程的虚拟地址空间对应的虚拟地址;
&&& shmflg是映射标志;
&&&&返回的是 在进程中的虚拟地址
&&& 该函数所做的工作如下:
&&& 1) 根据shmid找到共享内存对象。
&&& 2) 如果shmaddr为0,即用户没有指定该共享内存区域在它的虚拟空间中的位置,则由系统在进程的虚拟地址空间中为其找一块区域(从1G开始);否则,就用shmaddr作为映射的虚拟地址。
&& (If& shmaddr& is NULL, the system chooses a suitable (unused) address a他 which to attach the segment)
&&& 3) 检查虚拟地址的合法性(不能超过进程的最大虚拟空间大小—3G,不能太接近堆栈栈顶)。
&&& 4) 认证检查。
&&& 5) 申请一块内存用于建立数据结构vm_area_struct,填写该结构。
&&& 6) 检查该内存区域,将其加入到进程的mm结构和该共享内存对象的vm_area_struct队列中。
&&& 共享内存的粘附只是创建一个vm_area_struct数据结构,并将其加入到相应的队列中,此时并没有创建真正的共享内存页。
&&& 当进程第一次访问共享虚拟内存的某页时,因为所有的共享内存页还都没有分配,所以会发生一个page fault异常。当Linux处理这个page fault的时候,它找到发生异常的虚拟地址所在的vm_area_struct数据结构。在该数据结构中包含有这类共享虚拟内存的一组处理程序,其中的 nopage操作用来处理虚拟页对应的物理页不存在的情况。对共享内存,该操作是shm_nopage(定义在ipc/shm.c中)。该操作在描述这个共享内存的shmid_kernel数据结构的页表shm_pages中查找发生page fault异常的虚拟地址所对应的页表条目,看共享页是否存在(页表条目为0,表示共享页是第一次使用)。如果不存在,它就分配一个物理页,并为它创建一个页表条目。这个条目不但进入当前进程的页表,同时也存到shmid_kernel数据结构的页表shm_pages中。
&&& 当下一个进程试图访问这块内存并得到一个page fault的时候,经过同样的路径,也会走到函数shm_nopage。此时,该函数查看shmid_kernel数据结构的页表shm_pages时,发现共享页已经存在,它只需把这里的页表项填到进程页表的相应位置即可,而不需要重新创建物理页。所以,是第一个访问共享内存页的进程使得这一页被创建,而随后访问它的其它进程仅把此页加到它们的虚拟地址空间。
&&& 3. 分离。当进程不再需要共享虚拟内存的时候,它们与之分离(detach)。只要仍旧有其它进程在使用这块内存,这种分离就只会影响当前的进程,而不会影响其它进程。当前进程的vm_area_struct数据结构被从shmid_ds中删除,并被释放。当前进程的页表也被更新,共享内存对应的虚拟内存页被标记为无效。当共享这块内存的最后一个进程与之分离时,共享内存页被释放,同时,这块共享内存的shmid_kernel数据结构也被释放。
& 系统调用sys_ipc (call值为SHMDT) 用于共享内存区与进程虚拟地址空间的分离,而真正完成分离动作的是函数&&&&
&&& sys_shmdt,其定义如下:
&&& int sys_shmdt (char *shmaddr)
&&& 其中shmaddr是进程要分离的共享页的开始虚拟地址。
&&& 该函数搜索进程的内存结构中的所有vm_area_struct数据结构,找到地址shmaddr对应的一个,调用函数do_munmap将其释放。
&&& 在函数do_munmap中,将要释放的vm_area_struct数据结构从进程的虚拟内存中摘下,清除它在进程页表中对应的页表项(可能占多个页表项).&
&&& 如果共享的虚拟内存没有被锁定在物理内存中,分离会更加复杂。因为在这种情况下,共享内存的页可能在系统大量使用内存的时候被交换到系统的交换磁盘。为了避免这种情况,可以通过下面的控制操作,将某共享内存页锁定在物理内存不允许向外交换。共享内存的换出和换入,已在第3章中讨论。
&&& 4. 控制。Linux在共享内存上实现的第四种操作是共享内存的控制(call值为SHMCTL的sys_ipc调用),它由函数sys_shmctl实现。控制操作包括获得共享内存对象的状态,设置共享内存对象的参数(如uid、gid、mode、ctime等),将共享内存对象在内存中锁定和释放(在对象的mode上增加或去除SHM_LOCKED标志),释放共享内存对象资源等。
&&& 共享内存提供了一种快速灵活的机制,它允许进程之间直接共享大量的数据,而无须使用拷贝或系统调用。共享内存的主要局限性是它不能提供同步,如果两个进程企图修改相同的共享内存区域,由于内核不能串行化这些动作,因此写的数据可能任意地互相混合。所以使用共享内存的进程必须设计它们自己的同步协议,如用信号灯等。
以下是使用共享内存机制进行进程间通信的基本操作:
需要包含的头文件:
#include &sys/types.h&
#include &sys/ipc.h&
#include &sys/shm.h&
1.创建共享内存:
&int shmget(key_t key,int size,int shmflg);
参数说明:
key:用来表示新建或者已经存在的共享内存去的关键字。
size:创建共享内存的大小。
shmflg:可以指定的特殊标志。IPC_CREATE,IPC_EXCL以及低九位的权限。
shmid=shmget(IPC_PRIVATE,4096,IPC_CREATE|IPC_EXCL|0660);
if(shmid==-1)
perror("shmget()");
2.连接共享内存
char *shmat(int shmid,char *shmaddr,int shmflg);
shmid:共享内存的关键字
shmaddr:指定共享内存出现在进程内存地址的什么位置,通常我们让内核自己决定一个合适的地址位置,用的时候设为0。
shmflg:制定特殊的标志位。
shmp=shmat(shmid,0,0);
if(shmp==(char *)(-1))
perror("shmat()\n");
3.使用共享内存
在使用共享内存是需要注意的是,为防止内存访问冲突,我们一般与信号量结合使用。
4.分离共享内存:当程序不再需要共享内后,我们需要将共享内存分离以便对其进行释放,分离共享内存的函数原形如下:
int shmdt(char *shmaddr);
5. 释放共享内存
int shmctl(int shmid,int cmd,struct shmid_ds *buf);
阅读(9148)
&re: Linux 进程间通信 - 共享内存shmget方式(转)
很好&&&&&&
&re: Linux 进程间通信 - 共享内存shmget方式(转)
分析透彻,强&&&&&&
2930311234567891011121314151617181920212223242526272829303112345678&|&&|&&|&&|&&
当前位置: >
PostgreSQL 后台进程对共享内存的指针
作者:互联网 & 来源:转载 &
摘要: 开始
InitShmemIndex() --- set up or attach to shmem index table.
InitShmemIndex(void)
InitShmemIndex() --- set up or attach to shmem index table.
InitShmemIndex(void)
* Create the shared memory shmem index.
* Since ShmemInitHash calls ShmemInitStruct, which expects the ShmemIndex
* hashtable to exist already, we have a bit of a circularity problem in
* initializing the ShmemIndex itself.
The special &ShmemIndex& hash
* table name will tell ShmemInitStruct to fake it.
info.keysize = SHMEM_INDEX_KEYSIZE;
info.entrysize = sizeof(ShmemIndexEnt);
hash_flags = HASH_ELEM;
ShmemIndex = ShmemInitHash(&ShmemIndex&,
SHMEM_INDEX_SIZE, SHMEM_INDEX_SIZE,
&info, hash_flags);
其实 ShmemIndex 就是对共享内存的指针:
* ShmemInitHash -- Create and initialize, or attach to, a
shared memory hash table.
* We assume caller is doing some kind of synchronization
* so that two processes don't try to create/initialize the same
* table at once.
(In practice, all creations are done in the postmaster
* child processes should always be attaching to existing tables.)
* max_size is the estimated maximum number of hashtable entries.
* not a hard limit, but the access efficiency will degrade if it is
* exceeded substantially (since it's used to compute directory size and
* the hash table buckets will get overfull).
* init_size is the number of hashtable entries to preallocate.
For a table
* whose maximum size is certain, this should be equal to max_ that
* ensures that no run-time out-of-shared-memory failures can occur.
* Note: before Postgres 9.0, this function returned NULL for some failure
Now, it always throws error instead, so callers need not check
* for NULL.
ShmemInitHash(const char *name, /* table string name for shmem index */
long init_size,
/* initial table size */
long max_size,
/* max size of the table */
HASHCTL *infoP,
/* info about key and bucket size */
int hash_flags)
/* info about infoP */
* Hash tables allocated in shared memory ha it can't
* grow or other backends wouldn't be able to find it. So, make sure we
* make it big enough to start with.
* The shared memory allocator must be specified too.
infoP-&dsize = infoP-&max_dsize = hash_select_dirsize(max_size);
infoP-&alloc = ShmemA
hash_flags |= HASH_SHARED_MEM | HASH_ALLOC | HASH_DIRSIZE;
/* look it up in the shmem index */
location = ShmemInitStruct(name,
hash_get_shared_size(infoP, hash_flags),
* if it already exists, attach to it rather than allocate and initialize
* new space
if (found)
hash_flags |= HASH_ATTACH;
/* Pass location of hashtable header to hash_create */
infoP-&hctl = (HASHHDR *)
return hash_create(name, init_size, infoP, hash_flags);
看hash_create 的相关代码:
* hash_create -- create a new dynamic hash table
tabname: a name for the table (for debugging purposes)
nelem: maximum number of elements expected
*info: additional table parameters, as indicated by flags
flags: bitmask indicating which parameters to take from *info
* Note: for a shared-memory hashtable, nelem needs to be a pretty good
* estimate, since we can't expand the table on the fly.
But an unshared
* hashtable can be expanded on-the-fly, so it's better for nelem to be
* on the small side and let the table grow if it's exceeded.
* large nelem will penalize hash_seq_search speed without buying much.
hash_create(const char *tabname, long nelem, HASHCTL *info, int flags)
* For shared hash tables, we have a local hash header (HTAB struct) that
* we allocate in TopMemoryC all else is in shared memory.
* For non-shared hash tables, everything including the hash header is in
* a memory context created specially for the hash table --- this makes
* hash_destroy very simple.
The memory context is made a child of either
* a context specified by the caller, or TopMemoryContext if nothing is
* specified.
if (flags & HASH_SHARED_MEM)
/* Set up to allocate the hash header */
CurrentDynaHashCxt = TopMemoryC
/* Create the hash table's private memory context */
if (flags & HASH_CONTEXT)
CurrentDynaHashCxt = info-&
CurrentDynaHashCxt = TopMemoryC
CurrentDynaHashCxt = AllocSetContextCreate(CurrentDynaHashCxt,
ALLOCSET_DEFAULT_MINSIZE,
ALLOCSET_DEFAULT_INITSIZE,
ALLOCSET_DEFAULT_MAXSIZE);
/* Initialize the hash header, plus a copy of the table name */
hashp = (HTAB *) DynaHashAlloc(sizeof(HTAB) + strlen(tabname) +1);
MemSet(hashp, 0, sizeof(HTAB));
hashp-&tabname = (char *) (hashp + 1);
strcpy(hashp-&tabname, tabname);
if (flags & HASH_SHARED_MEM)
* ctl structure and directory are preallocated for shared memory
Note that HASH_DIRSIZE and HASH_ALLOC had better be set as
hashp-&hctl = info-&
hashp-&dir = (HASHSEGMENT *) (((char *) info-&hctl) + sizeof(HASHHDR));
hashp-&hcxt = NULL;
hashp-&isshared = true;
/* hash table already exists, we're just attaching to it */
if (flags & HASH_ATTACH)
/* make local copies of some heavily-used values */
hctl = hashp-&
hashp-&keysize = hctl-&
hashp-&ssize = hctl-&
hashp-&sshift = hctl-&
if (!hashp-&hctl)
hashp-&hctl = (HASHHDR *) hashp-&alloc(sizeof(HASHHDR));
if (!hashp-&hctl)
ereport(ERROR,
(errcode(ERRCODE_OUT_OF_MEMORY),
errmsg(&out of memory&)));
if (flags & HASH_FIXED_SIZE)
hashp-&isfixed = true;
在 src/backend/storage/ipc/shmem.c&的注释中也是这么说的:
/* * POSTGRES processes share one or more regions of shared memory. * The shared memory is created by a postmaster and is inherited * by each backend via fork() (or, in some ports, via other OS-specific * methods). The routines in this file are used for allocating and * binding to shared memory data structures.
static HTAB *ShmemIndex = NULL; /* primary index hashtable for shmem */
[作者:技术者高健@博客园 &mail:&&]
版权所有 IT知识库 CopyRight (C)
IT知识库 IT610.com , All Rights Reserved.并行程序设计分为共享内存和消息驱动(其实就是分布式内存)两种,
共享内存:所有CPU共内存,所有CPU由一个操作系统控制的,例如Windows和Linux/UNIX,目前流行的多核、多CPU机器都是属于这种;
消息驱动:其实就是分布式内存,CPU由不同的操作系统控制,不同的CPU之间通过网络通信。例如网格Grid是通过因特网通信、集群Cluster是通过局域网通信、MPP是通过专有的高速网络通信。
通过上面的对比,聪明的读者估计很快就想到了这两种系统并行程序实现方式的差异:
共享内存:通过操作系统的多进程多线程来完成并行任务,通过进程间通信来完成协作;
消息驱动:通过多台机器来完成并行任务,通过消息来完成协作。(MPP物理上看是一台机器,逻辑上是多台机器)。
当然,由于消息驱动系统中每个处理单元都是一台独立的机器,对这台独立的机器本身当然也可以通过共享内存来实现并行处理。
对于多进程和多线程来说,最有代表性且最常见的的莫过于Windows和Linux(作为UNIX类操作系统的代表,下同)这两个操作系统了。
真是冤家路窄,Windows和Linux这对冤家在这里又碰面了!!
当然,我这里不是要挑起Windows和Linux谁优谁劣的争论,对于一个真正的技术人来说,Windows和Linux本身并没有优劣之分,只有在不同的使用场景下用谁会更好的问题。之所以将Windows和Linux拿来对比,是因为对比更加容易让人理解,记忆也更加深刻!
下面我们首先从多进程和多线程的实现机制方面来对比Windows和Linux。
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&多进程多线程实现机制
说起进程和线程,估计大家都会立刻想起那句耳熟能详的解释“进程是资源分配的最小单位,线程是运行的最小单位”!。
理论上来说这是对的,但实际上来说就不一定了,例如Windows有进程和线程的概念,而传统UNIX却只有进程的概念(例如经典的《UNIX环境高级编程》中就没有多线程的概念,但Solaris、AIX等又有另外的实现,此处暂且不表),Linux也有进程和线程的概念,但实现机制和Windows又不一样,真是林子大了什么鸟都有:)
有几个进程、线程相关的概念首先要简单介绍一下:
1.1概念介绍
资源分配最小单位,有的操作系统还是运行最小单位;
运行最小单位,也是CPU调度的最小单位;
1.1.3ULT、KLT
用户态线程和内核态线程;主要的区分就是“谁来管理”线程,用户态是用户管理,内核态是内核管理(但肯定要提供一些API,例如创建)。
简单对比两者优劣势:
1)可移植性:因为ULT完全在用户态实现线程,因此也就和具体的内核没有什么关系,可移植性方面ULT略胜一筹;
2)可扩展性:ULT是由用户控制的,因此扩展也就容易;相反,KLT扩展就很不容易,基本上只能受制于具体的操作系统内核;
3)性能:由于ULT的线程是在用户态,对应的内核部分还是一个进程,因此ULT就没有办法利用多处理器的优势,而KLT就可以通过调度将线程分布在多个处理上运行,这样KLT的性能高得多;另外,一个ULT的线程阻塞,所有的线程都阻塞,而KLT一个线程阻塞不会影响其它线程。
4)编程复杂度:ULT的所有管理工作都要由用户来完成,而KLT仅仅需要调用API接口,因此ULT要比KLT复杂的多;
1.1.4POSIX
为了解决不同操作系统之间移植时接口不兼容而制定的接口标准,详见维基百科解释:。
为了解决Linux原有线程实现机制的缺陷而创立的一个开源项目,从2.4开始就有发布版本采用NPTL来实现多线程支持了。详见维基百科解释。
Lightweight Process,轻量级进程,看名字有点奇怪,为什么叫轻量级进程呢?为什么又要用轻量级线程呢?
看了前面ULT和KLT的比较,估计大家也发现了一个问题:所谓的ULT,因为不能利用多处理器的优势和线程互相阻塞,其实完全不能堪重任,但对于传统UNIX和Linux这类操作系统,内核设计和实现的时候就没有线程这种对象,那怎么实现多线程呢?
天才们于是想出了LWP这个招数,说白了这就是一个“山寨版的进程”,完全具有了山寨的一切特征:
文件系统是原来的进程的;
文件描述符是原来的进程的;
信号处理是原来的进程的;
地址空间是原来的进程的;
但就是进程ID不是原来的进程的,你说像不像BlackBerry的山寨版BlockBerry?
详情请参考维基百科解释:
1.2详细对比
1.2.1Windows
在此要向Windows致敬:至少相比Linux来说,Windows在线程上的支持是Linux不能比的(不要跟我提DOS哈)!
Windows的实现机制简单来说就是前面提到的KLT,即Windows在内核级别支持线程。每个Windows进程至少有一个线程,系统调度的时候也是调度线程。
当创建一个进程时,系统会自动创建它的第一个线程,称为主线程。然后,该线程可以创建其他的线程,而这些线程又能创建更多的线程。
Windows已经提供了线程编程系列的API,这里就不详述了。
1.2.2Linux
Linux不同的版本有不同的实现,2.0~2.4实现的是俗称LinuxThreads的多线程方式,到了2.6,基本上都是NPTL的方式了。下面我们分别介绍。
1.2.2.1&&&&&&
注:以下内容主要参考“杨沙洲&(mailto:?subject=Linux&线程实现机制分析&cc=)国防科技大学计算机学院”的“Linux&线程实现机制分析”。
这种实现本质上是一种LWP的实现方式,即通过轻量级进程来模拟线程,内核并不知道有线程这个概念,在内核看来,都是进程。
Linux采用的“一对一”的线程模型,即一个LWP对应一个线程。这个模型最大的好处是线程调度由内核完成了,而其他线程操作(同步、取消)等都是核外的线程库函数完成的。
在LinuxThreads中,专门为每一个进程构造了一个管理线程,负责处理线程相关的管理工作。当进程第一次调用pthread_create()创建一个线程的时候就会创建并启动管理线程。然后管理线程再来创建用户请求的线程。也就是说,用户在调用pthread_create后,先是创建了管理线程,再由管理线程创建了用户的线程。
这种通过LWP的方式来模拟线程的实现看起来还是比较巧妙的,但也存在一些比较严重的问题:
1)线程ID和进程ID的问题
按照POSIX的定义,同一进程的所有的线程应该共享同一个进程和父进程ID,而Linux的这种LWP方式显然不能满足这一点。
2)信号处理问题
异步信号是以进程为单位分发的,而Linux的线程本质上每个都是一个进程,且没有进程组的概念,所以某些缺省信号难以做到对所有线程有效,例如SIGSTOP和SIGCONT,就无法将整个进程挂起,而只能将某个线程挂起。
3)线程总数问题
LinuxThreads将每个进程的线程最大数目定义为1024,但实际上这个数值还受到整个系统的总进程数限制,这又是由于线程其实是核心进程。
4)管理线程问题
管理线程容易成为瓶颈,这是这种结构的通病;同时,管理线程又负责用户线程的清理工作,因此,尽管管理线程已经屏蔽了大部分的信号,但一旦管理线程死亡,用户线程就不得不手工清理了,而且用户线程并不知道管理线程的状态,之后的线程创建等请求将无人处理。
5)同步问题
LinuxThreads中的线程同步很大程度上是建立在信号基础上的,这种通过内核复杂的信号处理机制的同步方式,效率一直是个问题。
6)其他POSIX兼容性问题
Linux中很多系统调用,按照语义都是与进程相关的,比如nice、setuid、setrlimit等,在目前的LinuxThreads中,这些调用都仅仅影响调用者线程。
7)实时性问题
线程的引入有一定的实时性考虑,但LinuxThreads暂时不支持,比如调度选项,目前还没有实现。不仅LinuxThreads如此,标准的Linux在实时性上考虑都很少。
1.2.2.2NPTL的实现
NPTL,Native POSIX Thread Library,天生的POSIX线程库。从命名上也可以看出所谓的NPTL就是针对原来的LinuxThreads的,不然为啥叫“Native”呢:)
本质上来说,NPTL还是一个LWP的实现机制,但相对原有LinuxThreads来说,做了很多的改进。下面我们看一下NPTL如何解决原有LinuxThreads实现机制的缺陷。
1)线程ID和进程ID问题
新的exec函数能够创建和原有进程ID一样ID的新进程,这样所有的线程ID都是一样的;且/Proc目录下只会显示进程的初始线程(初始线程就代表整个进程,类似于Windows的进程中第一个线程s),不会再像以前LinuxThreads机制时每个线程在proc目录下都有记录。
2)信号处理问题
内核实现了POSIX要求的线程信号处理机制,发送给进程的信号将由内核分发给一个合适的线程处理,对于致命和全局的信号(例如Stop,Continue,&Pending),所有的线程都同步处理。
3)线程总数问题
内核经过扩展,能够处理任意数量的线程。PID空间经过扩展后,在IA-32系统上能够最大支持20亿线程。
4)管理线程问题
去掉管理进程,管理进程的任务由扩展后的clone函数完成;增加了exit_group的系统调用,用于退出整个进程;
5)信号同步问题
实现了一个叫做Futex(Fase Userspace Mutex,注意不是Mutex)机制用来完成线程间同步,Futex的主要操作是在用户态完成的,这样解决了依靠内核信号机制进行同步的效率问题。详细请参考。
当然,NPTL虽然做了很多改进,但依然不是100% POSIX兼容的,LinuxThreads的第6个和第7个问题在NPTL机制下依然没有解决,但这并不掩盖NPTL带来的巨大改进,下面是性能对比图:
NPTL官方的文档:。
看了前面的分析,大家可能纳闷了,这哪里是对决哦?全部是讲Linux的实现了。
其实我也郁闷,本来应该更加详细的介绍Windows实现机制的,但由于Linux的不争气,全部用来变成对它的分析了。
& & & & & &&&进程间通信
多进程和多线程本质上就是将原来一个进程或者线程处理的任务分给了多个进程或者线程,也可以说是将原来一个CPU处理的任务分给了多个CPU处理,类似于随着生产力的发展,原来一个人包打天下的个人英雄主义时代被分工合作的团队取代一样。
既然是一个团队,就必然涉及到分工合作问题,并行程序的设计本质上就是解决“分工”和“合作”的问题。其中“分工”主要是后面讲到“并行程序设计模式”,而“合作”则是本篇重点要讨论的问题。
相信大家做过项目的都知道所谓合作其实就是“通信”和“冲突解决”。常见的“通信”有开会、电话、邮件、甚至QQ等,无非是为了交换信息,而“冲突解决”其实就是为了解决资源不足,大家抢着用的问题。
可能许多菜鸟、大侠以及网上的很多博客都和和我开始一样,将“通信”和“冲突解决”混为一谈了,一说到进程间通信,就会口若悬河的冒出一大堆名词:管道、信号量、消息队列、互斥。。。。。这里面有的是“通信”(管道、消息队列),有的是“冲突解决”(信号量、互斥),并不是一类东东。
下面我们分别从“通信”和“冲突解决”两方面来比较Windows和Linux。
1.1Windows进程间通信
标准(例如《Windows系统编程》里面提到的)的Windows进程间通信有三个:匿名管道、命名管道(又叫FIFO)、邮槽(MailSlot),实际上常用的还有一个:共享内存。之所以说它不是标准的,我猜测可能是共享内存设计本意不是为了进程间通信用的,而是为了内存映射用的。
1.1.1匿名管道
顾名思义:匿名管道就是“匿名”的“管道”。为什么这样拆开呢?正所谓名如其人,通过名字我们就可以了解大概这是个什么东东。
匿名:之所以叫做“匿名”,当然是因为没有名字了,但为什么会没有名字呢?没有名字又有什么好处呢?其实很简单了,“匿名”当然是不想让其它人知道了,说白了这个“匿名管道”就是只给父子进程用的,别人不需要也不可能知道名字的。
管道:说道管道,你是否想到了“下水管道”、“煤气管道”等?对了,和这些管道本质上是一样的,就是可以传送东西的,所以叫做管道。要注意匿名管道是“单向流通”(也叫半双工)的。
1.1.2命名管道
聪明的你看到这个名字肯定就会产生如下两个想法,我们就一一来解答:
1)和匿名管道看起来很像
是的,命名管道就是相对匿名管道来说的。
2)命名管道和匿名管道有什么差别?
消息格式
命名管道可以控制读消息的长度
只能在一台机器上
可以跨网络
一对一,父子进程用
一对多,不同的进程都可以用
一个命名管道可以有多个实例
邮槽和命名管道类似,都是有名字的,也可以跨网络进行通信,既然是这样,为什么Windows还要设计邮槽呢?其实也没有什么玄虚,说简单点就是管道都是“点对点”的(命名管道虽然是一对多,但具体的通信还是1对1进行的),而邮槽是为了提供一种“广播”通信机制(王婆卖瓜一下:微软还不如将邮槽叫做“广播”:-P)。
下面我们看看邮槽和命名管道的对比:
消息格式
命名管道可以控制读消息的长度
广播当然是单向的了
可以跨网络
可以跨网络
1.1.4共享内存
就像前面提到的一样,共享内存并不是正统的进程间通信的机制,共享内存其实只不过是Windows“内存映射文件”的一个特殊用法而已。
然而实际中共享内存在进程间通信却比较常见,从使用方便性上来说,共享内存其实没有前面介绍的方便(必须结合互斥、事件等一起使用),但为什么应用比较多呢?关键在于共享内存性能很高。
共享内存应该是介于匿名管道和命名管道之间的通信方式,为什么这么说呢?主要有如下几个原因:
1)共享内存和匿名管道相比:共享内存有名称,可用于多个进程通信,这点像命名管道;
2)共享内存和命名管道相比:共享内存只能在一台机器上使用,不能跨网络,这点和匿名管道类似
3)共享内存是双向的,这点又和命名管道类似。
1.2Linux进程间通信
介绍完了Windows,介绍Linux就相对轻松一些了,虽然Windows和Linux形同水火,打的不可开交,但实际上说白了,它们并不是两个完全不同的东东,在很多的地方都相似,进程间通信也不例外。
Linux的进程间通信主要有管道、命名管道、消息队列、共享内存、信号量,其中信号量Semaphore其实是为了同步用的,因此我这里就放到下一篇关于同步的博文中去分析。另外,很多人将信号signal也作为进程间通信,但我认为信号更像是为了同步而设计的,因此也放到下一篇博文中去分析。
Linux的管道和Windows的管道是一样的,这里就不详细介绍了。
需要注意的是Linux多了一个叫做“流管道”的东东,除了流管道是全双工(也就是双向)外,流管道其它都和管道一样。
1.2.2命名管道
Linux的命名管道和Windows的命名管道差异就比较大了,主要对比如下:
消息格式
Windows更牛
Windows更牛
只能在一台机器上
可以跨网络
Windows更牛
1.2.3消息队列
这个是Linux特有的进程通信方式,我感觉它有点像邮槽,都能够实现一对多。不过消息队列和邮槽的方向正好相反:消息队列是一堆进程向一个进程发,邮槽是一个进程向一堆进程发。
消息队列还有一个牛B的特性就是消息队列的消息可以分优先级,进程不一定非要取第一个消息,也可以取指定消息优先级的消息。因此这个特性又可用于多对多通信,即:不同的收方指定不同的优先级。当然实际应用中应该没人会这么用,直接创建多个消息队列是最方便、最简单、效率最高的方法。
1.2.4共享内存
Linux的共享内存机制和Windows本质上是一样的,即都是利用了内存映射的功能。不过Linux将“内存映射到内存”包装成了“共享内存”,而不像Windows,在使用的时候通过指定不同的参数来区分是“内存映射到文件”还是“共享内存”,所以各位大侠可能在网上有时能够看到有人将“内存映射”和“共享内存”都说是Linux进程间通信的方式,原因就在这里。
1.3OS间进程通信
前面分别介绍了Windows和Linux进程间通信,看到这里,你肯定会有疑问:什么?还有OS进程间通信?不同OS之间的进程是不可能通信的!
是的,从操作系统层面来说,Windows的进程和Linux的进程当然是不能通信的,但如果从网络层面来说,两台机器总是要通信的吧?不可能Windows只能和Windows通信,Linux只和Linux通信吧?
说到这里估计你已经恍然大悟了:不就是Socket通信么?
是的,就是它,虽然它是用于机器间通信的,但大家想想,机器间通信不就是各个应用程序通信么?各个应用程序不就是对应操作系统中的一个或者多个进程么?
Windows和Linux对决(线程间同步)
1.1Windows线程同步
1.1.1关键代码区Critical
所谓“关键代码区”,相信大家看名字也能理解个大概了。首先:它很关键,第二:它是代码区。之所以关键,当然目的就是每次只能一个线程能够进入;既然是代码区,那就是只能在一组拥有同样代码的线程中用。
那什么情况下会用到关键代码区呢?当然是要保护多个线程都会用到的东西了,说到这里,想必你已经猜到了:全局变量和静态变量。
1.1.2互斥Mutex
互斥看起来和关键代码区是一样的,都是每次都是只允许一个线程使用。但互斥和关键代码区相比,具有如下特点:
关键代码区
不能跨进程
可以跨进程
因为有名字,所以可以跨进程
线程挂了其它线程就只能傻等了
线程挂了,操作系统会通知其它线程
所以关键代码区性能要高一些。
1.1.3信号量Semaphore
信号量本质上就是一个计数器,当计数器大于0时就意味着被保护的对象可用。每次申请计数器就减1,释放就加1.
信号量和互斥体相比,一个最明显的差别就在于互斥体每次只能有一个线程进行访问,而信号量可以有多个线程进行访问。
看到这里,大家可能都像我开始一样存在这样的问题:如果将信号量最大值设置为1,那么不就是相当于互斥量了吗?
看起来是一样的,而且在有些系统上也确实是这样的,据说是互斥体底层就是信号量来实现的,或者干脆就没有互斥体(例如传统UNIX),但在有的系统上还是有差别的,差别在于:申请和释放是否要同一个线程完成,Windows就是这种形式。互斥体要求同一线程来申请和释放,而信号量就可以由不同的线程申请和释放(但是我很难想象这样做有什么好处,难倒要给一个线程集中获取信号量,再来通知另外的线程工作?)。
1.1.4事件Event
事件本质上是一个系统信号,即:发生了某件事情后,发一个信号给其它关心这件事情的线程。
从事件的本质上来看,事件不是为了资源保护的,而是为了线程间通知用的。举个简单的例子:Socket接收完一个消息后,将其放入队列,然后需要通知消息处理线程进行处理。
大家想想,如果没有事件通知会怎么样呢?那接收线程只能设一个定时器或者循环,定时甚至循环去查询队列中是否有消息,这种定时和循环处理是对系统性能的极大浪费,所以,有了事件后,就不用这么浪费了。
1.2Linux线程同步
介绍完Windows,Linux介绍就很方便了,就像上一篇博文提到的一样,Windows和Linux其实很多地方相似,线程同步也不例外。
1.2.1关键代码区???
不好意思,Linux没有这个东东。
1.2.2互斥Mutex
Linux和Windows是一样的,这里就不详细介绍了,需要注意的是传统UNIX并没有互斥这个东东,传统UNIX的互斥是通过二元信号量(即最大值为1)来实现的。
1.2.3信号量Semaphore
需要注意的是Linux中信号量有两种:一种是内核POSIX标准的信号量,一种是用户态的传统UNIX
IPC信号量。两者的差别如下:
POSIX Semaphore
IPC Semaphore
IPC Semaphore可以通过semctrl函数修改对外表现。
不允许修改
用户可修改
如果进程退出时忘记关闭,POSIX会自动释放。
POSIX信号量和Windows的信号量是一样的。
1.2.4条件变量Conditions
看到这个名字有点莫名其妙,条件变量和线程同步有什么关系呢?
但其实是Linux(或者是POSIX)的名字取得不好才导致我们很难理解,本质上条件变量就是Windows的事件,作用也是一样的。唉,如果Linux或者POSIX不想和Windows同名,改成叫“通知”也能让我们这些小虾多省点脑力啊:)
1.2.5信号Signal
类似于“共享内存”也是一种进程间通信的方式一样,我把信号也列进来作为线程同步的一种,因为本质上信号不是为了线程间同步而设计的,但我们可以利用其作为线程同步来使用。
如何使用信号呢?既然信号本身就是一种通知(还记得上面我建议将“条件变量”建议改名为什么吗?),那我们就按照通知来使用了,例如:A做完了某事,发一个信号给B,B收到后开始启动做另外一件事。
请注意:和“条件变量”不同的是,条件变量支持广播机制,而信号只能是点对点,因此实际使用中应该还是“条件变量”方便一些。当然如果是传统UNIX,那就只能利用信号来进行通知了。
===============================================================================
注:看我的博客的朋友可能会发现一个现象,我几乎从来不介绍详细的函数或者API,而基本上都在“归纳、总结、对比”。这是我个人的一个风格或者理解吧,我认为函数或者API用的时候查一下就可以了,而在分析和设计的时候,关键是要知道有哪些东西可以给我们用,而且要知道我们具体究竟应该用哪个,因此在平时就必须多归纳、总结、对比,而不是背住各种函数和API。
&多机协作(又叫分布式处理)
嗯,费了九牛二虎之力,终于将Windows和Linux对比完了。你是否准备伸个懒腰,喝杯热咖啡,听点音乐来放松一下呢?
别急,革命尚未成功,同志还需努力,铁还得趁热打。还记得第二篇博文里面总结的两种并行实现技术没有?一个是“多进程多线程”,另一个是“多机协作”,到目前为止我们基本上只把“多进程多线程”分析完毕了,还有另外一个“多机协作”没有分析,本篇我们就探讨一下“多机协作”这种实现机制。
不过事先申明,由于“多机协作”这种实现机制范围太广:小到普通的双机、中到模拟地球运算的大型机或者巨型机、又如Google这样NB的公司发明的分布式文件系统、再到现在热热闹闹的“云计算”、或者大家最常用的BT、电骡等P2P技术都属于这一范畴,且实现太过复杂,不同的系统实现机制也相差很大,限于本人能力,只能“蜻蜓点水”了,如有兴趣研究,推荐《分布式系统概念与设计》这本书,我也没有读过,不过据说还不错。
幸运的是“多机协作”实现机制和“多核编程”并没有什么关系,因此即使蜻蜓点水对大家的多核设计水平影响不大。
废话了半天,让我们转到正题上。虽然前面提到了“多机协作”范围很广,实现机制也各不相同,但既然大家都划到“多机协作”这一类里面,自然有一些基因是相同的。那么就让我们对“多机协作”进行一次基因图谱制作,看看究竟有哪些共同基因。
u只有一种通信方式:消息。
是的,看到这个不要惊讶,虽然实现形式可以多种多样,但本质上来说,多机协作系统只有一种通信方式:消息。不管是基于TCP/IP的Socket消息,还是基于电信网7号信令的MAP消息,还是内部光纤通信的XX消息,本质上这些都是消息通信。和“多进程多线程”多种多样的方式来比,显得有点单薄,因此也就增大了设计的难度。
u没有多机同步机制
看到这个是不是更加吃惊?但现实就是如此残酷,多机协作没有一个现成可用的同步机制来实现诸如互斥、事件、信号量等同步功能。
但实际应用中你的应用又不得不用到这些东东,例如Google的分布式文件系统,因此你要自己去实现这些东东,这也增大了设计的难度。
u没有“老大”
这里的老大不是指黑社会的老大哈,相反它是一个好公仆,这个“老大”负责全局的事物管理。嗯,你可能会说“多进程多线程”里面也没有老大的啊?
多进程多线程之间确实没有天然的老大,谁当老大完全是由我们设计人员决定的,但还有一个我们无法决定的老大:操作系统!操作系统在“多进程多线程”实现机制里面完成了众多我们没有怎么关注但却不得不用到的功能:进程线程创建、调度、隔离、通信、同步等。
在“多机协作”实现机制中,唯一的老大就是我们设计人员!你要决定如何隔离、如何调度、如何通信、如何同步等所有这些事情,因此设计难度又上一层楼!
举个最简单的例子:多进程多线程实现机制中,时间或者时钟都是操作系统来控制和提供;而在多机协作中,光一个时间或者时钟同步就能让你累个半死!!
看完上面的初步分析,我们可以得出一个这样的结论:多机协作只有一个消息通信,然后要基于消息通信来实现同步、通信、管理、调度、分布式处理等所有你所需要的功能!所以“多机协作”要比“多进程多线程”设计复杂得多。
看到这里,你是否泄气了呢?本着“不抛弃,不放弃”的精神,我们还是要勇敢面对,而且幸好现在也有很多多机协作的解决方案了。流行的有三个:微软的COM/DCOM、ORG的CORBA、SUN的Java
RMI。这些多机协作(或者叫分布式)方案封装了很多底层的通信、调用、同步等机制,使得我们能够简单的实现多机协作。
详细的方案请各位参考COM、CORBA、RMI的相关文档。
Windows多进程编程
一、进程的概念
进程是是一个正在运行的程序的实例(飘~~~),是系统分配资源的单位(线程是执行的单位),包括内存,打开的文件、处理机、外设等,进程由两部分组成:
1、进程...
Windows核心编程之多进程概述
一、进程的概念
进程是是一个正在运行的程序的实例(飘~~~),是系统分配资源的单位(线程是执行的单位),包括内存,打开的文件、处理机、外设等,进程由两部分组成:
1、进程...
Windows进程父子关系小实验
Windows中进程A创建了另一个进程B,那么进程A就是进程B的父进程,B就是A的子进程。
在每一个进程的内存数据结构中,只保存了其父进程的Pid(Parent ProcessId),即使父进...
windows下Python使用多进程的问题
最近在学习爬虫,对于线程、进程、多线程、多进程、协程研究了很多。因为我用的是WIN7,所以一切都是在win下实现的。
在WIN下使用多进程的包multiprocessing(这是第三方包,不是模块),...
windows 多任务与进程
多任务,进程与线程的简单说明
多任务的本质就是并行计算,它能够利用至少2处理器相互协调,同时计算同一个任务的不同部分,从而提高求解速度,或者求解单机无法求解的大规模问题。以前的分布式计算正是利用这点...
浅谈linux和windows的线程机制的区别
在Linux内核中,描述一个进程主要是task_struct,一个称为进程描述符的数据结构。这个数据结构很庞大,包含了内核管理一个进程所需的所有信息,描述了一个正在执行的进程,包括进程ID,它打开的文...
linux和windows多线程的异同
linux多线程及线程同步和windows的多线程之间的异同
并不是所有的程序都必须采用多线程,有时候采用多线程性能还不如单线程。采用多线程的好处如下:
(1)多线程之间采用相同的地址空间...
多核时代:并行程序设计探讨(3)——Windows和Linux对决(多进程多线程)
并行程序设计探讨(3)——Windows和Linux对决(多进程多线程)前面的博文经过分析总结,最后得出两种并行技术:多进程多线程、多机协作。对于多进程和多线程来说,最有代表性且最常见的的莫过于Win...
没有更多推荐了,

我要回帖

更多关于 显卡共享内存 的文章

 

随机推荐