进程、线程及共享内存学习笔记
系统环境:deepin Linux,语言环境:Linux C
欢迎大家转载,转载请注明出处,谢谢!
理论基础:
- 进程:计算机上每个执行的活动,运行一个可执行程序是一个进程,打开一个软件是一个进程,打开一个终端是一个进程等等。
- 多进程:为了充分利用计算机资源产生了多进程的执行方式。通俗来讲就是在同一时间做多个事情,从而可以充分利用计算机资源还可以提高程序的执行效率。在创建一个新的子进程后,子进程会会获得计算机分配的资源,并拷贝父进程的数据。
- 子进程与父进程:我们编写的一段程序运行时就是一个进程,当我们在这段代码中调用特定函数新创建一个进程时,新创建的进程就是当前进程的子进程,而对于新创建的进程来说,当前进程就是它的父进程,子进程通过拷贝等手段继承父进程在创建子进程之前的数据、属性等。
- 多线程:上面说到进程是计算机上执行的每个活动。在实际编程中,一个进程中会有很多任务需要做。从而引发思考:一个进程中的多个任务是否也可以并发的执行?因为有些任务之间是没有联系的,也就是说有些任务完全可以同时进行而没有依赖问题。答案就是线程。与多进程相同的是,多线程也是为了让计算机资源得到充分利用并且程序的执行效率也会得到提高,与多进程不同的是,多线程并没有拷贝这种需求,它实际上是把一个进程分成多个片段。
- 重入性:尽管多线程系统开销少,但是也难免有缺陷,那就是重入性问题,使用多线程编程需要保证被多线程多次执行的函数的可重入性,所谓重入性就是函数被多个线程多次调用皆能正常执行。见附录2,有几个保证函数可重入性的条件。
- 线程安全:使用多线程编程除了需要保证函数的可重入性还需要保证线程安全,另外,可重入的函数一定是 线程安全的,但是反之不成立。
- 共享内存:根据字面意思就很好理解,通过把不同的进程中的逻辑内存映射到同一块物理内存中,进而允许两个进程共享同一块逻辑内存空间。它是进程间通信的一种方式。共享内存本身并没有提供同步机制。
相关函数:
多进程相关:
fork()
:原型:
1
2
3
4
pid_t fork(void);说明:
通过复制调用者来创建新进程。这个新的进程简称为子进程。调用该函数的进程称为父进程。
子进程和父进程运行在不同的内存空间中。在调用该函数的时候,两个进程内存空间都拥有相同的内容。其中一个进程执行的内存写入,文件映射和解除映射操作不会影响到另一个进程。该函数的返回值在父子进程中不同,在父进程中返回子进程PID,在子进程中返回0。
wait()
:函数原型及头文件:
1
2
3
4
pid_t wait(int *stat_loc);函数说明:
当调用这个函数的时候,当前进程会阻塞等待,该进程的某个子进程运行结束为止。
该结束的子进程的返回状态被存储在wait()函数的参数stat_loc变量中。
system();
函数原型及头文件:
1
2
3
int system(char *command);函数说明:
创建新的进程,执行制定命令。
exec函数族:
函数原型及头文件:
1
2
3
4
5
6
7
int execl(cONst char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *filename, char *const argv[], char *const envp[]);函数说明:
exec 函数族
execve 函数是该族的基础函数,其他函数是由该函数封装而来
exec 族被用启动新的指定路径下的程序来替换当前的程序。
函数名称后面的后缀不同参数不尽相同见下面附录1。
exit()
:函数原型及头文件:
1
2
void exit(int status);函数说明:
终止进程并返回状态码。
多线程相关:
pthread_create()
:函数原型及头文件:
1
2
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);函数说明:
- 作用:创建线程
- 该函数开始一个新的线程在调用该函数的进程中。新开始的线程通过调用
start_routine()
函数指针指向的函数来执行,arg参数是该函数指针指向的函数唯一的参数。 - 该线程可以通过一下方式中的一个终止:
- 调用
pthread_exit()
函数,并确定退出状态值,该值可用于该进程中的调用phread_join()
的其他线程。 - 该线程执行的函数退出,这相当于调用
phread_exit()
并带有返回值。 - 被取消(见
pthread_cancel()
) - 调用
exit()
函数,或者在主函数中执行return
。这会导致该进程中所有线程终止。
- 调用
- attr参数指向一个pthread_attr_t类型的结构体,这个参数被用来在线程创建之初确定该线程的属性。
- 如果调用该函数成功创建了一个线程,那么该线程的ID会存储在thread参数中。这个标志会在其他的函数中用到。
pthread_join()
:函数原型及头文件:
1
2
int pthread_join(pthread_t thread, void **retval);函数说明:
- 该函数会等待由thread参数标志的线程终止。如果这个线程已经终止了,那么该函数立即返回。由thread参数标志的线程必须是可连接的。
- 如果retval参数不为空,那么该函数会拷贝目标线程的目标线程的退出状态(目标线程提供返回状态值给pthread_exit();)到该参数中。如果目标线程已经被取消了,那么该参数会存储PTHREAD_CANCELED。
pthread_detach()
:函数原型及头文件:
1
2
int pthread_detach(pthread_t thread);函数说明:
该函数会把由thread参数指向的线程标记为分离。一个被标记分离的线程终止后,该线程的资源会被系统自动收回,不需要其他线程进程其他的操作。尝试标记已经分离的线程是不明操作,会导致未知错误。
pthread_self()
:函数原型及头文件:
1
2
pthread_t pthread_self(void);函数说明:
- 该函数会返回调用者的PID,该值与调用
thread_create()
函数返回的thread值相同。
- 该函数会返回调用者的PID,该值与调用
pthread_equal
:函数原型及头文件:
1
2
int pthread_equal(pthread_t t1, pthread_t t2);函数说明:
- 该函数比较两个线程的标记t1和t2是否指向同一个线程。
- 如果两个标记指向同一个线程会返回一个非零数,反之返回0。
pthread_exit()
:函数原型及头文件:
1
2
void pthread_exit(void *retval);函数说明:
该函数会终止调用该函数的线程,并通过参数retval返回一个值,该值可用于同一进程中的其他线程调用
pthread_join()
。
线程同步机制:
互斥:
pthread_mutex_init()
、pthread_mutex_destroy()
:函数原型及头文件:
1
2
3
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);函数说明:
pthread_mutex_destroy()
:- 该函数销毁参数mutex引用的的互斥对象,实际上,该互斥对象会变成未初始化的状态。
- 一个被销毁的互斥锁可以使用
pthread_mutex_init()
重新初始化。在互斥锁被销毁后引用该互斥锁会导致未定的引用的错误。 - 销毁一个未上锁的初始化互斥对象是安全的。当尝试销毁一个通过其他线程使用的锁定的互斥锁或一个已经被禁用的互斥锁会导致未定义行为的错误。
pthread_mutex_init()
:- 该函数会初始化一个被参数mutex引用的互斥锁,该互斥锁的属性在参数attr中规定。
pthread_mutex_lock()
、pthread_mutex_trylock()
和pthread_mutex_unlock
:函数原型及头文件:
1
2
3
4
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);函数说明:
pthread_mutex_lock()
- 该函数会把参数mutex引用的互斥锁上锁返回0或者EOWNERDEAD,如果该互斥锁已经被其他线程上锁了,那么该线程会阻塞,直到该互斥锁可用。
pthread_mutex_trylock()
- pthread_mutex_trylock()函数等效于pthread_mutex_lock(),除非mutex引用的互斥对象当前被锁定(由任何线程,包括当前线程),则调用应立即返回。
pthread_mutex_unlock()
- pthread_mutex_unlock()函数将释放互斥锁引用的互斥锁对象。 释放互斥锁的方式取决于互斥锁的类型属性。
读写锁:
``pthread_rwlock_init()
、
pthread_rwlock_destory()`:函数原型及头文件:
1
2
3
4
5
6
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;函数说明:
- ``pthread_rwlock_init()`函数会分配参数rwkock参数引用的读写所所需的所有资源,并将读写锁初始化为具有参数attr指定的属性,并置为解锁状态,如果attr为NULL,则应使用默认的读写锁属性; 效果与传递默认读写锁属性对象的地址相同。
pthread_rwlock_destory()
函数将销毁rwlock引用的读写锁对象,并释放锁使用的所有资源。
pthread_rwlock_rdlock()
:函数原型及头文件:
1
2
3
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);函数说明:
两个函数函数都会对由rwlock引用的读写锁解开读锁。但是对于
pthread_rwlock_rdlock()
函数如果写入权限没有上锁并且互斥锁上没有写入权限被阻塞,则调用线程获取读锁定。调用该函数的线程读权限会被上锁。try函数遇到这种情况会返回调用失败两个函数都是成功返回0。
pthread_rwlock_wrlock()
:函数原型及头文件:
1
2
3
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);函数说明:
两个函数同样都会为调用者加写权限锁,但是try函数回去尝试,也就是说如果任何线程当前持有rwlock(用于读或写),函数将调用失败。但是另一个函数遇到这种情况会阻塞。函数执行成功返回0。
pthread_rwlock_unlock()
:函数原型及头文件:
1
2
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);函数说明:
该函数将释放由rwlock引用的读写锁对象上的锁。 如果读写锁定rwlock未被调用线程保持,则结果是不确定的。
条件变量:
thread_cond_init()
、thread_cond_destory()
函数原型及头文件:
1
2
3
4
5
6
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;函数说明:
pthread_cond_destroy()
函数将销毁由cond指定的给定条件变量; 实际上,对象会处于未初始化状态。可以使用pthread_cond_init()
重新初始化已销毁的条件变量对象; 在对象被销毁之后引用该对象的结果是未定义的。pthread_cond_init()
函数应使用attr引用的属性初始化cond引用的条件变量。
thread_cond_wait()
、thread_cond_timedwait()
:函数原型及头文件:
1
2
3
4
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);函数说明:
两个函数都会阻塞条件变量。 应用程序应确保在调用线程锁定互斥锁的情况下调用这些函数; 否则,会产生错误或未定义的行为(对于其他互斥锁)。
thread_cond_signal()
、thread_cond_broadcast()
:函数原型及头文件:
1
2
3
4
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);函数说明:
pthread_cond_broadcast()
函数将取消阻止当前在指定条件变量cond上阻塞的所有线程。pthread_cond_signal()
函数应解除阻塞在指定条件变量cond上阻塞的至少一个线程(如果在cond上阻塞了任何线程)。
共享内存相关:
shmget()
:函数原型及头文件:
1
2
3
int shmget(key_t key, size_t size, int shmflg);函数说明:
该函数返回与参数key相关联的共享内存片段的标识符(identifier)。
该函数可以用于获取先前创建的共享内存的idenfifier(当shmflg为0并且key值不是IPC_PRIVATE< 预定义宏,见附录3 >的时候)
该函数还可以用于创建一个新共享内存,新的共享内存大小为size参数向上取整到PAGE_SIZE(预定义宏,见附录3)。使用该函数创建新的共享内存需满足如下条件:
- key没有与它相对应的共享内存(无论参数key的值是不是IPC_PRIVATE(预定义宏,见附录3))
- shmflg参数指定了IPC_CREAT(预定义宏,见附录3)
如果参数shmflg指定了IPC_CREAT和IPC_EXECL并且key 已经存在与其相关联的共享内存,那么shmget函数创建新的共享内存失败,并且erron会被设置为EEXIST。(这有点像O_CREAT | O_EXCL 组合对于open()函数带来的影响)
shmflg参数的取值如下:
IPC_CREAT IPC_EXCL、SHM_HUGETLB、SHM_HUGE2MB(SHM_HUGE_1GB)、SHM_NORESERVE(详情见附录3)
shmat()
、shmdt()
:函数原型及头文件:
1
2
3
4
void *shmat(int shmid, const void *shmaddr, int shmflg);
int shmdt(const void *shmaddr);函数说明:
- shmat();
- 该函数把通过shmid标志的共享内存段链接到调用该函数的进程所在的地址空间中。
- 这个被链接的地址空间由shmaddr参数的如下定义所规定:
- 如果shmaddr参数为空,系统会选择一个合适的(未使用的)对齐页地址去链接共享内存。
- 如果shmaddr参数不为空并且shmflg参数指定了SHMLBA(见附录3),会链接到shmaddr参数向下取整到离SHMLBA倍数最近值的地址。
- 除此之外,一定会链接到shmaddr 附近的对齐页地址。
- 除了SHM_RND以外,shmflg位掩码参数还可能使用到一下规定的标志:
- SHM_EXEC、SHM_RDONLY、SHM_REMAP(详细说明见附录3)
- shmdt();
- 与上面的函数相反,该函数通过shmid表示的共享内存段从调用该用书的进程所在的地址空间中断开。
- 这个被断开的共享内存段必须是目前链接的,并且shmaddr参数的值为调用shmat()函数的返回值。
- 如果shmdt()调用成功,系统会更新与共享内存相关联的shmid_ds结构体,该结构体成员如下:
- shm_ttime : 被设置为当前时间。
- shm_lpid : 被设置为被调用的进程的进程ID。
- shm_nattch :这个值逐一递减,如果这个值减为0并且这个共享内存段被标记为删除,那么这个共享内存段就被删除了。
shmctl()
:函数原型及头文件:
1
2
3
int shmctl(int shmid, int cmd, struct shmid_ds *buf);该函数执行由cmd参数指定的操作,cmd参数存储在由shmid参数存储的识别码指向的共享内存段。
buf参数是一个shmid_ds结构体指针,shmid_ds结构体在头文件<sys/shm.h>中定义,如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20struct shmid_ds {
struct ipc_perm shm_perm; /* 所有权和权限 */
size_t shm_segsz; /* 段大小(单位:字节) */
time_t shm_atime; /* 最后的链接时间 */
time_t shm_dtime; /* 最后的断开链接时间 */
time_t shm_ctime; /* 最后的更改时间 */
pid_t shm_cpid; /* 创建者的PID */
pid_t shm_lpid; /* 最后的shmat()函数或shmdt()函数的pid */
...
};
ipc_perm结构体的定义如下(其中udi、gid和mode可设置为IPC_SET):
struct ipc_perm {
key_t __key; /* 提供给shmget()函数的键值 */
uid_t uid; /* 有效的拥有者的UID */
gid_t gid; /* 有效的拥有者的GID */
uid_t cuid; /* 有效的创建者UID */
gid_t cgid; /* 有效的创建者GID */
unsigned short mode; /* 权限 + SHM_DEST 和 SHM_LOCKED 标记 */
unsigned short __seq; /* 序列号 */
};cmd参数的有效值如下:
IPC_STAT、IPC_SET IPC_RMID、IPC_INFO、SHM_INFO、SHM_STAT、SHM_LOCK、SHM_UNLOCK 。
附录1(摘自网络):
后缀 参数 l 接收以逗号为分隔的参数列表,以NULL为结束符 v 接收以NULL为结束的字符串数组 p 接收以NULL为结束的字符串数组指针,并可以利用DOS的PATH变量查找子程序文件 e 传递环境变量。 附录2(摘自网络):
- 不在函数内部使用静态或全局数据。
- 不返回静态或全局数据,所有数据都由函数的调用者提供。
- 使用本地数据,或通过拷贝全局数据到本地来保护全局数据。
- 不调用不可重入性函数。
附录三(翻译自man手册):
宏名称 意义 IPC_CREAT 创建一个新的段,如果这个标志没有被使用,shmget()函数将会寻找与这个段相对应的key参数值,并且检查使用者是否有读取这个段的权限。 IPC_EXCL 该标志与IPC_BREAT一起被用作确认本次创建共享内存的调用成功与否。 IPC_PRIVATE 它不是一个标志字段,而是一个key_t类型的值,如果函数调用的时候key参数中存储这个特殊的值,系统调用会忽略除了shmflg中最不重要的9位意外的所有并且创建一个新的共享内存。 IPC_STAT 从内核中拷贝shmid中由buf指向的shmid_ds结构相关的数据结构信息。对于共享内存段调用者必须拥有可读权限。 IPC_SET 往参数buf指针指向的shmid_ds结构体的某些成员写入与该共享内存段相关联的内核数据结构,并且更新该结构体中的shm_ctime成员。以下成员可能会被改变:shm_perm.uid、shm_perm.gid和(最不重要的九位)shm_perm.mode。调用进程的有效UID必须与拥有者(shm_perm.uid)或共享内存的创建者(shm_per.cuid)相匹配,或者与一定是拥有特权的调用者相匹配。 IPC_RMID 标记被销毁的共享内存段,实际上,这个共享内存段实在进程销毁的时候被销毁。(当shmid_ds相关构成员shm_nattch为空的时候)。调用者必须是共享内存的拥有者或者创建者,或者拥有特权。buf参数被忽略。 SHM_NORESREVE (从linux内核2.6.15以后)这个标志与mmap()函数的MAP_NORESERVE 标志具有同样的目的。不保留交换空间对于共享内存段,当交换空间被保留了,他会保证共享内存是可用的。当交换空间没有被保留,并且物理内存不可用,那么可能会在写入时获取SIGSEGV。也可以查阅 /proc/sys/vm/overcommit
文件。SHM_REMAP (LInux特有)这个标志指定了在共享内存段中从shmaddr参数开始到段最后的范围内的段映射应该被替换成现有的映射。(一般情况下,一个EINVAL错误可能是由于一个映射在地址范围内已经存在)在这个案例中,shmaddr参数必须不为空。 SHM_RDONLY 以只读的方式链接共享内存段。进程对共享内存段必须有读取权限。如果标志没有被指定,那么共享内存段会被以可读可写方式链接,并且进程对于该共享内存段必须拥有可读和可写权限。但这不意味着对共享内存段有只写权限。 SHM_EXEC (Linux特有,从linux2.6.9以后)允许执行共享内存段中的内容。调用者对于共享内存段必须拥有可执行权限。 SHM_HUGETLB (从linux内核2.6版以后)为共享内存段分配巨大空间,详情见linux内核源码文件中 Documentation/vm/hugetlbpage.txt
获取更多信息。SHM_HUGE2MB,SHM_HUGE1GB (从linux内核3.8之后)与SHM_HUGETLB一起被用作选择替代hugetlb大页面的大小(分别是2MB和1GB),并且支持超过hugetlb大页面的大小。 参考资料:
参考网站:多进程编程总结、C语言多进程编程、多线程和多进程的区别(小结)、百度百科:程序并发执行、linux下c语言编程exec函数使用
参考书籍:C语言核心技术 、man手册