项目学习地址:【牛客网C++服务器项目学习】
1.共享内存
进程可以将同一段共享内存连接到它们自己的地址空间,所有进程都可以访问共享内存中的地址,如果
某个进程向共享内存内写入数据,所做的改动将立即影响到可以访问该共享内存的其他所有进程。
相关接口
创建共享内存:int shmget(key_t key, int size, int flag);
成功时返回一个和key相关的共享内存标识符,失败范湖范围-1。
key:为共享内存段命名,多个共享同一片内存的进程使用同一个key。
size:共享内存容量。
flag:权限标志位,和open的mode参数一样。
连接到共享内存地址空间:void *shmat(int shmid, void *addr, int flag);
返回值即共享内存实际地址。
shmid:shmget()返回的标识。
addr:决定以什么方式连接地址。
flag:访问模式。
从共享内存分离:int shmdt(const void *shmaddr);
调用成功返回0,失败返回-1。进程退出后会自动执行。
释放共享内存:int shmctl(int shmid, int cmd, struct shmid_ds *buf);
该函数实际上是对shmid指定的共享内存,通过参数cmd进行相应的控制。要释放内存,我们需要使用命令IPC_RMID。
值得注意的是,一块共享内存可以被多个进程绑定,只有一块共享内存没有被进程其余进程绑定的时候,才会被真正的释放,否则只是引用计数减一
其他补充
2.守护进程
进程、进程组、会话、控制终端
传统上,Unix操作系统下运行的应用程序、 服务器以及其他程序都被称为进程,而Linux也继承了来自unix进程的概念。必须要理解下,程序是指的存储在存储设备上(如磁盘)包含了可执行机器指 令(二进制代码)和数据的静态实体;而进程可以认为是已经被OS从磁盘加载到内存上的、动态的、可运行的指令与数据的集合,是在运行的动态实体。这里指的 指令和数据的集合可以理解为Linux上ELF文件格式中的.text .data数据段。
进程组就是一个或多个进程的集合。这些进程并不是孤立的,他们彼此之间或者存在父子、兄弟关系,或者在功能上有相近的联系。每个进程都有父进程,而所有的进程以init进程为根,形成一个树状结构。
同一进程组中的各进程接收来自同一终端的各种信号,每个进程组有一个唯一的进程组ID。每个进程组有一个组长进程,该组长进程的ID = 进程组ID。从进程组创建开始到最后一个进程离开为止的时间称为进程组的生命周期。
Linux是多用户多任务的分时系统,所以必须要支持多个用户同时使用一个操作系统。当一个用户登录一次系统就形成一次会话 。会话是一个或者多个进程组的集合。进程组通常是由shell管道编制在一起的。一个会话可包含多个进程组,但只能有一个前台进程组。每个会话都有一个会话首领(leader),即创建会话的进程。 sys_setsid()调用能创建一个会话。必须注意的是,只有当前进程不是进程组的组长时,才能创建一个新的会话。调用setsid 之后,该进程成为新会话的leader。
一个会话可以有一个控制终端。这通常是登陆到其上的终端设备(在终端登陆情况下)或伪终端设备(在网络登陆情况下)。建立与控制终端连接的会话首进程被称为控制进程。一个会话中的几个进程组可被分为一个前台进程组以及一个或多个后台进程组。所以一个会话中,应该包括控制进程(会话首进程),一个前台进程组和任意后台进程组。
一次登录形成一个会话
一个会话可包含多个进程组,但只能有一个前台进程组
会话的领头进程打开一个终端之后, 该终端就成为该会话的控制终端 (SVR4/Linux)
与控制终端建立连接的会话领头进程称为控制进程 (session leader)
一个会话只能有一个控制终端
产生在控制终端上的输入和信号将发送给会话的前台进程组中的所有进程
终端上的连接断开时 (比如网络断开或 Modem 断开), 挂起信号将发送到控制进程(session leader)
进程属于一个进程组,进程组属于一个会话,会话可能有也可能没有控制终端
进程–》进程组–》会话–》控制终端
守护进程
守护进程(Daemon Process) , 也就是通常说的Daemon进程(精灵进程), 是Linux中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。-般采用以d结尾的名字。
守护进程具备下列特诊:
Linux的大多数服务器就是用守护进程实现的。比如,Internet 服务器inetd,Web服务器httpd 等。
守护进程的创建步骤
(1)让程序在后台执行。方法是调用fork()产生一个子进程,然后使父进程退出。解决会话ID冲突
(2)调用setsid()创建一个新对话期。控制终端、登录会话和进程组通常是从父进程继承下来的,守护进程要摆脱它们,不受它们的影响,方法是调用setsid()使进程成为一个会话组长。setsid()调用成功后,进程成为新的会话组长和进程组长,并与原来的登录会话、进程组和控制终端脱离。
(3)禁止进程重新打开控制终端。经过以上步骤,进程已经成为一个无终端的会话组长,但是它可以重新申请打开一个终端。为了避免这种情况发生,可以通过使进程不再是会话组长来实现。再一次通过fork()创建新的子进程,使调用fork的进程退出。
(4)关闭不再需要的文件描述符。子进程从父进程继承打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。首先获得最高文件描述符值,然后用一个循环程序,关闭0到最高文件描述符值的所有文件描述符。
(5)将当前目录更改为根目录。
(6)子进程从父进程继承的文件创建屏蔽字可能会拒绝某些许可权。为防止这一点,使用unmask(0)将屏蔽字清零。
(7)处理SIGCHLD信号。对于服务器进程,在请求到来时往往生成子进程处理请求。如果子进程等待父进程捕获状态,则子进程将成为僵尸进程(zombie),从而占用系统资源。如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。在Linux下可以简单地将SIGCHLD信号的操作设为SIG_IGN。这样,子进程结束时不会产生僵尸进程。
这两节看得我着实懵逼,一个是信号、一个是控制终端和会话这一堆的概念和逻辑关系,主要原因在于以前从未接触过这方面的内容,且没有可以参考的相关知识。对于这类独立、新的知识,刚开始的学习都是痛苦的,只能是先记住,后面学到其他知识,串起来后能够进一步理解
3. 线程
线程的概述
进程是分配资源的最小单位、线程是操作系统调度执行的最小单位
线程是轻量级的进程,在Linux环境下线程的本质仍是进程
查看指定进程的LWP(light weight process):ps -Lf xxx(进程号)
线程存在的必要性:
第一:线程之间(一个进程里)共享同一个地址空间和所有可用数据的能力,对于某些应用来说这是必需的,多进程模型是做不到的。
第二:线程比进程轻量化,启动一个线程比一个进程快10~100倍。
第三:性能更好
进程和线程的比较:
进程 | 线程 |
---|---|
操作系统分配资源的基本单位 | CPU调度的基本单位 |
私有完整的资源平台 | 只独享指令流执行的必要资源(寄存器和栈等) |
安全性高 | 线程可减少并发执行的时间和空间开销 |
线程共享 | 线程独享 |
---|---|
地址空间 | 程序计数器 |
全局变量 | 寄存器 |
打开文件 | 堆栈 |
子进程 | 状态 |
即将发生的定时器 | 特殊:errno(全局变量) |
信号与信号处理程序 | 信号屏蔽字 |
账户信息 | 调度优先级 |
线程ID |
创建线程
函数:int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
功能:在该进程中创建一个新线程,执行参数3 start_routine 的功能
参数:
thread:
attr:结构体,包含了相关的信息。可以指定为NULL,默认模式;
start_routine:函数指针,线程需要完成的功能
arg:传递到 start_routine回调函数中的参数
这里补充一些函数指针的概念:(参考《C++ Primer 5th》)
函数指针指向的是函数,而非对象
一个函数由【返回类型】+【函数名】+【形参】组成:
int add (int a, int b);
声明一个函数指针,只需要将函数名替换为指针即可:
int ( * pf) (int a, int b);
通过观察我们可以看到,变量名【pf】前有一个*号,可以判断为指针;并且,变量名的右边紧跟着的是形参列表,表示【pf】指向的是函数的指针。
使用函数指针:
把函数名当做变量作为一个值使用时,该函数自动地转换成指针:
pf = add;
此外,我们还能直接使用函数指针调用函数,无需提前解引用指针:
int sum = pf(1,2);