Linux高性能服务器开发-第二章

146 阅读15分钟

进程控制

Linux 内核的进程控制块是 task_struct结构体定义。其内部成员有很多,我们只需要掌握以下部分即可: 进程id:系统中每个进程有唯一的id,用pid_t类型表示,其实就是一个非负整数
进程的状态:有就绪、运行、挂起、停止等状态
进程切换时需要保存和恢复的一些CPU寄存器
描述虚拟地址空间的信息
描述控制终端的信息
当前工作目录Current Working Directory
umask 掩码
文件描述符表,包含很多指向 file 结构体的指针
和信号相关的信息
用户 id 和组 id 会话Session和进程组
进程可以使用的资源上限Resource Limit

shell命令

ps aux 打印当前快照 ps ajx
top 事实查看进程信息 kill -9 pid 强制杀死某个进程

进程信息STAT

STAT含义
D不可中断 Uninterruptible(usually IO)
R正在运行,或在队列中的进程
S(大写) 处于休眠状态
T停止或被追踪
Z僵尸进程
W 进入内存交换(从内核2.6开始无效)
X 死掉的进程
<高优先级
N低优先级
s包含子进程
+位于前台的进程组

top

可以按以下按键对显示的结果进行排序:(区分大小写)
M 根据内存使用量排序
P 根据 CPU 占有率排序
T 根据进程运行时间长短排序
U|u 根据用户名来筛选进程
k 输入指定的 PID 杀死进程 esc退出

后台运行

root#: ./a.out &

杀死进程

kill [-signal] pid
kill –l 列出所有信号
kill –SIGKILL 进程ID
kill -9 进程ID
killall name 根据进程名杀死进程

pid ppid pgid

每个进程都由进程号来标识,其类型为pid_t整型,进程号的范围0~32767
进程号总是唯一的,但可以重用。
任何进程(除init进程)都是由另一个进程创建

pid_t getpid(void);
pid_t getppid(void);
pid_t getpgid(pid_t pid); //传递null看自己

fork

pid_t fork(void);
返回值:

  • 成功:子进程中返回 0,父进程中返回子进程 ID
  • 失败:返回 -1
    失败的两个主要原因:
  1. 当前系统的进程数已经达到了系统规定的上限,这时 errno 的值被设置 为 EAGAIN
  2. 系统内存不足,这时 errno 的值被设置为ENOMEM

实际上,更准确来说,Linuxfork()使用是通过写时拷贝copy-on-write实现。 内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间。只用在需要写入的时候才会复制(需要写入的数据的)地址空间。在此之前,只有以只读方式共享。

  • 创建子进程时,将父进程的虚拟内存物理内存映射关系复制到子进程中,并将内存设置为只读(设置为只读是为了当对内存进行写操作时触发 缺页异常)。
  • 当子进程或者父进程对内存数据进行修改时,便会触发 写时复制 机制:将原来的内存页复制一份新的,并重新设置其内存映射关系,将父子进程的内存读写权限设置为可读写。 注意:fork之后父子进程共享文件,fork产生的子进程与父进程相同的文件文件描述符表指向相同的文件描述结构体,引用计数增加,共享文件偏移指针

fork gdb

使用GDB调试的时候,默认只能跟踪父进程,可以在fork函数调用之前,通过指令设置 跟踪子进程
set follow-fork-mode [parent(默认)| child]
show follow-fork-mode
设置调试模式:
set detach-on-fork [on(默认) | off]
show detach-on-fork
表示调试当前进程的时候,其它的进程继续运行,如果为off,调试当前进程的时候,其它进程被GDB挂起。

查看调试的进程:
info inferiors
切换当前调试的进程:
inferior id
使进程脱离 GDB 调试:
detach inferiors id

exec函数族

exec函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。
exec 函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程ID等一些表面上的信息仍保持原样, 只有调用失败了,它们才会返回-1,从原程序的调用点接着往下执行。

int execl(const char *path, const char *arg, ..., NULL);
- 参数:
    - path:需要指定的执行的文件的路径, 推荐使用绝对路径
    - arg:是执行可执行文件所需要的参数列表
        - 第一个参数一般没有什么作用,为了方便,一般写的是执行的程序的名称
        - 从第二个参数开始往后,就是程序执行所需要的的参数列表。
        - 参数最后需要以NULL结束(哨兵)
- 返回值:
    - 只有当调用失败,才会有返回值,返回-1,并且设置errno
    - 如果调用成功,没有返回值, 调用他的都没了, 返回给谁

exit() _exit()函数

void exit(int status); C库函数
void _exit(int status); Linux系统函数

exit()会先调用退出处理函数, 刷新IO缓冲区,关闭文件描述符再调用_exit()
父进程可以wait(int *status)获取子进程的退出状态

kill(), arise(), abort()

int kill(pid_t pid, int sig);
- 功能:给任何的进程或者进程组pid, 发送任何的信号 sig
- 参数:
    - pid :
        - > 0 : 将信号发送给指定的进程
        - = 0 : 将信号发送给当前的进程组
        - = -1 : 将信号发送给每一个有权限接收这个信号的进程
        - < -1 : 这个pid=某个进程组的ID取反 (-12345)
- sig : 需要发送的信号的编号或者是宏值,0表示不发送任何信号
- 返回值:
    - 成功 0
    - 失败 非0
    
int raise(int sig);
- 功能:给当前进程发送信号
- 参数:
    - sig : 要发送的信号
- 返回值:
    - 成功 0
    - 失败 非0
    
void abort(void);
- 功能: 发送SIGABRT信号给当前的进程,杀死当前进程

孤儿进程

父进程运行结束,但子进程还在运行,这样的子进程就称为孤儿进程Orphan Process
每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为init
因此孤儿进程并不会有什么危害

僵尸进程

在每个进程退出的时候,内核释放该进程所有的资源,但内核区的PCB没有办法自己释放掉,需要父进程去释放。
进程终止时,父进程尚未回收,子进程残留资源(PCB)存放于内核中,Zombie进程
僵尸进程不能被kill -9杀死,这样就会导致一个问题,如果父进程不调用wait()waitpid() 的话,那么保留的那段信息就不会释放,其进程号就会一直被占用, 但是系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程,此即为僵尸进程的危害,应当避免。

wait() waitpid()

pid_t wait(int *status);
功能:等待任意一个子进程结束,如果任意一个子进程结束了,此函数会回收子进程的资源。
参数:int *status
进程退出时的状态信息,传入的是一个int类型的地址,传出参数。
返回值:
- 成功:返回被回收的子进程的id
- 失败:-1 没有子进程了
调用wait函数的进程会被挂起(阻塞),直到它的一个子进程退出或者收到一个不能被忽略的信号时才被唤醒(相当于继续往下执行)
如果没有子进程了,函数立刻返回,返回-1;如果子进程都已经结束了,也会立即返回,返回-1.

pid_t waitpid(pid_t pid, int *wstatus, int options);
功能:回收指定进程号的子进程,可以设置是否阻塞。
- 参数:
    - pid:
        - pid > 0 : 某个子进程的pid
        - pid = 0 : 回收当前进程组的所有子进程
        - pid = -1 : 回收所有的子进程,相当于 wait() (最常用)
        - pid < -1 : 某个组id的绝对值,回收其中的子进程(自己孩子去了别的组)
    - options:设置阻塞或者非阻塞
        - 0 : 阻塞
        - WNOHANG : 非阻塞
    - status :获取子进程退出状态, 不需要填NULL
- 返回值:
    - > 0 : 返回子进程的id
    - = 0 : && options=WNOHANG, 表示还有子进程活着
    - = -1 :没有子进程了

wait()函数会阻塞, waitpid()可以设置不阻塞
一次waitwaitpid调用只能清理一个子进程

退出信息相关宏函数

WIFEXITED(status) 非0,进程正常退出
WEXITSTATUS(status) 如果上宏为真,获取进程退出的状态(exit的参数)

WIFSIGNALED(status) 非0,进程异常终止
WTERMSIG(status) 如果上宏为真,获取使进程终止的信号编号

WIFSTOPPED(status) 非0,进程处于暂停状态
WSTOPSIG(status) 如果上宏为真,获取使进程暂停的信号的编号
WIFCONTINUED(status) 非0,进程暂停后已经继续运行

ipc进程间通信

(匿名)管道

统计一个目录中文件的数目命令:
ls | wc –l,为了执行该命令,shell创建了两个进程来分别执行 ls 和 wc 管道其实是一个在内核内存中维护的缓冲器,这个缓冲器的存储能力是有限的,不同的操作系统大小不一定相同
管道拥有文件的特质:读操作、写操作
匿名管道没有文件实体
有名管道有文件实体,但不存储数据
可以按照操作文件的方式对管道进行操作
一个管道是一个字节流
管道是半双工的
匿名管道只能在亲缘进程之间使用,因为文件描述符表在fork的时候复制

int pipe(int fd[2]); fd[0]读端 fd[1]写端
查看管道缓冲大小命令 ulimit –a 
查看管道缓冲大小函数 
long fpathconf(int fd, int name);
long size = fpathconf(fd[0], _PC_PIPE_BUF);

有名管道

匿名管道,由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道 FIFO,也叫命名管道FIFO文件 FIFO在文件系统中作为一个特殊文件存在,但FIFO中的内容却存放在内存

通过命令创建有名管道: mkfifo 名字 
int mkfifo(const char *pathname, mode_t mode);

管道读写的特点

读管道:

  • 管道中有数据:read返回实际读到的字节数

  • 管道中⽆数据:

    • 写端被全部关闭,read返回0相当于读到⽂件的末尾;⾮阻塞返回0,不会设置errno
    • 写端没有完全关闭,read阻塞等待;⾮阻塞时返回-1 写管道:
  • 管道读端全部被关闭:进程异常终⽌,进程收到SIGPIPE信号

  • 管道读端没有全部关闭:

    • 管道已满,write阻塞;⾮阻塞返回-1
    • 管道没有满,write将数据写⼊,并返回实际写⼊的字节数

设置管道非阻塞

int flags = fcntl(fd[0], F_GETFL); // 获取原来的flag
flags |= O_NONBLOCK; // 修改flag的值
fcntl(fd[0], F_SETFL, flags); // 设置新的flag

内存映射

void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
- 功能:将一个文件或者设备的数据映射到内存中
- 参数:
    - void *addr: NULL, 由内核指定
    - length : 要映射的数据的长度,这个值不能为0。建议使用文件的长度。
    - prot : 对申请的内存映射区的操作权限
        - PROT_EXEC :可执行的权限
        - PROT_READ :读权限
        - PROT_WRITE :写权限
        - PROT_NONE :没有权限
        - 要操作映射内存,必须要有读的权限。
        - PROT_READ、 PROT_READ|PROT_WRITE
    - flags :
        - MAP_SHARED : 映射区的数据会自动和磁盘文件进行同步,进程间通信,必须要设置这个选项
        - MAP_PRIVATE :不同步,内存映射区的数据改变了,对原来的文件不会修改,会重新创建一个新的文件。(copy on write)
    - fd: 需要映射的那个文件的文件描述符
        - 通过open得到,open的是一个磁盘文件
- offset:偏移量,一般不用。必须指定的是4k的整数倍,0表示不偏移
- 返回值:返回创建的内存的首地址
    - 失败返回MAP_FAILED, (void *)-1
- 注意:文件的大小不能为0, open指定的权限大于等于prot
- 获取文件的长度:stat lseek

int munmap(void *addr, size_t length);
- 功能:释放内存映射
- 参数:
    - addr : 要释放的内存的首地址 必须和mmap得到的一样 否则错误
    - length : 要释放的内存的大小,要和mmap函数中的length参数的值一样。
使用内存映射实现进程间通信:
1.有关系的进程(父子进程)
    - 还没有子进程的时候
    - 通过唯一的父进程,先创建内存映射区
    - 有了内存映射区以后,创建子进程
    - 父子进程共享创建的内存映射区
2.没有关系的进程间通
    - 准备一个大小不是0的磁盘文件
    - 进程1 通过磁盘文件创建内存映射区
    - 得到一个操作这块内存的指针
    - 进程2 通过磁盘文件创建内存映射区
    - 得到一个操作这块内存的指针
    - 使用内存映射区通信
注意:内存映射区通信,是非阻塞。
https://www.cnblogs.com/alantu2018/p/8506381.html

使用内存映射实现没有关系的进程间的通信。
int main() {
    int fd = open("test.txt", O_RDWR|O_CREAT, 0777);
    if(fd == -1) {
        perror("open");
        exit(0);
    }
    
    int ret = truncate("test.txt", 40);
    if(ret == -1) {
        perror("truncate");
        exit(0);
    }

    off_t size = lseek(fd, 0, SEEK_END); 

    void *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if(ptr == MAP_FAILED) {
        perror("mmap");
        exit(0);
    }

    pid_t pid = fork();
    if(pid > 0) {
        wait(NULL);
        char buf[64];
        strcpy(buf, (char *)ptr);
        printf("read data : %s\n", buf);
    }else if(pid == 0){
        strcpy((char *)ptr, "nihao a, son!!!");
    }else{
        perror("fork");
        exit(0);
    }
    
    munmap(ptr, size);

    return 0;
}

匿名映射

匿名映射:不需要文件实体进程一个内存映射
fd -1
offset 0
MAP_SHARED | MAP_ANONYMOUS
int main() {
    int len = 409600000;
    
    void * ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    if(ptr == MAP_FAILED) {
        perror("mmap");
        exit(0);
    }
    
    pid_t pid = fork();
    if(pid > 0) {
        strcpy((char *) ptr, "hello, world");
        wait(NULL);
    }else if(pid == 0) {
        printf("%s\n", (char *)ptr);
    }

    int ret = munmap(ptr, len);
    if(ret == -1) {
        perror("munmap");
        exit(0);
    }

    return 0;
}

判断文件是否存在

int ret = access("test", F_OK);
存在0 
不存在-1

信号

也称之为软件中断,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式
查看系统定义的信号列表:kill –l
查看信号的详细信息:man 7 signal

2SIGINT当用户按下了<Ctrl+C>组合键时,用户终端向正 在运行中的由该终端启动的程序发出此信号终止进程
3SIGQUIT用户按下<Ctrl+>组合键时产生该信号,用户终 端向正在运行中的由该终端启动的程序发出些信号终止进程
6SIGABRT调用abort函数时产生该信号终止进程并产生core文件
9SIGKILL无条件终止进程。该信号不能被忽略,处理和阻塞终止进程,可以杀死任何进程
11SIGSEGV指示进程进行了无效内存访问(段错误)终止进程并产生core文件
13SIGPIPEBroken pipe向一个没有读端的管道写数据终止进程
17SIGCHLD子进程结束时,父进程会收到这个信号忽略这个信号
18SIGCONT如果进程已停止,则使其继续运行继续/忽略
19SIGSTOP停止进程的执行。信号不能被忽略,处理和阻塞为终止进程

信号的 5 中默认处理动作

  • Term 终止进程
  • Ign 当前进程忽略掉这个信号
  • Core 终止进程,并生成一个Core文件
  • Stop 暂停当前进程
  • Cont 继续执行当前被暂停的进程
    信号的几种状态:产生、未决、递达
    SIGKILLSIGSTOP 信号不能被捕捉、阻塞或者忽略,只能执行默认动作。不然非法程序你拿它没办法
信号相关的函数
unsigned int alarm(unsigned int seconds); 
- 功能:设置闹钟,自然计时法,当倒计时为0的时候,收到SIGALARM(默认终止当前的进程),一个进程只有一个闹钟,该函数是不阻塞的
- 参数:
    seconds: 秒, 为0,表示定时器无效,取消定时器alarm(0)
- 返回值:
    - 之前没有定时器,返回0
    - 之前有定时器,返回之前的定时器剩余的时间
int setitimer(int which, const struct itimerval *new_val, struct itimerval *old_value);
- 功能:精度微妙us,可以实现周期性定时
- 参数:
    - which : 定时器以什么时间计时
        - ITIMER_REAL: 真实时间,时间到达,发送 SIGALRM 常用
        - ITIMER_VIRTUAL: 发送 SIGVTALRM,执行时间(用户态)
        - ITIMER_PROF: 发送 SIGPROF(用户态+内核态)
    - new_value: 设置定时器的属性

struct itimerval { // 定时器的结构体
    struct timeval it_interval; // 闹钟之间的间隔时间
    struct timeval it_value;    // 延迟多长时间执行第一个闹钟
};

struct timeval { // 时间的结构体
    time_t tv_sec; // 秒数
    suseconds_t tv_usec; // 微秒
};

- old_value :记录上一次的定时的时间参数,一般不使用,传NULL
- 返回值:
    - 成功 0
    - 失败 -1 并设置错误号
信号捕捉函数
sighandler_t signal(int signum, sighandler_t handler); ANSI C标准基本不用
typedef void (*sighandler_t)(int); 
- 定义指向特定类型函数的指针别名(类型)
- 功能:注册信号捕捉函数
- 参数:
    - signum: 要捕捉的信号
    - handler: 捕捉到信号要如何处理, 函数指针
        - SIG_IGN : 忽略信号
        - SIG_DFL : 使用信号默认的行为
        - 回调函数 : 由内核调用,程序员只负责写,捕捉到信号后如何去处理信号。
回调函数:
- 回调函数就是一个被作为参数传递的函数。在C语言中,回调函数只能使用函数指针实现,
- 在C++、Python、ECMAScript等更现代的编程语言中还可以使用仿函数或匿名函数。
- 函数指针是实现回调的手段
- 返回值:
    - 成功,返回上一次注册的信号处理函数的地址。第一次调用返回NULL
    - 失败,返回SIG_ERR,设置错误号
SIGKILL SIGSTOP不能被捕捉,不能被忽略。

*/
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
- 功能:检查或者改变信号的处理。信号捕捉
- 参数:
    - signum : 需要捕捉的信号的编号或者宏值(信号的名称)
    - act :捕捉到信号之后的处理动作
    - oldact : 上一次对信号捕捉相关的设置,一般不使用,传递NULL
- 返回值:
    - 成功 0
    - 失败 -1
struct sigaction {
    //处理函数
    void (*sa_handler)(int); 
    //不常用,不用初始化
    void (*sa_sigaction)(int, siginfo_t *, void *);     
    //临时阻塞信号集,在信号捕捉函数执行过程中,使用这个。执行完之后使用内核的
    sigset_t sa_mask;    
    //使用哪个处理函数,0使用sa_handler,SA_SIGINFO使用sa_sigaction
    int sa_flags;    
    // 被废弃掉了,不用初始化 
    void (*sa_restorer)(void); 
}
信号集
  • 未决信号集
  • 阻塞信号集
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); 判断某个是否在里边

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); 设置阻塞信号集
- 功能:将自定义信号集中的数据设置到内核中(设置阻塞,解除阻塞,替换)
- 参数:
    - how : 如何对内核阻塞信号集进行处理
        - SIG_BLOCK: 将用户设置的阻塞信号集添加到内核中,内核中原来的数据不变
        - SIG_UNBLOCK: 根据用户设置的数据,对内核中的数据进行解除阻塞
        - SIG_SETMASK:覆盖内核中原来的值
    - set :已经初始化好的用户自定义的信号集
    - oldset : 保存设置之前的内核中的阻塞信号集的状态,可以是 NULL
- 返回值:
    - 成功:0
    - 失败:-1
    
int sigpending(sigset_t *set);
- 功能:获取内核中的未决信号集
- 参数:set,传出参数,保存的是内核中的未决信号集中的信息。
- 返回值
    - 成功:0
    - 失败:-1
什么时候处理未决信号

image.png 内核处理完异常准备回用户模式之前先处理当前进程中可以递送的信号

在这里初学者常常有一个误区,他们会觉得如果我在程序中没有设置中断、不出现异常、不使用系统调用,那就就不会进入内核态,也就不会处理信号了!这是对操作系统错误的理解,由于linux是分时操作系统,所以当时间片用完时,就会进入内核态,进行进程调度,在此就会处理信号了!

信号处理函数本身可以被中断吗? 可以被中断。不过posix标准保证当前处理信号会被阻塞。可设置信号掩码,让信号处理程序运行期间同时阻塞其他信号。

SIGCHLD信号

SIGCHLD信号产生的条件

  1. 子进程终止时
  2. 子进程接收到SIGSTOP信号停止时
  3. 子进程处在停止态,接受到SIGCONT后唤醒时 以上三种条件都会给父进程发送SIGCHLD信号,父进程默认会忽略该信号
使用SIGCHLD信号解决僵尸进程的问题
void myFun(int num) {
    printf("捕捉到的信号 :%d\n", num);
    while(1) {
        int ret = waitpid(-1, NULL, WNOHANG);
        if(ret > 0) {
            printf("child die , pid = %d\n", ret);
        } else {
            break;
        }
    }
}
int main() {
    // 提前阻塞SIGCHLD,防止子进程结束时,父进程还没有注册
    sigset_t set;
    sigemptyset(&set);
    sigaddset(&set, SIGCHLD);
    sigprocmask(SIG_BLOCK, &set, NULL);

    // 创建一些子进程
    pid_t pid;
    for(int i = 0; i < 20; i++) {
        pid = fork();
        if(pid == 0) break;
       
    }
    
    if(pid > 0) {
        // 父进程注册
        struct sigaction act;
        act.sa_flags = 0;
        act.sa_handler = myFun;
        sigemptyset(&act.sa_mask);
        sigaction(SIGCHLD, &act, NULL);
        
        // 注册完信号捕捉以后,解除阻塞
        sigprocmask(SIG_UNBLOCK, &set, NULL);
        
        //sleep模拟父进程做自己的事
        while(1) {
            printf("parent process pid : %d\n", getpid());
            //信号到达时处于sleep的会被唤醒执行下一次循环
            sleep(5);
        }
    }else if(pid == 0) {
        printf("child process pid : %d die\n", getpid());
    }
    return 0;
}

共享内存

速度最快

int shmget(key_t key, size_t size, int shmflg); 根据key创建一个共享内存,返回一个id, id从0开始递增, 不可重用
void *shmat(int shmid, const void *shmaddr, int shmflg); 链接共享内存到进程虚拟地址空间
int shmdt(const void *shmaddr); 解除链接
int shmctl(int shmid, int cmd, struct shmid_ds *buf); 控制共享内存,常用来删除
key_t ftok(const char *pathname, int proj_id); 可用来生成key

int shmget(key_t key, size_t size, int shmflg);
- 功能:创建一个新的共享内存段,或者获取一个既有的共享内存段的标识。新创建的内存段中的数据都会被初始化为0
- 参数:
    - key : key_t类型是一个整形,通过这个找到或者创建一个共享内存。一般使用16进制表示,非0值, 自己设置或利用ftok返回的值
    - size: 共享内存的大小, 会按照大于size的最小页来创建
    - shmflg: 属性
        - 访问权限
        - 附加属性:创建/判断共享内存是不是存在
            - 创建:IPC_CREAT 如果已经存在则打开,不存在则创建
            - 判断共享内存是否存在: IPC_EXCL , 需要和IPC_CREAT一起使用
            - 设置IPC_CREAT | IPC_EXCL | 0664 如果原本存在,报错返回-1
- 返回值:
    - 失败:-1 并设置错误号
    - 成功:>0 返回共享内存的引用的ID,后面操作共享内存都是通过这个值。
void *shmat(int shmid, const void *shmaddr, int shmflg);
- 功能:和当前的进程进行关联
- 参数:
    - shmid : 共享内存的标识(ID),由shmget返回值获取
    - shmaddr: 申请的共享内存的起始地址,指定NULL,内核指定
    - shmflg : 对共享内存的操作
        - 读 : SHM_RDONLY, 必须要有读权限
        - 读写: 0
- 返回值:
    - 成功:返回共享内存的首(起始)地址
    - 失败(void *) -1

int shmdt(const void *shmaddr);
- 功能:解除当前进程和共享内存的关联
- 参数:
    - shmaddr:共享内存的首地址
- 返回值:
    - 成功 0
    - 失败 -1
    
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
- 功能:对共享内存进行操作。
- 参数:
    - shmid: 共享内存的ID
    - cmd : 要做的操作
        - IPC_STAT : 获取共享内存的当前的状态
        - IPC_SET : 设置共享内存的状态
        - IPC_RMID: 标记共享内存销毁 删除共享内存,共享内存要删除才会消失,创建共享内存的进程被销毁了对共享内存是没有任何影响。
    - buf:需要设置或者获取的共享内存的属性信息
        - IPC_STAT : buf存储数据
        - IPC_SET : buf中需要初始化数据,设置到内核中
        - IPC_RMID : 没有用,NULL

key_t ftok(const char *pathname, int proj_id);
- 功能:根据指定的路径名,和int值,生成一个共享内存的key
- 参数:
    - pathname:指定一个存在的路径
    - proj_id: int类型的值,但是这系统调用只会使用其中的1个字节
        - 范围 : 0-255 一般指定一个字符 'a'
*/
共享内存删除问题

ipcs -m 查看共享内存
ipcrm -M shmkey 根据key标记删除 可以用16进制比如 0x64 ipcrm -m shmid 根据id标记删除

image.png

  1. 标记删除后key置为0,用同样的key shmget()得到的id不再是原来的
  2. id不会重用,从0一直++
  3. 可以通过id shmat()
  4. 进程退出后自动dt
  5. nattch0时,标记删除的真正删除
  6. 多次标记删除效果一样