linux内核网络子系统初探2---socket层

news2024/12/28 18:49:32

linux内核网络子系统初探2—socket层

一、内核网络socket层相关

接着上文,从这章开始,将按照五层网络模型的顺序逐层分析内核代码。
linux1.0网络协议栈部分代码如下:

[root@localhost linux-1.0]# ls net/
ddi.c  inet  Makefile  socket.c  Space.c  unix
[root@localhost linux-1.0]# ls net/inet/
arp.c       dev.h   icmp.h  loopback.c  protocol.c  README    skbuff.h  tcp.h    utils.c
arp.h       eth.c   inet.h  Makefile    protocol.h  route.c   sock.c    timer.c
datagram.c  eth.h   ip.c    packet.c    raw.c       route.h   sock.h    udp.c
dev.c       icmp.c  ip.h    proc.c      raw.h       skbuff.c  tcp.c     udp.h
[root@localhost linux-1.0]# ls net/unix/
Makefile  proc.c  sock.c  unix.h

socket部分主要看net/inet/sock.c、net/inet/protocol.c、net/socket.c这几个文件。
我个人的理解,在严格意义上,应用层指的应该是各类核外网络通信模块(例如ftp、http等),socket应该是介于应用层与内核间的接口层,本质不属于应用层。但按功能来讲它是属于应用层的核内接口部分。

二、以用户态API socket()为例,观察系统调用进入内核后的流程

用户态程序调用socket(struct proc *p, struct socket_args *uap, int retval)等系列API后,会调用对应的系统调用函数,从而切换进内核态。

系统调用函数接口定义见下,sys_socketcall包含了socket的所有系统调用API,通过传参确定具体的操作。以用户态API socket()为例,它的调用流程是:切入内核态->进入sys_socketcall的SYS_SOCKET分支->sock_socket()->pops[i]的create():

asmlinkage int
sys_socketcall(int call, unsigned long *args)
{
  int er;
  switch(call) {
        case SYS_SOCKET:
                er=verify_area(VERIFY_READ, args, 3 * sizeof(long));
                if(er)
                        return er;
                return(sock_socket(get_fs_long(args+0), //这里调用相应的sock_*函数,仅仅传递参数args,不做额外操作
                                   get_fs_long(args+1),
                                   get_fs_long(args+2)));
        case SYS_BIND:
                er=verify_area(VERIFY_READ, args, 3 * sizeof(long));
                if(er)
                        return er;
                return(sock_bind(get_fs_long(args+0),
                                 (struct sockaddr *)get_fs_long(args+1),
                                 get_fs_long(args+2)));
        case SYS_CONNECT:
                er=verify_area(VERIFY_READ, args, 3 * sizeof(long));
                if(er)
                        return er;
                return(sock_connect(get_fs_long(args+0),
                                    (struct sockaddr *)get_fs_long(args+1),
                                    get_fs_long(args+2)));
        ... ...
    }
}

sock_socket函数:

sock_socket(int family, int type, int protocol)
{
  int i, fd;
  struct socket *sock;
  struct proto_ops *ops;
  for (i = 0; i < NPROTO; ++i) { //遍历pops数组,匹配family值
        if (pops[i] == NULL) continue;//全局pops指针数组是通过sock_register注册的
        if (pops[i]->family == family) break;// pops全局定义:static struct proto_ops *pops[NPROTO];
  }
  if (i == NPROTO) {
        DPRINTF((net_debug, "NET: sock_socket: family not found\n"));
        return(-EINVAL);
  }
  ops = pops[i];
  ... ...
  if (!(sock = sock_alloc(1))) {//申请一个struct socket的空间
        printk("sock_socket: no more sockets\n");
        return(-EAGAIN);
  }
  sock->type = type;
  sock->ops = ops;//将上面通过family值匹配到的ops赋值给struct socket的ops指针对象
  if ((i = sock->ops->create(sock, protocol)) < 0) {//这里会调用ops->create,即inet_create
        sock_release(sock);
        return(i);
  }
// #define SOCK_INODE(S)   ((S)->inode)
  if ((fd = get_fd(SOCK_INODE(sock))) < 0) {//获取当前struct socket的inode指针地址,inode在sock_alloc里创建并初始化。sock_alloc中通过get_empty_inode函数获取文件系统中的一个inode对象
        sock_release(sock);
        return(-EINVAL);
  }
  return(fd);//实际返回用户态的值是内核中struct socket里指针对象inode的地址
}

这里补充下inode的相关知识:https://blog.csdn.net/smilejiasmile/article/details/121162741

文件储存在硬盘上,硬盘的最小存储单位叫做"扇区"(Sector)。每个扇区储存512字节(相当于0.5KB)。
操作系统读取硬盘的时候,不会一个个扇区地读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个"块"(block)。这种由多个扇区组成的"块",是文件存取的最小单位。“块"的大小,最常见的是4KB,即连续八个 sector组成一个 block。
文件数据都储存在"块"中,那么很显然,我们还必须找到一个地方储存文件的元信息,比如文件的创建者、文件的创建日期、文件的大小等等。这种储存文件元信息的区域就叫做inode,中文译名为"索引节点”。
每一个文件都有对应的inode,里面包含了与该文件有关的一些信息。
inode也会消耗硬盘空间,所以硬盘格式化的时候,操作系统自动将硬盘分成两个区域。一个是数据区,存放文件数据;另一个是inode区(inode table),存放inode所包含的信息。

pops数组成员是被sock_register初始化的,简化代码如下:

{
  for(i = 0; i < NPROTO; i++) {
        if (pops[i] != NULL) continue;//找到pops数组里第一个为空的对象,往里写入ops
        pops[i] = ops;
        pops[i]->family = family;
        return(i);
  }
  return(-ENOMEM);
}

在这里先简单总结下:socket层有个全局数组static struct proto_ops pops[NPROTO];,在inet_proto_init()函数内会调用sock_register()将static struct proto_ops inet_proto_ops*注册到这个pops数组里。之后,当用户态有API调用时,socket层就可以遍历pops数组,根据每个数组对象的family值来筛选出正确的struct proto_ops对象,从而获得相应的ops操作函数。在上文例子里,就是遍历pops数组筛选出inet_proto_ops,从而获得inet_create函数。
在这里插入图片描述接下来看下inet_proto_init(inet模块初始化函数),简化代码如下:

{
  (void) sock_register(inet_proto_ops.family, &inet_proto_ops);//这里会把inet_proto_ops注册到pops全局数组里,一共两处调用sock_register注册pops成员,另一处是unix代码内
  /* Add all the protocols. */
  for(i = 0; i < SOCK_ARRAY_SIZE; i++) {//这里的tcp_prot、udp_prot是全局变量,分别定义在tcp.c与udp.c。里面定义的是具体传输层协议的操作函数集合
        tcp_prot.sock_array[i] = NULL;//这里初始化下这几个struct proto *_prot全局变量
        udp_prot.sock_array[i] = NULL;//所有使用*协议的socket连接在内核里的struct sock结构,会被插到*_prot里的sock_array链表里
        raw_prot.sock_array[i] = NULL;
  }
  printk("IP Protocols: ");
  for(p = inet_protocol_base; p != NULL;) {//struct inet_protocol *inet_protocol_base全局变量是个链表头,存放各个协议的struct inet_protocol
        struct inet_protocol *tmp;
        tmp = (struct inet_protocol *) p->next;
        inet_add_protocol(p);//这里会把遍历到的struct inet_protocol对象添加到一个全局数组里struct inet_protocol *inet_protos[]
        printk("%s%s",p->name,tmp?", ":"\n");
        p = tmp;
  }
... ...
}

udp_prot、tcp_prot、raw_prot的sock_array是socket层用来存储记录对应协议的连接信息的位置,put_sock和get_sock是用于添加、获取sock_array链表里成员的操作函数。这样做便于socket层管理socket连接

inet_protocol_base链表的成员:

static struct inet_protocol udp_protocol = {
  udp_rcv,              /* UDP handler          */
  NULL,                 /* Will be UDP fraglist handler */
  udp_err,              /* UDP error control    */
  &tcp_protocol,        /* next                 */
  IPPROTO_UDP,          /* protocol ID          */
  0,                    /* copy                 */
  NULL,                 /* data                 */
  "UDP"                 /* name                 */
};
static struct inet_protocol icmp_protocol = {
  icmp_rcv,             /* ICMP handler         */
  NULL,                 /* ICMP never fragments anyway */
  NULL,                 /* ICMP error control   */
  &udp_protocol,        /* next                 */
  IPPROTO_ICMP,         /* protocol ID          */
  0,                    /* copy                 */
  NULL,                 /* data                 */
  "ICMP"                /* name                 */
};
struct inet_protocol *inet_protocol_base = &icmp_protocol;
struct inet_protocol *inet_protos[MAX_INET_PROTOS] = {
  NULL
};

各个协议的inet_protocol结构体间通过next指针连起来,协议族的链表头是inet_protocol_base。所有结构体通过inet_add_protocol添加到inet_protos数组里,后续会通过inet_protos来获取不同协议的struct inet_protocol对象。这里主要是便于后续ip层获取到tcp、udp等不同协议的接收接口

在这里插入图片描述

socket层的pops与udp_prot/tcp_prot均是为了便于管理,但是针对的地方不同。udp_prot/tcp_prot的存在,是为了便于操作不同协议类型里socket连接的struct sock;pops的存在,是为了便于查找到inet部分操作函数集,从而向下传递数据。

至此,用户API socket()的核内调用流程为:

sys_socketcall->sock_socket()-> pops[i]的create() ->inet_create()

syscall进入内核后层层向下调用的流程已经清晰了,其他的用户态API的系统调用也是按照这个顺序向下传递的,在此不再展开。进一步看下socket层几个核心结构体间的关联。

三、观察内核网络socket层核心结构体

先看下内核socket层关键的结构体定义:

  • socket结构体,里面比较关键的是成员struct proto_ops *ops和成员void *data.
    在这里插入图片描述

  • sock结构体,里面比较关键的是成员struct proto *prot,成员prot->sock_array数组指针,以及成员struct socket *socket.

在这里插入图片描述socket对象的初始化是在函数sock_socket内实现的,由系统调用sys_socketcall里的SYS_SOCKET。sock_socket伪代码:

sock_socket(int family, int type, int protocol){
  struct socket *sock;
  struct proto_ops *ops;
  ops = 根据传参family匹配到pops中的指定对象
  sock = sock_alloc(1);//分配空间给struct *socket指针,初始化部分成员变量
  sock->type = type;//赋值传参
  sock->ops = ops;//这里应该匹配到上文inet_proto_init里传给sock_register的参数inet_proto_ops
  sock->ops->create(sock,protocol);//将struct *socket指针传参,调用inet_create函数,inet_create内创建配置关联struct sock
  创建socket结构体的file descriptor;
}

inet_create函数的部分代码见下图:
在这里插入图片描述在inet部分,将网络抽象成了6种类型:

  1. SOCK_STREAM,面向字节流(tcp),工作在传输层
  2. SOCK_DGRAM,面向数据报(udp),工作在传输层
  3. SOCK_RAW,原始套接字(可以处理ICMP、IGMP等网络报文、特殊的IPv4报文、可以通过IP_HDRINCL套接字选项由用户构造IP头),工作在网络层。
  4. SOCK_RDM,一种可靠的UDP形式(保证交付数据报但不保证顺序)
  5. SOCK_SEQPACKET,可靠的连续数据包服务
  6. SOCK_PACKET,建立套接字的时候选择SOCK_PACKET类型,表示截取的数据帧在物理层,内核将不对网络数据进行处理而直接交给用户,即数据直接从网卡的协议栈交给用户。高版本内核仍然支持,但此功能比较过时,很少使用。

在inet_create里,会根据传参struct socket对象的type值,选择进入上面抽象出的不同分支,然后配置相应的sock结构体成员prot。

inet_create伪代码:

inet_create(struct socket *sock, int protocol){
  struct sock *sk;
  struct proto *prot;
  sk = kmalloc();//申请一个sock结构体空间
  配置sk的属性;
  sk->num = 0;
  switch(sock->type){//不同的SOCK_*网络类型
        case SOCK_STREAM://面向字节流
        case SOCK_SEQPACKET: //可靠的连续数据包服务
                传入参数protocol异常情况处理;
                protocol = IPPROTO_TCP;  
                sk->no_check = TCP_NO_CHECK;
                prot = &tcp_prot;
                break;
        case SOCK_DGRAM:
                传入参数protocol异常情况处理;
                protocol = IPPROTO_UDP;
                sk->no_check = UDP_NO_CHECK;
                prot=&udp_prot;
                break;
        case SOCK_RAW:
                传入参数protocol异常情况处理;
                prot = &raw_prot;
                sk->reuse = 1;
                sk->no_check = 0;
                sk->num = protocol;//在SOCK_RAW与SOCK_PACKET情况下,会配置sock成员num值
                break;

        case SOCK_PACKET:
                传入参数protocol异常情况处理;
                prot = &packet_prot;
                sk->reuse = 1;
                sk->no_check = 0;
                sk->num = protocol;//在SOCK_RAW与SOCK_PACKET情况下,会配置sock成员num值
                break;
        default:
                ... ...
  }
  sk->socket = sock;//将sock结构体中的成员socket指针指向当前传进来的socket对象
  sk->type = sock->type;//将sock结构体与socket结构体中的type配成一样的值。
  sk->protocol = protocol;//例如IPPROTO_TCP
  sk->prot = prot;//例如tcp_prot
  sock->data =(void *) ·sk;//将socket结构体的成员指针data指向sock结构体
  ... ...//配置sk->属性
  put_sock(sk->num, sk);//调用put_sock将当前sock对象挂到sock->prot->sock_array链表里便于管理,sock_array是个哈希链表
  ... ...
}

put_sock功能是将sock对象添加到对应协议的成员sock_array哈希链表中,伪代码如下,关键代码如图示:

void put_sock(unsigned short num, struct sock *sk){
    struct sock *sk1;
    struct sock *sk2;
    int mask;
    sk->num = num;
    sk->next = NULL;
    num = num &(SOCK_ARRAY_SIZE - 1);//用num(sk->num)做hash表的key
    if(sk->prot->sock_array[num] == NULL){ //如果hash表里key位置为空,说明当前sk是队列第一个节点
        sk->prot->sock_array[num] = sk;
        return;    
    }
    // mask为0xff000000 => 0xffff0000 => 0xffffff00 => 0xffffffff
    for(mask = 0xff000000; mask != 0xffffffff; mask = (mask >> 8) | mask) {
        if ((mask & sk->saddr) && (mask & sk->saddr) != (mask & 0xffffffff)) {
                mask = mask << 8;
                break;                
        }
  }
  sk1 = sk->prot->sock_array[num];//sk1指向哈希表对应key的链表头节点
  for(sk2 = sk1; sk2 != NULL; sk2=sk2->next) {//sk2、sk1做遍历指针,sk1代表已遍历过的最后一个原链表对象,sk2代表待检查位置
        if (!(sk2->saddr & mask)) {//当前sk2指针对应的位置没有ip地址,即sk2可插入元素,将插在sk2前。将空元素放到最后
                if (sk2 == sk1) {//代表链表为空,头插法
                        sk->next = sk->prot->sock_array[num];//将待插入sock的next指针指向 链表头(此时链表头为空)
                        sk->prot->sock_array[num] = sk;//链表头节点替换成当前的sock
                        return;//插入成功退出函数
                }
                sk->next = sk2;//将待插入sock的next指针指向sk2,即插在sk2前
                sk1->next= sk;//原链表的最后一个元素的next指针指向待插入sock
                return;//插入成功退出函数
        }
        sk1 = sk2;//当前位置不为空,不可插入,向后遍历,sk1移到当前遍历过的最后一个原链表元素
  }
  sk->next = NULL;
  sk1->next = sk;    
}

在这里插入图片描述

在这里总结一下上面列出的关键结构体间关系图:
在这里插入图片描述

四、观察内核网络socket层收发数据的调用路径:

用户态里,udp协议常用的发送接收编程API是sendto()、recvfrom(),tcp协议常用的发送接收API是send()、recv()。
它们的调用流程分别为:

  • sys_socketcall中SYS_SENDTO分支->sock_sendto->inet_sendto-> (sk->prot->sendto)
  • sys_socketcall中SYS_RECVFROM分支->sock_recvfrom->inet_recvfrom->(sk->prot->recvfrom)
  • sys_socketcall中SYS_SEND分支->sock_send->inet_send->(sk->prot->write)
  • sys_socketcall中SYS_RECV分支->sock_recv->inet_recv->(sk->prot->read)

在这里插入图片描述
这几个函数内部操作基本一致:

如果当前sock未指定端口,则内核调get_new_socknum函数获取一个空闲的端口号分配给sock用于数据传输。具体功能通过调用sock结构体里的->prot里相应的函数实现。这里的prot就对应的是初始化sock结构体时配置的tcp_prot等proto结构体指针,由此将数据传到了tcp+udp层(传输层)。

五、 小结

linux v1.0版本里,可以看出以下的层次关系:
在这里插入图片描述

在高版本的linux内核里,网络子系统进一步细化为BSD socket层、INET socket层、传输层、IP层、数据链路层这五层(见下图)。主要是新封装了一些socket相关函数作为BSD socket接口层单独放到socket.c文件里,并将sock.c文件中的inet操作部分提炼成INET socket接口层,单独放到af_inet.c文件中。高版本内核sock结构工作在INET socket层,socket结构工作在BSD socket层,所有BSD socket层的操作都通过struct sock及其域字段prot指针转化为相应的协议函数处理,所以sock结构是维系BSD socket层和INET socket层的纽带。

在这里插入图片描述

其中:
BSD(Berkeley Software Distribution)socket:提供向上统一的 SOCKET 操作接口,核心结构体是struct socket。
INET(指一切支持 IP 协议的网络) socket:INET socket 层,实现BSD的具体接口功能,向传输层传递请求与数据,核心结构体是struct sock。
对于内核socket层而言,主要是通过关键结构体struct socket与struct sock来实现向tcp/udp层传递数据的功能,并不会对数据本身做处理

附录:socket层的数据分流功能

数据传输过程中,socket层有个至关重要的功能就是数据分流,即为到达的数据筛选出一条正确的传输路径,传递给下一层。观察v1.0 socket层代码,可以发现在创建socket连接核内相关结构时,在inet_create里,会给每个socket连接,配置好传输层对应协议的ops,即筛选出该socket连接上后续数据向下传递的路径。后续数据到达socket层后,就可以直接调用传输层接口向下传递了。

高版本内核里,socket层是否也是在inet socket层里做了相同的工作呢?

以下是高版本4.19.90内核,以sendto() API为例,观察它在socket内部的调用流程:

SYSCALL_DEFINE6(sendto, ... ...) //socket.c,BSD socket层
{
        return __sys_sendto(fd, buff, len, flags, addr, addr_len);
}
int __sys_sendto(int fd, ... ...) //socket.c,BSD socket层
{
        struct socket *sock;
        ... ...
        err = sock_sendmsg(sock, &msg);
        ... ...
}
int sock_sendmsg(struct socket *sock, struct msghdr *msg) //socket.c,BSD socket层
{
        int err = security_socket_sendmsg(sock, msg, msg_data_left(msg));
        return err ?: sock_sendmsg_nosec(sock, msg);
}
EXPORT_SYMBOL(sock_sendmsg);
static inline int sock_sendmsg_nosec(struct socket *sock, struct msghdr *msg) //socket.c,BSD socket层
{
        int ret = sock->ops->sendmsg(sock, msg, msg_data_left(msg));//这里struct socket的ops,定义:const struct proto_ops  *ops;,这里调用的是传输层ops的API
        BUG_ON(ret == -EIOCBQUEUED);
        return ret;
}

来看下4.19.90 里socket的创建过程:

static int __init inet_init(void) //af_inet.c, INET socket层
{
        ... ...
        (void)sock_register(&inet_family_ops);//inet_family_ops定义如下,sock_register将inet_family_ops注册到net_families数组里,后续有用户态socket相关请求时会去查询net_families找到匹配项
        ... ...
        for (r = &inetsw[0]; r < &inetsw[SOCK_MAX]; ++r)//定义:static struct list_head inetsw[SOCK_MAX];
                INIT_LIST_HEAD(r);

        for (q = inetsw_array; q < &inetsw_array[INETSW_ARRAY_LEN]; ++q)
                inet_register_protosw(q);//将inetsw_array里的元素注册到inetsw链表里
        /*
        static struct inet_protosw inetsw_array[] = 
        {   
            {
                .type =       SOCK_STREAM,
                .protocol =   IPPROTO_TCP, 
                .prot =       &tcp_prot,
                .ops =        &inet_stream_ops,
                .flags =      INET_PROTOSW_PERMANENT |
                              INET_PROTOSW_ICSK,
            },
            {
                .type =       SOCK_DGRAM,
                .protocol =   IPPROTO_UDP,
                .prot =       &udp_prot,
                .ops =        &inet_dgram_ops,
                .flags =      INET_PROTOSW_PERMANENT,
           },
           ... ...
        };
        */
        ... ...
}
int sock_create(int family, int type, int protocol, struct socket **res) //构造struct socket的函数,socket.c, BSD socket层
{
        return __sock_create(current->nsproxy->net_ns, family, type, protocol, res, 0);
}
EXPORT_SYMBOL(sock_create);
int __sock_create(struct net *net, int family, ... ...) //socket.c, BSD socket层
{
        struct socket *sock;
        ... ...
        pf = rcu_dereference(net_families[family]); //找到inet_family_ops
        ... ...
        err = pf->create(net, sock, protocol, kern);//调的是inet_create
        ... ...
}        
static int inet_create(struct net *net, struct socket *sock, ... ...) //af_inet.c, INET socket层
{
        struct sock *sk;
        struct inet_protosw *answer;
        struct proto *answer_prot;
        ... ...
lookup_protocol:
        err = -ESOCKTNOSUPPORT;
        rcu_read_lock();
        list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {//遍历sock->type类型的成员
                err = 0;
                if (protocol == answer->protocol) { //匹配到合适的protocol就退出循环
                        if (protocol != IPPROTO_IP)//在inetsw里最后一个成员是IPPROTO_IP类型。
                                break;
               /*如果遍历到最后一个成员,说明前面都没匹配成功,即protocol就是IPPROTO_IP。
                 进入了"protocol == answer->protocol"的分支后,不会进入判断
                 "protocol != IPPROTO_IP"里,进而不会break,而是会将err置为-EPROTONOSUPPORT。
                 由于是最后一个成员,遍历过后会退出循环,进而对err的异常值进行处理。
               */
                } else {
                        if (IPPROTO_IP == protocol) {//sock->type是SOCK_RAW, protocol是IPPROTO_IP,则返回成员IPPROTO_IP
                                protocol = answer->protocol;
                                break;
                        }
                        if (IPPROTO_IP == answer->protocol)
                                break;
                        /*如果进了“IPPROTO_IP == answer->protocol”这个分支,
                          说明已经遍历到了最后一个成员
                        */
                }
                err = -EPROTONOSUPPORT;
        }
        ... ...
        sock->ops = answer->ops; //这里会匹配到下一层传输层的操作函数
        ... ...
}

所以实际上,在高版本内核里,socket层的逻辑依然是:

  • 在创建socket连接相关结构时,在INET socket层会为该条连接配置好向下层传输的路径,进行数据分流
  • 在数据到达socket层时,在BSD socket层可直接调用已配好的接口,向下传递数据。

最后总结下:

在每条socket连接创建时,INET socket层会为该条连接筛选出向下层传输数据的路径。后续该条连接上的数据到达内核后,在BSD socket层会直接调用已配好的接口,向传输层传递。所以不太好简单的概括为,INET socket层实现了数据分流,还是要BSD和INET一块作为socket层来看它的功能,二者合作完成了分流的功能

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/514268.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

uniapp安装uview-ui踏坑

1. 安装uview-ui npm install uview-ui -S 2. 创建vue.config.js 填写如下代码 module.exports {transpileDependencies: [uview-ui] }3. 配置main.js //uview import uView from "uview-ui" Vue.use(uView)4. App.vue中引入样式 <style lang"scss"…

文案智能改写-AI智能文章改写软件

随着人工智能技术的不断发展&#xff0c;越来越多的智能写作软件相继面世&#xff0c;其中&#xff0c;AI智能改写工具是一款非常有实用价值的工具。本文将从全自动批量改写、没有错别字和标准语法、支持图文模式改写、支持各种语言改写以及严格按照标准格式结构改写几个方面&a…

前端三剑客CSS篇——CSS选择器

初识CSS选择器 文章目录 初识CSS选择器CSS三大特征&#x1f44d;CSS的三种使用方法&#x1f44f;CSS常见选择器&#x1f440;标签选择器类选择器id选择器后代选择器属性选择器复合选择器 CSS代码风格&#x1f4dc; CSS是前端三剑客不可忽略的一部分&#xff0c;CSS对前端来说是…

知了汇智:坚持发展产教融合,做好高校、人才与企业之间的桥梁

6月将正式迎来高校毕业季&#xff0c;大学生就业是聚焦全社会关注的头等大事。5月9日&#xff0c;成都知了汇智科技有限公司&#xff08;以下简称“知了汇智”&#xff09;组织开展“深化产教融合、聚焦人才培养”的主题座谈会议&#xff0c;联动高校与合作企业参加&#xff0c…

天津专业python培训机构精选(犹豫不如学python)

说起python&#xff0c;有好多年轻开发者都学习过Python&#xff0c;而且到现在为止&#xff0c;还有好多人都在追着Python跑&#xff0c;即便其他语言也很优秀&#xff0c;但是对Python的爱真的是只增不减。接下来&#xff0c;小编就给大家浅说一下&#xff0c;为什么python这…

掌汇云创新鞋业会展,汇集专精特新企业,数字化连接上下游

国内&#xff1a;鞋业供需情况多变&#xff0c;对接难度较大。在一个基数庞大&#xff0c;且成长速度惊人的市场&#xff0c;要想快速地找到供应商显然不是一件简单的事&#xff1b; 国际&#xff1a;鞋业对于外贸的依赖程度很高&#xff0c;但是当前国际局势动荡&#xff0c;…

力扣 151. 反转字符串中的单词

一、题目描述 给你一个字符串 s&#xff0c;请你反转字符串中单词的顺序。 单词是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的单词分隔开。 返回单词顺序颠倒且单词之间用单个空格连接的结果字符串。 注意&#xff1a;输入字符串 s 中可能会存在前导空格、…

内存分段详解

内存分段 1.1 分段机制概述 1.1.1 分段机制产生的原因 对于分段机制&#xff0c;要从Intel的微处理器的8086开始说起&#xff0c;刚开始内存空间比较小&#xff0c;内存寻址采用的是直接访问物理地址的方式。由于技术的发展&#xff0c;计算机做的事情越来越多&#xff0c;程…

Connection closed, EOF detected错误

生产遇到了这个问题 原因&#xff1a;两个http网址&#xff0c;即没有启用SSL 解决方案 在 weblogic的启动文件 bin里面 的 setDomainEnv.sh 这个文件 。 加上这一句 set JAVA_OPTIONS%JAVA_OPTIONS% -DUseSunHttpHandlertrue

IS230STAOH2A开关柜的基本特征是什么以及开关柜如何用于电力系统的保护

​ IS230STAOH2A开关柜的基本特征是什么以及开关柜如何用于电力系统的保护 什么是开关柜 开关设备是用于开关控制和保护电路和设备的装置&#xff0c;电力在我们现代文明中的重要性非常高&#xff0c;因此为了获得持续的电力&#xff0c;我们必须确保电力系统免受大故障的影响…

电脑死机的常用排查思路

在工作过程中难免会遇到死机的问题&#xff0c;排查起来并不是那么轻松&#xff0c;下面分享一下我排查死机问题的思路。 判断是软件还是硬件级别的故障 在死机时先尝试移动鼠标&#xff0c;按大小写切换键或数字键盘锁定键&#xff0c;看看光标是否可以移动&#xff0c;大小…

LabVIEWCompactRIO 开发指南15 托管和监控网络发布的共享变量

LabVIEWCompactRIO 开发指南15 托管和监控网络发布的共享变量 托管 要使用网络发布的共享变量&#xff0c;共享变量引擎必须在分布式系统中的至少一个节点上运行。网络上的任何节点都可以读取或写入共享变量引擎发布的共享变量。所有节点都可以在不安装共享变量引擎的情况下…

【C语言】有符号整型(int) 与 无符号整型(unsigned int)的运算

/*有符号整型(int) 与 无符号整型(unsigned int)的运算1.全局变量&#xff0c;静态变量都是放在静态区&#xff0c;不初始化的时候&#xff0c;默认值为0。局部变量&#xff0c;放在栈区&#xff0c;不初始化的时候&#xff0c;默认值是随机值,编译器会报错2.int 与 unsigned i…

Linux知识点 -- Linux权限

Linux知识点 – Linux权限 文章目录 Linux知识点 -- Linux权限一、shell命令及运行原理二、Linux权限1.概念2.文件类型和访问权限3.更改文件的权限4.没有权限的情况5.更改文件的拥有者和所属组6.添加用户到信任列表7.umask权限掩码8.粘滞位 一、shell命令及运行原理 Linux严格…

【头歌】数组-稀疏矩阵的转置

数组-稀疏矩阵的转置 第1关&#xff1a;一般转置算法 任务描述 本关任务&#xff1a;实现稀疏矩阵的转置操作&#xff08;采用一般转置算法&#xff0c;即按列序转置&#xff09;。 相关知识 为了完成本关任务&#xff0c;你需要理解&#xff1a;1. 矩阵的压缩存储&#x…

np读取txt、csv文件的数据

目录 1、基础参数 2、参数详解 3、应用参数示例 机器学习中使用np.loadtxt()可以高效的导入数据&#xff0c;np.loadtxt()适合.txt文件和.csv文件。但是它默认读取float类型的值。 1、基础参数 numpy.loadtxt(fname, dtype, comments#, delimiterNone, convertersNone, s…

11. 类的继承

一、为什么要用继承 一个简化的Student类 class Student { private:string name;string studentID; public:string getName(){ return name; }void setName(string newName) { name newName; }string getStudentID(){ return studentID; }void setStudentID(string newID) {…

操作系统基础知识介绍之内存技术和优化(一)(包含SRAM和DRAM、SDRAM、GDRAMs)

使用 SRAM 可以满足最小化高速缓存访​​问时间的需要。 然而&#xff0c;当缓存未命中时&#xff0c;我们需要尽快将数据从主存中移出&#xff0c;这就需要高带宽内存。 这种高内存带宽可以通过将构成主内存的许多 DRAM 芯片组织成多个内存条并使内存总线更宽来实现&#xff0…

数字孪生可视化开发工具在各行业中的应用

数字孪生就是指在信息化平台内模拟物理实体、流程或者系统&#xff0c;即打造一个现实场景的数字化孪生双胞胎。出于成本和周期考虑&#xff0c;快速低成本搭建数字孪生系统成为中小型企业的期望&#xff0c;深圳华锐视点研发的UE4数字孪生编辑器&#xff0c;是一种能够帮助用户…

【Shell脚本】Linux安装Nginx以及开机自启

目录 一、Linux安装Nginx脚本1、把编写好的安装Nginx脚本放置到nginx.sh文件中2、在检查网络的时候&#xff0c;这里的IP地址&#xff0c;填写的需要安装Nginx服务器的IP地址3、这里的端口号可按照自己的需要进行修改4、安装Nginx脚本 二、Nginx开机自启 一、Linux安装Nginx脚本…