牛客课程笔记
gcc的使用
静态库制作
- 将c文件编译(.obj)、汇编(.o)为二进制文件
gcc -c file.c
- 使用ar工具,将二进制文件(.o)打包为一个库(命名方式:libxxxx.a)
ar rcs libxxx.a xxx.o
- 此时生成了一个只含有函数定义的文件,但是他们的入口并没有被包括(.h头文件)
- 对于一个引用了此库的文件,首先要包含头文件(-I include的头文件所在位置) 其次要包含库文件(-l 库文件名称 -L 库文件所在目录)
- 这样就形成了一条编译语句:
gcc main.c -o app -I ../include/ -l calc -L ../lib/
动态库制作
需要获得和位置无关的代码:
gcc -c -fpic/-fPIC a.c b.c
然后gcc获得动态库:
gcc -shared a.o b.o -o libxxxx.so
编译语句和静态库相同,但是动态库并不会被打包到可执行文件中。
- 程序启动之后,动态库会被动态加载到内存中,可以通过 ldd (list dynamic dependencies)命令检查动态库依赖关系
运行时动态加载需要知道绝对路径。此时就需要系统的动态载入器来获取该绝对路径。
对于 elf 格式的可执行程序,是由 ld-linux.so 来完成的,它先后搜索
elf 文件的 DT_RPATH段
——> 环境变量 LD_LIBRARY_PATH
——> /etc/ld.so.cache 文件列表
——> /lib/,/usr/lib 目录找到库文件后将其载入内存
由于自定义动态库在这些路径都没有,因此会报错
解决方法:
- 配置 LD_LIBRARY_PATH :在.bashrc 中export LD_LIBRARY_PATH = ${LD_LIBRARY_PATH}:动态库所处的绝对路径
- 配置 /etc/ld.so.cache :需要通过/etc/ld.so.conf配置,然后运行ldconfig,就可以连接到了
- 将动态库添加到 /lib/,/usr/lib (不推荐)
Makefile
打包的shell指令
Makefile文件:
目标xxxx :依赖xxxx
命令
。。。(必须有缩进)
Makefile上一条指令如果缺少依赖,会向下运行。如果下方命令和第一条命令无关,不会被执行
会自动检测文件更新,未更新的文件不需要新编译
变量
var=hello
#获取变量的值 $(变量名)
$(var)
预定义变量:
- AR : 归档维护程序的名称,默认值为 ar
- CC : C 编译器的名称,默认值为 cc
- CXX : C++ 编译器的名称,默认值为 g++
- $@ : 目标的完整名称
- $< : 第一个依赖文件的名称
- $^ : 所有的依赖文件
例:
#自动变量只能在规则的命令中使用
app:main.c a.c b.c
$(CC) -c $^ -o $@
可使用通配符进行模式匹配:
- %.o:%.c
%: 通配符,匹配一个字符串
两个%匹配的是同一个字符串 - %.o:%.c
gcc -c $< -o $@
函数
$(wildcard PATTERN…)
- 功能:获取指定目录下指定类型的文件列表
- 参数:PATTERN 指的是某个或多个目录下的对应的某种类型的文件,如果有多个目录,一般使用
空格间隔 - 返回:得到的若干个文件的文件列表,文件名之间使用空格间隔
$(patsubst ,)
- 功能:查找中的单词(单词以“空格”、“Tab”或“回车”“换行”分隔)是否符合模式,如果匹配的话,则以
替换。 - 可以包括通配符 % ,表示任意长度的字串。如果中也包含 % ,那么,中的这个 % 将是中的那个%所
代表的字串。(可以用 \ 来转义,以 % 来表示真实含义的 % 字符) - 返回:函数返回被替换过后的字符串
clean命令
make clean
会运行
clean:
rm -f $(OBJS) app
GDB
文件IO
open(path, mode)
read()
write()
文件带有一个结构体:stat,保存文件的基本信息
ret = lseek(fd, 0, SEEK_END); 可以获取文件长度
- Linux 系统函数:数据从用户空间缓冲区复制到内核页缓存,内核异步将数据刷新到磁盘。
- 标准 C 库函数:数据先写入标准 C 库的用户空间缓冲区,在缓冲区满或调用 fflush() 时,数据从用户空间缓冲区复制到内核页缓存,再由内核异步刷新到磁盘。
进程
linux命令
-
ps aux:显示所有进程的详细信息,a=all x:显示没有控制终端的进程
-
ps ajx: 显示与作业控制有关的进程
-
实时显示动态:top 在显示结果界面可以输入命令更改排序或杀死某个进程
函数
父进程号(PPID)
getid:
pid_t getpid(void);
pid_t getppid(void);
pid_t getpgid(pid_t pid);
fork
子进程返回的pid是0,父进程就是子进程的id,子进程有父进程的所有数据的副本(除了某些共享段)
当两者打开同一个文件,二者输出会相互混合
二者执行先后顺序随机
pid_t fork(void);
GDB调试:使用show|set follow-fork-mode (parent|child)选择调试的程序,运行还是父子进程同时运行,如果调试子进程,父进程将不受断点控制
set detach-on-fork [on | off] 调试时其他进程是否脱离调试继续运行(on),挂起就是off
查看调试的进程 info inferiors
切换 : inferior num
exec族
#include <unistd.h>
extern char **environ;
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 execvpe(const char *file, char *const argv[],char *const envp[]);
新程序会继承原进程的内核区数据(进程ID,pwd,控制终端)
替换用户区数据(堆、栈、bss、数据区、代码区)
excl族:单独传参数,然后传入(char * )0
excv族:传入参数数组(vector),数组同样用null结尾
exc*p族:传入一个路径或一个文件名,如果是路径名,直接读取路径,如果是文件名,那么会从环境变量寻找该文件名(可以是一个shell脚本,此时filename作为shell命令输入)
exc**e:带e说明最后可以传入一个指向环境字符串指针数组的指针
exit和_exit
区别在于是否刷新io
孤儿进程与僵死进程
孤儿:
提前结束的父进程的子进程会被init进程收养,然后等待善后
僵死:
提前结束的子进程资源被父进程管理,但父进程还没结束,不调用wait()或waitpid(),无法释放子进程,发生僵死,进程号被一直占用(进程号是有限的),进程状态表现为Z
wait和waitpid
当子进程结束时,将返回一个SIGHOLD信号。
调用wait会将父进程阻塞,等待子进程结束,发送信号给父进程,wait将立即返回结束进程的编号,传入的 *statloc 将被写入子进程的返回状态
wait调用成功返回进程编号,失败会返回-1或0(根据系统而定,一般是-1)
waitpid(等待某个特定进程结束再继续)
管道
- 管道如果读端停止,写端立即停止(防止管道破裂)
- 写端停止,读端会阻塞,等待写端继续写入
分为有名管道和匿名管道
匿名管道只能在父进程与子进程或子进程之间通信,返回值成功为0,失败为-1
- int pipe(int fd[2]) fd[0]是读,fd[1]是写
- read(fd[0], *char[], len)
- write(fd[1], *char[], len)
- 可设置两个文件描述符的模式:是否阻塞(fcntl)
有名管道通过创建管道文件读写数据
makefifo(*path, mode)
- 只打开一个只读或只写管道会阻塞,等待另一个只写或只读管道打开
- 管道有数据,read返回
- 管道无数据
– 写端被关闭,read返回0
– 写端没关闭,读端阻塞 - 读端关闭,写端会被终止(SIGPIPE)
- 管道没关闭:
– 管道满,写端阻塞
– 管道不满,写端write写入数据,返回实际写入的字节数
内存映射
将磁盘内数据映射到内存地址空间中,程序即可直接操作内存,操作的数据会同步到磁盘中
有名映射
void* ptr = mmap(addr, len, prot,flags, fd, off_t offset);
参数prot:映射区域的保护方式。可以为以下几种方式的组合:
PROT_EXEC 映射区域可被执行
PROT_READ 映射区域可被读取
PROT_WRITE 映射区域可被写入
PROT_NONE 映射区域不能存取
参数flags:影响映射区域的各种特性。在调用mmap()时必须要指定MAP_SHARED 或MAP_PRIVATE。
MAP_FIXED
如果参数start所指的地址无法成功建立映射时,则放弃映射,不对地址做修正。通常不鼓励用此旗标。
MAP_SHARED
对映射区域的写入数据会复制回文件内,而且允许其他映射该文件的进程共享。
MAP_PRIVATE
对映射区域的写入操作会产生一个映射文件的复制,即私人的“写入时复制”(copy onwrite)对此区域作的任何修改都不会写回原来的文件内容。
MAP_ANONYMOUS
建立匿名映射。此时会忽略参数fd,不涉及文件,而且映射区域无法和其他进程共享。
MAP_DENYWRITE
只允许对映射区域的写入操作,其他对文件直接写入的操作将会被拒绝。
MAP_LOCKED 将
映射区域锁定住,这表示该区域不会被置换(swap)。
- 打开文件的长度len必须大于零
- 偏移量必须是4k的整数倍
- prot必须有读权限才能写
PROT_READ|PROT_WRITE
- .open一个新文件来创建映射区的话,必须通过lseek或truncate对新文件大小进行扩展,大小为0一定不可用
- truncate(file, size);
释放:munmap(ptr, len)
信号
信号捕捉函数
-
void (*signal(int signo, void (8func)(int))(int handler)
- signum 要捕捉的信号
- handler:SIG_IGN 忽略 、SIG_DEL默认行为 、回调函数
-
int sigaction(int signo, cont struct sigaction *restrict act, oact)
- 未决信号集:信号产生,但是没有被处理
- 阻塞信号集:信号被当前进程捕捉,但是被阻塞,没有被处理(处理指系统默认动作或进程捕获该信号),此时被阻塞的信号将保持未决状态,(多个阻塞信号可能产生信号排队),当它解除阻塞,或该信号动作设置为忽略,此时解除未决状态
- 这样进程在信号递送给他之前仍可以改变对该信号的动作
对于多个相同的信号被解除
阻塞的情况,未决信号集只会记录该信号的一次出现
信号集相关函数
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);
int sigpending(sigset_t *set);
设置阻塞信号集需要先清空原来预定义的信号集
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGCHLD);
sigprocmask(SIG_BLOCK, &set, NULL);
|信号捕捉要注意是否在捕捉之前就已经有信号产生了(因为信号捕捉的定义需要时间)
|因此需要先将要捕捉的信号阻塞,定义完捕捉后再将信号非阻塞
// 捕捉子进程死亡时发送的SIGCHLD信号
struct sigaction act;
act.sa_flags = 0;
act.sa_handler = myFun;
sigemptyset(&act.sa_mask);
sigaction(SIGCHLD, &act, NULL);
// 注册完信号捕捉以后,解除阻塞
sigprocmask(SIG_UNBLOCK, &set, NULL);
共享内存
shgmdt
守护进程的创建
在 Linux 系统中,守护进程(Daemon)是一种在后台持续运行的进程,它通常在系统启动时启动,在系统关闭时终止,并且不与任何控制终端关联。下面详细介绍创建守护进程的步骤和示例代码。
创建守护进程的步骤
-
创建子进程,终止父进程
通过fork()
系统调用创建一个子进程,然后让父进程退出。这样做的目的是让子进程在后台继续运行,并且子进程会被init
进程(进程 ID 为 1)收养。 -
创建新会话
使用setsid()
系统调用创建一个新的会话。调用该函数的进程将成为新会话的会话首进程、新进程组的组长进程,并且没有控制终端。 -
改变工作目录
使用chdir()
系统调用将工作目录更改为根目录/
。这样可以避免守护进程的工作目录被卸载,从而导致文件操作失败。 -
重设文件权限掩码
使用umask()
系统调用重设文件权限掩码。这样可以确保守护进程创建的文件具有合适的权限。 -
关闭文件描述符
关闭所有从父进程继承的文件描述符,通常包括标准输入(文件描述符为 0)、标准输出(文件描述符为 1)和标准错误输出(文件描述符为 2)。可以将它们重定向到/dev/null
,以避免守护进程产生不必要的输出。 -
执行守护进程的核心任务
在完成上述步骤后,守护进程可以开始执行它的核心任务,例如定期检查系统状态、处理网络请求等。
示例代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
void daemonize() {
pid_t pid;
// 第一步:创建子进程,终止父进程
pid = fork();
if (pid < 0) {
perror("fork");
exit(EXIT_FAILURE);
}
if (pid > 0) {
exit(EXIT_SUCCESS);
}
// 第二步:创建新会话
if (setsid() < 0) {
perror("setsid");
exit(EXIT_FAILURE);
}
// 第三步:改变工作目录
if (chdir("/") < 0) {
perror("chdir");
exit(EXIT_FAILURE);
}
// 第四步:重设文件权限掩码
umask(0);
// 第五步:关闭文件描述符
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
// 可选:将标准输入、标准输出和标准错误输出重定向到 /dev/null
open("/dev/null", O_RDONLY);
open("/dev/null", O_WRONLY);
open("/dev/null", O_WRONLY);
}
int main() {
// 调用守护进程初始化函数
daemonize();
// 第六步:执行守护进程的核心任务
while (1) {
// 这里可以添加守护进程的核心任务代码
sleep(1);
}
return 0;
}
代码解释
daemonize()
函数:该函数实现了守护进程的创建步骤,包括创建子进程、创建新会话、改变工作目录、重设文件权限掩码和关闭文件描述符。main()
函数:调用daemonize()
函数初始化守护进程,然后进入一个无限循环,模拟守护进程的核心任务。在实际应用中,你可以在循环中添加具体的任务代码。
编译和运行
将上述代码保存为 daemon.c
,然后使用以下命令编译和运行:
gcc -o daemon daemon.c
./daemon
运行后,守护进程将在后台持续运行。你可以使用 ps -ef
命令查看守护进程的状态。
注意事项
- 守护进程的核心任务应该具有错误处理和日志记录功能,以便在出现问题时能够及时发现和解决。
- 守护进程的资源管理非常重要,需要避免内存泄漏和文件描述符泄漏等问题。
关闭标准输入输出的原因
守护进程的特点决定其对标准输入输出需求不大
守护进程是在后台持续运行的进程,一般在系统启动时自动启动,在系统关闭时终止。它独立于控制终端,不需要用户的交互操作,其主要目的是提供系统服务,如网络服务、定时任务处理等。因此,从功能上来说,它没有像普通交互式程序那样对标准输入输出的强烈依赖。
关闭标准输入输出可以:
- 避免与控制终端关联
守护进程的设计理念是在后台默默运行,不与任何控制终端绑定。如果不关闭标准输入输出,它可能会受到控制终端状态的影响,例如当控制终端关闭或出现异常时,可能会导致守护进程异常退出。关闭标准输入输出可以确保守护进程的独立性和稳定性。 - 防止产生不必要的输出
守护进程的运行通常是自动化的,不需要向用户实时反馈信息。如果不关闭标准输出和标准错误输出,守护进程产生的输出可能会在控制终端上显示,干扰用户操作或者造成信息混乱。而且,过多的输出还可能占用系统资源。 - 避免资源浪费
打开的文件描述符会占用系统资源,如果守护进程不需要使用标准输入输出,关闭它们可以释放这些资源,提高系统的资源利用率。
常用函数:
-
access(path, mode):检查一个文件是否满足mode的条件,不满足返回-1
-
memset(*ptr[], 要填充的内容, size);
-
fgets(
-
ptr[], size, stdin);
str – 这是指向一个字符数组的指针,该数组存储了要读取的字符串。
n – 这是要读取的最大字符数(包括最后的空字符)。通常是使用以str 传递的数组长度。
stream – 这是指向 FILE 对象的指针,该 FILE 对象标识了要从中读取字符的流。 -
int kill(pid_t pid, int sig) 给pid发送信号
-
int raise(int sig) 给当前进程发送信号,成功发送0
-
void abort(void) 给当前进程发送SIGABRT,杀死当前进程
-
unsigned int alarm(unsigned int seconds):
- 计时器是系统计时,无关进程的当前状态
- 开始倒计时,倒计时为0发送SIGALARM到当前进程。SIGALARM默认终止当前进程
- 计时器一个程序只有一个,如果当前已经有一个计时器,新计时器会返回当前计时器的剩余时间,然后将计时器设置为新传入的数值,如果之前没有计时器,返回0
-
int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue) 循环定时器,非阻塞
struct itimerval
{
struct timeval it_interval; 计时器要循环的时间间隔
struct timeval it_value; 计时器延迟时间
};
struct timeval
{
long tv_sec; 秒
long tv_usec; 毫秒
};