LINUX应用开发


<Unix/Linux基本哲学之一就是“一切皆文件”>

LINUX命令

参考:Linux常用命令

1. 进程相关

  1. 查看进程的状态
    1)当不知道端口号,但是知道程序名称时,使用ps aux查看
ps -aux 

2)根据PID可以查询进程名称,命令如下:

ll /proc/进程号
  1. 杀掉进程
kill -9 进程号
  1. 查看所有正在运行的进程
ps -ef   

2. 查找

  1. grep
  • 1)grep命令是一种强大的文本搜索工具
    使用实例:
ps -ef | grep sshd  查找指定ssh服务进程 
ps -ef | grep sshd | grep -v grep 查找指定服务进程,排除gerp身 
ps -ef | grep sshd -c 查找指定进程个数 
  • 2)grep(检索文件内容)
grep [options] pattern file

全称:Global Regular Expression Print。
作用:查找文件里符合条件的字符串。

grep "start" test*    //从test开头文件中,查找含有start的行
  • 3)Grep命令搜索多个字符串
    通常我们认为,文字字符串是最基本的模式。

接下来我们将示例,搜索某用户日志错误文件中出现的所有 fatal、error 和 critical 字符串。语法如下:

$ grep 'fatal\|error\|critical' /var/log/nginx/error.log 

还需要注意的是,如果要搜索的字符串包含空格,需要用双引号将其括起来。

下面是使用扩展正则表达式的同一个示例,它不需要转义字符:

$ grep -E 'fatal|error|critical' /var/log/nginx/error.log 
  • 4)grep同时匹配多个关键字或任意关键字
    2.1与操作
    grep pattern1 files | grep pattern2 :显示既匹配 pattern1 又匹配 pattern2 的行。
grep word1 file.txt | grep word2 |grep word3

必须同时满足三个条件(word1、word2和word3)才匹配。

  1. find
    find命令在目录结构中搜索文件,并对搜索结果执行指定的操作。

find 默认搜索当前目录及其子目录,并且不过滤任何结果(也就是返回所有文件),将它们全都显示在屏幕上。

使用实例:

find . -name "*.log" -ls  在当前目录查找以.log结尾的文件,并显示详细信息。 
find /root/ -perm 600   查找/root/目录下权限为600的文件 
find . -type f -name "*.log"  查找当目录,以.log结尾的普通文件 
find . -type d | sort   查找当前所有目录并排序 
find . -size +100M  查找当前目录大于100M的文件

学习方式

  • 看正点原子的教程
  • 朱老师视频
  • UNIX高级应用编程

0、LINUX课件

1、 朱友鹏LINUX课程课件

朱友鹏LINUX课程课件

1、 基础

网络版man手册(英文) – > Linux man pages online

1、《文件I/O》

0.减缩版:open、write、read、lseek

参考CSDN
1、open函数 : 成功返回文件描述符,失败返回 -1

 int open(const char *pathname, int flags);

功能:打开某一文件,获得文件描述符 fd

1、const char *pathname :输入型参数,打开文件的路径和名字
2、 flags: O_RDONLY, O_WRONLY, or O_RDWR.
示例:open(“pathname”,O_RDWR)
常用:(1)O_APPEND 文件末尾追加
(2)O_CREAT 文件不存在则创建
(3)O_TRUNC 将文件清零截断,一般搭配(2)使用
(4)O_EXCL 搭配(2)使用,文件不存在则创建,
如果存在再创建(存在就报错吧?);就靠这个标志进行提醒。

  int open(const char *pathname, int flags, mode_t mode);
    同上一样,只是多了个文件权限,mode

2,write写函数:成功返回写的字节数,失败返回 -1

   ssize_t write(int fd, const void *buf, size_t count);

功能 : 从某个buf里面读取多少字节的内容,写入到 fd中

1、int fd : open打开的文件所保存的文件描述符
2、 const void *buf : 输入型参数,从哪个buf里面读
3、 size_t count : 写多少个字节

3,read读函数:成功返回读取的字节数,文件结束返回0;失败返回 -1

 ssize_t read(int fd, void *buf, size_t count);

1、 int fd : 文件描述符
2、 void *buf : 输出型参数,读到哪个buf
3、size_t count :读多少个字节

功能 : 从fd中读取count个字节,到buf中

3,lseek函数 :成功返回 偏移量,失败返回 -1,可以设置errno

 off_t lseek(int fd, off_t offset, int whence);

whence :(1)SEEK_SET 文件偏移量设置为偏移字节
(2)SEEK_CUR 文件偏移量设置为其当前位置加上偏移量
字节。
(3) SEEK_END 文件偏移量设置为文件大小加上偏移量
字节。

    **功能 :用lseek计算文件长度,和构成空洞文件**

1.flag标志

  • 不常用
  1. O_APPEND
  2. O_ASYNC // 异步IO用
  3. O_DSYNC
  4. O_NOATIME
  5. O_NONBLOCK // 添加非阻塞属性
  6. O_SYNC
  7. O_TRUNC

1.open

open.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

int main(){
    int fd = -1;
    fd = open("1.txt", O_RDWR | O_CREAT);
    if(fd<0){
        perror("open");
        return -1;
    }
    return 0;
}

2.write

3.read

4.lseek

5.dup/dup2

dup 和 dup2 : 成功返回新的文件描述符,失败返回 -1 可以设置errno

int dup(int oldfd);
int dup2(int oldfd, int newfd);

区别:dup2和dup的作用是一样的,都是复制一个新的文件描述符。
但是dup2允许用户指定新的文件描述符的数字。

示例:
dup : fd2 = dup2(fd1); 复制fd1文件描述符
dup2 : fd2 = dup2(fd1, 16); 将fd1文件描述符复制为16

6.atexit()程序退出时执行函数

#include  <stdio.h>
#include <stdlib.h>


void func1(void);
void func2(void);
int main()
{
    printf("hellow world.\n");

    //
    atexit(func2);
    atexit(func1);      //程序结束的时候先执行func1再执行func2
    printf("fucking world\n");
    return 0; 
}
void func1(){
    printf("func1\n");
}
void func2(){
    printf("func2\n");
}

2、《标准I/O》

1.fopen

2.fwrite

6.fcntl

fcnt(fd control)可以修改已打开的文件描述符的属性。

 int fcntl(int fd, int cmd, ... /* arg */ );

介绍:fcntl函数接收2个参数+1个变参。
int fd:表示要操作哪个文件
int cmd: 表示要进行哪个命令操作。
变参是用来传递参数的,要配合cmd来使用。

  • fcntl函数实例

    实例功能:实现鼠标和键盘的异步输入

  • (1)F_GETFL (void) //获取当前文件描述符的标志
    Return (as the function result) the file access mode and
    the file status flags; arg is ignored.
    文件访问和文件状态标志,arg被忽略

  • (2)F_SETFL (int) //设置文件描述符的标志
    Set the file status flags to the value specified by arg.
    将文件状态标志设置为arg指定的值。
    O_ASYNC : 异步IO

  • (3)F_SETOWN (int) //设置异步IO的接受进程为某个进程 ,一般是当前进程
    Set the process ID or process group ID that will receive SIGIO and SIGURG signals for events on the file descriptor fd.
    设置将接收文件描述符fd上事件的SIGIO和SIGRUG信号的进程ID或进程组ID。

//不懂--------------------------------------
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
 
int mouse = 0;
char buf[50] = {0};
 
//鼠标处理函数
void func1(int sig)
{
	if(sig != SIGIO)
		return;
		
	read(mouse,buf,50);
	printf("mouse [%s]\n",buf);
		
}
 
 
int main(void)
{
	int flag = 0;
	//打开鼠标文件,获取文件描述符
	mouse = open("/dev/input/mouse0",O_RDONLY);
	if(mouse < 0)
	{
		perror("open:\n");
		return -1;
	}
	//将鼠标事件设定为 异步IO
	flag = fcntl(mouse,F_GETFL);   //获取文件描述符当前标志
	flag |= O_ASYNC;  // 将当前标识符增加O_ASYNC   接受异步IO
	fcntl(mouse,F_SETFL,flag);      //最后把增加好的标志赋值给当前文件描述符
	
	//设置异步IO的接受进程为当前进程
	fcntl(mouse,F_SETOWN,getpid());
	
	//注册当前进程的signal函数捕获函数
	signal(SIGIO, func1);
	
	//键盘处理函数
	while(1)
	{
		read(0,buf,50);
		printf("keyboard [%s]\n",buf);
	
	}
	
	
	return 0;
}

7. iocntl

ioctl()函数的原型:

#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);

iocntl一般用于操作特殊文件或设备文件,并获取这些设备的信息
譬如触摸屏支持的最大触摸点数、触摸屏 X、Y 坐标的范围
等信息。

3、《高级IO》

高级IO

阻塞式IO的困境

用轮流读取鼠标和键盘的例子
read()标准输入文件默认是阻塞的

非阻塞式IO

轮循方式
解决并发式IO问题
用非阻塞式IO完成鼠标和键盘输入的并发功能
seniorIO+.c

  • 将阻塞式改为非阻塞式的方式:
    第一:
fd = open(“dev/input/mouse1”, O_NONBLOCK);

第二种:

int flag; // 把0号文件描述符(stdin)变成非阻塞式的
flag = fcntl(0, F_GETFL); // 先获取原来的flag
flag |= O_NONBLOCK; // 添加非阻塞属性
fcntl(0, F_SETFL, flag);// 更新flag
// 这3步之后,0就变成了非阻塞式的了

分别使用两种方式将鼠标和键盘更改为非阻塞IO

#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>

int main(void){
    char buf[200];
    int ret;

    int fd;
    fd=open("/dev/input/mouse0", O_RDONLY | O_NONBLOCK);  //《第一种方式》
    if (fd < 0)
    {
        perror("open:");
        return -1;
}


//把0号文件描述符(stdin)变成非阻塞式的  《第二种方式》
int flag = -1;
flag = fcntl(0, F_GETFL);     //先获取原来的flag
flag |= O_NONBLOCK;         //添加非阻塞属性
fcntl(0, F_SETFL, flag);       //更新flag
//这三步之后就把fd0变成非阻塞的


while(1){
 //读取鼠标
memset(buf, 0, sizeof(buf));
//printf("before 鼠标 read.\n");
ret=read(fd, buf, 100);
if(ret>0){                //read读到数据时返回值大于零
    printf("鼠标读出的内容是: [%s].\n", buf);
}

//读取键盘
memset(buf, 0, sizeof(buf));
//printf("before read.\n");
ret=read(0, buf, 2);
if(ret>0){                
printf("读出的内容是: [%s].\n", buf);
}

}
return 0;
}

多路复用IO

什么是IO多路复用?

举一个简单地网络服务器的例子,如果你的服务器需要和多个客户端保持连接,处理客户端的请求,属于多进程的并发问题,如果创建很多个进程来处理这些IO流,会导致CPU占有率很高。所以人们提出了I/O多路复用模型:一个线程,通过记录I/O流的状态来同时管理多个I/O。

select只是IO复用的一种方式,其他的还有:poll,epoll等。

外部阻塞式,内部以非阻塞式自动轮询多路阻塞式IO

B站视频:
1 、select poll epoll
2、有动画演示select poll epoll

2.select()

socket select函数用法,图文并茂讲解,初学者必备

  1. 函数原型:

/* According to POSIX.1-2001 */
#include <sys/select.h>

/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout);

void FD_CLR(int fd, fd_set *set);
int  FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);
nfds 为集合中最大文件描述符+1,下文由解释;
select 参数中用各个fd构成三个集合,当然这些集合也可以为空,其中;
readfds  读集;
writefds 写集;
exceptfds 异常条件集

void FD_CLR(int fd, fd_set *set);//清除某一个被监视的文件描述符。
int  FD_ISSET(int fd, fd_set *set);//测试一个文件描述符是否是集合中的一员
void FD_SET(int fd, fd_set *set);//添加一个文件描述符,将set中的某一位设置成1;
void FD_ZERO(fd_set *set);//清空集合中的文件描述符,将每一位都设置为0;

select()函数允许程序监视多个文件描述符,等待所监视的一个或者多个文件描述符变为“准备好”的状态。所谓的”准备好“状态是指:文件描述符不再是阻塞状态,可以用于某类IO操作了,包括可读,可写,发生异常三种。

返回值
成功时:返回三中描述符集合中”准备好了“的文件描述符数量。
超时:返回0
错误:返回-1,并设置 errno

简单理解select模型
理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。

(1)执行fd_set set; FD_ZERO(&set);则set用位表示是0000,0000。

(2)若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)

(3)若再加入fd=2,fd=1,则set变为0001,0011

(4)执行select(6,&set,0,0,0)阻塞等待

(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空

实例:
select.c

#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include <sys/select.h>
#include <sys/time.h>


int main(void){
    char buf[200];
    int ret = -1;
    fd_set myset;
    int fd;
    struct timeval tm;

    fd=open("/dev/input/mouse0", O_RDONLY | O_NONBLOCK); //普通文件IO(鼠标)默认非阻塞
    if (fd < 0)
    {
        perror("open:");
        return -1;
    }

    int flag = -1;                      //键盘这个标准输入IO默认为阻塞,所以要设置为非阻塞
flag = fcntl(0, F_GETFL); //先获取原来的flag
flag |= O_NONBLOCK;       //添加非阻塞属性
fcntl(0, F_SETFL, flag);  //更新flag
//这三步之后就把fd0变成非阻塞的
//以上两行代码等价于:fcntl(0,F_SETFL,flag|O_NONBLOCK);

    //当前有两个fd,一个是fd一个是0
    //处理myset
FD_ZERO(&myset);
FD_SET(fd, & myset);
FD_SET(0, &myset);

tm.tv_sec = 10;  //阻塞时间10s
tm.tv_usec = 0;

ret = select(fd + 1, &myset, NULL, NULL,&tm);
if(ret<0){ 
    perror("select: ");
    return -1;
}
else if(ret==0){
    printf("超时: \n");
}
else{
//等到了IO,然后去检测到底是哪个IO到了,处理他
if (FD_ISSET(0, &myset)){
    //处理键盘
     memset(buf, 0, sizeof(buf));
    ret = read(0, buf, 2);
        printf("读出的内容是: [%s].\n", buf);
}

if (FD_ISSET(fd, &myset)){
    //处理鼠标
  memset(buf, 0, sizeof(buf));
   ret = read(fd, buf, 100);
    printf("鼠标读出的内容是: [%s].\n", buf);
         }
  }

}

注释:

  1. select(fd + 1, &myset, NULL, NULL,&tm);
    待测试的描述集总是从0, 1, 2, …开始的。 所以, 假如你要检测的描述符为8, 9, 10, 那么系统实际也要监测0, 1, 2, 3, 4, 5, 6, 7, 此时真正待测试的描述符的个数为11个, 也就是max(8, 9, 10) + 1

在这里插入图片描述

3.poll()

socket poll用法,深度探讨poll实现原理

在这里插入图片描述

4.epoll()

epoll详解
epoll原理分析,图文并茂讲解epoll,彻底弄懂epoll机制

在这里插入图片描述

  1. epoll_create函数 //用于创建epoll文件描述符
  2. epoll_ctl函数 //用于增加,删除,修改epoll事件
  3. epoll_wait函数 //用于监听套接字事件
#include <sys/epoll.h>
 
int epoll_create(int size);
 
参数:
size:目前内核还没有实际使用,只要大于0就行
 
返回值:
返回epoll文件描述符(epoll)
#include <sys/epoll.h>
 
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
 
参数:
epfd:epoll文件描述符
op:操作码
EPOLL_CTL_ADD:插入事件
EPOLL_CTL_DEL:删除事件
EPOLL_CTL_MOD:修改事件
fd:事件绑定的套接字文件描述符
events:事件结构体
 
返回值:
成功:返回0
失败:返回-1
#include <sys/epoll.h>
 
int epoll_wait(int epfd, struct epoll_event *events,              
int maxevents, int timeout);
 
参数:
epfd:epoll文件描述符
events:epoll事件数组
maxevents:epoll事件数组长度
timeout:超时时间
小于0:一直等待
等于0:立即返回
大于0:等待超时时间返回,单位毫秒
 
返回值:
小于0:出错
等于0:超时
大于0:返回就绪事件个数

主要数据结构:

#include <sys/epoll.h>
 
struct epoll_event{
  uint32_t events; //epoll事件,参考事件列表 
  epoll_data_t data;
} ;
typedef union epoll_data {  
    void *ptr;  
    int fd;  //套接字文件描述符
    uint32_t u32;  
    uint64_t u64;
} epoll_data_t;

头文件:<sys/epoll.h>
 
enum EPOLL_EVENTS
{
    EPOLLIN = 0x001, //读事件
    EPOLLPRI = 0x002,
    EPOLLOUT = 0x004, //写事件
    EPOLLRDNORM = 0x040,
    EPOLLRDBAND = 0x080,
    EPOLLWRNORM = 0x100,
    EPOLLWRBAND = 0x200,
    EPOLLMSG = 0x400,
    EPOLLERR = 0x008, //出错事件
    EPOLLHUP = 0x010, //出错事件
    EPOLLRDHUP = 0x2000,
    EPOLLEXCLUSIVE = 1u << 28,
    EPOLLWAKEUP = 1u << 29,
    EPOLLONESHOT = 1u << 30,
    EPOLLET = 1u << 31 //边缘触发
  };

工作站中的代码实现:
./epoll_client

/*======================================================

运行时要传入两个参数:
./epoll_client <server端IP> <server端口号>

./epoll_client 10.197.61.238  9999

======================================================*/

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
 
#define LISTEN_BACKLOG (5)
#define BUF_SIZE (30)
 
#define REQUEST_STR "tcp pack"
 
void usage(void) {
    printf("*********************************\n");
    printf("./epoll_client 10.197.61.238  9999\n");
    printf("*********************************\n");
}
 
int main(int argc, char *argv[])
{
    struct sockaddr_in client;
    struct sockaddr_in server;
    int sock_fd = 0;
    int ret = 0;
    socklen_t addrlen = 0;
    char send_buf[BUF_SIZE] = {0};
    char recv_buf[BUF_SIZE] = {0};
 
    if (argc != 3) {
        usage();
        return -1;
    }
 
    char *ip = argv[1];
    unsigned short port = atoi(argv[2]);
    printf("ip:port->%s:%u\n", argv[1], port);
 
    sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (sock_fd == -1) {
        perror("socket error");
        return -1;
    }
 
    memset(&server, 0, sizeof(struct sockaddr_in));
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = inet_addr(ip);
    server.sin_port = htons(port);
 
    ret = connect(sock_fd, (struct sockaddr *)&server, sizeof(struct sockaddr));
    if (ret == -1) {
        close(sock_fd);
        perror("connect error");
        return -1;
    }
 
    char seq = 0x31;
    while(1) {
        memset(send_buf, seq, BUF_SIZE);
        send(sock_fd, send_buf, BUF_SIZE, 0);
        printf("send %s\n", send_buf);
        sleep(2);
        seq++;
    }
    
    close(sock_fd);
 
    return 0;
}

./epoll_server

/*======================================================

运行时要传入两个参数:
./epoll_server <server端IP> <server端口号>

./epoll_server 10.197.61.238  9999

======================================================*/

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <errno.h>
#include <stdbool.h>
#include <sys/epoll.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
 
#define LISTEN_BACKLOG (5)
#define BUF_SIZE (100)
#define ONCE_READ_SIZE (1500)
 
#define EPOLL_SIZE (100);
#define MAX_EVENTS (10)
 
void usage(void) {
    printf("*********************************\n");
    printf("./epoll_server 10.197.61.238  9999\n");
    printf("*********************************\n");
}
 
void setnonblocking(int fd) {
    int flag = fcntl(fd, F_GETFL, 0);
    fcntl(fd, F_SETFL, flag | O_NONBLOCK);
}
 
int main(int argc, char *argv[])
{
    struct sockaddr_in local;
    struct sockaddr_in peer;
    socklen_t addrlen = sizeof(peer);
    int sock_fd = 0, new_fd = 0;
    int ret = 0;
    char send_buf[BUF_SIZE] = {0};
    char recv_buf[BUF_SIZE] = {0};
 
    if (argc != 3) {
        usage();
        return -1;
    }
 
    char *ip = argv[1];
    unsigned short port = atoi(argv[2]);
    printf("ip:port->%s:%u\n", argv[1], port);
 
    sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (sock_fd == -1) {
        perror("socket error");
        return -1;
    }
 
    memset(&local, 0, sizeof(struct sockaddr_in));
    local.sin_family = AF_INET;
    local.sin_addr.s_addr = inet_addr(ip);
    local.sin_port = htons(port);
 
    ret = bind(sock_fd, (struct sockaddr *)&local, sizeof(struct sockaddr));
    if (ret == -1) {
        close(sock_fd);
        perror("bind error");
        return -1;
    }
 
    ret = listen(sock_fd, LISTEN_BACKLOG);
    if (ret == -1) {
        close(sock_fd);
        perror("listen error");
        return -1;
    }
 
    int epoll_size = EPOLL_SIZE;
    int efd = epoll_create(epoll_size);
    if (efd == -1) {
        perror("epoll create error");
        return -1;
    }
 
    struct epoll_event ev, events[MAX_EVENTS];
    ev.data.fd = sock_fd;
    ev.events = EPOLLIN;
    if (epoll_ctl(efd, EPOLL_CTL_ADD, sock_fd, &ev) == -1) {
        perror("epoll ctl ADD error");
        return -1;
    }
 
    int timeout = 1000;
    while (1) {
        int nfds = epoll_wait(efd, events, MAX_EVENTS, timeout);
        if (nfds == -1) {
            perror("epoll wait error");
            return -1;
        } else if (nfds == 0) {
            printf("epoll wait timeout\n");
            continue;
        } else {
 
        }
 
        for (int i = 0; i < nfds; i++) {
            int fd = events[i].data.fd;
            printf("events[%d] events:%08x\n", i, events[i].events);
            if (fd == sock_fd) {
                //accept() 函数返回一个新的套接字描述符,该描述符用于与特定客户端建立通信。原始套接字(服务器套接字)仍然保持打开状态,以便接受更多的连接。
                new_fd = accept(sock_fd, (struct sockaddr *)&peer, &addrlen);
                if (new_fd == -1) {
                    perror("accept error");
                    continue;
                }
                setnonblocking(new_fd);
                ev.data.fd = new_fd;
                ev.events = EPOLLIN|EPOLLET;
                if (epoll_ctl(efd, EPOLL_CTL_ADD, new_fd, &ev) == -1) {
                    perror("epoll ctl ADD new fd error");
                    close(new_fd);
                    continue;
                }
            } else {
                if (events[i].events & EPOLLIN) {
                    printf("fd:%d is readable\n", fd);
                    memset(recv_buf, 0, BUF_SIZE);
                    unsigned int len = 0;
                    while(1) {
                        ret = recv(fd, recv_buf + len, ONCE_READ_SIZE, 0);
                        if (ret ==  0) {
                            printf("remove fd:%d\n", fd);
                            epoll_ctl(efd, EPOLL_CTL_DEL, fd, NULL);
                            close(fd);
                            break;
                        } else if ((ret == -1) && ((errno == EINTR) || (errno == EAGAIN) || (errno == EWOULDBLOCK))) {
                            printf("fd:%d recv errno:%d done\n", fd, errno);
                            break;
                        } else if ((ret == -1) && !((errno == EINTR) || (errno == EAGAIN) || (errno == EWOULDBLOCK))) {
                            printf("remove fd:%d errno:%d\n", fd, errno);
                            epoll_ctl(efd, EPOLL_CTL_DEL, fd, NULL);
                            close(fd);
                            break;
                        }else {
                            printf("once read ret:%d\n", ret);
                            len += ret;
                        }
                    }
                    printf("recv fd:%d, len:%d, %s\n", fd, len, recv_buf);
                } if (events[i].events & EPOLLOUT) {
                    printf("fd:%d is sendable\n", fd);
                } else if ((events[i].events & EPOLLERR) ||
                        ((events[i].events & EPOLLHUP))) {
                    printf("fd:%d error\n", fd);
                }
            }
        }
    }
 
    return 0;
}

异步IO

  1. SIGIO信号->进程,实现异步IO //有待再研究
//no_same_IO.c
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include <sys/select.h>
#include <sys/time.h>
#include <signal.h>

int mouse_fd=-1;
//异步IO(键盘输入为主程序,鼠标为异步输入)
//绑定到SIGIO信号,在函数内处理异步通知事件
void func(int sig){
    char buf[200] = {0};
    if(sig!=SIGIO)
    return;

//处理鼠标(异步通知事件)
    read(mouse_fd, buf, 100);
    printf("鼠标读出的内容是: [%s].\n", buf); 
}


int main(void){
    
    int flag=-1;
    char buf[200];
mouse_fd=open("/dev/input/mouse0", O_RDONLY | O_NONBLOCK); //普通文件IO(鼠标)默认非阻塞
    if (mouse_fd < 0)
    {
        perror("open:");
        return -1;
    }

//注册异步通知
//把鼠标的文件描述符设置为可以接受异步IO
flag=fcntl(mouse_fd,F_GETFL);//获取文件状态标志
flag|=O_ASYNC;  //异步IO
fcntl(mouse_fd, F_SETFL,flag);//添加O_ASYNC标志
//以上两行等价与:fcntl(fd,F_SETFL,flag|O_ASYNC);

//把异步IO事件的接收设置为当前进程
fcntl(mouse_fd,F_SETOWN,getpid());    //在当前进程下动鼠标就会触发信号事件

//注册当前进程的SIGIO信号捕获函数
signal(SIGIO,func);

while(1){
//处理键盘(主任务)
    memset(buf , 0, sizeof(buf));
    read(0, buf, 5);
    printf("读出的内容是: [%s].\n", buf);
}
return 0;
}
    

  1. 父子进程实现异步IO
//two_procces_two_IO.c
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(void){
//创建子进程,父子进程分别进行鼠标和键盘的读取工作
//两个文件IO都是阻塞的
int ret = -1;
int fd = -1;
char buf[200];

ret = fork();
if(ret<0){
    perror("fork:");
    return -1;
}

if(ret == 0){
    //子进程
    //处理鼠标
    fd = open("/dev/input/mouse0", O_RDONLY); //此处要用阻塞式,不用O_NONBLOCK标志
    if(fd<0){
        perror("open:");
        return -1;
    }
    while(1){
    memset(buf , 0, sizeof(buf));
    read(fd, buf, 100);
    printf("鼠标读出的内容是: [%s].\n", buf); 
    }
}

if(ret > 0){
    //父进程
    //处理键盘(主任务)
    while(1){
    memset(buf , 0, sizeof(buf));
    read(0, buf, 5);
    printf("读出的内容是: [%s].\n", buf); 
    }
}
    return 0;
}

4.【存储映射IO】mmap()/munmap()

存储映射 I/O

存储映射 I/O(memory-mapped I/O)是一种基于内存区域的高级 I/O 操作,它能将一个文件映射到进程地址空间中的一块内存区域中,当从这段内存中读数据时,就相当于读文件中的数据(对文件进行 read 操
作),将数据写入这段内存时,则相当于将数据直接写入文件中(对文件进行 write 操作)。这样就可以在
不使用基本 I/O 操作函数 read()和 write()的情况下执行 I/O 操作。

  1. mmap()建立映射
    为了实现存储映射 I/O 这一功能,我们需要告诉内核将一个给定的文件映射到进程地址空间中的一块
    内存区域中,这由系统调用 mmap()来实现。其函数原型如下所示:
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

在这里插入图片描述
2. munmap()解除映射
通过 open()打开文件,需要使用 close()将将其关闭;同理,通过 mmap()将文件映射到进程地址空间中
的一块内存区域中,当不再需要时,必须解除映射,使用 munmap()解除映射关系,其函数原型如下所示:

#include <sys/mman.h>
int munmap(void *addr, size_t length);

4、文件系统

0. LINUX文件系统

  1. Linux 下的文件系统主要有 ext2、ext3、ext4 等文件系统。
  2. ext4 向下兼容 ext3 和 ext2,因此可以将 ext2 和 ext3 挂载为 ext4。

1. nfs

  1. nfs(Network File System)网络文件系统,通过 nfs 可以在计算机之间通过网络来分享资源,
    比如我们将 linux 镜像和设备树文件放到 Ubuntu 中,然后在 uboot 中使用 nfs 命令将 Ubuntu 中 的 linux 镜像和设备树下载到开发板DRAM 中。这样做的目的是为了方便调试 linux 镜像和设备树,也就是网络调试,通过网络调试是 Linux 开发中最常用的调试方法

rootfs:根文件系统
网络文件系统 NFS
常用于驱动、内核调试挂载的虚拟文件系统?

  1. NFS是一种基于TCP/IP传输的网络文件系统协议。
  2. 通过使用NFS协议,客户机可以像访问本地目录一样访问远程服务器中的共享资源;
  3. 对于大多数负载均衡群集来说,使用NFS协议来共享数据存储 是比较常见的方法,NFS也是NAS存储设备必然支持的一种协议;

虚拟文件系统

虚拟文件系统

  1. 文件系统的种类众多,而操作系统希望对用户提供一个统一的接口,于是在用户层与文件系统层引入了中间层,这个中间层就称为虚拟文件系统(Virtual File System,VFS)。

  2. VFS 定义了一组所有文件系统都支持的数据结构和标准接口,这样程序员不需要了解文件系统的工作原理,只需要了解 VFS 提供的统一接口即可。
    在这里插入图片描述
    Linux支持的文件系统
    Linux 支持的文件系统也不少,根据存储位置的不同,可以把文件系统分为三类:

  3. 磁盘的文件系统,它是直接把数据存储在磁盘中,比如 Ext 2/3/4、XFS 等都是这类文件系统。
    内存的文件系统,这类文件系统的数据不是存储在硬盘的,而是占用内存空间,我们经常用到的 /proc 和 /sys 文件系统都属于这一类,读写这类文件,实际上是读写内核中相关的数据数据。

  4. 网络的文件系统,用来访问其他计算机主机数据的文件系统,比如 NFS、SMB 等等。
    文件系统首先要先挂载到某个目录才可以正常使用,比如 Linux 系统在启动时,会把文件系统挂载到根目录。

LINUX根目录“/”中的一些重要的文件夹(部分):
在这里插入图片描述

2. devfs

  • 用于存放字符设备
  1. Linux 系统中,可将硬件设备分为字符设备块设备,所以就有了字符设备文件和块设备文件两种文件类型。
  2. 虽然有设备文件,但是设备文件并不对应磁盘上的一个文件,也就是说设备文件并不存在于磁盘中,而是由文件系统虚拟出来的,一般是由内存来维护,当系统关机时,设备文件都会消失;
  3. 字符设备文件一般存放在 Linux 系统**/dev/目录下,所以/dev 也称为虚拟文件系统 devfs**。
    以 Ubuntu 系统为例,如下所示:
    在这里插入图片描述
    上图中 agpgart、autofs、btrfs-control、console 等这些都是字符设备文件,而 loop0、loop1 这些便是块设备文件。

3. proc

  • 用于访问系统或者进程信息。
  1. proc 文件系统是一个虚拟文件系统,它以文件系统的方式为应用层访问系统内核数据提供了接口,用户和应用程序可以通过
  2. 应用层可以通过proc 文件系统得到系统信息进程相关信息,对 proc 文件系统的读写作为与内核进行通信的一种手段。

4. sysfs

  • 用于管理系统设备。
  1. sysfs是一个基于内存的文件系统,同 devfs、proc 文件系统一样,称为虚拟文件系统;
  2. 它的作用是将内核信息文件的方式提供给应用层使用。
  3. 应用层可以通过proc 文件系统得到系统信息进程相关信息
  4. 与 proc 文件系统类似,sysfs 文件系统的主要功能便是对系统设备进行管理,它可以产生一个包含所有系统硬件层次的视图。

5、信号

正点原子:第八章信号基础

1.常见信号与默认行为

参考正点原子:275
SIGINT(2号信号)
当用户在终端按下中断字符(通常是 CTRL + C)时,内核将发送 SIGINT 信号给前台进程组中的每一
个进程。
SIGKILL(9号信号)
此信号为“必杀(sure kill)”信号,用于杀死进程的终极办法,此信号无法被进程阻塞、忽略或者捕
获,故而“一击必杀”,总能终止进程。使用 SIGINT 信号和 SIGQUIT 信号虽然能终止进程,但是前提条
件是该进程并没有忽略或捕获这些信号,如果使用 SIGINT 或 SIGQUIT 无法终止进程,那就使用“必杀信
号”SIGKILL 吧。Linux 下有一个 kill 命令,kill 命令可用于向进程发送信号,我们会使用"kill -9 xxx"命令
来终止一个进程,这里的-9 其实指的就是发送编号为 9 的信号,也就是 SIGKILL 信
号。

kill -9 xxx  //(xxx 表示进程的 pid)

2.进程对信号的处理

当进程接收到内核用户发送过来的信号之后,根据具体信号可以采取不同的处理方式:

  1. 忽略信号
  2. 捕获信号
  3. 执行系统默认操作

3. 常用信号函数————

Linux 系统提供了系统调用 signal()和 sigaction()两个函数用于设置信号的处 理方式

4. signal()

signal()函数是 Linux 系统下设置信号处理方式最简单的接口,可将信号的
处理方式设置为捕获信号、忽略信号以及系统默认操作,此函数原型如下所示:

#include <signal.h>

typedef void (*sig_t)(int);   //函数指针,sig_t 等价于 (*void)(int)
sig_t signal(int signum, sig_t handler);

signum:此参数指定需要进行设置的信号,可使用信号名(宏)或信号的数字编号,建议使用信号名。
handler:sig_t 类型的函数指针,指向信号对应的信号处理函数,当进程接收到信号后会自动执行该处
理函数;
返回值:此函数的返回值也是一个 sig_t 类型的函数指针,成功情况下的返回值则是指向在此之前的信
号处理函数;如果出错则返回 SIG_ERR,并会设置 errno。

  1. 参数 handler 既可以设置为用户自定义的函数,也就是捕获信号时需要执行的处理函数
    也可以设置为 SIG_IGNSIG_DFL
  2. SIG_IGN 表示此进程需要忽略该信号,
  3. SIG_DFL 则表示设置为系统默认操作

Tips:SIG_IGN、SIG_DFL 分别取值如下:
/* Fake signal functions. /
#define SIG_ERR ((sig_t) -1) /
Error return. /
#define SIG_DFL ((sig_t) 0) /
Default action. /
#define SIG_IGN ((sig_t) 1) /
Ignore signal. */

    /*signal()函数使用示例*/
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
typedef void (*sig_t)(int);
static void sig_handler(int sig) {  //信号自定义处理函数
 printf("Received signal: %d\n", sig);
}
int main(int argc, char *argv[])
{
 sig_t ret = NULL;
 //SIGINT 信号的系统默认操作是终止进程,这里将它与自定义处理函数绑定。
 ret = signal(SIGINT, (sig_t)sig_handler);
 if (SIG_ERR == ret) {
 perror("signal error");
 exit(-1);
 }
 /* 死循环 */
 for ( ; ; ) { }
 exit(0);
}

测试程序中捕获了(CTRL+ C)信号,而对应的处理方式仅仅只是打印一条语句、而并不终止进程。
可向该进程发送 SIGKILL 暴力终止该进程
在这里插入图片描述

5. sigaction()

sigaction()虽比signal更复杂,但作为回报,sigaction()也更具灵活性以及移植性。
sigaction()允许单独获取信号的处理函数而不是设置,并且还可以设置各种属性对调用信号处理函数时的行为施以更加精准的控制,其函数原型如下所示:

#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

signum:需要设置的信号,除了 SIGKILL 信号和 SIGSTOP 信号之外的任何信号。
act:如果参数 act 不为 NULL,则表示需要为信号设置新的处理方式;如果参数 act 为 NULL,则表示无需改变信号当前的处理方式。
act 参数是一个 struct sigaction 类型指针,指向一个 struct sigaction 数据结构,该数据结构描述了信号的处理方式;
oldact:如果参数oldact 不为 NULL,则会将信号之前的处理方式等信息通过参数 oldact 返回出来;如果无意获取此类信息,那么可将该参数设置为 NULL。
返回值:成功返回 0;失败将返回-1,并设置 errno。

  • struct sigaction 结构体
struct sigaction {
 void (*sa_handler)(int);
 void (*sa_sigaction)(int, siginfo_t *, void *);
 sigset_t sa_mask;
 int sa_flags;
 void (*sa_restorer)(void);
};
/* 
         示例代码
功能:接收CTRL + C并打印信息 
*/
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
static void sig_handler(int sig) {
 printf("Received signal: %d\n", sig);
}
int main(int argc, char *argv[])
{
 struct sigaction sig = {0};
 int ret;
 sig.sa_handler = sig_handler;
 sig.sa_flags = 0;
 ret = sigaction(SIGINT, &sig, NULL);
 if (-1 == ret) {
 perror("sigaction error");
 exit(-1);
 }
 /* 死循环 */
 for ( ; ; ) { }
 exit(0);
}

6. 向进程发送信号——————

与 kill 命令相类似,Linux 系统提供了 kill()系统调用,一个进程可通过 kill()向另一个进程发送信号;
除了 kill()系统调用之外,Linux 系统还提供了系统调用 killpg()以及库函数 raise(),也可用于实现发送信号的功能

7. kill()

kill()系统调用可将信号发送给指定的进程或进程组中的每一个进程,其函数原型如下所示:

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);

pid:用于指定接收此信号的进程 pid;除此之外,参数 pid 也可设置为 0 或 -1 以及小于-1 等不同值,稍后给说明。
sig:参数 sig 指定需要发送的信号,也可设置为 0,如果参数 sig 设置为 0 则表示不发送信号,但任执行错误检查,这通常可用于检查参数 pid 指定的进程是否存在。
返回值:成功返回 0;失败将返回-1,并设置 errno。
参数 pid 不同取值含义:
⚫ 如果 pid 为正,则信号 sig 将发送到 pid 指定的进程。
⚫ 如果 pid 等于 0,则将 sig 发送到当前进程的进程组中的每个进程。
⚫ 如果 pid 等于-1,则将 sig 发送到当前进程有权发送信号的每个进程,但进程 1(init)除外。
⚫ 如果 pid 小于-1,则将 sig 发送到 ID 为-pid 的进程组中的每个进程。

  • 使用 kill()函数向一个指定的进程发送信号。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
 int pid;
 /* 判断传参个数 */
 if (2 > argc)
 exit(-1);
 /* 将传入的字符串转为整形数字 */
 pid = atoi(argv[1]);
 printf("pid: %d\n", pid);
 /* 向 pid 指定的进程发送信号 */
 if (-1 == kill(pid, SIGINT)) {
 perror("kill error");
 exit(-1);
 }
 exit(0);
}

8. raise()

有时进程需要向自身发送信号,raise()函数可用于实现这一要求,raise()函数原型如下所示(此函数为 C库函数):

#include <signal.h>
int raise(int sig);

sig:需要发送的信号。
返回值:成功返回 0;失败将返回非零值。

raise()其实等价于:

kill(getpid(), sig);  //获取自己的进程号然后发信号

测试:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig) {
 printf("Received signal: %d\n", sig);
}
int main(int argc, char *argv[])
{
 struct sigaction sig = {0};
 int ret;
 sig.sa_handler = sig_handler;
 sig.sa_flags = 0;
 ret = sigaction(SIGINT, &sig, NULL);
 if (-1 == ret) {
 perror("sigaction error");
 exit(-1);
 }
 for ( ; ; ) {
 /* 向自身发送 SIGINT 信号 */
 if (0 != raise(SIGINT)) {
 printf("raise error\n");
 exit(-1);
 }
 sleep(3); // 每隔 3 秒发送一次
 }
 exit(0);
}

9. alarm()

使用 alarm()函数可以设置一个定时器(闹钟),当定时器定时时间到时,内核会向进程发送 SIGALRM
信号,其函数原型如下所示:

#include <unistd.h>
unsigned int alarm(unsigned int seconds);

函数参数和返回值:

seconds:设置定时时间,以秒为单位;如果参数 seconds 等于 0,则表示取消之前设置的 alarm 闹钟。
返回值:如果在调用 alarm()时,之前已经为该进程设置了 alarm 闹钟还没有超时,则该闹钟的剩余值作
为本次 alarm()函数调用的返回值,之前设置的闹钟则被新的替代;否则返回 0。

10. pause()

pause()系统调用可以使得进程暂停运行、进入休眠状态直到进程捕获到一个信号为止
只有执行了信号处理函数并从其返回时,pause()才返回,
在这种情况下,pause()返回-1,并且将 errno 设置为 EINTR

#include <unistd.h>
int pause(void);

测试:
通过 **alarm()和 pause()**函数模拟 sleep 功能。

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig) {
puts("Alarm timeout");
}
int main(int argc, char *argv[])
{
 struct sigaction sig = {0};
 int second;
 /* 检验传参个数 */
 if (2 > argc)
 exit(-1);
 /* 为 SIGALRM 信号绑定处理函数 */
 sig.sa_handler = sig_handler;
 sig.sa_flags = 0;
 if (-1 == sigaction(SIGALRM, &sig, NULL)) {
 perror("sigaction error");
 exit(-1);
 }
 /* 启动 alarm 定时器 */
 second = atoi(argv[1]);
 printf("定时时长: %d 秒\n", second);
 alarm(second);
 /* 进入休眠状态 */
 pause();
 puts("休眠结束");
 exit(0);
}

在这里插入图片描述

5、进程

进程的前世今生

start_kernel()
kernel_thread()

LINUX进程
在这里插入图片描述

0.1进程环境打印

#include <stdio.h>
//每个进程中都有一份所有环境变量构成的表格(一个字符串数组,用二重指针environ指向它)
int main(){
    extern char **environ;   //声明就可使用
    int i = 0;
    while(environ[i]!=NULL){
        printf("%s \n", environ[i]);
        i++;
    }
    return 0;
}

0.2getpid()

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main(){
    pid_t p1 = -1;
    p1 = getpid();
    printf("pid=%d\n",p1);
    pid_t p2 = -1;
    p2 = getppid();   //获取父进程的pid
    printf("pid=%d\n",p2);
    return 0;
}

0.3syslog()让进程打印日志

syslog.c

#include<stdio.h>
#include<syslog.h>
#include <sys/types.h>
#include <unistd.h>


//让进程打印日志
int main(){
    printf("pid = %d\n", getpid());

    openlog("a.out", LOG_PID | LOG_CONS, LOG_USER);  //打开日志
    syslog(LOG_INFO, "this is a.out log info");                         //写日志
    closelog();

}

0.4system()

system.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include<stdlib.h>

//system() = fork() + exec()
int main(){
    system("ls");
    return 0;
}

0.PCB

在系统中表示一个进程的实体是进程控制块
个人推测:
进程创建之后,PCB在系统区,进程被切换时,PCB暂时还是保存在系统区,进程结束后由父进程将PCB销毁,进程占用的内存和IO由系统来回收。
PCB(进程控制块)理解

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

1.fork()/vfork()

概述
fork和vfork都是用来创建一个子进程的。

区别:

fork()子进程拷贝父进程的数据段和代码段,这里通过拷贝页表实现。
vfork()子进程与父进程共享地址空间,无需拷贝页表,效率更高。
fork()父子进程的执行次序不确定。
vfork()保证子进程先运行,在调用 exec 或 exit 之前与父进程数据是共享的。父进程在子进程调用 exec 或 exit 之后才可能被调度运行,如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。

vfork()是Linux提供的另一个用来生成一个子进程的系统调用。
系统调用vfork()

与fork()不同,vfork()并不把父进程全部复制到子进程中,而只是用用赋值指针的方法使子进程与父进程的资源实现共享。由于函数vfork()生成的子进程与父进程共享同一块内存空间,因此实质上vfork()创建的是一个线程,但习惯上人们还是叫它子进程。

用vfork()创建子进程且调用execve()之后,子进程的内存映像如下所示:
在这里插入图片描述

朱老师视频fork()示例:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

int main(){
    //返回值=0是子进程,>0是父进程
    
    pid_t p1 =-1;
    p1 = fork();
    //调用fork()之后,子进程也有一份和父进程一样的运行程序
    //所以以下代码会同在父子进程中运行,至于谁先运行就看处理机调度了
    if(p1==0){
    printf("这是子进程 pid=%d\n",getpid());
    }
    if(p1>0){
    printf("这是父进程 pid=%d\n",getpid());
    }
    printf("这个打印父子进程都执行一遍 pid=%d\n" ,getpid());

    return 0;
}

2.fork() / exec() / vfork()

子进程的创建
在Linux中,父进程以分裂的方式来创建子进程,创建一个子进程的系统调用叫做fork()。

系统调用fork()
为了在一个进程中分裂出子进程,Linux提供了一个系统调用fork()。这里所说的分裂,实际上是一种复制。因为在系统中表示一个进程的实体是进程控制块,创建新进程的主要工作就是要创建一个新控制块,而创建一个新控制块最简单的方法就是复制。

当然,这里的复制并不是完全复制,因为父进程控制块中某些项的内容必须按照子进程的特性来修改,例如进程的标识、状态等。另外,子进程控制块还必须要有表示自己父进程的域和私有空间,例如数据空间、用户堆栈等。下面的两张图就表示父进程和相对应的子进程的内存映射:
在这里插入图片描述

fork()创建子进程后,系统并未在内存中给子进程配置独立的程序运行空间,而只是简单地将程序指针指向父进程的代码;
与进程相关的系统调用
函数execv()
为了在程序运行中能够加载并运行一个可执行文件,Linux提供了系统调用execv()。其原型为:

int execv(const char* path, char* const argv[]);

其中,参数path为可执行文件路径,argv[]为命令行参数。

如果一个进程调用了execv(),那么该函数便会把函数参数path所指定的可执行文件加载到进程的用户内存空间,并覆盖掉原文件,然后便运行这个新加载的可执行文件。

在实际应用中,通常调用execv()的都是子进程。人们之所以创建一个子进程,其目的就是执行一个与父进程代码不同的程序,而系统调用execv()就是子进程执行一个新程序的手段之一。子进程调用execv()之后,系统会立即为子进程加载可执行文件分配私有程序内存空间,从此子进程也成为一个真正的进程。

如果说子进程是父进程的“儿子”,那么子进程在调用execv()之前,它所具有的单独用户堆栈和数据区也仅相当于它的私有“房间”;但因它还没有自己的“住房”,因此也只能寄住在“父亲”家,而不能“自立门户”,尽管它有自己的“户口”(进程控制块)。
调用execv()后,父进程与子进程存储结构的示意图如下:
在这里插入图片描述

与上文刚fork()的内存映像图相比,刚调用fork()之后,父子共同使用同一块程序代码;而调用execv()之后,子程序拥有了自己的程序代码区。

#include <stdio.h>
#include <sys/types.h>
 
int main(void)
{
    pid_t pid;
 
    if(!(pid=fork())){  //子进程返回零,因此执行exec
        execv("./hello.o",NULL);
    }else {
        printf("my pid is %d\n", getpid()); //父进程返回0打印子进程pid
    }
 
    return 0;
}

3.wait() / waitpid()

参考CSDN
系统调用wait()
虽然子进程调用函数execv()之后拥有自己的内存空间,称为一个真正的进程,但由于子进程毕竟由父进程所创建,所以按照计算机技术中谁创建谁负责销毁的惯例,父进程需要在子进程结束之后释放子进程所占用的系统资源。

为实现上述目标,当子进程运行结束后,系统会向该子进程的父进程发出一个信息,请求父进程释放子进程所占用的系统资源。但是,父进程并没有准确的把握一定结束于子进程结束之后,那么为了保证完成为子进程释放资源的任务,父进程应该调用系统调用wait()。

如果一个进程调用了系统调用wait(),那么进程就立即进入等待状态(也叫阻塞状态),一直等到系统为本进程发送一个消息。在处理父进程与子进程的关系上,那就是在等待某个子进程已经退出的信息;如果父进程得到了这个信息,父进程就会在处理子进程的“后事”之后才会继续运行。

也就是说,wait()函数功能是:父进程一旦调用了wait就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。

1、fork后wait回收实例

WIFEXITED、WIFSIGNALED、WEXITSTATUS
这几个宏用来(结合status)获取子进程的退出状态。
WIFEXITED宏用来判断子进程是否正常终止(return、exit、_exit退出)
WIFSIGNALED宏用来判断子进程是否非正常终止(被信号所终止)
WEXITSTATUS宏用来得到正常终止情况下的进程返回值的。

伪代码:

pid_t ret = -1;
int status = -1;    //status被用来和宏定义返回子进程终止状态
	
printf("parent.\n");
ret = wait(&status);  //父进程阻塞等待子进程被回收
		
printf("子进程已经被回收,子进程pid = %d.\n", ret);
printf("子进程是否正常退出:%d\n", WIFEXITED(status));
printf("子进程是否非正常退出:%d\n", WIFSIGNALED(status));
printf("正常终止的终止值是:%d.\n", WEXITSTATUS(status));

朱老师视频示例:
wait.c

#include<stdio.h>
 #include <sys/types.h>
 #include <sys/wait.h>
 #include<unistd.h>

 int  main(void){
     pid_t p1 = -1;
     pid_t ret = -1;
     int status = -1;
    p1 =  fork();

if(p1>0){
         printf("父进程\n");
         ret = wait(&status);
          printf("zi被回收,子pid= %d\n",ret );
}
     
else  if(p1==0){
         printf("子进程\n");
    }
else{
         perror("fork");
         return -1;
     }
     return 0;
 }

2,waitpid函数与wait函数的差别

    (1)基本功能一样,都是用来回收子进程
    (2)waitpid可以回收指定PID的子进程
    (3)waitpid可以阻塞式或非阻塞式两种工作模式

3,使用waitpid实现wait的效果部分实例代码解释

(1)ret = waitpid(-1, &status, 0);
-1表示不等待某个特定PID的子进程而是回收任意一个子进程,
0表示用默认的方式(阻塞式)来进行等待,
返回值ret是本次回收的子进程的PID
(2)ret = waitpid(pid, &status, 0);
等待回收PID为pid的这个子进程,
如果当前进程并没有一个ID号为pid的子进程,则返回值为-1;
如果成功回收了pid这个子进程则返回值为回收的进程的PID
(3)ret = waitpid(pid, &status, WNOHANG);
这种表示父进程要非阻塞式的回收子进程。
此时如果父进程执行waitpid时子进程已经先结束等待回收则waitpid直接回收成功,返回值是回收的子进程的PID;
如果父进程waitpid时子进程尚未结束则父进程立刻返回(非阻塞),
但是返回值为0(表示回收不成功)。

4.内核进程和线程

内核中的进程和线程
操作系统是一个先于其他程序运行的程序,那么当系统中除了操作系统之外没有其他的进程时怎么办?同时,系统中的所有用户进程必须由父进程来创建,那么“祖宗进程”是什么呢?

前一个问题由Linux的进程0来解决,后一个问题由Linux的进程1来解决。其实,前者是一个内核进程,而后者先是一个内核进程,后来又变为用户进程

内核线程及其创建
Linux实现线程的机制非常独特,它没有另外定义线程的数据结构和调度算法,只不过在创建时不为其分配单独的内存空间。如果线程运行在用户空间,那么它就是用户线程;如果运行在内核空间,那么它就是内核线程。并且,无论是线程还是进程,Linux都用同一个调度器对它们进行调度。

但是Linux却单独提供了一个用于创建内核线程的函数kernel_thread()。该函数的原型如下:

int kernel_thread(int (* fn)(void *), void* arg, unsigned long flags);

其中,fn指向线程代码;arg为线程代码所需的入口参数。

内核线程周期性执行,通常用来完成一些需要对内核幕后完成的任务,例如磁盘高速缓存的刷新、网络连接的维护和页面的交换等,所以它们也叫做内核任务。

5. 0、1、守护进程

进程0
计算机启动之后,首先进行Linux的引导和加载,在Linux内核加载之后,初始化函数start_kernel()会立即创建一个内核线程。因为Linux对线程和进程没有太严格的区分,所以就把这个由初始化函数创建的线程叫做进程0。

进程0的执行代码时内核函数cpu_idel(),该函数中只有一条hlt(暂停)指令;也就是说,这个进程什么工作也没做,所以也叫做空闲进程。该空闲进程的PCB叫做init_task(),当系统没有可运行的其它进程时,调度器才会选择进程0来运行。

进程1
进程1也叫做init进程,它是内核初始化时创建的第2个内核线程,其运行代码为内核函数init()。

该函数首先创建kswapd()等4个与内存管理有关的内核线程。接下来,在内核的init()中调用execve()系统调用装入另一个需要在用户空间运行的init函数代码,于是进程1就变成一个普通的进程。它是用户空间的第一个进程,所以它就成了其他用户进程的根进程。

只要系统,init进程就永不中止,它负责创建和监控操作系统外层所有进程的活动。

  • 守护进程
    守护进程是不受终端控制并在后台运行的进程。Linux使用了很多守护进程,在后台做一些经常性的注入定期进行页交换之类的例行工作。

守护进程一般可以通过以下方式启动:

在系统启动时由启动脚本启动,这些启动脚本通常放在/etc/rc.d目录下;
利用inetd超级服务器启动,大部分网络服务都是这样启动的,如ftp、telnet等;
由cron定时启动以及在终端用nohup启动的进程也是守护进程。
守护进程示例
daemon.c

#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
 #include <fcntl.h>
 #include<stdlib.h>

void creat_daemon(void);
//编写一个守护进程
int main(){
    creat_daemon();
    while(1){
        printf("i am running\n");
        sleep(1);
    }
    return 0;
}

void  creat_daemon(){
    pid_t pid = fork ();

    if(pid<0){
        perror("fork");
        exit(-1);
    }

    if(pid>0){
        exit(0);

    }

    //执行到这里一定是子进程
   pid= setsid();  //setsid将当前进程设置为一个新的会话期session,目的是让当前进程脱离控制台,变成守护进程。

   chdir("/");  //将当前的进程的工作目录设置为根目录;
   umask(0);  //umask设置为零,确保将来进程有最大的文件操作权。
   //关闭所有文件描述符
   //先要获取当前系统中所允许的最大文件描述符数目
   int cnt = sysconf(_SC_OPEN_MAX);

   for (int i = 0; i < cnt;i++){
       close(i);
   }

   open("/dev/null", O_RDWR);    //0    //将标准输入/输出/错误(1,2,3)全部放在垃圾堆
    open("/dev/null", O_RDWR);   //1
     open("/dev/null", O_RDWR);  //2     //这样的话守护进程就没法打印输出
}

6.孤儿进程

alone.c

#include<stdio.h>
#include <sys/types.h>
       #include <sys/stat.h>
       #include <fcntl.h>
       #include<errno.h>
       #include<stdlib.h>
       #include<unistd.h>

void delete_file(void);

int main(){

//先判断文件是否已打开
int fd = -1;
fd = open("/home/xu/linux/C/alone.txt",  O_RDWR | O_TRUNC | O_CREAT | O_EXCL,0664);

if(fd<0){
    if(errno == EEXIST){
        printf("错误!文件已经存在\n");
    }
}
else printf("打开成功!\n");

atexit(delete_file);
for(int i=0;i<10;i++)
{
    printf("i am running...\n");
    sleep(1);
}

return 0;
}

void delete_file(){
    remove("/home/xu/linux/C/alone.txt");
}

6、线程

1.线程常见函数

//线程相关程序编译时要加上动态库pthread
gcc thread_scanf.c -lpthread  
  • 线程创建与回收
    (1)pthread_create 主线程用来创造子线程的
    (2)pthread_join 主线程用来等待(阻塞)回收子线程
    (3)pthread_detach 主线程用来分离子线程,分离后主线程不必再去回收子线程
  • 线程取消
    (1)pthread_cancel 一般都是主线程调用该函数去取消(让它赶紧死)子线程
    (2)pthread_setcancelstate 子线程设置自己是否允许被取消
    (3)pthread_setcanceltype
  • 线程函数退出相关
    (1)pthread_exit与return退出
    (2)pthread_cleanup_push
    (3)pthread_cleanup_pop
  • 获取线程id
    (1)pthread_self
  • 主线程和子线程实现异步IO
1. pthread_create()

参考:提出问题 解决问题——pthread详解

  1. 函数原型
    在Linux下创建的线程的API接口是pthread_create(),它的完整定义是:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg); 

函数参数:

  1. 线程句柄 thread:当一个新的线程调用成功之后,就会通过这个参数将线程的句柄返回给调用者,以便对这个线程进行管理。
  2. 线程属性 attr: pthread_create()接口的第二个参数用于设置线程的属性。这个参数是可选的,当不需要修改线程的默认属性时,给它传递NULL就行。具体线程有那些属性,我们后面再做介绍。
  3. 入口函数 start_routine(): 当你的程序调用了这个接口之后,就会产生一个线程,而这个线程的入口函数就是start_routine()。如果线程创建成功,这个接口会返回0。
  4. *入口函数参数 arg : start_routine()函数有一个参数,这个参数就是pthread_create的最后一个参数arg。这种设计可以在线程创建之前就帮它准备好一些专有数据,最典型的用法就是使用C++编程时的this指针。start_routine()有一个返回值,这个返回值可以通过pthread_join()接口获得。
  • 例子:用线程和主进程(while())实现异步鼠标和键盘IO
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <pthread.h>

char buf[200];
//创建一个线程函数
void *func(void *arg){
 //处理键盘(分支任务)
    while(1){
    memset(buf , 0, sizeof(buf));
    read(0, buf, 5);
    printf("读出的内容是: [%s].\n", buf); 
    }

}

int main(void){
//用线程实现并发
//两个文件IO都是阻塞的
int ret = -1;
int fd = -1;
pthread_t th =-1;
ret = pthread_create(&th,NULL,func,NULL);
if(ret!=0){
    printf("pthread_create error!\n");
}
    //主任务
    //处理鼠标
    fd = open("/dev/input/mouse0", O_RDONLY); //此处要用阻塞式,不用O_NONBLOCK标志
    //如果使用O_NONBLOCK标识将鼠标IO改为非阻塞式,程序一运行鼠标就会不停滴空打印。
    if(fd<0){
        perror("open:");
        return -1;
    }
    while(1){
    memset(buf , 0, sizeof(buf));
    read(fd, buf, 100);//阻塞式的话是不是只有IO接收到信息才能从fd获取到队列并打印?
    printf("鼠标读出的内容是: [%s].\n", buf); 
    }
    return 0;
}
//编译:gcc two_thread_two_IO.c -lpthread
//-lpthread  链接动态库

执行结果如下:
在这里插入图片描述
疑问
1.鼠标IO作为主线程,键盘IO作为子线程;
鼠标是阻塞式打开,为什么还能和键盘的输入实现并发?
答:如果使用O_NONBLOCK标识将鼠标IO改为非阻塞式,程序一运行鼠标就会不停滴空打印。

2.线程同步(信号量、互斥锁、条件变量)

1. 线程同步之信号量12

3.7.4.1、任务:用户从终端输入任意字符然后统计个数显示,输入end则结束
3.7.4.2、使用多线程实现:主线程获取用户输入并判断是否退出,子线程计数
(1)为什么需要多线程实现
(2)问题和困难点是?
(3)理解什么是线程同步
3.7.4.3、信号量的介绍和使用

  • 任务代码
  1. 使用scanf()简单实现
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
//用户从终端输入任意字符然后统计个数显示,输入end则结束
int main(void)
{
    char buff[100];
    printf("请输入一个字符串:\n");
    while(scanf("%s", buff))   //当按下回车时scanf停止接收并返回接受的个数
    { //当输入“end"时程序终止
        if(!memcmp(buff, "end", 3))
        {
            printf("程序结束!\n");
            exit(0);
        }
        printf("一共%d个字符\n", strlen(buff));
        memset(buff, 0, sizeof(buff));
    }
    return 0;
}
  1. 用主线程和子线程来实现
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <pthread.h>
#include <unistd.h>
//用户从终端输入任意字符然后统计个数显示,输入end则结束
char buff[100];
sem_t sem;          //定义一个信号量
int flag = 0;
//子线程程序,用于统计buff中的字符个数并打印
void *func(int *arg)
{
 while(flag == 0)
 {
     sem_wait(&sem);      //在这里阻塞住,接收到信号后阻塞打开
    printf("一共%d个字符\n", strlen(buff)); 
    memset(buff, 0, sizeof(buff));
 }
}

int main(void)
{
    int ret = -1;
    pthread_t th =-1;

    
    sem_init(&sem, 0, 0);       //初始化信号量其

    ret = pthread_create(&th, NULL, func, NULL);
    if(ret != 0)
    {
        printf("pthread_create error!\n");
        exit(-1);
    }

    printf("请输入一个字符串:\n");
    while(scanf("%s", buff))
    {
        //当输入“end"时程序终止
        if(!memcmp(buff, "end", 3)) //也可以用strncmp()
        {
            flag = 1;
            printf("程序结束!\n");
            //exit(0);
            sem_post(&sem);                  //激活信号量
            break;
        }
        sem_post(&sem);                  //激活信号量
        //sleep(1);
        sem_destroy(&sem);               //消灭信号量
       
    }
    //回收子线程
    printf("等待回收子线程\n");
    ret = pthread_join(th, NULL);
    if (ret != 0)
    {
        printf("pthread_join error\n");
        exit(-1);
    }
    printf("子线程回收成功\n");
    return 0;
}   
    //gcc thread_scanf.c -lpthread   编译
2. 线程同步之互斥锁

3.7.6.1、什么是互斥锁
(1)互斥锁又叫互斥量(mutex)
(2)相关函数:pthread_mutex_init pthread_mutex_destroy
pthread_mutex_lock pthread_mutex_unlock
(3)互斥锁和信号量的关系:可以认为互斥锁是一种特殊的信号量
(4)互斥锁主要用来实现关键段保护
3.7.6.2、用互斥锁来实现上节的代码

注意:man 3 pthread_mutex_init时提示找不到函数,说明你没有安装pthread相关的man手册。安装方法:1、虚拟机上网;2、sudo apt-get install manpages-posix-dev

3. 线程同步之条件变量

3.7.7.1、什么是条件变量
3.7.7.2、相关函数
pthread_cond_init pthread_cond_destroy
pthread_cond_wait pthread_cond_signal/pthread_cond_broadcast

8、网络编程

  1. 如何找到网络中的一个进程?
    首要解决的问题是如何唯一标识一个进程,在本地可以通过进程PID来唯一标识一个进程,但是在网络中这是行不通的。
    利用三元组(ip地址协议端口)就可以标识网络的进程了

1.soket

应用层通过传输层进行数据通信时,TCP和UDP会遇到同时为多个应用程序进程提供并发服务的问题。多个TCP连接或多个应用程序进程可能需要 通过同一个TCP协议端口传输数据。为了区别不同的应用程序进程和连接,许多计算操作系统为应用程序与TCP/IP协议交互提供了称为套接字 (Socket)的接口,区分不同应用程序进程间的网络通信和连接。

计算网络(王道考研—参考——BitHachi)
目录
第 1 章 计算网络体系结构
第 2 章 物理层
第 3 章 数据链路层
第 4 章 网络
第 5 章 传输层
第 6 章 应用层

在这里插入图片描述

tftp
------------------------------5-应用层--------------------------------------------
3. xml (extensiable markup language) 可扩展标记语言
4. HTML(HyperText Markup Language) 超文本标记语言
5. 统一资源定位符(URL)
6. 超文本传输协议(HTTP)
7. 文件传输协议( File Transfer Protocol, FTP)
8. 域名系统(Domain Name System, DNS)

-----------------------------4-传输层---------------------------------------------
TCP/IP(Transmission Control Protocol/Internet Protocol)传输控制协议;
UDP
在这里插入图片描述

在这里插入图片描述

2.soket相关函数

在这里插入图片描述`````````
在套接字(socket)通信中,send 和 write 函数都可以用于发送数据
在这里插入图片描述

三次握手:
在这里插入图片描述

四次挥手:

int socket(int domain, int type, int protocol);

1\socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。

正如可以给fopen的传入不同参数值,以打开不同的文件。创建socket的时候,也可以指定不同的参数创建不同的socket描述符,socket函数的三个参数分别为:

1 • domain:即协议域,又称为协议族(family)。常用的协议族有,AF_INET(IPv4)、AF_INET6(IPv6)、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。

2 • ype:指定socket类型。常用的socket类型有,SOCK_STREAM(流式套接字)、SOCK_DGRAM(数据报式套接字)、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等
3 • protocol:就是指定协议。常用的协议有,IPPROTO_TCP、PPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。

注意:并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议

  • bind()函数
    bind()函数把一个地址族中的特定地址赋给socket。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • listen()、connect()函数

如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。

int listen(int sockfd, int backlog);
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • accept()函数
    TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就向TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。
    返回值:accept() 函数返回一个新的套接字描述符,该描述符用于与特定客户端建立通信。原始套接字(服务器套接字)仍然保持打开状态,以便接受更多的连接。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • recv()、send()等函数
    至此服务器与客户已经建立好连接了。可以调用网络I/O进行读写操作了,即实现了网咯中不同进程之间的通信!网络I/O操作有下面几组:
read()/write()recv()/send()readv()/writev()recvmsg()/sendmsg()recvfrom()/sendto()

它们的声明如下:

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int  flags,const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
  • 相关函数
 #include <sys/socket.h>
       #include <netinet/in.h>
       #include <arpa/inet.h>

       int inet_aton(const char *cp, struct in_addr *inp);

       in_addr_t inet_addr(const char *cp);

       in_addr_t inet_network(const char *cp);

       char *inet_ntoa(struct in_addr in);

       struct in_addr inet_makeaddr(in_addr_t net, in_addr_t host);

       in_addr_t inet_lnaof(struct in_addr in);

       in_addr_t inet_netof(struct in_addr in);

2.soket实践

1. ubuntu下两个终端分别实现服务器和客户端
  1. server
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

/*
编写服务器应用程序的流程如下: 
①、调用 socket()函数打开套接字,得到套接字描述符;
②、调用 bind()函数将套接字与 IP 地址、端口号进行绑定;
③、调用 listen()函数让服务器进程进入监听状态;
④、调用 accept()函数获取客户端的连接请求并建立连接;
⑤、调用 read/recv、write/send 与客户端进行通信;
⑥、调用 close()关闭套接字。
//各个函数原型
int socket(int domain, int type, int protocol);
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
int listen(int sockfd, int backlog);
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
*/

#define SERVER_PORT 9999 //端口号不能发生冲突,不常用的端口号通常大于 5000
int main(void) {
 struct sockaddr_in server_addr = {0};
 struct sockaddr_in client_addr = {0};
 char ip_str[20] = {0};
 int sockfd, connfd;
 int addrlen = sizeof(client_addr);
 char recvbuf[512];
 int ret;
 /* 打开套接字,得到套接字描述符 */
 sockfd = socket(AF_INET, SOCK_STREAM, 0);
 if (0 > sockfd) {
 perror("socket error");
 exit(EXIT_FAILURE);
 }
 /* 将套接字与指定端口号进行绑定 */
 server_addr.sin_family = AF_INET;
 server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
 server_addr.sin_port = htons(SERVER_PORT);
 ret = bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
 if (0 > ret) {
 perror("bind error");
 close(sockfd);
 exit(EXIT_FAILURE);
 }
 /* 使服务器进入监听状态 */
 ret = listen(sockfd, 50);
 if (0 > ret) {
 perror("listen error");
 close(sockfd);
 exit(EXIT_FAILURE);
 }
 /* 阻塞等待客户端连接 */
 connfd = accept(sockfd, (struct sockaddr *)&client_addr, &addrlen);
 if (0 > connfd) {
 perror("accept error");
 close(sockfd);
 exit(EXIT_FAILURE);
 }
 printf("有客户端接入...\n");
 inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip_str, sizeof(ip_str));
 printf("客户端主机的 IP 地址: %s\n", ip_str);
 printf("客户端进程的端口号: %d\n", client_addr.sin_port);
 /* 接收客户端发送过来的数据 */
 for ( ; ; ) {
     // 接收缓冲区清零
 memset(recvbuf, 0x0, sizeof(recvbuf));
 // 读数据
 ret = recv(connfd, recvbuf, sizeof(recvbuf), 0);
 if(0 >= ret) {
 perror("recv error");
 close(connfd);
 break;
 }
 // 将读取到的数据以字符串形式打印出来
 printf("from client: %s\n", recvbuf);
 // 如果读取到"exit"则关闭套接字退出程序
 if (0 == strncmp("exit", recvbuf, 4)) {
 printf("server exit...\n");
 close(connfd);
 break;
 }
 }
 /* 关闭套接字 */
 close(sockfd);
 exit(EXIT_SUCCESS);
}
  1. client
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

#define SERVER_PORT 9999 //服务器的端口号
#define SERVER_IP "192.168.1.10" //服务器的 IP 地址(从终端查看)

int main(void)
{
    struct sockaddr_in server_addr = {0};
    char buf[512];
    int sockfd;
    int ret;

    //打开套接字,得到套接字描述符
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
     if (0 > sockfd) {
        perror("socket error");
        exit(EXIT_FAILURE);
    }
    printf("sockfd = %d\n",sockfd);

    //调用connect连接远端服务器
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT); //端口号
    inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr);//IP 地址

      printf("go to connect...\n");
    ret = connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
    if (0 > ret) {
        perror("connect error");
        close(sockfd);
        exit(EXIT_FAILURE);
     }

     printf("服务器连接成功...\n\n");

     //向服务器发送数据
     for ( ; ; ) {
     // 清理缓冲区
        memset(buf, 0x0, sizeof(buf));
     // 接收用户输入的字符串数据
        printf("Please enter a string: ");
        fgets(buf, sizeof(buf), stdin);
 
     // 将用户输入的数据发送给服务器
         ret = send(sockfd, buf, strlen(buf), 0);
         if(0 > ret){
         perror("send error");
         break;
         }
     //输入了"exit",退出循环
         if(0 == strncmp(buf, "exit", 4))
         break;
     }
         close(sockfd);
         exit(EXIT_SUCCESS);
}

2. 父子进程分别实现server和client
#include "procces.h"
#include "socket_server.h"
#include "socket_client.h"

int main(void){
    printf("all is well!\n");
    printf("change\n");
    
    //父进程运行服务器打印客户端的消息,子进程做客户端。
    int ret = -1;
    char buf[512];

    ret = fork();
    if(ret<0){
        perror("fork:");
        return -1;
    }

    if(ret == 0){
        printf("son_procces\n");
        client_init();   //将实验一中的client.c封装成函数
    }

    if(ret > 0){
        printf("father_procces\n");
        server_init();    //将实验一中的server.c封装成函数
    }

    return 0;
}

//gcc *.c -o main   统一编译
//或者用Makefile     一键make

//su root 密码    进入root
//su xwave          回到普通模式

2、 进阶(操作系统相关)

华为工程师说多线程和并发是重点

  • 多线程
  • 并发
  • 《Unix高级应用编程》
  1. 文件系统、
  2. 内存管理、
  3. 调度机制、
  4. 任务通讯和管理等;

1、LINUX内核

Linux 内核由如下几部分组成:内存管理进程管理设备驱动程序文件系统网络管理等。
在这里插入图片描述

1. 内核空间和用户空间

通常32位Linux内核地址空间划分03G为用户空间,34G为内核空间。64位内核地址空间划分是不同的。
在这里插入图片描述

2、进程间通讯

参考:进程间通信(每个通讯方式都有实验)

进程间通信的几种方式总结

在这里插入图片描述

1、管道

1.无名管道(匿名管道)

在有亲缘关系的进程中通信
参考:CSDN

  1. 管道的创建
    管道是由调用pipe函数来创建
#include <unistd.h>
int pipe (int fd[2]);
//返回:成功返回0,出错返回-1     
// fd参数返回两个文件描述符,
fd[0]指向管道的读端,
fd[1]指向管道的写端。
fd[1]的输出是fd[0]的输入。

  1. 管道通信原理
    在这里插入图片描述
    参考:https://blog.csdn.net/skyroben
  2. 代码实现
    =读写端指的是向管道的读写
#include <stdio.h>  
#include <unistd.h>  
#include <string.h>  
#include <errno.h>  
int main() {  
    int fd[2];  //两个参数分别代表管道的输入输出端
    int ret = pipe(fd);  
    if (ret == -1)  {  
        perror(”pipe error\n”);  
        return 1;  
    }  
    pid_t id = fork();  //创建管道后再fork
    if (id == 0)  {//child  
        int i = 0;  
        close(fd[0]);  //关掉读端
        char *child = “I am  child!;  
        while (i<5)  
        {  
            write(fd[1], child, strlen(child) + 1);  
            sleep(2);  
            i++;  
        }  
    }  
    else if (id>0)  {//father  
        close(fd[1]);  //关掉写端
        char msg[100];  
        int j = 0;  
        while (j<5)  
        {  
            memset(msg,’\0,sizeof(msg));  
            ssize_t s = read(fd[0], msg, sizeof(msg));  
            if (s>0)  
            {  
                msg[s - 1] = ’\0;  
            }  
            printf(%s\n”, msg);  
            j++;  
        }  
    }  
    else  
    {//error  
        perror(”fork error\n”);  
        return 2;  
    }  
    return  0;  
}  
————————————————
版权声明:本文为CSDN博主「sky_Mata」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/skyroben/article/details/71513385
  1. 运行结果:
    每隔2秒打印一次I am child! 并且打印了五次。
    在这里插入图片描述
2.命名管道

可以在无亲缘关系的进程间通信
mkfifo()

2、消息队列

3、 共享内存

参考CSDN
实践参考:进程间通信——共享内存(Shared Memory)

1. 简介
  1. 顾名思义,共享内存就是允许多个不相关的进程访问同一个逻辑内存。
  2. 共享内存由一个进程在内存上申请一段共享区域,可支持多个进程访问;
  3. 共享内存可以支持较大的文件通讯,是最快的IPC方式;
  4. 我们可以通过下面这张图示来很好的说明。
    在这里插入图片描述
2. 作用:
  1. 它是在两个正在运行的进程之间共享和传递数据的一种非常有效的方式
  2. 不同进程之间共享的内存通常安排为同一段物理内存
  3. 如果某个进程向共享内存写入数据,所做的改动立即影响到可以访问统一段共享内存的任何其他进程

二、共享内存的原理
我们都知道,在Linux中,每一个进程都有属于自己的进程控制块和地址空间,并且都有与之对应的页表,负责将进程的虚拟地址空间和物理地址进行映射。如下图所示。

在这里插入图片描述
共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但是多个进程都可以访问

3.特点
  1. 最快的IPC(进程间通讯方式),效率高
    为了充分的说明这一点,我们引入之前进程间通讯的另一种方式——管道和消息队列。因为管道和消息队列进行共享的空间都是由内核对象提供管理,所执行的操作也都是系统调用,而这些数据最终还是要在内存中存储。概括来说,AB两个进程通过管道的方式来进行进程间通信如下:
    在这里插入图片描述
    所以需要四次数据拷贝

从内存空间缓冲区将数据拷贝到内核空间缓冲区
从内核缓冲区将数据拷贝到内存
从内存将数据拷贝内核空间缓冲区
从内核空间缓冲区将数据拷贝到用户空间缓冲区
而对于共享内存来说,A、B两个进程都有一个各自的指针指向共享内存,可以通过指针访问里面的数据,少了两次拷贝,没有传输的概念。相应的,效率也就变快了。
在这里插入图片描述2. 共享内存属于临界资源
就是由于这个原因,所以我们需要用信号量来进行同步控制。
2. 可以实现任意两个进程之间的通讯
3. 父进程fork子进程或者exec执行一个新的程序,在子进程和新进程里面不会继承父进程之前使用的共享内存

4. 共享内存的代码实现

以下是chat_GPT给出的一段共享内存的简单示例

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/shm.h>

#define SHM_SIZE 1024

int main() {
    int shmid;
    char *shmaddr;

    // 创建共享内存
    shmid = shmget(IPC_PRIVATE, SHM_SIZE, IPC_CREAT | 0666);
    if (shmid == -1) {
        perror("shmget");
        exit(1);
    }

    // 映射共享内存
    shmaddr = (char*)shmat(shmid, NULL, 0);
    if (shmaddr == (char*)-1) {
        perror("shmat");
        exit(1);
    }

    // 在共享内存中写入数据
    snprintf(shmaddr, SHM_SIZE, "Hello, shared memory!");

    // 解除映射
    if (shmdt(shmaddr) == -1) {
        perror("shmdt");
        exit(1);
    }

    // 删除共享内存
    if (shmctl(shmid, IPC_RMID, NULL) == -1) {
        perror("shmctl");
        exit(1);
    }

    return 0;
}

其中:

  1. shmget是一个用于创建或获取共享内存段的系统调用函数。它的原型在头文件sys/shm.h中声明如下:
#include <sys/ipc.h>
#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);

key: 共享内存段的标识符,通过ftok函数可以根据文件路径和一个整数标识符生成一个key值。不同进程通过相同的key值可以访问同一个共享内存段。
size: 需要创建的共享内存段的大小,以字节为单位。
shmflg: 共享内存段的创建和访问权限标志,通常是一个权限标志和一个IPC_CREAT标志的组合,用来指定共享内存段的创建方式和访问权限。
shmget函数返回一个整数值,即共享内存段的标识符(共享内存ID)。共享内存ID是一个非负整数,可以用于后续的共享内存操作,如映射共享内存到进程的地址空间等。

  1. shmat是一个用于将共享内存段映射到进程的地址空间的系统调用函数。通过映射共享内存,进程可以像访问本地内存一样访问共享内存,从而实现进程间的数据共享。

shmat的原型在头文件sys/shm.h中声明如下:

#include <sys/types.h>
#include <sys/shm.h>

void *shmat(int shmid, const void *shmaddr, int shmflg);

参数说明:

shmid: 共享内存段的标识符,即在shmget调用中返回的共享内存ID。
shmaddr: 指定共享内存映射的地址,通常设置为NULL,让系统自动选择映射地址。
shmflg: 映射的标志位,通常设为0。
shmat函数返回一个void*类型的指针,即共享内存段的起始地址,可以用于后续对共享内存的读写操作。

  1. shmdt是一个用于解除共享内存映射的系统调用函数。当进程不再需要访问共享内存时,应该调用shmdt来解除与该共享内存段的映射,从而将共享内存从进程的地址空间中分离。

shmdt的原型在头文件sys/shm.h中声明如下:

#include <sys/shm.h>

int shmdt(const void *shmaddr);

参数说明:

shmaddr: 指向共享内存段映射的地址,即通过shmat函数获得的共享内存起始地址。
shmdt函数没有返回值,它将共享内存段从进程的地址空间中分离后,不再对该共享内存进行访问。

  1. shmctl是一个用于对共享内存进行控制操作的系统调用函数。通过shmctl函数,可以对共享内存段进行各种管理操作,比如获取共享内存信息、删除共享内存段等。

shmctl的原型在头文件sys/shm.h中声明如下:

#include <sys/shm.h>

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

参数说明:

shmid: 共享内存段的标识符,即在shmget调用中返回的共享内存ID。
cmd: 控制命令,用于指定执行的具体操作,比如获取共享内存信息、删除共享内存段等。常用的命令有:
IPC_STAT: 获取共享内存的状态信息,将共享内存的状态信息保存在struct shmid_ds *buf中。
IPC_RMID: 删除共享内存段。
IPC_SET: 设置共享内存的权限等信息,需要提供struct shmid_ds *buf中的数据作为参数。
buf: 指向struct shmid_ds类型的指针,用于保存共享内存的状态信息或提供设置共享内存属性的参数。
shmctl函数的返回值为执行成功时返回0,执行失败时返回-1,并设置相应的错误码,可以通过errno变量获取错误信息。

3、内存管理

1. 常见的内存管理方式:

1)块式管理
2)页式管理
3)段式管理
4)段页式管理

现代操作系统内存管理用的是段页式管理

  1. Linux 采用了称为“虚拟内存”的内存管理方式。
  2. Linux 将内存划分为容易处理的“内存页”(对于大部分体系结构来说都是 4KB)。
  3. Linux 包括了管理可用内存的方式,以及物理和虚拟映射所使用的硬件机制。

2. MMU的三大作用

  1. MMU在ARM架构下的位置框图
    在这里插入图片描述
    补充说明:
    Table Walk Unit页表)的作用:当TLB miss块表未命中时)的时候,该部件负责查询页表实现地址转换。
  2. MMU的三大作用
  • 1)虚拟地址映射到物理地址
  • 2)物理地址访问权限管理
  • 3)cache缓存控制(比如:物理页是否允许缓存等)
  1. 存在即合理的TLB
    参考CSDN
    TLB英文全称:Translation Look-aside Buffer
    TLB中文全称:地址变换高速缓存
    TLB中文简称:快表
    TLB实际性质:它是一种cache
    在这里插入图片描述
    在MMU拼命干的时候,CPU却很闲,多级列表遍历需要的时间太长了,程序员们一看,这不行啊,好不容易提起来的速度,就被你MMU耽误了可不行,必须找到一个方法解决这个问题。
    之后程序员们发现,CPU经常使用的只有那一小部分页表,但MMU每次使用同样的数据还是需要重复之前的步骤,为什么不把经常使用的那一部分放在一个地方,在CPU需要时直去拿就是了(是不是有一种熟悉的味道,没错就像缓存的思想)。
    所以就给MMU创造了一个小助手快表TLB(Translation Look-aside Buffers)。

4、进程管理

1.进程的祖先

0号进程:也叫swapper进程
1号进程:所有普通进程的父亲
2号进程:所有内核进程的祖先

  1. 0号进程
    是一个内核进程,是所有进程的祖先,也叫做swapper进程。

主要作用:
执行start_kernel()函数,初始化内核需要的所有数据结构,激活中断,创建1号内核进程(init进程)。

只有当没有可运行的进程的时候,才会运行0号进程,0号进程不是一个实实在在可见的进程,它
是单用户,单任务的系统启动代码

  1. 1号进程
    由0号进程创建的1号进程,pid=1,1号进程共享0号进程的所有的数据结构。

1号进程一开始是内核进程,先执行init()函数完成内核初始化,然后调用exec()装入可执行程序init(),这样
init就变成可一个普通进程,1号进程也叫做systemed进程,是所有普通进程的父亲,所有普通进程由它创建。

在系统关闭之前,init进程一直存活,因为它创建和监控在操作系统外层执行的所有进程的活动。

  1. 2号进程
    是一个内核进程,是所有内核进程的祖先

2.进程管理机制

  1. 通过多任务机制,每个进程可认为只有自己独占计算机,从而简化程序的编写。
  2. Linux 中常见的进程间通讯机制有信号管道共享内存信号量套接字等。

5、并发

1.多进程并发

2.多线程并发

3.不 / 可重入函数

不 / 可重入函数

  1. 可重入函数:一个函数在被调用执行期间(尚未调用结束),由于某种时序又被重复调用,称之为重入。根据函数实现的方法分为可重入函数和不可重入函数两种。
  2. 不可重入函数:a) 使用了静态数据结构 b) 调用了malloc或free c) 是标准IO函数

注意事项:定义可重入函数,函数内不能含有全局变量及static变量,不能使用malloc、free。
信号捕捉函数应该设计为可重入函数。
信号处理程序可以调用的可重入函数可以参考man 7 signal。

  1. Linux常见的可重入函数
    在这里插入图片描述

  2. 可重入函数不能使用全局/静态变量的原理:(搭配C语言内存五区理解)

对于多线程并发不可重入函数不能使用全局/静态变量,因为每个线程有自己的栈放置局部变量,但要共享进程中数据段的全局变量,因此并发时会导致全局变量被篡改。
在这里插入图片描述
参考:CSDN

6、关中断

  1. 关中断后,除了不可屏蔽中断能产生中断,软中断、硬中断、可屏蔽中断都被关了。

3、项目实战

0. gcc编译

gcc编译

  1. GCC 编译器在编译一个C语言程序时需要经过以下 4 步:

1.将C语言源程序预处理,生成.i文件。
2.预处理后的.i文件编译成为汇编语言,生成.s文件。
3.将汇编语言文件经过汇编,生成目标文件.o文件。
4.将各个模块的.o文件链接起来生成一个可执行程序文件。

  1. 常用编译命令选项
    1.无选项

用法:#gcc test.c

作用:将test.c预处理、汇编、编译并链接形成可执行文件。

这里未指定输出文件,默认输出为a.out。

2.选项 -o

用法:#gcc test.c -o test

作用:将test.c预处理、汇编、编译并链接形成可执行文件test。

-o选项用来指定输出文件的文件名。

1.项目收集

  1. 基于正点原子的IMX6ULL开发板的智能车载系统(Qt)
0. 开发板开跑(命令 / )
  1. 串口模拟软件下基本命令
init 0 // 挂起
init 6 // 重启
halt // 挂起
poweroff // 关机
reboot // 重启
shutdown // 挂起

/opt/QDesktop > /dev/null 2>&1 &  //重启桌面程序

1. 向开发板的出厂系统中拷贝文件
1. 利用(ubunt以下称其为主机)开启NFS服务器挂载根文件系统

主机开启NFS服务器,建立/home/xu/linux/nfs/rootfs根文件目录,作为网络根文件系统供开发板内核启动的时候挂载;

开发板通过网线路由器和主机相连,在uboot命令行模式下设置环境参数,以便boot启动系统后能连接上主机的网络根文件系统;

以上工作完成后,开发板可以直接共享主机根文件系统目录下的文件,也就可以通过这种方式将交叉编译后的可执行文件交由开发板执行。

2. Windows 通过MobaXterm ssh 连接到开发板

开发板通过网线/(USB WiFi)-路由器和主机windows相连,将wn中的文件直接拖拽到ssh界面即可。

使用前提:开发板与 PC 机用网线连接在同一路由器上,路由器能上网。
请注意,这里使用的是出厂的文件系统,支持 ssh 协议。默认开发板文件系统不支持 FTP
传输,其他文件系统请确认是否支持 ssh 协议。
在这里插入图片描述

【正点原子】I.MX6U 嵌入式 Linux 驱动开发指南 V1.x 第 4.9 小节已经安装过 MobaXterm,
使用 ifconfig 在 MobaXterm 查看开发板的 ip,如下图,记下开发板的 ip 为 192.168.1.222。

ifconfig

win中查看ip用

ipconfig
3. USB拷贝法

1.准备一个 U 盘(FAT32 格式,不能用 NTFS 格式的)

2.插上 U 盘
在这里插入图片描述
3.开发板上电启动系统后,在串口终端里输入指令 df 查看当前挂载的内容。

df

在这里插入图片描述
在这里插入图片描述
4.进入 /run/media/sda1 下去拷贝文件

cd /run/media/sda1
ls

5.使用 cp 指令拷贝把SD中的文件复制一份到/home/root 目录下

cp test /home/root/
ls /home/roo

6.退出 U 盘
防止U盘文件损毁,将数据写回U盘。

sync

在这里插入图片描述
7.退出 U 盘时需要退出这个挂载的目录,然后用 umount 指令去卸载这个挂载的目录.
8.使用 cd - 指令返回到上一次目录,再使用 umount /run/media/sda1/ 卸载这个 sda1 目录。然
后用 df 指令查看 sda1 这个目录已经不存在了,表明已经卸载了,U 盘可以正常拔出了!

cd -
umount /run/media/sda1
df
2.交叉编译器编译工程

交叉编译器:arm-linux-gnueabihf

粘贴文件没有权限时 sudo nautilus

  1. 交叉编译器命令:
arm-linux-gnueabihf-gcc

在这里插入图片描述
上图采用了Makefile,因此只需要make即可一键编译

1、arm 表示这是编译 arm 架构代码的编译器。
2、linux 表示运行在 linux 环境下。
3、gnueabihf 表示嵌入式二进制接口。
4、gcc 表示是 gcc 工具。

  1. 交叉编译器安装完成之后,在使用之前先对交叉编译工具的环境进行设置,使用 source 执行安装目录下的
    environment-setup-cortexa7hf-neon-poky-linux-gnueabi 脚本文件即可,如下所示:
source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi

/opt/fsl-imx-x11/4.1.15-2.1.0 便是笔者在 Ubuntu 系统下安装交叉编译工具时对应的安装目录,大家根据
自己的情况设置正确的路径。

  1. 编译命令:
//先对交叉编译工具的环境进行设置
source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi

${CC} -o testAPP testAPP.c  //CC 变量其实就是交叉编译工具

编译完成后查看文件属性file testAPP

1.V4L2摄像头应用编程

  1. 硬件:
    I.MX6ULL ov5640

  2. 软件:
    头文件:
    系统调用函数:

  3. 实验步骤:
    1.将历程源码拷贝到UBUNTU,然后用命令行编译

```bash
//先对交叉编译工具的环境进行设置
source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi
${CC} -o camera camera.c  //CC 变量其实就是交叉编译工具

2.用U盘将可执行文件拷贝到开发板/home/root/目录
3.运行可执行文件

./camera /dev/video1
  1. 中间遇到屏幕显示了十六个小花屏,最终请教群里大佬通过更新出厂系统解决,实验成功!
    在这里插入图片描述
    下载最新的开发工具文件,在这个目录下更新出厂系统。
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐