目录
grep命令的-r 当前目录 -w 精确查找 -n 显示行
vim中的一些操作:
gcc, 数字+gcc多行注释
:e 文件名 在当前的代码当中打开另外的代码, ctrl+n就可以切换代码
ctrl + v,= 可以调整格式
批量删除、复制的快捷键, d +要删除到的行数 + G
1.进程基础知识
进程:动态执行的程序 + 虚拟内存 + 虚拟CPU
进程调度 : 调度程序(分配CPU)
时间片轮转
完全公平调度算法:时间片轮转+动态修改优先级
三个基础状态:运行、就绪、阻塞
cpu的状态:用户态和内核态
查看cpu 信息:cat /proc/cpuinfo
Linux管理进程:
进程控制块(pcb)[Windows] --> task_struct进程控制块[Linux下进程与线程都用该数据结构管理]
用一个双向循环队列[任务队列]管理task_struct结构体
进程id:pid 父进程id: ppid
ps命令查看进程:
ps -l 当前与自己bash相关的进程的信息
ps -elf 所有进程的信息
开机流程:
bootloader进程->0号进程->1号进程(init)->/etc/ttys终端->login->shell
->2号进程(启动页面守护进程)(负责管理内核线程)
开机成功后0号进程死亡,然后又重新开启一个新的0号进程idle,是空闲进程
内核线程ps查看时用[]括起来的
2.id获取与动态权限
一些获取函数:
pid_t getpid(void); pid_t getppid(void);
uid_t getuid(void); uid_t geteuid(void);
gid_t getgid(void); gid_t getegid(void); //真实/有效
//进程的(e)uid、(e)gid默认身份是启动者,权限也是启动者的;
动态权限之suid:
问题引出:/etc/shadow 中存储的用户密码,只有root有读写权限,但自己用passwd可以修改自己的密码的原因?其他例如sudo命令也有类似功能,sudo就是用suid来实现的。
条件:拥有suid和user的x权限
效果:任何用户通过该文件(可执行程序)启动进程,euid变成文件拥有者id
加suid权限操作:chmod u+s xxx可执行程序名
动态权限之sgid:
同时拥有sgid和group的x权限
任何用户通过该文件(可执行程序)启动进程,egid改为文件拥有者所在组的gid
加suid权限操作:chmod g+s xxx可执行程序名
动态权限之sticky:
目录文件的sticky
条件:拥有sticky和other的w权限
效果:对于非root的其他用户,可新建文件、删除自己的文件,不可删除别人的文件
加sticky权限操作:chmod o+t dir
3.主要命令
查看相关
ps -l和ps -elf 、ps aux各列所代表的意思
标记 状态 uid(有效) pid ppid c(cpu占用率) PRI(priority) NI(nice) ADDR(内存位置) SZ(占据内存大小) WCHAN(使进程阻塞的系统调用) STIME(启动时间) TTY(终端,tty普通终端,pts网络终端) TIME(消耗总CPU时间) CMD(启动该进程的命令)
状态:
ps aux:
VSZ(所占虚拟内存大小) RSS(所占真实内存空间大小) STAT见下图
free命令:查看内存使用的情况
top命令:动态查看进程,各项含义
Linux中的虚拟机:qemu 速度最快的处理器模拟机,使用KVM虚拟机技术。KVM原理…
优先级相关
优先级:140个优先级,Ubuntu为[-40,99],数值越低,优先级越高
NICE:[-20,19] PRI = 80+NICE,修改NICE值,调度策略不变
PRI: [60,99]普通调度策略CFS
[-40,59]实时调度策略: FIFO和RR(最高优先级的进程平分时间)
修改NICE不会改变调度策略
renice:改变进程的优先级eg:renice -n 10 -p 进程ID
nice: 指定优先级启动进程eg: nice -n 10 ./while
无sudo权限,只能提高数值,降低优先级
发送信号
kill命令:给进程发信号
kill -l:查看信号
kill -9 进程号:杀死进程
kill -19 进程号:暂停进程
前台与后台转换
前台和后台进程:
可执行程序后加&,后台执行进程 eg: ./while &
前台响应键盘信号:ctrl+c:终止 ctrl+z:前台拉后台,暂停
ctrl+\:终止(退出),生成core文件,保存退出时的栈帧
jobs查看后台进程
fg 任务编号:后台拉前台
bg 任务编号:后台暂停变运行
ctrl+z:前台拉后台,暂停
设置定时任务
crontab命令:设置定时任务
crontab -e 然后在文件里设置,如果是周期性的任务,就将对应时间选项编程*。
sudo vim /etc/crontab 所有用户的定时任务
4.多进程编程
1.新进程的开启
int system(const char *command); 执行一个命令(包括启动一个可执行程序,包括其他语言程序,脚本等等)
#include <func.h>
int main()
{
system("python hello.py");
sleep(5);
return 0;
}
pid_t fork(void):创建子进程,子进程是父进程的完全拷贝,父进程返回子进程pid,子进程返回0,用户态完全拷贝:堆、栈、数据段、代码段。内核态部分共享。
fork的流程:
执行do_fork()系统调用
1.根据旧的task_struct创建新的task_struct;
2.do_fork()存在一些参数,决定了新旧任务的资源共享程度。
do_fork()的步骤:
1.申请资源,创建task_struct
2.修改一些必要数据,如pid,ppid [这两步不可抢占]
3.加入就绪队列
线程:创建一个任务,新任务和旧任务共享进程地址空间,线程创建也是用do_fork()系统调用。
线程和进程都有自己独立的task_struct,只不过对进程地址空间的共享程度有一定的区别。
中断:异步事件
fork写时复制(COW):虚拟内存地址映射到相同物理地址,只要任意一个进程修改内存内容,才会执行物理层面上的复制,避免多余的开销
fork的共享和拷贝:
用户态拷贝(cow,只读的是共享,比如代码段)
内核态一部分共享,一部分拷贝
文件流和文件缓冲区是拷贝的,因为是行缓冲,若未加\n输出,内容会暂存于缓冲区,fork时会将缓冲区中的内容一同拷贝
文件对象是不拷贝的,共享,只是执行了类似dup()函数的效果
exec函数族:
int execl(const char *path,const char *arg,...,NULL);
//命令参数以可变参数传递
int execv(const char * path,char *const argv[]);
//先用数组存储参数,再传递数组
//都要以NULL结尾,如:
int main()
{
printf("before execl, you can see me!\n");
//execl("./add","./add","3","4",NULL);
char *argv[] = {"./add","4","5",NULL};
execv("./add",argv);
printf("after execl, you can't see me!\n");
return 0;
}
exec的原理:
1.代码段、数据段替换
2.堆栈清空
3.pc指针重新回代码段的开始
相当于用新程序占领了旧程序的躯壳
用fork+exec实现system的效果
2.资源回收
pid_t wait(int *wstatus);
父进程等待子进程的终止,回收子进程的资源。
bash原理:while(1){ puts(“$”);fork+exec(CMD);wait}
孤儿进程:父进程终止早于子进程,子进程是孤儿进程,孤儿进程由1号进程收养,孤儿进程终止,资源由1号进程释放;孩子是后台进程。
僵尸进程:进程终止,资源未回收,可以通过杀死父进程来让init进程接管子进程,来进行清理。(子进程先死,父进程未死,不回收子进程的资源)
wait原理:
子进程终止时会触发一个信号SIGCHLD通知给父进程的wait。
wait的参数:获取子进程的终止状态和原因。有以下宏来获取信息:
WIFEXITED(wstatus)是否正常终止:WEXITSTATUS(wstatus)获取返回值
WIFSIGNALED(wstatus)是否信号终止:WTERMSIG(wstatus)获取信号的类型
WIFSTOPPED(wstatus)是否暂停:WSTOPSIG(wstatus)返回导致暂停的信号
int main()
{
if(fork() == 0){
printf("child pid = %d, ppid = %d\n", getpid(), getppid());
//return -1;
//while(1);
//_exit(2);
abort();
}
else{
int wstatus;
printf("parent pid = %d, ppid = %d\n", getpid(), getppid());
wait(&wstatus);
if(WIFEXITED(wstatus)){
printf("normal exit, return val = %d\n",WEXITSTATUS(wstatus));
}
else if(WIFSIGNALED(wstatus)){
printf("killed by signal, signal num = %d\n",WTERMSIG(wstatus));
}
}
return 0;
}
echo $? 命令是查看上一个进程的返回值,若大于128,则是该数-128对应的信号原因退出。实际上用了wait系统调用,查了wstatus。
pid_t waitpid(pid_t pid,int *wstatus,int options);
等待指定的进程,参数为-1等待任意子进程,大于0是指定子进程。
第三个参数:WNOHANG,非阻塞的等待,若存在指定的进程但还未终止,则返回0。需配合循环来使用。
int main()
{
pid_t pid;
if((pid = fork()) == 0){
printf("child pid = %d, ppid = %d\n", getpid(), getppid());
//return -1;
while(1);
}
else{
int wstatus;
printf("parent pid = %d, ppid = %d\n", getpid(), getppid());
while(1){
int ret = waitpid(pid,&wstatus,WNOHANG);
if(ret != 0){
if(WIFEXITED(wstatus)){
printf("normal exit, return val = %d\n",WEXITSTATUS(wstatus));
}
else if(WIFSIGNALED(wstatus)){
printf("killed by signal, signal num = %d\n",WTERMSIG(wstatus));
}
break;
}
else{
printf("child has not dead yet!\n");
sleep(1);
}
}
}
return 0;
}
3.进程的终止
正常终止:
1.在main()函数中使用return,终止进程;
2.在任何位置调用exit(返回值),终止进程,会清空缓冲区(打印);
3.在任何位置使用_exit()和_Exit(),缓冲区内容丢弃;
异常终止:
1.abort,给自己发送一个SIGABORT信号;void abort(void);
2.信号终止:kill -9
4.进程的组织
1.进程组是进程的集合,组ID是进程组组长的pid,组长终止,组ID不变,故组长不能脱离原进程组再新建组。
int setpgid(pid_t pid,pid_t pgid); 设置进程的组id,如果pid和pgid为0,指本进程。
pid_t getpgid(pid_t pid); 获取进程的组id,参数为0,代表本进程。
2.bash创建进程时,不仅fork+exec,还setpgid把该进程设置为进程组的组长,子进程默认属于父进程的进程组。
3.父进程和子进程都while(1),都属于前台进程组,可用ctrl+c中断,若对子进程setpgid(0,0),子进程脱离前台进程组,将不再受键盘中断的影响。
4.前台进程组只能有0-1组,后台进程组可以有0-多组。
5.会话:会话是进程组的集合,一个会话可以包含多个进程组,只能对应一个控制终端。每个会话有一个会话首进程,即创建会话的进程,建立与终端连接的就是这个会话首进程,也被称为控制进程。一个会话可以包括多个进程组,这些进程组可被分为一个前台进程组和一个或多个后台进程组。每个终端对应一个会话。
6.终端和会话断开连接,会话中所有进程会收到断开连接信号,一般会终结进程。
pid_t getsid(pid_t pid); 获取会话ID,参数为0表示本进程。
pid_t setsid(void); 设置会话id,创建新会话,意味着要创建新的进程组,所以进程组组长不能创建新会话。
7.守护进程
守护进程完全脱离控制终端控制,daemon(所以一般以d结尾,如sshd),生命周期是从系统开启到结束。
创建守护进程的流程:
1.fork(),关闭父进程
2.setsid(),创建会话,脱离原来会话
3.关闭所有文件描述符
4.改变当前工作目录为根目录
5.去掉掩码
#include <func.h>
#define MAXFD 64
void Daemon(){
if(fork() != 0){
exit(0);
}
setsid();
for(int i = 0; i < MAXFD; ++i){
close(i);
}
chdir("/");
umask(0);
}
int main()
{
Daemon();
return 0;
}
-
使用守护进程记录log:
void syslog(int priority,const char *format,…)
第一个参数代表优先级,后面跟参数printf一样。
log记录在/var/log/syslog中。
//在Daemon函数最后添加如下内容:
for(int i = 0; i < 10; ++i){
time_t now;
time(&now);
struct tm* pTm = localtime(&now);
syslog(LOG_INFO,"%4d%02d%02d %02d:%02d", pTm->tm_year+1900,pTm->tm_mon+1, pTm->tm_mday,pTm->tm_hour,pTm->tm_min);
sleep(2);
}
5.管道
1.无名管道int pipe(int pipefd[2]);
1. 成功返回0,失败返回-1,参数是一个数组,保存是管道的两端(文件描述符)
2. fd[1]是管道的是写端,fd[0]是管道的读端
fork()之后父进程和子进程最好把自己不用的那一端的文件描述符关闭掉,良好的习惯。
2.无名管道的特点
1. 无名管道只能在有亲缘关系之间的进程进行通信(父子,兄弟)
2. 半双工
3. 依赖于文件系统,生命周期随进程的结束而结束
4. 管道是基于字节流来通信的,数据没有边界,多次写管道,数据是粘在一起的
5. 管道关闭读端,写管道,程序回收到SIGPIPE信号,进程的退出码是141,(echo $? 查看进程退出码,若大于128,就说明,进程是被信号打断的)
6. 关闭管道的写端(父子进程都要关闭写端),使用read读管道,那么read会变成非阻塞的,返回0;
7.先写入,再关闭写端,读管道,则会把缓冲区的内容正常读出,再读,会得到0。
//关闭写端,读端返回0
int main(int argc, char **argv)
{
int fds[2];
int ret = 0;
ret = pipe(fds);
ERROR_CHECK(ret, -1, "pipe");
if(fork()){
printf("main process\n");
close(fds[1]); //父进程关闭
wait(NULL);
}
else{
close(fds[1]); //子进程也要关闭
char buf[64] = {0};
ret = read(fds[0], buf, sizeof(buf));
printf("buf = %s, ret =%d \n", buf, ret);
}
return 0;
}
//关闭读端来写,触发SIGPIPE信号
int main(int argc, char **argv)
{
int fds[2];
int ret = 0;
ret = pipe(fds);
ERROR_CHECK(ret, -1, "pipe");
if(fork()){
close(fds[0]);
sleep(1); //父进程先睡,让子进程能执行关闭,否则会正常退出
ret = write(fds[1], "hello", 5);
printf("ret = %d\n", ret);
ERROR_CHECK(ret, -1, "write");
wait(NULL);
}
else{
close(fds[0]);
}
return 0;
}
3.有名管道(命名管道)特点
1. 可以在非亲缘关系的进程间通信
2. 是一种特殊类型的文件,不会随着进程的结束而消失。
4.int mkfifo(const char *pathname, mode_t mode)
1. 成功返回0, 失败返回-1,
2. 参数1:创建的命名管道,
3. 参数2:权限
5.删除有名管道 int unlink(const char *pathname)
1. 可以删除有名管道文件,还可以删除普通文件
2. 删除的连接,当问文件的连接数为0的时候,才真正的删除该文件。
应用小结:mkfifo创建以后可以用open打开来读写。
6.标准流管道
7.FILE *popen(const char *command, const char *type)
1. 参数1:启动另一个进程
2. 参数2:打开方式
3.以w方式启动,将写入的内容重定向到以命令启动的进程的标准输入。
4.以r方式启动,被启动进程的标准输出重定向到该进程的fread()。
//popen_w.c
int main(int argc, char **argv)
{
FILE* fp = popen("./read", "w");
fwrite("hello", 1, 5, fp);
fclose(fp);
return 0;
}
//read.c
int main(int argc, char **argv)
{
char buf[64] = {0};
read(STDIN_FILENO, buf, sizeof(buf));
printf("buf = %s\n", buf);
return 0;
}
//popen_r.c
int main(int argc, char **argv)
{
FILE* fp = popen("./print", "r");
char buf[64] = {0};
fread(buf, 1, sizeof(buf), fp);
printf("buf = %s", buf);
return 0;
}
//print.c
int main(int argc, char **argv)
{
printf("world\n");
return 0;
}
8.popen其实就是对管道的操作进行一些封装
1. 创建一条管道
2. fork一个子进程
3. 在父进程中关闭不需要使用的文件描述符
4. 执行exec函数族的调用
5. 执行函数中所指定的命令
6.进程间通信的高级方式
1.共享内存
1.system V的通信方式
-
共享内存
-
信号量
-
消息队列
2.共享内存:一段特殊的内存区域, 可以被多个进程共享,进程想要使用这块共享内存,需要把共享区域映射到本进程的地址空间中,就可以实现进程间的数据交互。
3.创建共享内存的接口: int shmget(key_t key, size_t size, int shmflg)
-
成功返回共享内存id, 失败返回-1,
-
key是一个整型值,可以使用ftok函数生成,
-
参数2:创建共享内存的大小
-
参数3:IPC_CREAT|0666;
4.key_t ftok(const char *pathname, int proj_id)
-
成功返回一个key(8位的整型值),失败返回-1,
-
参数1:是一个路径,路径所指向的文件(文件夹)必须真实存在,可访问的文件)
-
参数2:是一个整型值(必须是非零值)
5.ftok函数的参数如果每次都是一样的,那么生成的key值,每次都是一样的、
6.查看共享内存命令: ipcs
7.删除共享内存的方式
-
ipcrm -m 共享内存id
-
ipcrm -M 共享内存的键值
8.共享内存一旦创建好之后,就会一直存在,不会随进程的结束而消失,直到使用命令删除,或者重启系统。
9.shmat:at是attach:功能是将创建好的共享内存映射到本进程的地址空间,方便使用。
10.映射函数void *shmat(int shmid, const void *shmaddr, int shmflg)
-
成功返回指向该共享内存的指针,失败是返回(void*)-1,
-
参数1:共享内存的id,
-
参数2:填NULL,表示让内核决定一个合适的位置
-
参数3:标志位,填0;
11.shmdt:dt是detach分离
12.解除映射int shmdt(const void *shmaddr)
参数:shmat的返回指针
成功返回0,失败返回-1
//shm_wrirw.c
#include <head.h>
int main(int argc, char **argv)
{
//生成key值
key_t key = ftok("../shm", 1);
ERROR_CHECK(key, -1, "ftok");
int shmid = shmget(key, 1024, IPC_CREAT|0666);
ERROR_CHECK(shmid, -1, "shmget");
//讲共享内存映射到本进程的地址空间
char *p = (char*)shmat(shmid, NULL, 0);
ERROR_CHECK(p, (void*)-1, "shmat");
printf("shmid = %d\n", shmid);
strcpy(p, "hello");
int ret = shmdt(p);
ERROR_CHECK(ret, -1, "shmdt");
return 0;
}
//shm_read.c
#include <head.h>
int main(int argc, char **argv)
{
//生成key值
key_t key = ftok("../shm", 1);
ERROR_CHECK(key, -1, "ftok");
int shmid = shmget(key, 1024, IPC_CREAT|0666);
ERROR_CHECK(shmid, -1, "shmget");
//讲共享内存映射到本进程的地址空间
char *p = (char*)shmat(shmid, NULL, 0);
ERROR_CHECK(p, (void*)-1, "shmat");
printf("shmid = %d\n", shmid);
printf("p = %s\n", p);
int ret = shmdt(p);
ERROR_CHECK(ret, -1, "shmdt");
return 0;
}
13.共享内存控制函数int shmctl(int shmid, int cmd, struct shmid_ds *buf)
-
参数2:IPC_STAT获取共享内存的信息、IPC_SET设置共享内存相关信息、IPC_RMID删除该共享内存
-
参数3:是一个结构体,保存共享内存的相关信息,只有IPC_STAT、IPC_SET时会用来存储信息。
14.对于共享内存的这种删除,叫标记删除:此时删除一段共享内存时,该共享内存正在使用(连接数不为0),该共享内存不会立即被删除,当该共享内存不在被使用的时候,才会真正的删除。
15.当键值为全0的时候,是私有的共享内存。只能在有亲缘关系间的进程之间使用。
16.私有方式的共享内存不受key约束(shmget创建私有共享内存时,key值填0),并且每次执行都会生成新的一块共享内存。[而普通共享内存,只要存在,每次执行都只是打开]
#include <head.h>
int main(int argc, char **argv)
{
int shmid = shmget(0, 1024, IPC_CREAT|0666);
ERROR_CHECK(shmid, -1, "shmget");
printf("shmid = %d\n", shmid);
return 0;
}
2.内存管理
1.虚拟地址:进程实际看到的地址。
2.物理地址:存放程序数据的真实位置。
3.物理地址和虚拟地址转换:使用MMU内存管理单元进行转换(硬件),不同的系统,转换机制不同。
4.32位系统,进程的地址空间是0-4G(2的32次方), 64位系统当中,进程的地址空间是2的48次方(256T)。
5.可以通过 cat /proc/cpuinfo 查看
6.内存的最小分配大小4k,对于虚拟地址来讲又叫页,对于物理地址叫页框
7.申请内存的时候,系统是采用lazy模式分配的
-
lazy模式:当申请内存时,但是没有使用,系统不会立刻分配内存空间。只有当真正的使用该内存空间的时候,系统才分配内存空间。
-
页表:保存虚拟地址和物理地址的对应关系。当申请的内存空间没有被使用的时候,页表是不会存储物理地址和虚拟地址的对应关系。
8.一级页表的对应关系
9.二级页表
10.为什么要采用多级页表? 节约内存空间
11.大页,linux默认的大页是2M, 可以通过 grep Huge /proc/meminfo查看。
12.快表TLb:记录下一次或者频率比较高的,虚拟和物理地址的对应关系。
快表更新算法:FIFO、LRU
13.快表存储原理:局部性原理:
-
时间局部性:
-
空间局部性:
14.不同进程:不同的虚拟地址是否能对应相同的物理地址? 可以
15.不同进程:相同的虚拟地址能否对应不同的物理地址? 可以
16.虚拟地址和物理地址并没有一个绝对的对应关系。
3.大页的使用
1.想使用大页的方式申请内存,需要系统支持的,默认的情况下不使用大页的方式分配,需要手动设置
2.如果切换到root报认证失败,
-
密码写错了
-
没有设置密码, sudo passwd root + 密码
3.设置系统支持大页方式,
-
切换到root用户
-
echo 10 > /proc/sys/vm/nr_hugepages
-
cat /proc/sys/vm/nr_hugepages 发现如果和你设置的大页数量一样,就说明设置好了
4.查看大页详细信息 grep Huge /proc/meminfo
//上述设置后,在代码中使用大页的例子
#include <head.h>
#define SHM_HUGE_2MB 1<<21
int main(int argc, char **argv)
{
//使用大页的方式分配共享内存
int shmid = shmget(1000, 1<<21, IPC_CREAT|0666|SHM_HUGETLB|SHM_HUGE_2MB);
ERROR_CHECK(shmid, -1, "shmget");
char *p = (char*)shmat(shmid, NULL, 0);
ERROR_CHECK(p, (void*)-1, "shmat");
strcpy(p, "hello");
shmdt(p);
return 0;
}
5.使用mmap实现共享内存
-
创建一个文件touch file
-
给文件开辟空间ftruncate 函数,命令:truncate -s +开辟空间的大小 + 文件名
//函数 int truncate(const char *path, off_t length); int ftruncate(int fd, off_t length); void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset); int munmap(void *addr, size_t length);
//例子
//mmap_sharem_w.c
#include <head.h>
int main(int argc, char **argv)
{
int fd = open("file", O_RDWR);
ERROR_CHECK(fd, -1, "open");
char *pMap = (char*)mmap(NULL, 10, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
ERROR_CHECK(pMap, (void*)-1, "mmap");
strcpy(pMap, "hello");
munmap(pMap, 10);
return 0;
}
//mmap_sharem_r.c
#include <head.h>
int main(int argc, char **argv)
{
int fd = open("file", O_RDWR);
ERROR_CHECK(fd, -1, "open");
char *pMap = (char*)mmap(NULL, 10, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
ERROR_CHECK(pMap, (void*)-1, "mmap");
printf("pMap = %s\n", pMap);
munmap(pMap, 10);
return 0;
}
6.mmap使用大页的方式申请共享内存
要有下面两步然后再用mmap的大页方式
-
sudo mount none /mnt/huge/ -t hugetlbfs
-
在huge目录下创建file
#include <head.h>
#define MAP_HUGE_2MB 1<<21
int main(int argc, char **argv)
{
int fd = open("/mnt/huge/file", O_RDWR);
ERROR_CHECK(fd, -1, "open");
char *pMap = (char*)mmap(NULL, 10, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_HUGETLB|MAP_HUGE_2MB, fd, 0);
ERROR_CHECK(pMap, (void*)-1, "mmap");
strcpy(pMap, "hello");
munmap(pMap, 10);
return 0;
}
7.不要对一个指针取sizeof,64位系统当中指针占8个字节
8.当两个进程对共享每次+1,各自相加100,正确的原因,是进程在+100,没有用完时间片,没有进程间切换,或者切换没有在+1的过程当中进程切换。
9.当两个进程对共享区值每次+1,各加1000w,有时候能成功,有时失败的原因是?
-
成功的原因:进程间切换没有在+1这个操作过程中发生切换
-
失败的原因:在+1的过程当中发生了进程间切换
4.信号量
1.信号量的提出者是荷兰计算机科学家Dijkstra,信号量的作用:可以用于进程间的同步与互斥。
2.同步:两个或两个以上的量在变化过程中,保持一定的相对关系。
3.互斥:两个事物不能同时存在。对资源的独占式访问
4.信号量种类分三种:system V、posix、posix基于内存的信号量
5.信号量又叫信号灯
6.对于只有两种状态的信号量又叫二进制信号量,常用于互斥操作
7.对于具有多个资源的信号量,又称为计数信号量。常用于同步
8.信号量的值:资源的数量
9.二进制信号量:初始值为1, 只有两种值,0和1
10.计数信号量:值要大于1
11.临界资源:可以被共享,但是需要互斥访问的资源
12.临界区:访问临界资源的代码,临界区也需要互斥的访问
13.如何使用信号量控制资源, p v操作
14.P操作:测试、探查操作,测试控制该资源信号量的值是否大于0, 大于0表示有资源可以使用,程序会进入临界区,访问临界资源
15.V操作: 增加操作, 访问完资源之后,释放资源,资源的数量+1;
16.passeren, 通过的意思,vrijgeven 释放
17.p、v是一个原子操作。是由内核保证的
18.创建信号int semget(key_t key, int nsems, int semflg)
-
成功返回信号量的id、失败返回-1
-
参数2,是信号量的个数,
-
参数3:IPC_CREAT|0600;
19.对信号量的操作int semctl(int semid, int semnum, int cmd, …)
-
失败返回-1
-
参数2:对那个信号量操作,就那个的编号,编号从零开始
-
参数3:cmd
IPC_RMID,删除,
GETVAL:获取该信号量所代表资源的数量,semctl返回该值
GETALL: 获取所有信号的各自所代表的资源数量
SETVAL:设置当前信号量所代表的资源数量,semctl成功返回0
SETALL:设置所有信号量所代表的资源数量
-
可变参数
//使用举例 #include <head.h> int main(int argc, char **argv) { //创建信号量 int semid = semget(2000, 3, IPC_CREAT|0666); ERROR_CHECK(semid, -1, "semget"); unsigned short arr[3] = {1, 2, 3}; int ret = 0; unsigned short retArr[3] = {0}; //设置资源数量 semctl(semid, 0, SETALL, arr); //获取资源数量 semctl(semid, 0, GETALL, retArr); for(int i = 0; i < 3; ++i){ printf("retArr[%d] = %d\n", i, retArr[i]); } for(int i = 0; i < 3; ++i){ printf("sem[%d] = %d\n", i, semctl(semid, i, GETVAL)); } return 0; }
20.信号量的操作函数int semop(int semid, struct sembuf *sops, size_t nsops)
-
成功返回0, 失败-1
-
参数2:操作信号的结构体
sem_flg一般填0 -
参数3:结构体的数量
21.如何使用pv操作
-
进入临界区前p操作 p: -1
-
出临界区V操作 V: +1
22.p、v操作,系统为了保证原子性,所以每次p、v都会消耗比较多的时间
#include <head.h>
#define N 10000000
int main(int argc, char **argv)
{
int ret = 0;
int shmid = shmget(1000, 4, IPC_CREAT|0666);
ERROR_CHECK(shmid, -1, "shmget");
int *p = (int *)shmat(shmid, NULL, 0);
ERROR_CHECK(p, (void*)-1, "shmat");
//对该地址所存储值清空
memset(p, 0, sizeof(int));
int semid = semget(2000, 1, IPC_CREAT|0666);
ERROR_CHECK(semid, -1, "semget");
semctl(semid, 0, SETVAL, 1);
struct sembuf P, V;
//p操作,申请一个资源
P.sem_num = 0;
P.sem_op = -1;
P.sem_flg = 0;
//v操作,释放一个资源
V.sem_num = 0;
V.sem_op = 1;
V.sem_flg = 0;
time_t beg, end;
time(&beg);
//父进程+100次
if(fork()){
for(int i = 0; i < N; ++i){
//进入临界区前p操作
ret = semop(semid, &P, 1);
ERROR_CHECK(ret, -1, "semop");
(*p)++;
//出临界区V操作
ret = semop(semid, &V, 1);
ERROR_CHECK(ret, -1, "semop2");
}
wait(NULL);
printf("p = %d\n", *p);
time(&end);
printf("cost time = %ld\n", end - beg);
}
//子进程+100次
else{
for(int i = 0; i < N; ++i){
//进入临界区前p操作
ret = semop(semid, &P, 1);
ERROR_CHECK(ret, -1, "semop");
(*p)++;
//出临界区V操作
ret = semop(semid, &V, 1);
ERROR_CHECK(ret, -1, "semop2");
}
}
shmdt(p);
return 0;
}
23.sem_flag设置为SEM_UNDO的作用
-
如果某个操作指定SEM_UNOD, 那么进程终止时自动撤销资源操作
-
好处:可以避免死锁。
-
为了防止p操作后,进程意外退出,资源没有被释放,造成信号量的值发生错误。
24.删除信号量:ipcrm -s semid
25.生产者消费者模型
#include <head.h>
int main(int argc, char **argv)
{
int ret = 0;
int semid = semget(3000, 2, IPC_CREAT|0666);
ERROR_CHECK(semid, -1, "semget");
//0代表商品的数量,5代表货架的数量
unsigned short arr[2] = {0, 5};
semctl(semid, 0, SETALL, arr);
struct sembuf sop[2];
memset(sop, 0, sizeof(sop));
//父进程充当生产者
if(fork()){
//生产者生产商品,消耗货架
sop[0].sem_num = 0;
sop[0].sem_op = 1;
sop[0].sem_flg = 0;
sop[1].sem_num = 1;
sop[1].sem_op = -1;
sop[1].sem_flg = 0;
while(1){
ret = semop(semid, sop, 2);
ERROR_CHECK(ret, -1, "semop1");
printf("生产者:商品的数量 = %d, 货架的数量 = %d\n", semctl(semid, 0, GETVAL), semctl(semid, 1, GETVAL));
sleep(1);
}
}
else{
//消费者,消耗商品,释放货架
sop[0].sem_num = 0;
sop[0].sem_op = -1;
sop[0].sem_flg = 0;
sop[1].sem_num = 1;
sop[1].sem_op = 1;
sop[1].sem_flg = 0;
while(1){
ret = semop(semid, sop, 2);
ERROR_CHECK(ret, -1, "semop2");
printf("消费者:商品的数量 = %d, 货架的数量 = %d\n", semctl(semid, 0, GETVAL), semctl(semid, 1, GETVAL));
sleep(2);
}
}
return 0;
}
5.消息队列(Message Queue)MQ
1.是一种多进程之间通信一种机制:解耦、异步、削峰
-
解耦:就是解除两个或者两个以上事物之间的关联性,使其具有独立性
-
异步:
-
削峰:
2.创建消息队列使用的函数int msgget(key_t key, int msgflg)
- 成功返回消息队列id,失败返回-1,
3.往消息队列传数据int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg)
-
成功返回0, 失败返回-1,
-
参数1,msgid
-
参数2:该结构体需要重构,重构的地方时结构的第二个成员,改成自己需要使用的大小
struct msgbuf{ long mtype; //message type,must be > 0 char mtext[1]; //message data }
-
参数3:发送数据的长度
-
参数4:标志位,填0;
4.从消息队列里面接收数据的函数ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
-
成功返回接收的字节数,失败返回-1,
-
参数2:接收的信息保存在该结构体中
-
参数3:最多接收的数据量
-
参数4:指定接收哪个类型的数据
-
参数 5:标志位,填0
5.消息队列,是先进先出结构,相同类型下,先写入队列的数据,先接收;数据之间是有边界的
//msgsnd.c
#include <head.h>
struct mymsgbuf {
long mtype;
char mtext[10];
};
int main(int argc, char **argv)
{
int ret = 0;
int msgid = msgget(1000, IPC_CREAT|0666);
ERROR_CHECK(msgid, -1, "msgget");
struct mymsgbuf mbuf;
memset(&mbuf, 0, sizeof(mbuf));
mbuf.mtype = atoi(argv[1]);
strcpy(mbuf.mtext, argv[2]);
ret = msgsnd(msgid, &mbuf, strlen(mbuf.mtext), 0);
ERROR_CHECK(ret, -1, "msgsnd");
return 0;
}
//msgrcv.c
#include <head.h>
struct mymsgbuf {
long mtype;
char mtext[10];
};
int main(int argc, char **argv)
{
int ret = 0;
int msgid = msgget(1000, IPC_CREAT|0666);
ERROR_CHECK(msgid, -1, "msgget");
struct mymsgbuf mbuf;
memset(&mbuf, 0, sizeof(mbuf));
long type = atoi(argv[1]);
//从消息队列里面接收数据
ret = msgrcv(msgid, &mbuf, sizeof(mbuf.mtext), type, 0);
ERROR_CHECK(ret, -1, "msgrcv");
printf("ret = %d\n", ret);
printf("buf = %s\n", mbuf.mtext);
return 0;
}
6.msgrcv接收类型如果是填0,代表无差别类型接收;如果填负数,就取小于该负数绝对值的数据
7.函数小结:
6.信号
1.信号不同于信号量
2.信号也是进程间通信的一种
3.系统当中信号的定义:信号是进程运行过程中,由自身产生,或者由进程外部发过过来的消息。
4.中断:中断是指计算运行过程当中,出现某些意外情况需要主机干预,机器能自动停止正在运行的程序,并且转入处理新情况的程序,处理完毕之后,又会返回原来被暂停的程序中继续运行。
-
硬中断:硬中断是由外设产生的,硬中断信号是由中断控制器提供,硬中断可以屏蔽的。
-
软中断:软中断是利用硬中断的概念,用软件的方式进行的模拟。宏观上异步的执行效果
-
软中断是实现系统API函数调用的手段
5.软中断不会去抢占另一个软中断,硬中断可以抢占软件中断。硬件中断可以保证时效性
6.每一个信号都自己的默认行为,它决定了收到信号后的行为。
-
Term:终止当前进程
-
Ign:忽略该信号
-
Core:终止当前进程,并且产生core dump
-
Stop:停止(挂起)一个进程
-
Cont:使当前停止的进程,继续运行
7.常见信号
-
SIGINT 2 term 中断来自键盘 ctrl c
-
SIGQUIT 3 Core 从键盘退出 ctrl \
-
SIGKILL 9 Term Kill signal 不能被捕捉也能被修改
-
SIGPIPE 13 Term 写一个关闭读端的管道回收这个信号
-
SIGALRM 14 Term sleep函数就是使用这个信号
-
SIGUSR1 SIGUSR2 是提供给用户使用的信号 默认行为 Term
8.signal、sigaction这两个函数可以捕捉信号,并且可以修改信号的行为
9.由进程内部产生的信号又称为同步信号, 由进程外部产生的信号又叫异步信号
10.进程收到信号后有三种行为处理该信号
1.接收默认处理
2.忽略信号
3.捕捉信号并处理
11. signal函数
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
//signal() returns the previous value of the signal handler, or SIG_ERR on error.
//参数1:捕捉或处理的信号
//参数2:处理信号的函数
//参数2还可以填SIG_DFL(默认行为),SIG_IGN(忽略)
//例子
#include <head.h>
void sigFunc(int sigNum)
{
printf("sig %d is comming\n", sigNum);
}
int main(int argc, char **argv)
{
if(SIG_ERR == signal(2, sigFunc)){
printf("signal is error\n");
return -1;
}
else{
printf("signal is success\n");
while(1);
}
return 0;
}
12.两个函数功能类似,参数越多,功能越强
13.signation函数
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
//参数1:信号值
//参数2,如下结构体
struct sigaction {
void (*sa_handler)(int);//信号处理函数1
void (*sa_sigaction)(int, siginfo_t *, void *);//信号处理函数2
sigset_t sa_mask; //阻塞信号的集合
int sa_flags;//信号处理方式,使用信号处理函数1填0,使用信号处理函数2填SA_SIGINFO
void (*sa_restorer)(void);//保留不用
};
//参数3,保存原来信号的一些信息
/*struct sigaction 中第二个成员的第二个参数(结构体)为传入传出参数,该结构体中有许多保存信号信息的变量,
如信号值,信号产生的时间,若想获取监听到的信号的相关信息的话,可选取这个函数。第一个参数自动传入的是信号的编号。*/
13.第二个成员的最后一个参数一般为NULL,其实是一个ucontext结构体的指针,查看ucontext_t结构体的方法:源码文件里glibc里查找
grep -rwn "typedef struct ucontext_t {"
vim sysdeps/unix/sysv/linux/sparc/sys/ucontext.h
14.使用举例
#include <head.h>
void sigFunc(int sigNum)
{
printf("sig %d is comming\n", sigNum);
}
void newFunc(int sigNum, siginfo_t* pInfo, void * p)
{
printf("new sig %d is comming\n", sigNum);
}
int main(int argc, char **argv)
{
int ret = 0;
struct sigaction act;
memset(&act, 0, sizeof(act));
//使用旧类型的信号处理函数
/* act.sa_handler = sigFunc; */
/* act.sa_flags = 0; */
//使用新类型的信号处理函数
act.sa_sigaction = newFunc;
act.sa_flags = SA_SIGINFO;
ret = sigaction(2, &act, NULL);
ERROR_CHECK(ret, -1, "sigaction");
while(1);
return 0;
}
15.在signal处理机制下,有四种行为需要考虑
-
情形1:处理完一个信号后,是否需要重新注册捕捉下一个信号。(不需要)
-
sigaction函数的sa_flag可以按位或SA_RESETHAND,第一次捕捉信号去执行信号处理函数,第二次执行信号的默认行为。
-
情形2:如果正执行当前的信号处理函数,还没有执行完, 此时来了一个相同的信号,它的行为是什么?
- 先执行完当前的信号处理函数,然后再只执行新的相同信号一次
- 先执行完当前的信号处理函数,然后再只执行新的相同信号一次
-
情形3:如果正执行当前的信号处理函数,还没有执行完, 此时来了一个不同的信号,它的行为是什么?
- 会优先执行新的信号(前提是该新信号没有与之相同的信号在被打断中),新的信号处理完成之后再继续执行之前的信号处理函数。
- 会优先执行新的信号(前提是该新信号没有与之相同的信号在被打断中),新的信号处理完成之后再继续执行之前的信号处理函数。
-
情形4:如果当前进程阻塞在系统调用上(比如read函数),那么当它收到一个信号后,它的处理行为是什么?
-
对于signal函数,先处理信号处理函数,然后再返回系统调用上
-
对于sigaction函数:先处理信号处理函数,然后不会返回到系统调用上,系统调用read函数直接返回-1
-
sigaction的sa_flag按位或上SA_RESTART就可以实现重启系统调用
-
16.同时使用sigaction的新类型和旧类型信号处理函数
-
会执行最后设置类型的处理函数
-
因为结构体sigaction类型实际上是一个联合体
//源码 struct sigaction { union{ void (*sa_handler)(int); void (*sa_sigaction)(int, siginfo_t *, void *); } _u; sigset_t sa_mask; int sa_flags; void (*sa_restorer)(void); };
-
sigaction 使用新类型sa_flags填SA_SIGINFO才可在第二个参数精确保存该信号的相关信息。
-
查看当前bash的进程id : echo $$
17.信号的阻塞
-
阻塞与忽略的区别,
-
阻塞:阻塞一会,之后还会继续执行
-
忽略:不会再理会该信号
-
-
使用sigaction阻塞信号,阻塞的作用域就在信号处理函数这个范围内
阻塞操作步骤(注意要修改sigaction中结构体参数中的sa_mask的值)
//操作函数 int sigemptyset(sigset_t *set); int sigfillset(sigset_t *set); int sigaddset(sigset_t *set, int signum); int sigdelset(sigset_t *set, int signum); int sigismember(const sigset_t *set, int signum); //第一个参数要用sigpending来获得 int sigpending(sigset_t *set); //获取阻塞信号集合
//例如 #include <head.h> void newFunc(int sigNum, siginfo_t * pInfo, void* p) { printf("new sig %d is comming\n", sigNum); sleep(5); printf("after sig\n"); } int main(int argc, char **argv) { int ret = 0; struct sigaction act; memset(&act, 0, sizeof(act)); sigset_t set; sigemptyset(&set); //把3号信号放到阻塞集合当中 sigaddset(&set, 3); act.sa_sigaction = newFunc; act.sa_flags = SA_SIGINFO; act.sa_mask = set; ret = sigaction(2, &act, NULL); ERROR_CHECK(ret, -1, "sigaction"); while(1); return 0; }
-
sigprocmask它可以实现全局范围内的信号阻塞。
-
查看进程中阻塞集合路径 vim include/linux/sched.h +931
-
sigprocmask函数
int sigprocmask(int how,const sigset_t *set,sigset_t *oldset); //成功返回0,失败返回错误码 //参数1:表示怎么处理第二个参数这个集合 //SIG_BLOCK:把集合加入到原有的阻塞集合当中 //SIG_UNBLOCK:把集合从原有的集合中删除 //SIG_SETMASK:把集合特换原有的阻塞集合 //参数2:阻塞集合 //参数3:原有的集合
//使用举例 #include <head.h> void newFunc(int sigNum, siginfo_t * pInfo, void* p) { printf("new sig %d is comming\n", sigNum); sleep(5); printf("after sig\n"); } int main(int argc, char **argv) { int ret = 0; struct sigaction act; memset(&act, 0, sizeof(act)); sigset_t set; sigemptyset(&set); //把2号信号放到阻塞集合当中 sigaddset(&set, 2); //使用新类型 act.sa_sigaction = newFunc; act.sa_flags = SA_SIGINFO; ret = sigaction(2, &act, NULL); ERROR_CHECK(ret, -1, "sigaction"); //设置阻塞 sigprocmask(SIG_BLOCK, &set, NULL); printf("before sleep\n"); sleep(5); //运行到此时收到2号信号会阻塞,并不会去执行信号处理函数 printf("after sleep\n"); //解除阻塞 sigprocmask(SIG_UNBLOCK, &set, NULL); //解除阻塞后才会根据阻塞队列执行信号处理函数 while(1); return 0; }
18.kill函数:int kill(pid_t pid, int sig);
19.查看后台是否有任务:jobs
20.睡眠函数
-
sleep:1秒
-
usleep:1微秒
-
1秒 = 100W微秒
-
alarm:睡眠函数, pause
-
alarm的参数,设置闹钟多长时间后相应:其实就是在该时间后发送SIGALRM信号。pause函数收到这个信号后返回。
-
pause是一个阻塞性函数,alarm后阻塞在pause,直到收到SIGALRM函数。
-
alarm函数返回值:是指上一次闹钟还剩多长时间
-
注意:alarm函数不要和sleep一起使用。alarm到时间后直接发信号给pause,使中间的sleep失效。
#include <head.h> int main(int argc, char **argv) { int ret = 0; ret = alarm(3); printf("alarm1 ret = %d\n", ret); sleep(10); ret = alarm(3); printf("alarm2 ret = %d\n", ret); pause(); return 0; }
21.计时器
-
真实计时器:程序实际运行的时间(从进程启动那一刻起一直到进程结束,进程不占cpu的时候的时间也被计入)
-
虚拟计时器:程序在用户态所消耗的时间(不含系统调用的时间,与不含睡眠所占用的时间)
-
实用计时器:程序在用户态和内核态所占用的时间之和。
-
三个计时器对应的发送的信号:SIGALARM、SIGVTALARM、SIGPROF
int getitimer(int which, struct itimerval *curr_value); //参数1:计时器的种类, ITIMER_REAL ITIMER_VIRTUAL ITIMER_PROF //参数2:保存计时器的初始时间和间隔时间 int setitimer(int which, const struct itimerval *new_value,struct itimerval *old_value); struct itimerval { struct timeval it_interval; /* Interval for periodic timer */ struct timeval it_value; /* Time until next expiration */ }; /*struct itimerval的第一个成员表示信号开始后每隔多长之间发一个,setitimer第一个参数指定的情况下的对应信号,第二个成员指函数执行后多久开始发信号。*/ struct timeval { time_t tv_sec; /* seconds */ suseconds_t tv_usec; /* microseconds */ };
-
例子
#include <head.h> void sigFunc(int sigNum) { time_t now; time(&now); printf("now time = %s\n", ctime(&now)); } int main(int argc, char **argv) { signal(SIGPROF, sigFunc); struct itimerval val; memset(&val, 0, sizeof(val)); val.it_value.tv_sec = 1; val.it_interval.tv_sec = 2; time_t now; time(&now); printf("now time = %s\n", ctime(&now)); setitimer(ITIMER_PROF, &val, NULL); printf("before sleep\n"); sleep(3); printf("after sleep\n"); while(1); return 0; } //当有其他进程也在运行,打印出来的time结果并不是按照预想的那样,因为其他进程占用了时间片,而实用计时器只计算该进程占用时间片时的时间来发SIGPROF信号 return 0; }