一、信号量
在学习信号量之前,我们必须先知道——Linux提供两种信号量:
内核信号量,由内核控制路径使用
用户态进程使用的信号量,这种信号量又分为POSIX信号量和SYSTEM V信号量。
POSIX信号量又分为有名信号量和无名信号量
有名信号量,其值保存在文件中, 所以它可以用于线程也可以用于进程间的同步。无名信号量,其值保存在内存中。
1.1 无名信号量接口函数
信号量的函数都以sem_开头,线程中使用的基本信号量函数有4个,它们都声明在头文件semaphore.h中。
sem_init函数
该函数用于创建信号量,其原型如下:
sem_t sem //sem信号量
下面传输的即为&sem
int sem_init(sem_t *sem,int pshared,unsigned int value);
该函数初始化由sem指向的信号对象,设置它的共享选项,并给它一个初始的整数值。
pshared控制信号量的类型,如果其值为0,就表示这个信号量是当前进程的局部信号量,否则信号量就可以在多个进程之间共享,value为sem的初始值。调用成功时返回0,失败返回-1.
1.2 有名信号量接口函数
int sem_getvalue(sem_t *sem, int *sval);
sem_getvalue() 把 sem 指向的信号量当前值放置在 sval 指向的整数上。 如果有一个或多个进程或线程当前正在使用 sem_wait 等待信号量,POSIX.1-2001 允许返回两种结果在 sval 里:要么返回 0;要么返回一个负值,它的绝对等于当前正在 sem_wait里阻塞的进程和线程数。Linux 选择了前面的行为(返回零)。
二、进程
2.1 fork()函数
函数fork()
所需头文件:#include<sys/types.h>
#include<unistd.h>
函数原型:pid_t fork()
函数参数:无
函数返回值:
0 子进程
>0 父进程,返回值为创建出的子进程的PID
-1 出错
以发现子进程和父进程之间并没有对各自的变量产生影响。
一般来说,fork之后父、子进程执行顺序是不确定的,这取决于内核调度算法。进程之间实现同步需要进行进程通信
vfork与fork对比:
相同:
返回值相同
不同:
fork创建子进程,把父进程数据空间、堆和栈复制一份;vfork创建子进程,与父进程内存数据共享;
vfork先保证子进程先执行,当子进程调用exit()或者exec后,父进程才往下执行
为什么需要vfork?
因为用vfork时,一般都是紧接着调用exec,所以不会访问父进程数据空间,也就不需要在把数据复制上花费时间了,因此vfork就是”为了exec而生“的。
结束子进程的调用是exit()而不是return,如果你在vfork中return了,那么,这就意味main()函数return了,注意因为函数栈父子进程共享,所以整个程序的栈就跪了。
2.2 exec函数族
目的:让子进程不执行父进程正在执行的程序
exec函数族提供了让进程运行另一个程序的方法。exec函数族内的函数可以根据指定的文件名或目录名找到可执行程序,并加载新的可执行程序,替换掉旧的代码区、数据区、堆区、栈区与其他系统资源。这里的可执行程序既可以是二进制文件,也可以是脚本文件。在执行exec函数族函数后,除了该进程的进程号PID,其他内容都被替换了。
-----------
所需头文件:#include<unistd.h>
函数原型:
//excel("/bin/ps","ps","-ef",NULL)
//系统执行ps -ef,注意参数的写法
int execl(const char *path, const char *arg,…)
//execlp("ps","ps","-ef",NULL)
//第一个参数只需要写ps即可,系统会根据环境变量自行寻找ps程序的位置
int execlp(const char *file, const char *arg,…)
//execle()函数将一个新的环境变量添加到子进程中
// char *envp[]={"PATH=/tmp","USER=liyuge",NULL};
//设定新的环境变量,注意使用NULL结尾
//execle("/usr/bin/env","env",NULL,envp)
int execle(const char *path, const char *arg,…, char *const envp[])
//char *arg[]={"ps","-ef",NULL};execvp("ps",arg)
//注意参数与execlp()函数的区别
int execv(const char *path, char *const argv[])
int execvp(const char *file, char *const argv[])
//char *arg[]={"env",NULL};//设定参数向量表,注意使用NULL结尾
//char *envp[]={"PATH=/tmp","USER=liyuge",NULL};
//设定新的环境变量,注意使用NULL结尾
int execve(const char *path, char *const argv[], char *const envp[])
execl(完整的路径名,列表……);
execlp(文件名,列表……);
execle(完整的路径,列表……,环境变量的向量表)
execv(完整的路径名,向量表);
execvp(文件名,向量表);
execve(完整的路径,向量表,环境变量的向量表) //系统调用函数
函数参数:
path:文件路径,使用该参数需要提供完整的文件路径
file:文件名,使用该参数无需提供完整的文件路径,终端会自动根据$PATH的值查找文件路径
arg:以逐个列举方式传递参数/命令行参数
argv:以指针数组方式传递参数
envp:环境变量数组
返回值:-1(通常情况下无返回值,当函数调用出错才有返回值-1)
区别1:参数传递方式(函数名含有l还是v)
exec函数族的函数传参方式有两种:逐个列举或指针数组。
若函数名内含有字母'l'(表示单词list),则表示该函数是以逐个列举的方式传参,每个成员使用逗号分隔,其类型为const char *arg,成员参数列表使用NULL结尾
若函数名内含有字母'v'(表示单词vector),则表示该函数是以指针数组的方式传参,其类型为char *const argv[],命令参数列表使用NULL结尾
区别2:查找可执行文件方式(函数名是否有p)
函数名内没有字母'p',则形参为path,表示我们在调用该函数时需要提供可执行程序的完整路径信息
若函数名内含有字母'p',则形参为file,表示我们在调用该函数时只需给出文件名,系统会自动按照环境变量$PATH的内容来寻找可执行程序
区别3:是否指定环境变量(函数名是否有e)
exec可以使用默认的环境变量,也可以给函数传入具体的环境变量。其中:
若函数名内没有字母'e',则使用系统当前环境变量
若函数名内含有字母'e'(表示单词environment),则可以通过形参envp[]传入当前进程使用的环境变量
exec函数族简单命名规则如下:
后缀 能力
l 接收以逗号为分隔的参数列表,列表以NULL作为结束标志
v 接收一个以NULL结尾的字符串数组的指针
p 提供文件的完整的路径信息 或 通过$PATH查找文件
e 使用系统当前环境变量 或 通过envp[]传递新的环境变量
execl("/bin/ps","ps","-ef",NULL) //子进程执行命令行参数ps -ef,注意参数的写法,且需要使用NULL结尾
运行该程序会发现,子进程会运行ps -ef命令,这与我们在终端直接输入ps -ef得到的结果是相同的。
注意我们在调用exec函数族的函数时,一定要加上错误判断语句。当exec函数族函数执行失败时,返回值为-1,并且报告给内核错误码,我们可以通过perror将这个错误码的对应错误信息输出。常见的exec函数族函数执行失败的原因有:
1.找不到文件或路径
2.参数列表arg、数组argv和环境变量数组列表envp未使用NULL指定结尾
3.该文件没有可执行权限
2.3 exit()与_exit()函数
当我们需要结束一个进程的时候,我们可以使用exit()函数或_exit()函数来终止该进程。当程序运行到exit()函数或_exit()函数时,进程会无条件停止剩下的所有操作,并进行清理工作,最终将进程停止。
函数exit()
所需头文件:#include<stdlib.h>
函数原型:
void exit(int status)
函数参数:
status 表示让进程结束时的状态(会由主进程的wait();负责接收这个返回值【也可以不接收】-->类似函数的返回值),默认使用0表示正常结束
返回值:无
exit()函数与_exit()函数用法类似,但是这两个函数还是有很大的区别的:
_exit()函数直接使进程停止运行,当调用_exit()函数时,内核会清除该进程的内存空间,并清除其在内核中的各种数据。
exit()函数则在_exit()函数的基础上进行了升级,在退出进程之间增加了若干工序。exit()函数在终止进程之前会检测进程打开了哪些文件,并将缓冲区内容写回文件。
因此,exit()函数与_exit()函数最主要的区别就在于是否会将缓冲区数据保留并写回。_exit()函数不会保留缓冲区数据,直接将缓冲区数据丢弃,直接终止进程运行;而exit()函数会将缓冲区内数据写回,待缓冲区清空后再终止进程运行。
2.4 wait()函数与waitpid()函数
虽然子进程调用函数execv()之后拥有自己的内存空间,称为一个真正的进程,但由于子进程毕竟由父进程所创建,所以按照计算机技术中谁创建谁负责销毁的惯例,父进程需要在子进程结束之后释放子进程所占用的系统资源。
为实现上述目标,当子进程运行结束后,系统会向该子进程的父进程发出一个信息,请求父进程释放子进程所占用的系统资源。但是,父进程并没有准确的把握一定结束于子进程结束之后,那么为了保证完成为子进程释放资源的任务,父进程应该调用系统调用wait()。
如果一个进程调用了系统调用wait(),那么进程就立即进入等待状态(也叫阻塞状态),一直等到系统为本进程发送一个消息。在处理父进程与子进程的关系上,那就是在等待某个子进程已经退出的信息;如果父进程得到了这个信息,父进程就会在处理子进程的“后事”之后才会继续运行。
wait()函数功能是:父进程一旦调用了wait就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。
如果父进程先于子进程结束进程,则子进程会因为失去父进程而成为“孤儿进程”。在Linux中,如果一个进程变成了“孤儿进程”,那么这个进程将以系统在初始化时创建的init进程为父进程。也就是说,Linux中的所有“孤儿进程”以init进程为“养父”,init进程负责将“孤儿进程”结束后的资源释放任务。
这里区分一下僵尸进程和孤儿进程:
孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作;
僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程(也就是进程为中止状态,僵死状态)。
Linix提供了一种机制可以保证只要父进程想知道子进程结束时的状态信息, 就可以得到。这种机制就是: 在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。 但是仍然为其保留一定的信息(包括进程号the process ID,退出状态the termination status of the process,运行时间the amount of CPU time taken by the process等)。直到父进程通过wait / waitpid来取时才释放。 但这样就导致了问题,如果进程不调用wait / waitpid的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。
使用wait()函数与waitpid()函数让父进程回收子进程的系统资源,两个函数的功能大致类似,waitpid()函数的功能要比wait()函数的功能更多。
函数wait()
所需头文件:#include<sys/types.h>
#include<sys/wait.h>
函数原型:
pid_t wait(int *status)
函数参数:
status 保存子进程结束时的状态(由exit();返回的值)。使用地址传递,父进程获得该变量。若无需获得状态,则参数设置为NULL
返回值:
成功:已回收的子进程的PID
失败:-1
函数waitpid()
所需头文件:#include<sys/types.h>
#include<sys/wait.h>
函数原型:
pid_t waitpid(pid_t pid, int *status, int options)
函数参数:
pid pid是一个整数,具体的数值含义为:
pid>0 回收PID等于参数pid的子进程
pid==-1 回收任何一个子进程。此时同wait()
pid==0 回收其组ID等于调用进程的组ID的任一子进程
pid<-1 回收其组ID等于pid的绝对值的任一子进程
status 同wait()
options
0:同wait(),此时父进程会阻塞等待子进程退出
WNOHANG:若指定的进程未结束,则立即返回0(不会等待子进程结束)
返回值:
>0 已经结束运行的子进程号
0 使用WNOHANG选项且子进程未退出
-1 错误
当进程结束时,该进程会向它的父进程报告。wait()函数用于使父进程阻塞,直到父进程接收到一个它的子进程已经结束的信号为止。如果该进程没有子进程或所有子进程都已结束,则wait()函数会立即返回-1。
waitpid()函数的功能与wait()函数一样,不过waitpid()函数有若干选项,所以功能也比wait()函数更加强大。实际上,wait()函数只是waitpid()函数的一个特例而已,Linux内核总是调用waitpid()函数完成相应的功能。
wait(NULL)等价于waitpid(-1,NULL,0)。
三、共享内存
3.1 System V共享内存
概述
共享内存是一种最为高效的进程间通信方式,因为进程可以直接读写内存,不需要任何数据的复制。为了在多个进程间交换信息,内核专门留出了一块内存区,这段内存区可以由需要访问的进程将其映射到自己的私有地址空间。因此,进程就可以直接读写这一段内存区而不需要进行数据的复制,从而大大提高了效率。当然,由于多个进程共享一段内存,因此也需要依靠某种同步机制,如互斥锁和信号量等。
共享内存使用步骤
① 创建共享内存。也就是从内存中获得一段共享内存区域,这里用到的函数是shmget();shmget()可以创建一个新的共享内存段或者取得一个既有共享内存段的标识符(其他进程创建的共享内存段),这个调用返回共享内存标志符(shmid)
② 映射共享内存。也就是把这段创建的共享内存映射到具体的进程空间中,这里使用的函数是shmat()。到这一步就可以使用这段共享内存了,也就是可以使用不带缓冲的I/O读写命令对其进行操作,为了引用这块共享内存,程序需要使用由shmat()调用返回的addr值,它是一个指向进程的虚拟地址空间中该共享内存段起点的指针。
③ 撤销映射。使用完共享内存就需要撤销,用到的函数是shmdt(),这个调用以后,进程就无法再引用这块共享内存了。
4 删除共享内存段,只有当当前所有附加内存段的进程都与之分离之后内存段才会销毁,只有一个进程需要执行这一步。
3.2 函数说明
3.2.1 shmget函数
int shmget(key_t key, size_t size, int shmflg);
//shmflg 读写的权限
//函数返回共享内存段标识符(shmid)
key标识共享内存的键值: 0/IPC_PRIVATE。 当key的取值为IPC_PRIVATE,则函数shmget()将创建一块新的共享内存;如果key的取值为0,而参数shmflg中设置了IPC_PRIVATE这个标志,则同样将创建一块新的共享内存。
在IPC的通信模式下,不管是使用消息队列还是共享内存,甚至是信号量,每个IPC的对象(object)都有唯一的名字,称为“键”(key)。通过“键”,进程能够识别所用的对象。“键”与IPC对象的关系就如同文件名称之于文件,通过文件名,进程能够读写文件内的数据,甚至多个进程能够共用一个文件。而在IPC的通讯模式下,通过“键”的使用也使得一个IPC对象能为多个进程所共用。
Linux系统中的所有表示System V中IPC对象的数据结构都包括一个ipc_perm结构,其中包含有IPC对象的键值,该键用于查找System V中IPC对象的引用标识符。如果不使用“键”,进程将无法存取IPC对象,因为IPC对象并不存在于进程本身使用的内存中。
int size(单位字节Byte)
-----------------------------------------------
size是要建立共享内存的长度。所有的内存分配操作都是以页为单位的。所以如果一段进程只申请一块只有一个字节的内存,内存也会分配整整一页(在i386机器中一页的缺省大小PACE_SIZE=4096字节)这样,新创建的共享内存的大小实际上是从size这个参数调整而来的页面大小。即如果size为1至4096,则实际申请到的共享内存大小为4K(一页);4097到8192,则实际申请到的共享内存大小为8K(两页),依此类推。
int shmflg
-----------------------------------------------
shmflg主要和一些标志有关。其中有效的包括IPC_CREAT和IPC_EXCL,它们的功能与open()的O_CREAT和O_EXCL相当。
IPC_CREAT 如果共享内存不存在,则创建一个共享内存,否则打开操作。
IPC_EXCL 只有在共享内存不存在的时候,新的共享内存才建立,否则就产生错误。
如果单独使用IPC_CREAT,shmget()函数要么返回一个已经存在的共享内存的操作符,要么返回一个新建的共享内存的标识符。如果将IPC_CREAT和IPC_EXCL标志一起使用,shmget()将返回一个新建的共享内存的标识符;如果该共享内存已存在,或者返回-1。IPC_EXEL标志本身并没有太大的意义,但是和IPC_CREAT标志一起使用可以用来保证所得的对象是新建的,而不是打开已有的对象。对于用户的读取和写入许可指定SHM_R和SHM_W,(SHM_R>3)和(SHM_W>3)是一组读取和写入许可,而(SHM_R>6)和(SHM_W>6)是全局读取和写入许可。
3.2.2 使用共享内存shmat()
shmat()的地址函数结果是返回附加共享内存段的地址,开发人员可以向对待普通C指针那样对待这个值。
3.2.3 分离共享内存段shmdt()函数
3.2.4 shmctl()函数
该函数完成对共享内存区的各种操作(主要删除共享内存段落)
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
//其中
struct shmid_ds {
struct ipc_perm shm_perm; /* Ownership and permissions */
size_t shm_segsz; /* Size of segment (bytes) */
time_t shm_atime; /* Last attach time */
time_t shm_dtime; /* Last detach time */
time_t shm_ctime; /* Last change time */
pid_t shm_cpid; /* PID of creator */
pid_t shm_lpid; /* PID of last shmat(2)/shmdt(2) */
shmatt_t shm_nattch; /* No. of current attaches */
...
};
The ipc_perm structure is defined as follows (the highlighted fields are settable using IPC_SET):
struct ipc_perm {
key_t __key; /* Key supplied to shmget(2) */
uid_t uid; /* Effective UID of owner */
gid_t gid; /* Effective GID of owner */
uid_t cuid; /* Effective UID of creator */
gid_t cgid; /* Effective GID of creator */
unsigned short mode; /* Permissions + SHM_DEST and
SHM_LOCKED flags */
unsigned short __seq; /* Sequence number */
};
该函数有三个命令:
IPC_RMID:删除
IPC_SET:设置
IPC_STAT:获取
4 CPU核与进程或者线程绑定
cpu_set_t这个结构体类似于select中的fd_set,可以理解为cpu集,也是通过约定好的宏来进行清除、设置以及判断:
void CPU_ZERO (cpu_set_t *set); //初始化,设为空
void CPU_SET (int cpu, cpu_set_t *set); //将某个cpu加入cpu集中
void CPU_CLR (int cpu, cpu_set_t *set); //将某个cpu从cpu集中移出
int CPU_ISSET (int cpu, const cpu_set_t *set); //判断某个cpu是否已在cpu集中设置了
cpu集可以认为是一个掩码,每个设置的位都对应一个可以合法调度的 cpu,而未设置的位则对应一个不可调度的 CPU。换而言之,线程都被绑定了,只能在那些对应位被设置了的处理器上运行。通常,掩码中的所有位都被置位了,也就是可以在所有的cpu中调度。
sched_setaffinity(俗称进程亲核性)系统调用,设置某进程(或线程)只能运行在某些cpu上
sched_setaffinity(pid_t pid, unsigned int cpusetsize, cpu_set_t *mask)
该函数设置进程为pid的这个进程,让它运行在mask所设定的CPU上.如果pid的值为0,则表示指定的是当前进程,使当前进程运行在mask所设定的那些CPU上.第二个参数cpusetsize是mask所指定的数的长度.通常设定为sizeof(cpu_set_t).如果当前pid所指定的进程此时没有运行在mask所指定的任意一个CPU上,则该指定的进程会从其它CPU上迁移到mask的指定的一个CPU上运行.