标签(空格分隔): Linux
Author:atao
程序映像布局
(高)命令行参数,环境变量->栈->共享内存->堆->BSS->DATA->代码段(低)
第一章 linux系统编程概述
一、Linux系统层次结构
1、APP负责系统的功能与行为
2、OS负责与硬件交互
3、结构如下
应用程序(APP)->C库->Linux系统调用接口->Linux操作系统(OS)->硬件接口
二、基本概念
1、进程:一个正在执行的程序实例,有自己的地址空间和执行状态
特点:1)进程是一个实体
2)进程是一个“执行中的程序”
2、线程:是“进程”中某个单一顺序的控制流。
特点:1)一个进程中允许有多个线程执行
2)线程是进程中的一个实体
三、Linux出错处理
1、strerror函数
原型:char *strerror(int errnum);
int errnum;
errnum = errno;
printf("错误提示%s\n", strerror(errnum));
2.perror函数
原型:void perror(const char *str);
perror("open file failed:");
四、时间函数
1.实例
#include <stdio.h>
#include <time.h>
// clock_t clock(void);
int main(void)
{
clock_t clk;
int i;
for(i = 0; i < 100000000; i++);
clk = clock();
printf("clock = %d, secs = %f\n", clk,
clk * 1.0 /CLOCKS_PER_SEC);//将毫秒转换为秒
printf("CLOCKS_PER_SEC = %d\n", CLOCKS_PER_SEC);
return 0;
}
2.time命令测试程序运行时间
- time 可执行文件
3.strace 命令跟踪一种程序系统调用使用情况
- strace 可执行文件
第二章 系统I/O操作
一、Linux文件
1、普通文件(regular file)
2、目录:包含了其他文件的名字以及指向与这些文件在关信息的指针
3、设备文件(/dev):字符特殊文件、块特殊文件
4、FIFO:用于进程间的通信,有时也将其称为命名管道
5、套接口(socket):用于宿主机间网络通信,也可用于在一台宿主机上的进程间的通信
常用的五个基本函数
open、close、read、write和ioctl
二、文件描述符
1、Linux内核是通过文件描述符区分和引用文件的。
2、通常,一个进程启动时,会打开3个文件
STDIN_FILENO //对应的描述符为:0(标准输入)
STDOUT_FILENO //对应的描述符为:1(标准输出)
STDERR_FILENO //对应的描述符为:2(标准错误处理)
三、文件操作
1、文件处理函数
– open – 打开或创建一个文件
– creat – 建立一个空文件
– close – 关闭一个文件
– read – 从文件读入数据
– write – 向文件写入一个数据
– lseek – 在文件中移动读写位置
– unlink – 删除一个文件
– remove – 删除一个文件本身
– fcntl – 控制一个文件属性
– link –创建一个硬链接
– symlink –创建一个软链接
– mkdir – 创建一个目录
– rmdir – 删除一个目录
– chdir – 切换一个目录
– getcwd – 把当前目录的名字写到给定的缓冲区buf里
– opendir – 打开一个目录并建立一个目录流
– readdir – 指针指向的结构里保存着目录流drip中下一个目录项的有关资料
– telldir – 记录着一个目录流里的当前位置
– seekdir – 设置目录流dir的目录项指针位置
– closedir – 关闭一个目录流并释放与之关联的资源
– stat – 返回指定文件的详细情况
– fstat – 根据文件描述符获取状态
– lstat – 根据文件名来获取状态(常用)
2、模式位(flags)
– O_WRONLY //以只写方式打开文件
– O_RDONLY //以只读方式打开文件
– O_RDWR //以可读写方式打开文件
– O_CREAT //若欲打开的文件不存在则自动建立该文件
– O_EXCL //表示可执行,不能与O_CREAT同时设置,否则会出错
– O_TRUNC //覆盖已存在的文件
– O_APPEND //表示追加读写
3、权限位(mode)
Linux总共用5个八进制数字来表示文件的各种权限,0000
第一位:表示设置用户ID
第二位:表示设置组ID
第三位:表示用户自己的权限位,即所有者权限
第四位:表示组的权限
第五位:表示其他人的权限
4、返回值
1)错误代码,成功打开文件返回文件描述符,否则返回一个负数。
2)失败的原因可以用全局的errno来指明
EEXIST //表示文件已存在
EACCESS //表示文件
ENOTDIR //表示不是目录
5、lseek函数中的whence参数
– SEEK_SET 从文件头增加
– SEEK_CUR 从当前位置增加
– SEEK_END 从文件尾后增加
6、access判断当前里程是否有某个权限
如:access(argv[1], F_OK);//检测该文件是否存在
权限参数:
R_OK //有读权限
W_OK //写权限
X_OK //执行权限
F_OK //测试文件是否存在
7、用法
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
// ssize_t write(int fd, const void *buf, size_t count);
int main(void)
{
int fd = -1;
char buf[128] = "Hello world";
ssize_t count;
char ch = 0;
// 打开文件
fd = open("test", O_WRONLY);
if(-1 == fd)
{
perror("open failed");
return -1;
}
// 写文件
count = write(fd, buf, strlen(buf));
if(-1 == count)
{
perror("write failed");
goto _out;
}
else if(0 == count)
{
printf("write nothing\n");
}
// 显示写入的内容
printf("%s\n", buf);
// 制造文件空洞
lseek(fd, 10000, SEEK_SET);
write(fd, &ch, 1);
_out:
// 关闭文件
close(fd);
return 0;
}
8、fcntl文件属性控制函数
原型:int fcntl(int fd, int cmd, struct flock *lock);
cmd参数:
F_GETLK //获取文件锁
F_SETLK //设置文件锁
通常为整个文件加锁的方法是:
l_whence = SEEK_SET;
l_start = 0;
l_len = 0;
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#if 0
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd, struct flock *lock);
struct flock {
...
short l_type; /* Type of lock: F_RDLCK,
F_WRLCK, F_UNLCK */
short l_whence; /* How to interpret l_start:
SEEK_SET, SEEK_CUR, SEEK_END */
off_t l_start; /* Starting offset for lock */
off_t l_len; /* Number of bytes to lock */
pid_t l_pid; /* PID of process blocking our lock
(F_GETLK only) */
...
};
#endif
int set_lock(int fd, short type);
int main(int argc, char **argv)
{
int fd;
char *p;
/*
命令行提供 4 个参数:
1. 命令
2. 文件名
3. l/u(l - 加写锁, u - 不加写锁)
4. 要写入文件中的字符串
*/
if((argc != 4) || ((argv[2][0] != 'l') &&
(argv[2][0] != 'u')) || (argv[2][1] != '\0'))
{
printf("Usage: %s filename l/u(lock/unlock) string\n", argv[0]);
return -1;
}
fd = open(argv[1], O_WRONLY | O_APPEND);
if(-1 == fd)
{
perror("open failed");
return -1;
}
if(argv[2][0] == 'l')
{
// 加写锁
if(-1 == set_lock(fd, F_WRLCK))
{
goto _out;
}
}
// 开始写文件
p = argv[3];
while(*p)
{
write(fd, p, 1);
fprintf(stderr, "%c", *p); // stderr 标准出错, 不带缓冲
//fprintf(stdout, "%c", *p); // stdout 标准输出, 行缓冲
sleep(1);
p++;
}
if(argv[2][0] == 'l')
{
// 解锁
if(-1 == set_lock(fd, F_UNLCK))
{
goto _out;
}
}
_out:
close(fd);
return 0;
}
// 设置文件锁: F_RDLCK 读锁, F_WRLCK 写锁, F_UNLCK 解锁
// 返回 0 成功, -1 失败
int set_lock(int fd, short type)
{
// lock 必须先清 0, 否则获取锁时会产生无效参数的错误
struct flock lock = {0};
int ret = 0;
// 获取文件锁状态
if(-1 == fcntl(fd, F_GETLK, &lock))
{
perror("fcntl getlk failed");
ret = -1;
goto _out;
}
// 对整个文件设置锁
lock.l_type = type;
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0;
// 设置文件锁
while(-1 == fcntl(fd, F_SETLK, &lock));
_out:
return ret;
}
9.文件描述符变为非阻塞
fcntl( sockfd, F_SETFL, O_NONBLOCK);
// sockfd 是要改变状态的文件描述符.
// F_SETFL 表明要改变文件描述符的状态
// O_NONBLOCK 表示将文件描述符变为非阻塞的.
四、I/O模型
1.FILE结构提供了三各缓冲方式
1)全缓冲:磁盘文件通常是完全缓冲的
2)行缓冲:与终端相关的文件是行缓冲的
3)不缓冲:标准出错是不缓冲的,如stderr
2.高效处理I/O复用的函数:select函数
原型:int select(int numfds, fd_set *readfds, fd_set *writefds, \
fd_set *exeptfds, struct timeval *timeout);
参数说明:
readfds:select监视的可读文件句柄集合。
writefds: select监视的可写文件句柄集合。
exceptfds:select监视的异常文件句柄集合。
timeout:本次select()的超时结束时间。
相关的宏解释:
FD_ZERO(fd_set *fdset):清空fdset与所有文件句柄的联系。
FD_SET(int fd, fd_set *fdset):建立文件句柄fd与fdset的联系。
FD_CLR(int fd, fd_set *fdset):清除文件句柄fd与fdset的联系。
FD_ISSET(int fd, fdset *fdset):检查fdset联系的文件句柄fd是否可读写,>0表示可读写。
关于超时的设置:
struct timeval
{
long tv_sec; /* seconds */设置秒
long tv_usec; /* microseconds */设置微秒
};
实用举例:
main()
{
int sock;
FILE *fp;
struct fd_set fds;
struct timeval timeout; //用于设置超时时间
char buffer[256]={0}; //256字节的接收缓冲区
//select等待3秒,3秒轮询,要非阻塞就置0
timeout.tv_sec = 3;
timeout.tv_usec = 0;
/* 假定已经建立TCP/UDP连接 */
sock=socket(...);
bind(...);
fp=fopen(...);
while(1)
{
FD_ZERO(&fds);
//每次循环都要清空集合,否则不能检测描述符变化
FD_SET(sock,&fds); //添加通信描述符
FD_SET(fp,&fds); //添加打开文件描述符
maxfdp=sock>fp?sock+1:fp+1; //描述符最大值加1
switch(select(maxfdp,&fds,&fds,NULL,&timeout))//select使用
{
case -1:
exit(-1);
break; //select错误,退出程序
case 0:
break; //再次轮询
default:
if(FD_ISSET(sock,&fds))
//测试sock是否可读,即是否网络上有数据
{
recvfrom(sock,buffer,256,.....);//接受网络数据 if(FD_ISSET(fp,&fds)) //测试文件是否可写
{
fwrite(fp,buffer...);//写入文件
}
buffer清空;
}// end if break;
}// end switch
}//end while
}//end main
3.高效处理I/O复用的函数:poll函数
原型:int poll(struct pollfd *ufds, unsigned int nfds, int timeout);
返回值说明:
大于0,代表满足响应事件的文件描述符的个数
等于0,超时,代表在规定事件内没有事件发生
小于0,函数使用出错
参数说明:
struct pollfd
{
int fd; /* 文件描述符 */
short events; /* 等待的事件 */
short revents; /* 实际发生了的事件 */
};
nfds //给出要监视的描述符的数目
timeout //是一个以毫秒为单位的时间
/*
如果timeout为0,poll不阻塞
如果timeout 为-1,poll永远不会超时,直到有事件返回
*/
关于ufds参数的说明:
events //包含所有该文件描述符需要监视的正常事件(读/写).
revents //包含poll返回时该文件描述符已发生的事件(包含异常事件).
events和revents是通过对各种事件的标志符进行逻辑或(|)运算构建的.
事件标志定义:
POLLIN 数据可读
POLLOUT 数据可写
POLLHUP 设备已经断开
POLLRPI 高优先级数据可读
POLLERR 出现异常
POLLNVAL 文件描述符无效
五、进程编程
1.常见进程控制函数
- fork //创建一个新进程
- exec //用一个新进程去执行一个命令行
- exit //正常退出进程
- abort //非正常退出一个进程
- kill //杀死进程或向一个进程发送信号
- wait //等待子进程结束
- sleep //将当前进程休眠一段时间
- getpid //取得当前进程编号
- getppid //取得父进程编号
2.wait 和 waitpid
– wait将暂停父进程直到它有一个子进程结束.
– waitpid 可以指定等待哪一个子进程结束.
– 成功则返回结束的子进程号.
1)函数原型:
– pid_t wait (int *status)
– pid_t waitpid (pid_t pid, int *status, int options);
2)参数与返回值:
– status返回子进程退出时的状态.
– pid > 0 等待指定pid的子进程结束
– pid = -1 等待任何一个子进程结束
– options 为0,阻塞等待.
如果为WNOHANG,不阻塞,没有等到子进程结束返回0.
3)退出时的状态(status)说明:
– WIFEXITED(status)
• 如果子进程正常结束,宏非零
• WEXISTSTATUS(status)
WIFEXITED非零,宏返回子进程退出码低8位
– WIFSIGNALED(status)
• 子进程被信号终止,宏非零
• WTERMSIG(status)
WIFSIGNALED非零,宏返回终止子进程的信号编码
– WIFSOPPED(status)
• 子进程被信号停止,宏非零
• WSTOPSIG(status)
WIFSOPPED非零,宏返回停止子进程的信号编码
3.exec函数族:
相关函数:
execl\execv\execle\execve\execlp\execvp
说明:其中只有execve是真正意义上的系统调用,其它都是在此基础上经过包装的库函数
4.system函数
格式:system(“控制台命令”);
六、守护进程
1.特性:能在后台运行的进程
2.守护进程的编程要点
1)调用fork(),将让父进程终止
实现:if(pid = fork()) {exit(0);}
2)脱离控制终端,登录会话和进程组
实现:调用setsid()函数使进程成为会话组长
3)禁止进程重新打开控制终端
实现:if(pid = fork()) {exit(0);}//结束第一子进程,创建第二子进程
4)关闭打开的文件描述符
实现:for(i = 0; 关闭打开的文件描述符; i++) {close(i);}
5)改变当前工作目录为根目录
实现:chdir("/");
6)重设文件创建掩模
实现:umask(0);
第三章 进程间通信
一、无名管道(pipe)和命名管道(fifo)
1.无名管道的读写
实现:
/*目的:父进程写,子进程读*/
int pipe_fd[2];
pipe(pipe_fd);
//fork()一个进程
/*这里是子进程
1.关闭写端close(pipe_fd[1]);
2.sleep(2);让父进程先写
3.read(pipe_fd[0], buf, sizeof(buf));子进程读
4.关闭读端close(pipe_fd[0]);
5.exit(0);
*/
/*这里是父亲进程
1.关闭读端close(pipe_fd[0]);
2.write(pipe_fd[1], buf, sizeof(buf));父进程写
3.关闭写端close(pipe_fd[1]);
4.waitpid(pid, NULL, 0);等待子进程结束
5.exit(0);
*/
2.创建有名管道
原型:int mkfifo(const char *pathname,mode_t mode);
/*创建有名管道*/
if((mkfifo(FIFO_SERVER,O_CREAT|O_EXCL|O_RDWR)<0)&&(errno!=EEXIST))
{
perror("mkfifo failed:");
exit(1);
}
/*
errno != EEXIST //说明不是文件已经存在错误
*/
/*打开管道*/
fd=open(FIFO_SERVER,O_WRONLY |O_NONBLOCK,0);
if(fd==-1)
{
perror("open failed:");
exit(1);
}
/*
1.设置了O_NONBLOCK 说明要求无法满足时不阻塞,立即出错返回(ENXIO).
2.未设置O_NONBLOCK 说明访问要求无法满足时进程阻塞,如读取FIFO为空时
*/
close(fd); //关闭管道
unlink(FIFO_SERVER); //删除文件
第四章 信号与定时器
一、常用命令
1.查看当前系统的所有信号(kill -l)
2.查看当前系统终端的可执行命令(stty -a)
二、signal信号处理机制
1.调用signal()注册一个信号捕捉函数
原型为:
#include <signal.h>
typedef void(*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
参数说明:
第一个参数(signum):表示要捕捉的信号
第二个参数(handler):是函数指针,表示要对该信号进行捕捉的函数
注意第二个参数:
1)SIG_IGN //表示忽略捕捉到的这个信号
2)SIG_DEF //采用默认处理
3)SIGKILL和SIGSTOP信号即不能忽略也不能被捕捉
三、用程序发送信号
1.向其他进程发送信号
1)kill -9 进程号 //在终端杀死指定进程
2)原型:int kill(pid_t pid, int sig);//编程杀进程
实现:kill(getppid(), SIGTERM);//杀死父亲
2.向自己发送信号
1)原型:int raise(int sig);
实现:
- raise(SIGTERM);//自杀
- raise(SIGSTOP);//自闭
小技巧:子进程自杀或自闭后,父进程通过wait可以检查子进程的状态。
2)void abort(void);
功能描述:向进程发送SIGABORT信号,默认情况下进程会异常退出
3)int pause(void);
功能描述:将调用进程挂起直到捕捉到信号为止
3.sigqueue信号发送函数
原型:int sigqueue(pid_t pid,int sig, const union sigval value);
功能描述:即可以发送信号,并且能传递附加的信息.
参数说明:
第一个参数pid为接收信号的进程;
第二个参数sig为要发送的信号;
第三个参数value为一整型与指针类型的联合体:
union sigval
{
int sival_int;
void *sival_ptr;
};
作用:由sigqueue函数发送的信号的第3个参数value的值,可以被进程的信号处理函数的第2个参数info->si_ptr接收到。
4. sigprocmask信号阻塞
原型:
int sigprocmask(int how,const sigset_t *set, sigset_t *oldset);
功能描述:函数sigprocmask是全程阻塞,在sigprocmask中设置了阻塞集合后,被阻塞的信号将不能再被信号处理函数捕捉,直到重新设置阻塞信号集合。
参数说明:
第一个参数how的值为如下3者之一:
a:SIG_BLOCK ,将参数2的信号集合添加到进程原有的阻塞信号集合中
b:SIG_UNBLOCK ,从进程原有的阻塞信号集合移除参数2中包含的信号
c:SIG_SET,重新设置进程的阻塞信号集为参数2的信号集
第二个参数set为阻塞信号集
第三个参数oldset是传出参数,存放进程原有的信号集。
四、计时器与信号
- Linux下的两个睡眠函数
原型:
unsigned int sleep(unsigned int seconds);
void usleep(unsigned long usec);
功能描述:
函数sleep让进程睡眠seconds秒,函数usleep让进程睡眠usec毫秒。
注意:
因为sleep在内部是用alarm实现的,所以在程序中最好不要sleep与alarm混用,以免造成混乱。
2.定时器alarm()函数
原型:
unsigned int alarm(uinsigned int seconds);
功能描述:
实际是一个延时器,好比sleep,两者用其一。
3.定时器setitimer()
原型:int setitimer(int which, const struct itimerval *value,
struct itimerval *ovalue));
参数说明:
第一个参数(which):指定了3种类型计时器
ITIMER_REAL(真实计时器)
ITIMER_VITUAL(虚拟计时器)
ITIMER_PROF(实用计时器)
第二个参数(value):value为一结构体的传入参数,指定该计时器的初始间隔时间和重复间隔时间。
第三个参数(ovalue):ovalue为一结构体传出参数,用于传出以前的计时器时间设置。
struct itimerval
{
struct timeval it_interval; /* next value */ //重复间隔
struct timeval it_value; /* current value */ //初始间隔
};
五、信号量
1.概述
1)信号量的使用主要是用来保护共享资源,使得资源在一个时刻只有一个进程(线程)所拥有。
2)信号量为正->说明是空闲的
3)信号量为0->说明被占用,测试的线程要进入睡眠队列中,等待被唤醒
2.无名信号量(互斥锁)
sem_t sem_id;//创建无名信号量
sem_init(&sem_id, 0, 1);//初始化无名信号量
sem_wait(&sem_id);//加锁
//读写操作
sem_post(&sem_id);//解锁
3.有名信号量(/dev/shm)
区别:
1)使用时与无名信号量一样,共享sem_wait和sem_post函数.
2)有名信号量使用sem_open代替sem_init.
3)在结束的时候要像关闭文件一样去关闭这个有名信号量.
实现步骤:
//1.新建有名信号量,设置权限和信号量的值
shm_fd = sem_open(mysem, ,O_CREAT,0755, 1);
sem_wait(shm_fd);//加锁
//读写操作
sem_post(shm_fd);//解锁
sem_close(shm_fd);//关闭打开的命名信号量
sem_unlink(mysem);//销毁有名信号量文件
注意:
编译程序时要带:-lrt或-lpthread
4.sem_getvalue()函数获取当前信号量值:
原型:int sem_getvalue(sem_t *sem,int *valp);
返回值:返回0表示当前信号量已上锁,否则返回等待该信号量解锁的进程数